热修复框架源码剖析(上)

前言

在一个多月前,我写过一篇热修复初探,主要介绍了各类被普遍讨论和使用的热修复的技术实现原理,在那篇文章中,我也说本身会继续研究基于dex分包的热修复技术的源码。javascript

基于dex分包的热修复技术应该是QQ空间团队最早提出来的,但是他们只是经过技术文章分享了实现原理,其自己的源码并无公开,因此QQ的热修复实现细节以及编码风格是没有机会观摩了,可是仍是有不少团队基于QQ空间介绍的原理实现了热修复而且公开了源码,好比@dodola大神的RocooFix和AnoleFix(没错,他弄了俩),还有一个是在饿了么工做的Android前辈开发的Amigojava

由于这位前辈特地在个人热修复初探这篇文章下面留言向我宣传他的框架,因此首先我想来分析他的热修复实现细节。不过他本身也已经写了源码解读,虽然因为目前的代码的更新致使他的源码解读和源码有部分差别,但整体来讲逻辑是一致的。因此实际上我没有必要在这里详细的分析他的框架,只挑主要的来说。android

Amigo热修复框架剖析

Amigo github: github.com/eleme/Amigogit

总得来讲,从我看代码的状况来看,这是一个比较完备的,能够应用的热修复框架,从检测apk,到取出资源文件,dex文件,再到插入dex包到dexElements中,在重启apk一系列过程都比较完善,考虑周到。因此,在这里我只想讲一件Amigo具体是如何将dex插入到dexElements中的,由于这个才是基于dex分包的热修复技术的关键,不过他的修复方式和QQ空间团队提出的de仍是有一点不一样。github

Amigo.java数组

