热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

性能优化二十二之dex热修复原理

前言热修复作为当下热门的技术,在业界内比较著名的有阿里巴巴的AndFix、Dexposed属于阿里系,腾讯QQ空间的超级补丁技术和微信的Tinker属于腾讯系。最近阿里百川

前言

        热修复作为当下热门的技术,在业界内比较著名的有阿里巴巴的AndFix、Dexposed属于阿里系,腾讯QQ空间的超级补丁技术和微信的Tinker属于腾讯系。最近阿里百川推出的HotFix热修复服务就基于AndFix技术,定位于线上紧急BUG的即时修复,这两大系的主要区别在哪儿呢?阿里系着重从底层C的二进制入手(详细还没有去了解),而腾讯系的着重从Java的类加载机制入手。说到这里可能很多人连热修复是什么都不知道?一般的bug修复,都是等下一个版本解决,然后发布新的apk,而热修复可以直接在客户已经安装的程序当中修复bug。
        为什么可以这么做呢?一般我们的bug会出现在某个类的某个方法的某个地方,我们只需要动态地将客户手机里面的apk里面的某个类给替换成我们已经修复好的类就行了。
        上面所介绍的就是DEX分包方案,使用了多DEX加载的原理,而腾讯系的超级补丁和微信的Tinker都是基于dex分包。大致的过程就是:把BUG方法修复以后,放到一个单独的DEX里,插入到dexElements数组的最前面,让虚拟机去加载修复完后的方法。

Dex分包的由来:

        相信很多开发者都遇到过65536的问题,当Apk解压后里面会有一个classes.dex文件,这里面就包含了我们项目的所有class,但是随着项目的越来越复杂,由于一个dvm中存储方法id用的是short类型,所以当一个dex包中的方法超过65536的时候,就会导致编译失败,幸好谷歌官方提供了multiDex来解决这种问题,具体怎么解决这里不细说。除此之外还有人提供了其他方式:原理就是将编译好的class文件拆分打包成两个dex,绕过dex方法数量的限制以及安装时的检查,在运行时再动态加载第二个dex文件中。除了第一个dex文件(即正常apk包唯一包含的Dex文件),其它dex文件都以资源的方式放在安装包中,并在Application的onCreate回调中被注入到系统的ClassLoader。因此,对于那些在注入之前已经引用到的类(以及它们所在的jar),必须放入第一个Dex文件中。

Dex分包原理–ClassLoader

        在java中,程序执行的时候需要将字节码加载到Jvm之后才会被执行,这个过程就使用到了ClassLoader类加载器,Android中也类似:
这里写图片描述
        由于在AS中看不到DexClassLoader和PathClassLoader源码,所以需要到其他网站去看,这里推荐http://androidxref.com/
        从文档中看出,DexClassLoader可以加载指定的某个dex文件,但是必须要在应用程序的目录下面,而PathClassLoader是用来加载应用程序的dex,到了这里我们应该可以猜到热修复用的就是ClassLoader去加载新的dex包。
BaseDexClassLoader源码:

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    /** * Constructs an instance. * * @param dexPath the list of jar/apk files containing classes and * resources, delimited by {@code File.pathSeparator}, which * defaults to {@code ":"} on Android * @param optimizedDirectory directory where optimized dex files * should be written; may be {@code null} * @param libraryPath the list of directories containing native * libraries, delimited by {@code File.pathSeparator}; may be * {@code null} * @param parent the parent class loader */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        List suppressedExceptiOns= new ArrayList();
        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时,实际是从pathList中去找的,而pathList则是DexPathList的一个实体。
DexPathList部分源码:

 final class DexPathList {
    private static final String DEX_SUFFIX = ".dex";
    private static final String JAR_SUFFIX = ".jar";
    private static final String ZIP_SUFFIX = ".zip";
    private static final String APK_SUFFIX = ".apk";

    /** class definition context */
    private final ClassLoader definingContext;

    /** * List of dex/resource (class path) elements. * Should be called pathElements, but the Facebook app uses reflection * to modify 'dexElements' (http://b/7726934). */
    //重点就在于这个dexElements
    private final Element[] dexElements;

    /** * Finds the named class in one of the dex files pointed at by * this instance. This will find the one in the earliest listed * path element. If the class is found but has not yet been * defined, then this method will define it in the defining * context that this instance was constructed with. * * @param name of class to find * @param suppressed exceptions encountered whilst finding the class * @return the named class or {@code null} if the class is not * found in any of the dex files */
    public Class findClass(String name, List 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;
    }
}

