美文网首页AndroidAndroid模块化与组件化等Android进阶
Android热修复之 - 打补丁原来如此简单

Android热修复之 - 打补丁原来如此简单

作者: 红橙Darren | 来源:发表于2017-02-15 10:50 被阅读6162次

    1.概述


    今天我们来看一看纯java代码打补丁的方式会是怎样,纯Java代码是什么意思?因为上一期讲到阿里开源的热补丁里面涉及到NDK,会是会用但要自己去写NDK很多人估计不考谱,今天我们就用一种最简单的方式去实现,灵感来自腾讯提供的解决方案Tinker,但是我们自己的实现方式与它又不相同。上一周要大家去看类的加载机制也不知道大家看得怎么样了,某些估计连BaseDexClassLoader的源码都找不到,这里提供一个在线阅读网站http://androidxref.com

    视频讲解:http://pan.baidu.com/s/1dE4UsbZ

    相关文章:

    2017Android进阶之路与你同行
      
      Android热修复之 - 收集崩溃信息上传至服务器

    Android热修复之 - 阿里开源的热补丁

    Android热修复之 - 打补丁原来如此简单

    GIF.gif

    2.源码阅读


    2.1 Activity启动流程
     
      为很么要读Activity的流程呢?因为到后面我们要讲插件开发那也是个蒙B的坎,了解了解也好,但这里我就不介绍那么详细,后面插件开发再说,我只想知道Activity是怎么创建的呢?我贴点源码出来:

         private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
                // ........省略代码
    
                Activity activity = mInstrumentation.newActivity(
                             cl, component.getClassName(), r.intent);
                // ........省略代码
         }
    
    
         /**
         * Perform instantiation of the process's {@link Activity} object.  The
         * default implementation provides the normal system behavior.
         * 
         * @param cl The ClassLoader with which to instantiate the object.
         * @param className The name of the class implementing the Activity
         *                  object.
         * @param intent The Intent object that specified the activity class being
         *               instantiated.
         * 
         * @return The newly instantiated Activity object.
         */
        public Activity newActivity(ClassLoader cl, String className,
                Intent intent)
                throws InstantiationException, IllegalAccessException,
                ClassNotFoundException {
            // 利用ClassLoader通过名字加载类然后通过反射创建对象
            return (Activity)cl.loadClass(className).newInstance();
        }
    
    

    2.2 ClassLoader源码解析
      
      Activity的ClassLoader我们仔细看源码是 PathClassLoader extends BaseDexClassLoader extends ClassLoader 而loadClass这个方法在ClassLoader 中:

    20160314140715580.png
     protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
                // First, check if the class has already been loaded
                Class c = findLoadedClass(name);
                if (c == null) {
                     // If still not found, then invoke findClass in order
                     // to find the class.
                     c = findClass(name);
                     // this is the defining class loader; record the stats
                }
                return c;
        }
    

    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<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时,实际是从pathList中去找的,而pathList则是DexPathList的一个实体。

    DexPathList部分源码:

    /*package*/ 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).
         */
        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<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;
        }
    }
    

    从这段源码可以看出,dexElements是用来保存dex的数组,而每个dex文件其实就是DexFile对象。遍历dexElements,然后通过DexFile去加载class文件,加载成功就返回,否则返回null,看到这里应该基本知道我们想干啥了,我打算在dexElements上面做手脚,而且解释也说了Should be called pathElements, but the Facebook app uses reflection 我想问问跟Facebook啥子关系,不管了它说可以用reflection反射。

    无标题.jpg

    我们打算采用dex分包,从服务上获取到fix.dex后,采用反射机制往正在运行的ClassLoader中的PathList中的dexElements中去插入我们fix.dex中的dexElements,并且把它插入到正在运行的ClassLoader最前面,这样我们for循环找class类的时候就会找我们的fix.dex中的class了,原来有Bug的calss就不会被遍历到了。

    3.Dex分包问题


    如果你是用的Eclipse做开发,不过现在应该很少了吧,Eclipse分包比较蛋疼需要去写脚本不过好就好在这一快的资料比较多。
      Android Studio我们可以直接配置,但是网上的资料都是用来解决方法数超过65536,问题是我们现在没有超过按照网上提供的配置根本就不会分包,当然我们还有很多方法如获取到class类自己用命令制作dex包,但是我想问一下就算是知道哪一个class报错了修改混淆后牵连的东西会太多估计也行不同。
      我们其实还是可以用Android Studio自带的我们去官网找最新的,百度搜索提供的分包方案比较老了所以行不通,且看我如何配置:

        dexOptions {//dex配置
            javaMaxHeapSize "4g"
            preDexLibraries = false
            def listFile = project.rootDir.absolutePath+'/app/maindexlist.txt'
            additionalParameters = [//dex参数详见 dx --help
                                    '--multi-dex',//多分包
                                    '--set-max-idx-number=60000',//每个包内方法数上限
                                    '--main-dex-list='+listFile,//打包进主classes.dex的文件列表
                                    '--minimal-main-dex'//使上一句生效
            ]
        }
    

    在build.gradle中加入以上配置,我们再在maindexlist.txt中保存我们主dex的类即可运行,解压apk如果可以看到有两个dex,代表这一步已经成功了,如果想保险一点可以反编译dex看看主classes.dex里面到底是不是只要我们配置的下面这三个类。

    com/hc/multidexdemo/MainActivity.class
    com/hc/multidexdemo/BuildConfig.class
    com/hc/multidexdemo/BaseApplication.class
    
    GZJ@RKECOHF1~MI}(5BM@2U.png

    4.合并补丁Dex包

    假如我们某个类出现了异常闪退的情况,那么我们修改完成重新打包获取classes2.dex作为我们的补丁包fix.dex放在我们的服务器上面,我们客户端访问服务器下载fix.dex进行合并即可修复。

        /**
         * 合并注入
         * @param context
         * @throws Exception
         */
        private static void injectDexElements(Context context) throws Exception {
            ClassLoader pathClassLoader = context.getClassLoader();
    
            File outDexFile = new File(context.getDir("odex", Context.MODE_PRIVATE).getAbsolutePath()
                    + File.separator + "out_dex");
    
            if (!outDexFile.exists()) {
                outDexFile.mkdirs();
            }
    
            // 合并成一个数组
            Object applicationDexElement = getDexElementByClassLoader(pathClassLoader);
    
            for (File dexFile : mFixDex) {
                ClassLoader classLoader = new DexClassLoader(dexFile.getAbsolutePath(),// dexPath
                        outDexFile.getAbsolutePath(),// optimizedDirectory
                        null,
                        pathClassLoader
                );
                // 获取这个classLoader中的Element
                Object classElement = getDexElementByClassLoader(classLoader);
                Log.e("TAG", classElement.toString());
                applicationDexElement = combineArray(classElement, applicationDexElement);
            }
    
            // 注入到pathClassLoader中
            injectDexElements(pathClassLoader, applicationDexElement);
        }
    
        /**
         * 把dexElement注入到已运行classLoader中
         * @param classLoader
         * @param dexElement
         * @throws Exception
         */
        private static void injectDexElements(ClassLoader classLoader, Object dexElement) throws Exception {
            Class<?> classLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
            Field pathListField = classLoaderClass.getDeclaredField("pathList");
            pathListField.setAccessible(true);
            Object pathList = pathListField.get(classLoader);
    
            Class<?> pathListClass = pathList.getClass();
            Field dexElementsField = pathListClass.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);
            dexElementsField.set(pathList, dexElement);
        }
    
        /**
         * 合并两个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 < j; ++k) {
                if (k < i) {
                    Array.set(result, k, Array.get(arrayLhs, k));
                } else {
                    Array.set(result, k, Array.get(arrayRhs, k - i));
                }
            }
            return result;
        }
    
        /**
         * 获取classLoader中的DexElement
         * @param classLoader ClassLoader
         */
        public static Object getDexElementByClassLoader(ClassLoader classLoader) throws Exception {
            Class<?> classLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
            Field pathListField = classLoaderClass.getDeclaredField("pathList");
            pathListField.setAccessible(true);
            Object pathList = pathListField.get(classLoader);
    
            Class<?> pathListClass = pathList.getClass();
            Field dexElementsField = pathListClass.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);
            Object dexElements = dexElementsField.get(pathList);
    
            return dexElements;
        }
    

    就这么几个方法即可完成修复其实挺简单的,大家也可以去看看腾讯提供的修复方案,但是我一看到要防止打标记就开始蒙B了,后面几篇我们又要回归设计模式的讲解了,视频讲解需要等每周六日晚上八点。


    视频讲解:http://pan.baidu.com/s/1dE4UsbZ

    相关文章:

    2017Android进阶之路与你同行
      
      Android热修复之 - 收集崩溃信息上传至服务器

    Android热修复之 - 阿里开源的热补丁

    Android热修复之 - 打补丁原来如此简单

    相关文章

      网友评论

      • 蓝雪lin:在整个dex文件混淆的情况下:
        问题1:这种热修复方法可以改变原有方法的参数吗?如给某个类的方法添加参数
        问题2:可以修改原有类的包路径吗?
        蓝雪lin:@红橙Darren 在新的dex中,调用到了增加参数的方法,只能在原有类中增加方法,而不能直接修改方法的参数,是这样吧
        蓝雪lin:@红橙Darren 也就是说,即使是加载了新的dex,查找类或者方法的时候也会按照原先的类路径或者方法来查找,是吗?
        红橙Darren:@蓝雪_f913 都不行
      • 天道__:视频在哪看啊 有地址吗
        天道__:@红橙Darren 我问的是直播视频,qq现在不能加了:disappointed_relieved:
        红橙Darren:@天道__ 文章末尾链接
      • 兜兜里面没有钱:我看着视频敲完,然后自己再看一遍你的博客,感觉恍然大悟。
        世道无情:赞一个
      • Marilzing::+1: 花了好久看了很多博文不比看辉哥的视频
      • 青春的尾巴啊:好厉害的样子
      • 夨落旳尐孩:请收下我的膝盖
      • angcyo:Demo地址: https://github.com/angcyo/DexFixDemo
        红橙Darren:@angcyo 不错啊,给你star
      • a54c04743257:参照视频写出来打的包解压后有两个class.dex文件,然而并没有进行Dex分包配置
        a54c04743257:@红橙Darren 奇怪,debug解压是两个,release是一个
        红橙Darren: @颀天 没道理,自动分包不可能的
      • d878c4c94786:辉哥,可以滴啊!
        红橙Darren: @田田田_2cd0 你这名字好像我一个同事,看头像才认出来
      • 1d65221a9608:有demo吗?
        我ClassLoader的源码与你的贴出来的源码有些差异,不过大体意思相同。
        能有个demo就更好了。
        合并补丁Dex包中,不是很理解第一个方法传的上下文是什么意思。是出现BUG的类的上下文,还是直接传Application呢?还请详细解答一下。
        红橙Darren: @x张星星 看不懂就看看视频吧,估计你思路都没搞懂
        红橙Darren: @x张星星 我是想获取应用的ClassLoader,所以传上下文就可以了,context.getClassLoader()即应用的ClassLoader就是PathClassLoader。

      本文标题:Android热修复之 - 打补丁原来如此简单

      本文链接:https://www.haomeiwen.com/subject/qlfqwttx.html