转载请注明出处:https://juejin.im/post/5a712b696fb9a01cb74eacd6

写在开头

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

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

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

怎么实现的

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

在 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 apkif (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 processattachPatchApk(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 startupclear(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 学习(二)类和资源是怎么热更的?相关推荐

  1. s3c2440芯片累加汇编语言,S3C2440学习二(基础资源的使用)

    ②大概有多少资源模块? ARM处理器共有37个寄存器:1) 31个通用寄存器,包括程序计数器(PC).这些寄存器都是32位的:2)6个状态寄存器.这些寄存器也是32位的,但是只使用了其中的12位. S ...

  2. 实现iOS图片等资源文件的热更新化(二):自定义的动态 imageNamed

    这篇文章,要解决的是,使用一个自定义的 imageNamed 函数来替代系统的 imageNamed 函数.内部逻辑,将贯穿对比论证 关于"合适"的图片的定义.对iOS加载图片的规 ...

  3. Amigo学习(一)解决使用中遇到的问题

    写在开头 上一篇博文中对比了当下比较热门的热修复框架,并看到了Tinker 框架Demo 的展现过程.Tinker 的开源时间较晚,功能和文档更加完善.但不能对四大组件进行热更也是比较遗憾.不过我们发 ...

  4. cs231n 课程学习 二

    cs231n 课程学习 二 cs231n 课程资源:Stanford University CS231n: Convolutional Neural Networks for Visual Recog ...

  5. Docker学习二:Docker镜像与容器

    前言 本次学习来自于datawhale组队学习: 教程地址为: https://github.com/datawhalechina/team-learning-program/tree/master/ ...

  6. (转)MyBatis框架的学习(二)——MyBatis架构与入门

    http://blog.csdn.net/yerenyuan_pku/article/details/71699515 MyBatis框架的架构 MyBatis框架的架构如下图:  下面作简要概述: ...

  7. java从零开始系统性学习完整超全资源+笔记(还在更新中)

    java从零开始系统性学习完整超全资源+笔记(还在更新中) 前言 资源来自:王汉远java基础, B站视频: https://www.bilibili.com/video/BV1b4411g7bj/? ...

  8. 个人主页类经济学资源概览

    个人主页类经济学资源概览 这里选定的一些经济学者的个人主页,既有较大的信息量,又有一定的学术参考价值.当然也在一定程度上反映了笔者的研究偏好. 1. http://www.people.hbs.edu ...

  9. 2020-用多通道卷积神经网络学习单类特征用于人脸表现攻击检测

    2020年,Anjith George等人,期刊:TIFS,CCFA刊,Learning One Class Representations for Face Presentation Attack ...

最新文章

  1. 布局自动驾驶L3级,探访北汽福田发动机生产基地!
  2. c 语言文字输出函数,c/c++语言中文字输出函数总结
  3. WPF 触发器Triggers
  4. 在选择屏幕的标准应用工具条上增加自定义按钮
  5. go 自定义error怎么判断是否相等_Go Web 小技巧(二)GORM 使用自定义类型
  6. Spark没有读取HDFS文件的方法?那textFile是怎么读的?
  7. Webpack 2 视频教程 002 - NodeJS 安装与配置
  8. Oracle常见错误
  9. 【mysql基础知识】查询当前时间之前5分钟内的数据
  10. 吐槽:Lambda表达式
  11. 第三届人本沙龙12月活动小结
  12. 关于Scala和面向对象的函数式编程
  13. 详解 inner join with another 'dataframe' df1.join(df2, $df1Key === $df2Key)
  14. Winodws10 system进程占用磁盘100%
  15. Linux之RPM包的命名规则和包的依赖性
  16. anconda各个版本下载
  17. 【应用安全】什么是身份和访问管理 (IAM)?
  18. 【NodeJs-5天学习】第四天存储篇④ ——基于MQTT的环境温度检测,升级存储为mysql
  19. 我的成神之路!Python 兵器谱(绝世神兵!收藏必备!)
  20. FPGA中的分频器-偶数分频

热门文章

  1. 什么是数据结构?什么是算法
  2. FND Debug Log FND LOG MESSAGES
  3. Mathmatica 与 VS2008 链接建立问题:NETLink与MathLink
  4. 基于python的证件照_不到20行实现Python代码即可制作精美证件照
  5. 代码风格自动化(二)——husky + lint-staged
  6. js和ts两种 将 小写金额转中文大写汉字,阿拉伯数字金额格式化成中文大写汉字,数字金额转换成财务发票大写中文
  7. 计算机英语邓广慧,地方高校理工类大学生专业英语教学改革探讨
  8. easyUI datagrid editor扩展dialog
  9. 基于深度学习的车辆信息识别(一):车辆颜色识别
  10. Spark SQL的selectExpr用法