从这段源码可以看出,dexElements是用来保存dex的数组,而每个dex文件其实就是DexFile对象。遍历dexElements,然后通过DexFile去加载class文件,加载成功就返回,否则返回null。
通常情况下,dexElements数组中只会有一个元素,就是apk安装包中的classes.dex
而我们则可以通过反射,强行的将一个外部的dex文件添加到此dexElements中,这就是dex的分包原理了,这也是热补丁修复技术的原理。

疑问:如果两个dex中存在相同的class文件会怎样?

针对上面的问题不需要担心,一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类。

实现

首先需要去制造一个错误

public class MyTestClass {
    public  void testFix(Context context){
        int i = 10;
        int a = 0;
        Toast.makeText(context, "shit:"+i/a, Toast.LENGTH_SHORT).show();
    }
}

修复类:

public class FixDexUtils {
    private static HashSet loadedDex = new HashSet();

    static{
        loadedDex.clear();
    }

    public static void loadFixedDex(Context context){
        if(cOntext== null){
            return ;
        }
        //遍历所有的修复的dex
        File fileDir = context.getDir(MyConstants.DEX_DIR,Context.MODE_PRIVATE);
        File[] listFiles = fileDir.listFiles();
        for(File file:listFiles){
            if(file.getName().startsWith("classes")&&file.getName().endsWith(".dex")){
                loadedDex.add(file);//存入集合
            }
        }
        //dex合并之前的dex
        doDexInject(context,fileDir,loadedDex);
    }

    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);
    }

    private static void doDexInject(final Context appContext, File filesDir,HashSet loadedDex) {
        String optimizeDir = filesDir.getAbsolutePath()+File.separator+"opt_dex";
        File fopt = new File(optimizeDir);
        if(!fopt.exists()){
            fopt.mkdirs();
        }
        //1.加载应用程序的dex
        try {
            PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();

            for (File dex : loadedDex) {
                //2.加载指定的修复的dex文件。
                DexClassLoader classLoader = new DexClassLoader(
                        dex.getAbsolutePath(),//String dexPath,
                        fopt.getAbsolutePath(),//String optimizedDirectory,
                        null,//String libraryPath,
                        pathLoader//ClassLoader parent
                );
                //3.合并
                Object dexObj = getPathList(classLoader);
                Object pathObj = getPathList(pathLoader);
                Object mDexElementsList = getDexElements(dexObj);
                Object pathDexElementsList = getDexElements(pathObj);
                //合并完成
                Object dexElements = combineArray(mDexElementsList,pathDexElementsList);
                //重写给PathList里面的lement[] dexElements;赋值
                Object pathList = getPathList(pathLoader);
                setField(pathList,pathList.getClass(),"dexElements",dexElements);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static Object getField(Object obj, Class cl, String field)
            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }
    private static Object getPathList(Object baseDexClassLoader) throws Exception {
            return getField(baseDexClassLoader,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList");
    }

    private static Object getDexElements(Object obj) throws Exception {
            return getField(obj,obj.getClass(),"dexElements");
    }

    /** * 两个数组合并 * @param arrayLhs * @param arrayRhs * @return */
    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class localClass = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);
        int j = i + Array.getLength(arrayRhs);
        Object result = Array.newInstance(localClass, j);
        for (int k = 0; k if (k else {
                Array.set(result, k, Array.get(arrayRhs, k - i));
            }
        }
        return result;
    }
}

MainActivity.java中模拟了两个按钮,一个是test,一个是fix修复:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void test(View v){
        MyTestClass myTestClass = new MyTestClass();
        myTestClass.testFix(this);
    }

    public void fix(View v){
        fixBug();
    }

    private void fixBug() {
        //目录:/data/data/packageName/odex
        File fileDir = getDir(MyConstants.DEX_DIR,Context.MODE_PRIVATE);
        //往该目录下面放置我们修复好的dex文件。
        String name = "classes2.dex";
        String filePath = fileDir.getAbsolutePath()+File.separator+name;
        File file= new File(filePath);
        if(file.exists()){
            file.delete();
        }
        //搬家:把下载好的在SD卡里面的修复了的classes2.dex搬到应用目录filePath
        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);
            }

            File f = new File(filePath);
            if(f.exists()){
                Toast.makeText(this ,"dex 重写成功", Toast.LENGTH_SHORT).show();
            }
            //热修复
            FixDexUtils.loadFixedDex(this);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

