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

980 阅读6分钟

转载请注明出处:juejin.cn/post/684490…

写在开头

本文主要是跟着官方文档以自己的理解,捋一遍 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 学习(二)类和资源是怎么加载的?

记录在此,仅为学习!
感谢您的阅读!欢迎指正!