美文网首页Android开发经验谈Android开发Android技术知识
Amigo 学习(二)类和资源是怎么热更的?

Amigo 学习(二)类和资源是怎么热更的?

作者: GoileoLee | 来源:发表于2018-02-07 14:25 被阅读133次

    转载请注明出处:https://www.jianshu.com/p/813e36b321f8

    写在开头

    本文主要是跟着官方文档以自己的理解,捋一遍 Amigo 的流程。
    在 GitHub 上 Amigo 的 Wiki 中,How it works 分为三个大的步骤:

    • 检查补丁包
    • 释放 Apk
      • 释放 Dex 到指定目录
      • 拷贝 So 文件到 Amigo 的指定目录
      • 优化 Dex 文件
    • 替换修复
      • 替换 ClassLoader
      • 替换 Dex
      • 替换动态链接库
      • 替换资源文件
      • 替换原有 Application
      • Amigo 插件

    官方文档讲解的都是精华部分、核心部分。
    而这里我们按照 Amigo 一次成功修复的流程来学习它。

    怎么实现的

    通过学习源码发现,替换用户的 Application 是 Amigo 的第一步,因为它在编译的时候就完成了替换工作。

    AmigoPlugin.groovy

    在 buildSrc/src/main/groovy/me.ele.amigo/AmigoPlugin.groovy 脚本文件中完成了替换原有 Application 的工作。

    1. 编译时替换 Application

    me.ele.amigo.AmigoPlugin.groovy

    manifestFile = output.processManifest.manifestOutputFile
    //fake original application as an activity, so it will be in main dex
    Node node = (new XmlParser()).parse(manifestFile)
    Node appNode = null
    for (Node n : node.children()) {
        if (n.name().equals("application")) {
        appNode = n;
        break
        }
    }
    QName nameAttr = new QName("http://schemas.android.com/apk/res/android", 'name', 'android');
    applicationName = appNode.attribute(nameAttr)
    if (applicationName == null || applicationName.isEmpty()) {
        applicationName = "android.app.Application"
    }
    // 将原来的 Application 替换成 Amigo
    appNode.attributes().put(nameAttr, "me.ele.amigo.Amigo")
    // new 一个 Node,将原来的 Application 设置为 Activity,以保证其一定会在主 dex 中。
    Node hackAppNode = new Node(appNode, "activity")
    hackAppNode.attributes().put("android:name", applicationName)
    manifestFile.bytes = XmlUtil.serialize(node).getBytes("UTF-8")
    

    而Amigo 框架最核心的代码都在 Amigo.java 中,我们接下来看看 Amigo.java 中都做了哪些事情。

    2. 核心类 Amigo.java

    核心方法 attachBaseContext() --> attachApplication()

    public void attachApplication() {
        try {
            String workingChecksum = PatchInfoUtil.getWorkingChecksum(this);
            Log.e(TAG, "#attachApplication: working checksum = " + workingChecksum);
            if (TextUtils.isEmpty(workingChecksum)
                    || !PatchApks.getInstance(this).exists(workingChecksum)) {
                Log.d(TAG, "#attachApplication: Patch apk doesn't exists");
                PatchCleaner.clearPatchIfInMainProcess(this);
                attachOriginalApplication();
                return;
            }
            if (PatchChecker.checkUpgrade(this)) {
                Log.d(TAG, "#attachApplication: Host app has upgrade");
                PatchCleaner.clearPatchIfInMainProcess(this);
                attachOriginalApplication();
                return;
            }
            // ensure load dex process always run host apk not patch apk
            if (ProcessUtils.isLoadDexProcess(this)) {
                Log.e(TAG, "#attachApplication: load dex process");
                attachOriginalApplication();
                return;
            }
            if (!ProcessUtils.isMainProcess(this) && isPatchApkFirstRun(workingChecksum)) {
                Log.e(TAG,
                        "#attachApplication: None main process and patch apk is not released yet");
                attachOriginalApplication();
                return;
            }
            
            // only release loaded apk in the main process
            attachPatchApk(workingChecksum);
        } catch (LoadPatchApkException e) {
            e.printStackTrace();
            loadPatchError = LoadPatchError.record(LoadPatchError.LOAD_ERR, e);
            //if patch apk fails to run, Amigo will clear working dir with app's next startup
            clear(this);
            try {
                attachOriginalApplication();
            } catch (Throwable e2) {
                throw new RuntimeException(e2);
            }
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
    

    主要是做一些判断,判断校验和是否为空;判断补丁包是否需要更新;判断当前是否运行在主线程中;判断补丁包是否第一次运行;
    当条件都满足时,执行 attachPatchApk(),加载补丁包。
    否则,执行 attachOriginalApplication(),将 Application 类替换回到以前的类。(此时的 Application 类是 Amigo)。

    这里的检验和 workingChecksum 是什么?
    利用 CRC32 生成的一串 long 型的数值。
    CRC32 —— CRC32会把字符串,生成一个long长整形的唯一性ID(虽然科学证明不绝对唯一,但是还是可用的)。

    attachPatchApk() 是重点

    private void attachPatchApk(String checksum) throws LoadPatchApkException {
        try {
            if (isPatchApkFirstRun(checksum) || !AmigoDirs.getInstance(this).isOptedDexExists(checksum)) {
                PatchInfoUtil.updateDexFileOptStatus(this, checksum, false);
                releasePatchApk(checksum);
            } else {
                PatchChecker.checkDexAndSo(this, checksum);
            }
            setAPKClassLoader(AmigoClassLoader.newInstance(this, checksum));
            setApkResource(checksum);
            revertBitFlag |= getClassLoader() instanceof AmigoClassLoader ? 1 : 0;
            attachPatchedApplication(checksum);
            PatchCleaner.clearOldPatches(this, checksum);
            shouldHookAmAndPm = true;
            Log.i(TAG, "#attachPatchApk: success");
        } catch (Exception e) {
            throw new LoadPatchApkException(e);
        }
    }
    

    判断是否第一次运行补丁包;判断 dex 文件夹是否创建。
    满足条件就存入状态,并释放补丁包,加载布局和主题文件。
    否则,检查补丁包中 dex 和 so 文件的校验和。
    接下来是设置补丁包的 ClassLoader 和 Resource 对象及attachPatchedApplication()。

    3. 类加载器 AmigoClassloader

    private void setAPKClassLoader(ClassLoader classLoader) throws Exception {
        writeField(getLoadedApk(), "mClassLoader", classLoader);
    }
    

    这个方法里面只有一行代码

    writeField() 是对反射的字段进行写操作的封装,第一个参数为需要反射的类的对象,第二个参数为需要反射的字段名,第三个参数为写入的值,即所赋的值。

    • 那么,这里是反射替换了什么类的 classLoader 对象呢?

    继续看 getLoadedApk().

    private static Object getLoadedApk() throws Exception {
        @SuppressWarnings("unchecked")
        Map<String, WeakReference<Object>> mPackages =
                (Map<String, WeakReference<Object>>) readField(instance(), "mPackages", true);
        for (String s : mPackages.keySet()) {
            WeakReference wr = mPackages.get(s);
            if (wr != null && wr.get() != null) {
                return wr.get();
            }
        }
        return null;
    }
    

    然后反射对象是 instance()

    sActivityThread = MethodUtils.invokeStaticMethod(clazz(), "currentActivityThread");  
    

    再是 clazz()

    sClass = Class.forName("android.app.ActivityThread");
    

    好了~ 可见 instance() 中调用了 ActivityThread 类的 currentActivityThread()。
    接着 getLoadedApk() 中反射获取了 mPackages 属性的值。我们看一下 mpackages 是什么类型

    final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<String, WeakReference<LoadedApk>>();
    

    回过头来,再看 getLoadedApk()
    返回的是一个 Object 对象,但其实这个对象本质是 LoadedApk 类型。

    LoadedApk 是什么?看官方的注释

    Local state maintained about a currently loaded .apk.

    本地状态保持关于当前加载的 .apk 。
    就是当前加载的 apk 文件的信息管理类。从源码中的命名 packageInfo 也能看出来。

    那最后再回到 setAPKClassLoader(ClassLoader classLoader),可以看到是传入了一个 classLoader,通过反射赋值到 .apk 文件的信息管理类 LoadedApk 中的类加载器对象,也就是加载这个 .apk 文件的 ClassLoader 类的对象。

    • 那传入的这个 classLoader 对象是怎么来的?
    public class AmigoClassLoader extends DexClassLoader {
    
        ...
        
        public AmigoClassLoader(String patchApkPath, String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
            super(dexPath, optimizedDirectory, libraryPath, parent);
            try {
                patchApk = new File(patchApkPath);
                zipFile = new ZipFile(patchApkPath);
            } catch (IOException e) {
                e.printStackTrace();
                zipFile = null;
            }
        }
        
        public static AmigoClassLoader newInstance(Context context, String checksum) {
            return new AmigoClassLoader(PatchApks.getInstance(context).patchPath(checksum),
                    getDexPath(context, checksum),
                    AmigoDirs.getInstance(context).dexOptDir(checksum).getAbsolutePath(),
                    getLibraryPath(context, checksum),
                    AmigoClassLoader.class.getClassLoader().getParent());
        }
        
        ...
    

    AmigoClassLoader 继承了 DexClassLoader,调用了 super() 传入了

    1. 自定义的补丁 dex 地址;
    2. dex 解压缩后存放的目录;
    3. C/C++ 依赖的本地库文件目录;
    4. 上一级的类加载器;

    小结:通过继承 DexClassLoader 自定义的 ClassLoader,替换当前 ActivityThread 中的 Apk 包信息里的类加载器,以实现加载补丁包的目的。

    4. 补丁资源加载 PatchResourceLoader

    private void setApkResource(String checksum) throws Exception {
        PatchResourceLoader.loadPatchResources(this, checksum);
        Log.i(TAG, "hook Resources success");
    }
    

    处理补丁包资源加载的类 PatchResourceLoader

    static void loadPatchResources(Context context, String checksum) throws Exception {
        AssetManager newAssetManager = AssetManager.class.newInstance();
        invokeMethod(newAssetManager, "addAssetPath", PatchApks.getInstance(context).patchPath(checksum));
        invokeMethod(newAssetManager, "ensureStringBlocks");
        replaceAssetManager(context, newAssetManager);
    }
    

    loadPatchResources() 中先是实例化了一个 AssetManager 对象,又调用了三个方法。
    第一个方法,通过反射调用 addAssetPath 添加 /sdcard 上补丁包的新资源。
    第二个方法,通过源码发现,是确保 mStringBlocks 对象不为 null。

    /*package*/ final void ensureStringBlocks() {
        if (mStringBlocks == null) {
            synchronized (this) {
                if (mStringBlocks == null) {
                    makeStringBlocks(sSystem.mStringBlocks);
                }
            }
        }
    }
    

    那为什么要反射这个方法?兼容 Android 4.4。在网上找到了这样的注释,这句话的核心是,“do it”,大致意思是,“写上它就是了”...

    // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
    // in L, so we do it unconditionally.
    

    第三个方法,得到 Resources 的弱引用集合,把他们的 AssetManager 成员替换成 newAssetManager。代码较多,就不贴出来了,自行去看 PatchResourceLoader.java 文件吧。

    写在后头

    本想一篇文章写完核心类Amigo分析、类加载、资源加载、so 文件加载、四大组件修复实现原理及回到项目的 Application。但写完前三个就感觉篇幅有点长了,后面的东西又不能用三言两语能够说清楚。那就到此分篇吧,下一篇再接着写。

    如果文中有没有讲明白的地方,或者是错误之处,烦请指出,笔者一定立即更正。

    推荐阅读:Amigo学习(一)解决使用中遇到的问题
    Amigo 学习(二)类和资源是怎么热更的?

    记录在此,仅为学习!
    感谢您的阅读!欢迎指正!
    欢迎加入 Android 技术交流群,群号:155495090

    相关文章

      网友评论

        本文标题:Amigo 学习(二)类和资源是怎么热更的?

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