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

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热修复之 - 打补丁原来如此简单

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容