• 性能优化-热修复


    Android的新技术在不断更迭,各种bug修复也如火如荼,增量更新,插件化开发,热修复等等,数不胜数,这一节,就来盘点盘点热修复的来龙去脉

    热修复说明

    目前在热修复发面,国内众多公司都提出了解决方案,比较出名的例如阿里的Andfix,现在更新到到第三代,新名字叫做Sophix,腾讯的Tinker,饿了么的Amigo,美团的Robust等等,这里先做阿里和腾讯的例子,因为这两个热修复方案比较典型,一个是从底层C出发,一个是从Java层出发,接下来我们就来看看Tinker的方案是怎么实现热修复的

    仿腾讯热修复

    腾讯的热修复是基于Android加载class来实现热修复的,那么知道这个原理以后,我们也来自己做一个热修复工具,要知道怎么实现这个功能的,就需要先了解Android的类加载机制

    实现原理

    众所周知,其实Android的apk就是一个压缩包,在这个安装包里面放了资源文件(res),资源描述文件(rasc),类文件集合(dex),在这些文件里面,代码编译生成的字节码文件被打包到了dex中,那么Android在使用类的时候会去加载这个dex文件,而加载这个类则是通过PathClassLoader这个类去加载的,那么这个类里面做了些什么事情呢?

    我们查看其源代码,由于这是一个系统级的类,Google在SDK中并没有将其提供出来,那么要查看这个类要么通过下载源代码,要么在线查看,这里推荐一个国内的地址,可以在线查看源代码:androidxref还有androidos

    鉴于PathClassLoader的代码并不多,这里贴出源代码,在源代码里面有这样一句话JAR/ZIP/APK files, possibly containing a "classes.dex" file as well as arbitrary resources. Raw ".dex" files (not inside a zip file),也就是说其可以加载JAR,ZIP,APK格式的压缩包,但在里面必须有classes.dex文件,这里仅仅是两个构造方法,那在父类里面又做了什么呢?

    package dalvik.system;
    
    public class PathClassLoader extends BaseDexClassLoader {
    
    	public PathClassLoader(String dexPath, ClassLoader parent) {
    		super(dexPath, null, null, parent);
    	}
    
    	public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
    		super(dexPath, null, librarySearchPath, parent);
    	}
    }
    

    查看源代码,我们得知其继承了BaseDexClassLoader,那么在这个父类里面又做了啥,这里就不细细展开来讲了,这里只关注我们需要的东西,我们注意到,DexPathList这个类里面就是一系列的集合,最需要注意的就是private Element[] dexElements;这个属性,这个属性,这里就是dex的集合,我们的类方法就是从这个获取到的,使用DexPathListfindClass()方法查找类,从而获取到类的,这就是Android类的加载过程,那么我们的想法就可以得以实现。也就是说通过反射得到PathClassLoader这个类就可以调用方法去查找我们的类了,当然也可以调用BaseDexClassLoader

    ···
    private final DexPathList pathList;
    
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class "" + name + "" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }
    ···
    

    上面得到的Class这个方法是怎么实现的呢,浏览DexPathList源代码发现,是通过类的名字获取到类的

    public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;
    
            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }
    

    这里的关键地方在于DexFile的loadClassBinaryName()方法,那么这个方法是怎么查找类的,再继续查看DexFile的源代码,这里直接调到Native方法里面去了,为了一探究竟,我们再去Native方法一探究竟

    public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
        return defineClass(name, loader, mCookie, this, suppressed);
    }
    
    private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                     DexFile dexFile, List<Throwable> suppressed) {
        Class result = null;
        try {
            result = defineClassNative(name, loader, cookie, dexFile);
        } catch (NoClassDefFoundError e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        } catch (ClassNotFoundException e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        }
        return result;
    }
    ···
    private static native Class defineClassNative(String name, ClassLoader loader, Object cookie, DexFile dexFile)
                throws ClassNotFoundException, NoClassDefFoundError;
    

    defineClassNative()这个方法的实现位于../art/runtime/native/dalvik_system_DexFile.cc,仔细观察,我们在这里的重心在于找到类是怎么被找到的,我们会发现在将jstring做了一系列转化以后,又调用了FindClassDef()方法,在这个方法里面才真实的去查找类

    static jclass DexFile_defineClassNative(JNIEnv* env,
                                            jclass,
                                            jstring javaName,
                                            jobject javaLoader,
                                            jobject cookie,
                                            jobject dexFile) {
      std::vector<const DexFile*> dex_files;
      const OatFile* oat_file;
      if (!ConvertJavaArrayToDexFiles(env, cookie, /*out*/ dex_files, /*out*/ oat_file)) {
        VLOG(class_linker) << "Failed to find dex_file";
        DCHECK(env->ExceptionCheck());
        return nullptr;
      }
    
      ScopedUtfChars class_name(env, javaName);
      if (class_name.c_str() == nullptr) {
        VLOG(class_linker) << "Failed to find class_name";
        return nullptr;
      }
      const std::string descriptor(DotToDescriptor(class_name.c_str()));
      const size_t hash(ComputeModifiedUtf8Hash(descriptor.c_str()));
      for (auto& dex_file : dex_files) {
        const DexFile::ClassDef* dex_class_def = dex_file->FindClassDef(descriptor.c_str(), hash);
        if (dex_class_def != nullptr) {
          ScopedObjectAccess soa(env);
          ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
          StackHandleScope<1> hs(soa.Self());
          Handle<mirror::ClassLoader> class_loader(
              hs.NewHandle(soa.Decode<mirror::ClassLoader*>(javaLoader)));
          class_linker->RegisterDexFile(*dex_file, class_loader.Get());
          mirror::Class* result = class_linker->DefineClass(soa.Self(),
                                                            descriptor.c_str(),
                                                            hash,
                                                            class_loader,
                                                            *dex_file,
                                                            *dex_class_def);
          // Add the used dex file. This only required for the DexFile.loadClass API since normal
          // class loaders already keep their dex files live.
          class_linker->InsertDexFileInToClassLoader(soa.Decode<mirror::Object*>(dexFile),
                                                     class_loader.Get());
          if (result != nullptr) {
            VLOG(class_linker) << "DexFile_defineClassNative returning " << result
                               << " for " << class_name.c_str();
            return soa.AddLocalReference<jclass>(result);
          }
        }
      }
      VLOG(class_linker) << "Failed to find dex_class_def " << class_name.c_str();
      return nullptr;
    }
    

    那么顺理成章的,我们应该去查找这个FindClassDef()是怎么实现的,在../art/runtime/dex_file.cc终于找到了我们想要的答案,在这里,我们看到了在Native层,其处理就是通过遍历dex,查找到符合的类名就返回,这一点对于我们猜想的实现十分重要

    const DexFile::ClassDef* DexFile::FindClassDef(const char* descriptor, size_t hash) const {
      DCHECK_EQ(ComputeModifiedUtf8Hash(descriptor), hash);
      if (LIKELY(lookup_table_ != nullptr)) {
        const uint32_t class_def_idx = lookup_table_->Lookup(descriptor, hash);
        return (class_def_idx != DexFile::kDexNoIndex) ? &GetClassDef(class_def_idx) : nullptr;
      }
    
      // Fast path for rate no class defs case.
      const uint32_t num_class_defs = NumClassDefs();
      if (num_class_defs == 0) {
        return nullptr;
      }
      const TypeId* type_id = FindTypeId(descriptor);
      if (type_id != nullptr) {
        uint16_t type_idx = GetIndexForTypeId(*type_id);
        for (size_t i = 0; i < num_class_defs; ++i) {
          const ClassDef& class_def = GetClassDef(i);
          if (class_def.class_idx_ == type_idx) {
            return &class_def; //通过一系列计算,找到就返回
          }
        }
      }
      return nullptr;
    }
    

    那么我们在回来看看Java层的代码还有啥可以榨取的,通过对同级目录的观察,发现另外一类DexClassLoader,这个类用途在于加载dex文件,这里是不是和PathClassLoader类似,这就对了,这两者的区别就在于继承父类时候初始化的方式不同,PathClassLoader没有传递optimizedDirectory参数,也就是说其默认了路径,这个路径就是/data/data/{应用包名},而DexClassLoader可以加载任意目录的dex文件,而PathClassLoader只可以加载指定目录下的dex文件

    package dalvik.system;
    
    import java.io.File;
    
    public class DexClassLoader extends BaseDexClassLoader {
    
        public DexClassLoader(String dexPath, String optimizedDirectory,
                String librarySearchPath, ClassLoader parent) {
            super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
        }
    }
    

    经过上述的分析,我们已经知道怎么Android是怎么实现类的加载的,简单来说就是先加载dex,然后遍历dex里面的类,有符合的类名就返回,不在继续向下查找。基于此,我们可以在这里搞点事情,既然他是从头到尾去遍历类名,那么我将有修复好的类搞到dex里面去,并且放在最前面,那是不是要使用有bug那个类的时候,先找到的是我搞上去的类,然后不再向下查找,那基于Java层的修复不就可以搞定了么,至此,Java层修复的原理已经介绍完毕,接下来便是去实现这个想法了

    准备工作

    要实现这个大胆的想法,我们先要有dex的加载器,选择上面提到的三者都可以,这里选择DexClassLoader,因为这个可以从任何目录加载dex,在没有root权限的时候也很方便做实验,由于这里的初始化需要一个参数,那就选择没有过的PathClassLoader好了,思路有了,也规划好了,那么就开始码代码吧,首先模拟出一个bug,这里就编写一个带有除零操作的类好了

    首先我们是要用到一个工具,这个工具就是multidex,这个工具的作用就是每个类生成单独的.class字节码文件
    使用multidex就需要先配置gradle,先是要引入依赖

    dependencies {
    	implementation 'com.android.support:multidex:1.0.3'
    }
    

    并在gradle中激活

    android {
        ···
        defaultConfig {
            ···
            multiDexEnabled true
    		···
        }
    	···
    }
    

    在Application中使用

    public class MyApplition extends Application {
    	···
        @Override
        protected void attachBaseContext(Context base) {
            super.attachBaseContext(base);
    		···
            MultiDex.install(base);
    		···
        }
    	···
    }
    

    记得在AndroidManifest里面的application标签中引入Application

    <application
        ···
        android:name=".MyApplition"
        ··· >
    	···
    </application>
    

    然后还要记得关闭Instant run,因为这个功能会影响我们的实验,这个功能也是类似热修复的
    Settings -> Build,Exection,Deployment -> Instant run,在这里关闭这个功能
    热修复的实验环境搭建完毕,接下来就可以开始做实验

    实现过程

    首先我们来模拟一个bug的出现,就用一个带有除零错误的类为例,这个类里面就一个方法,这个方法除零了,产生了bug

    public class HotFixTest {
        public void div(Context context){
            int a = 10;
            int b = 0;
            Toast.makeText(context, a + " / " + b + " = " + (a / b), Toast.LENGTH_SHORT).show();
        }
    }
    

    首先构建出必要的布局,由于这里是热修复测试,这里就简单的两个按键,一个代表出错方法的调用,一个代表热修复
    布局

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="有除零bug"
            android:onClick="onTouch"/>
    
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="修复bug"
            android:onClick="onHotFix"/>
    </LinearLayout>
    

    主活动代码,在这里hotFix()方法只是移动了dex文件,其具体实现在热修复工具类里面

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    
        public void onTouch(View view) {
            HotFixTest hotFixTest = new HotFixTest();
            hotFixTest.div(getApplicationContext());
        }
    
        public void onHotFix(View view) {
            hotFix();
        }
    
        private void hotFix() {
            //目标目录:/data/data/packageName/odex
            File fileDir = getDir(Constants.DEX_DIR, Context.MODE_PRIVATE);
            String name = "classes2.dex";
            String filePath = fileDir.getAbsolutePath() + File.separator + name;
            File file = new File(filePath);
            if (file.exists()) {
                file.delete();
            }
            //将修复好的classes2.dex移动到目标目录
            InputStream is = null;
            FileOutputStream os = null;
            try {
                is = new FileInputStream(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + name);
                os = new FileOutputStream(filePath);
                int len = 0;
                byte[] buffer = new byte[1024];
                while ((len = is.read(buffer)) != -1) {
                    os.write(buffer, 0, len);
                }
                if (file.exists()) {
                    Toast.makeText(this, "bug修复完成", Toast.LENGTH_SHORT).show();
                }
                //热修复
                HotFixUtils.loadFixedDex(this);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    热修复工具类,在代码中有详细注释,这里就不赘述了

    public class HotFixUtils {
        private static HashSet<File> loadedDex = new HashSet<File>();
    
        public static void loadFixedDex(Context context) {
            if (context == null) {
                return;
            }
            File fileDir = context.getDir(Constants.DEX_DIR, Context.MODE_PRIVATE);
            File[] listFiles = fileDir.listFiles();
            loadedDex.clear();
            //遍历所有的dex,有的应用可能有多个dex,然后存入集合
            for (File file : listFiles) {
                if (file.getName().startsWith("classes") && file.getName().endsWith(".dex")) {
                    loadedDex.add(file);
                }
            }
            doDexInject(context, fileDir, loadedDex);
        }
    
        //将新的dex合并到旧的dex
        private static void doDexInject(final Context context, File filesDir, HashSet<File> loadedDex) {
            String optimizeDir = filesDir.getAbsolutePath() + File.separator + "opt_dex";
            File fopt = new File(optimizeDir);
            if (!fopt.exists()) {
                fopt.mkdirs();
            }
            try {
                //加载当前应用程序的dex,此时加载的是/data/data/{包名}下的dex
                PathClassLoader pathLoader = (PathClassLoader) context.getClassLoader();
                //这里采用的是每个dex都写入新的类,如果知道在哪个dex里面的类有bug,也可以直接加在里面
                for (File dex : loadedDex) {
                    //加载指定的修复的dex文件。
                    DexClassLoader classLoader = new DexClassLoader(dex.getAbsolutePath(),
                            fopt.getAbsolutePath(), null, pathLoader);
                    //通过反射得到ElementsList
                    Object oldDexObj = getPathList(classLoader);
                    Object newDexObj = getPathList(pathLoader);
                    Object oldDexElementsList = getDexElements(oldDexObj);
                    Object newDexElementsList = getDexElements(newDexObj);
                    //合并新旧dex,将class添加至头部
                    Object dexElements = combineArray(oldDexElementsList, newDexElementsList);
                    //重写给dexElements赋值
                    Object pathList = getPathList(pathLoader);
                    setField(pathList, pathList.getClass(), "dexElements", dexElements);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        //获取pathList属性值
        private static Object getPathList(Object baseDexClassLoader) throws Exception {
            return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
        }
    
        //获取dexElements属性值
        private static Object getDexElements(Object obj) throws Exception {
            return getField(obj, obj.getClass(), "dexElements");
        }
    
        //属性获取
        private static Object getField(Object obj, Class<?> clazz, String field)
                throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
            Field localField = clazz.getDeclaredField(field);
            localField.setAccessible(true);
            return localField.get(obj);
        }
    
        //属性赋值
        private static void setField(Object obj, Class<?> cl, String field, Object value) throws Exception {
            Field localField = cl.getDeclaredField(field);
            localField.setAccessible(true);
            localField.set(obj, value);
        }
    
        //数组合并,其实就是将新加入的class写到以前的dex最前面
        private static Object combineArray(Object arrayOld, Object arrayNew) {
            Class<?> localClass = arrayOld.getClass().getComponentType();
            int i = Array.getLength(arrayOld);
            int j = Array.getLength(arrayNew);
            int sum = i + j;
            Object result = Array.newInstance(localClass, sum);
            for (int k = 0; k < sum; k++) {
                if (k < i) {
                    Array.set(result, k, Array.get(arrayOld, k));
                } else {
                    Array.set(result, k, Array.get(arrayNew, k - i));
                }
            }
            return result;
        }
    }
    

    上面又到了一个常量类,这个类里面就一个静态属性

    public class Constants {
        public static final String DEX_DIR = "odex";
    }
    

    到这里,我们的测试就搭建完毕了,此时我们执行除法运算的时候,会直接crash掉,修改源代码,编译生成.class文件
    这里我简单地修改一下除法里面的b的值,将其修改为2,在编译项目,记得不要运行,不然就覆盖了原来的apk了

    public class HotFixTest {
        public void div(Context context){
            int a = 10;
            int b = 2;
            Toast.makeText(context, a + " / " + b + " = " + (a / b), Toast.LENGTH_SHORT).show();
        }
    }
    

    编译完成以后,在当前项目的build->intermediates->classes->debug->{包名}下面会有生成的class文件,将有问题的class文件复制出来,记得连同文件夹包名文件夹一起,例如我的包名是com.cj5785.hotfixtest,那么我复制出来的文件夹结构图如下

    └─dex
    	└─com
    	    └─cj5785
    	        └─hotfixtest
    	                HotFixTest.class
    

    使用sdk里面的一个工具将这个文件打包成dex文件,这个文件夹里面可以有多个class文件,也可以有多个目录,但是其相对位置一定要正确
    这个工具在build-tool -> {版本号}下面,dx.bat就是我们要使用的工具
    然后在命令函中输入参数,由于我这里直接把这个工具所在路径加到环境变量中了,这里就直接使用了

    dx --dex --output=E:	empclasses2.dex E:	empdex
    

    --output的参数就是输出目录,后面接的就是我们刚才复制出来的文件根目录
    根据我们上面写的代码,这里我们将生成的classes2.dex文件放入根目录,启动软件进行验证
    以下是我的验证效果,说明这这种方案的可行性还是有的
    修复效果图

  • 相关阅读:
    快速排序(java实现)
    java8 stream一些知识
    Lombok安装、简单使用、入门
    explain mysql 结果分析
    MySQL调优三部曲(二)EXPLAIN
    MySQL调优三部曲(一)慢查询
    排查问题
    Dynamics 365 获取值 设置值
    MySql CP 表 和 数据
    Dynamics 365单项选项集&多项选项集
  • 原文地址:https://www.cnblogs.com/cj5785/p/10664631.html
Copyright © 2020-2023  润新知