MyApplication.java

public class MyApplication extends Application{
    @Override
    public void onCreate() {
        // TODO Auto-generated method stub
        super.onCreate();
    }
    @Override
    protected void attachBaseContext(Context base) {
        FixDexUtils.loadFixedDex(base);
        super.attachBaseContext(base);
    }
}

新的修复dex(补丁包)来源:
将上面的错误进行修改,然后重新编译项目,最好这个时候把AS的Instant run关闭:

public class MyTestClass {
    public  void testFix(Context context){
        int i = 10;
        int a = 1;
        Toast.makeText(context, "shit:"+i/a, Toast.LENGTH_SHORT).show();
    }
}

去保存项目的的地方,将MyTestClass类的全路径拷贝出来,这里我们需要用到Android中的一个工具:dx.bat,在AndroidSDK的工具的目录(D:\AndroidStudioSDK\build-tools\25.0.1)下打开cmd命令行输入如下命令:

dx --dex --output=D:\Users\kiven\Desktop\dex\classes2.dex D:\Users\kiven\Desktop\dex
命令解释:
    --output=D:\Users\kiven\Desktop\dex\classes2.dex   指定输出路径
    D:\Users\kiven\Desktop\dex    最后指定去打包哪个目录下面的class字节文件(注意要包括全路径的文件夹,也可以有多个class)

这样就生成了新的dex包。

参考链接:
http://blog.csdn.net/u010386612/article/details/50885320
http://blog.csdn.net/lisdye2/article/details/52119602


推荐阅读
  • PHP反射API的功能和用途详解
    本文详细介绍了PHP反射API的功能和用途,包括动态获取信息和调用对象方法的功能,以及自动加载插件、生成文档、扩充PHP语言等用途。通过反射API,可以获取类的元数据,创建类的实例,调用方法,传递参数,动态调用类的静态方法等。PHP反射API是一种内建的OOP技术扩展,通过使用Reflection、ReflectionClass和ReflectionMethod等类,可以帮助我们分析其他类、接口、方法、属性和扩展。 ... [详细]
  • 初识java关于JDK、JRE、JVM 了解一下 ... [详细]
  • 近来有一个需求,是需要在androidjava基础库中插入一些log信息,完成这个工作需要的前置条件有编译好的android源码具体android源码如何编译,这 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • Netty源代码分析服务器端启动ServerBootstrap初始化
    本文主要分析了Netty源代码中服务器端启动的过程,包括ServerBootstrap的初始化和相关参数的设置。通过分析NioEventLoopGroup、NioServerSocketChannel、ChannelOption.SO_BACKLOG等关键组件和选项的作用,深入理解Netty服务器端的启动过程。同时,还介绍了LoggingHandler的作用和使用方法,帮助读者更好地理解Netty源代码。 ... [详细]
  • 今天就跟大家聊聊有关怎么在Android应用中实现一个换肤功能,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • t-io 2.0.0发布-法网天眼第一版的回顾和更新说明
    本文回顾了t-io 1.x版本的工程结构和性能数据,并介绍了t-io在码云上的成绩和用户反馈。同时,还提到了@openSeLi同学发布的t-io 30W长连接并发压力测试报告。最后,详细介绍了t-io 2.0.0版本的更新内容,包括更简洁的使用方式和内置的httpsession功能。 ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • 如何搭建Java开发环境并开发WinCE项目
    本文介绍了如何搭建Java开发环境并开发WinCE项目,包括搭建开发环境的步骤和获取SDK的几种方式。同时还解答了一些关于WinCE开发的常见问题。通过阅读本文,您将了解如何使用Java进行嵌入式开发,并能够顺利开发WinCE应用程序。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • Java 11相对于Java 8,OptaPlanner性能提升有多大?
    本文通过基准测试比较了Java 11和Java 8对OptaPlanner的性能提升。测试结果表明,在相同的硬件环境下,Java 11相对于Java 8在垃圾回收方面表现更好,从而提升了OptaPlanner的性能。 ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • 开发笔记:Python之路第一篇:初识Python
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了Python之路第一篇:初识Python相关的知识,希望对你有一定的参考价值。Python简介& ... [详细]
author-avatar
拍友2602924913
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有