@Override
    public void onCreate() {
        super.onCreate();
        ......
        ......
        ......
        Log.e(TAG, "demoAPk.exists-->" + demoAPk.exists() + ", this--->" + this);

        ClassLoader originalClassLoader = getClassLoader();

        try {
            SharedPreferences sp = getSharedPreferences(SP_NAME, MODE_MULTI_PROCESS);

            if (checkUpgrade(sp)) {
                Log.e(TAG, "upgraded host app");
                clear(this);
                runOriginalApplication(originalClassLoader);
                return;
            }

            if (!demoAPk.exists()) {
                Log.e(TAG, "demoApk not exist");
                clear(this);
                runOriginalApplication(originalClassLoader);
                return;
            }

            if (!isSignatureRight(this, demoAPk)) {
                Log.e(TAG, "signature is illegal");
                clear(this);
                runOriginalApplication(originalClassLoader);
                return;
            }

            if (!checkPatchApkVersion(this, demoAPk)) {
                Log.e(TAG, "patch apk version cannot be less than host apk");
                clear(this);
                runOriginalApplication(originalClassLoader);
                return;
            }

            if (!ProcessUtils.isMainProcess(this) && isPatchApkFirstRun(sp)) {
                Log.e(TAG, "none main process and patch apk is not released yet");
                runOriginalApplication(originalClassLoader);
                return;
            }

            // only release loaded apk in the main process
            runPatchApk(sp); //这是最重要的一句话
            ......
            ......
            ......
    }复制代码

在Amigo这个类的onCreate方法里调用了runPatchApk(),开始准备替换apk.再查看这个runPatchApk()方法app

private void runPatchApk(SharedPreferences sp) throws LoadPatchApkException {
        try {
            String demoApkChecksum = getCrc(demoAPk);
            boolean isFirstRun = isPatchApkFirstRun(sp);
            Log.e(TAG, "demoApkChecksum-->" + demoApkChecksum + ", sig--->" + sp.getString(NEW_APK_SIG, ""));
            if (isFirstRun) {
                //clear previous working dir
                Amigo.clearWithoutApk(this);

                //start a new process to handle time-tense operation
                ApplicationInfo appInfo = getPackageManager().getApplicationInfo(getPackageName(), GET_META_DATA);
                String layoutName = appInfo.metaData.getString("amigo_layout");
                String themeName = appInfo.metaData.getString("amigo_theme");
                int layoutId = 0;
                int themeId = 0;
                if (!TextUtils.isEmpty(layoutName)) {
                    layoutId = (int) readStaticField(Class.forName(getPackageName() + ".R$layout"), layoutName);
                }
                if (!TextUtils.isEmpty(themeName)) {
                    themeId = (int) readStaticField(Class.forName(getPackageName() + ".R$style"), themeName);
                }
                Log.e(TAG, String.format("layoutName-->%s, themeName-->%s", layoutName, themeName));
                Log.e(TAG, String.format("layoutId-->%d, themeId-->%d", layoutId, themeId));

                ApkReleaser.work(this, layoutId, themeId);
                Log.e(TAG, "release apk once");
            } else {
                checkDexAndSoChecksum();
            }
            //建立一个继承自PathClassLoader的类的对象,把补丁APK的路径传入构造一个加载器
            AmigoClassLoader amigoClassLoader = new AmigoClassLoader(demoAPk.getAbsolutePath(), getRootClassLoader());
            //这个方法是将该app所对应的ActivityThread对象中LoadApk的加载器经过反射的方式替换掉。
            setAPKClassLoader(amigoClassLoader);
            //这个就是准备替换dex的方法
            setDexElements(amigoClassLoader);
            //顾名思义,设置加载本地库
            setNativeLibraryDirectories(amigoClassLoader);
            //下面是加载一些资源文件
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = getDeclaredMethod(AssetManager.class, "addAssetPath", String.class);
            addAssetPath.setAccessible(true);
            addAssetPath.invoke(assetManager, demoAPk.getAbsolutePath());
            setAPKResources(assetManager);

            runOriginalApplication(amigoClassLoader);
        } catch (Exception e) {
            throw new LoadPatchApkException(e);
        }
    }复制代码

在此,咱们先不进入setDexElements(amigoClassLoader)这个方法,先看看设置类加载器的setAPKClassLoader(amigoClassLoader)方法,由于这也是很难忽略的一个关键点,所以,咱们先看看他是怎么设置加载器的框架

private void setAPKClassLoader(ClassLoader classLoader)
            throws IllegalAccessException, NoSuchMethodException, ClassNotFoundException, InvocationTargetException {
        //把getLoadedApk()返回的对象中“mClassLoader”属性替换成咱们刚才本身new的类加载器
        writeField(getLoadedApk(), "mClassLoader", classLoader);
    }复制代码

writeFiled这个方法的主要功能就是经过反射的机制,把咱们的classloader设置到mClassLoader中去,关键是getLoadedApk()究竟是什么鬼?less

private static Object getLoadedApk()
            throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, ClassNotFoundException {
        //instance()返回一个“android.app.ActivityThread”类,readField是读取ActivityThread类中的mPackages属性
        Map<String, WeakReference<Object>> mPackages = (Map<String, WeakReference<Object>>) readField(instance(), "mPackages", true);
        //而这个mPackage属性中包含有一个LoadedApk
        for (String s : mPackages.keySet()) {
            WeakReference wr = mPackages.get(s);
            if (wr != null && wr.get() != null) {
                //最终应该返回了一个LoadedApk
                return wr.get();
            }
        }
        return null;
    }复制代码

好了,最终获得了LoadedApk对象,这个对象其实很重要,一个 apk加载以后全部信息都保存在此对象(好比:DexClassLoader、Resources、Application),一个包对应一个对象,以包名区别,而咱们正好就用咱们本身的类加载器对象替换掉这个LoadedApk对象中的classloader,就能够加载咱们本身的apk了。因为咱们本身的amigoClassLoader实际上继承自PathClassLoader,因此智能加载特定目录下的apk,也就是说,咱们的补丁apk须要放在特定目录下才行。ide

好了,扯了这么远,咱们仍是赶忙回到正题,替换dex实现热修复。继续从setDexElements(amigoClassLoader)往下走

private void setDexElements(ClassLoader classLoader) throws NoSuchFieldException, IllegalAccessException {
        //getPathList这是经过反射的方式去读取BaseDexClassLoader中的pathList对象,这个对象中有一个dexElements数组,包裹了运行的APK中的全部的dex。
        Object dexPathList = getPathList(classLoader);
        //文件目录下,补丁apk的dex文件对象数组
        File[] listFiles = dexDir.listFiles();

        List<File> validDexes = new ArrayList<>();
        for (File listFile : listFiles) {
            if (listFile.getName().endsWith(".dex")) {
                //添加到列表中
                validDexes.add(listFile);
            }
        }
        //建立一个同样大的文件数组
        File[] dexes = validDexes.toArray(new File[validDexes.size()]);
        //经过反射读取dexPathList对象中的本来的dexElements数组对象
        Object originDexElements = readField(dexPathList, "dexElements");
        //返回dexElements数组中元素的类型
        Class<?> localClass = originDexElements.getClass().getComponentType();
        int length = dexes.length;
        //而后根据这个类型建立一个一样大的新数组
        Object dexElements = Array.newInstance(localClass, length);
        for (int k = 0; k < length; k++) {
            为数组赋值
            Array.set(dexElements, k, getElementWithDex(dexes[k], optimizedDir));
        }
        //最后,经过反射的方式把这个新数组放到dexPathList这个对象中去。
        writeField(dexPathList, "dexElements", dexElements);
    }复制代码

好了,如今对于dex的替换基本上完成了,最后是一些重启或者从新运行Application的工做。假如对于BaseDexClassLoader,dexPathList,dexElements这些还不是很清楚,能够看一看我以前的那篇文章热修复初探,里面有相关的介绍。

小结

若是你真的认真看了个人上一篇文章热修复初探的话,你会发现这个框架其实跟我介绍了那种基于dex分包的热修复原理还有一些出入,由于这是总体把全部的dex包的替换掉,也就意味着当须要热修复时,下载的文件要大一些,多是整个apk;其次,这个框架使用的类加载器是PathClassLoader而不是DexClassLoader,原本PathClassLoader是有局限的,由于它只能加载指定的私有路径,而做者经过大量使用了反射的方式,直接替换原来的类加载器,而后经过本身的类加载器来完成整个dex的彻底替换。整体来看,这个框架除了体积较大,优势是不少的。(不过这么使用反射,APP应该很难在Google play中上线吧?)

原本我工做中对于反射基本没用到,因此算不上熟悉,可是如今看来,这玩儿真的很好使啊,由于用这种方式,能够获取不少Android系统不公开的私有API和属性......

卧槽,我决定好好研究反射,我发四。

勘误

暂无

后记

原本还有继续分析其余的热修复框架源码,可是这篇文章的篇幅已经不小了,中场休息,找机会我再把其余的框架源码的实现细节写在新的文章中分享出来

最后是各个热修复框架的性能表(不保证准确)

相关文章
相关标签/搜索