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

转载请注明出处: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

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