写在开头
本文主要是跟着官方文档以自己的理解,捋一遍 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 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() 传入了
- 自定义的补丁 dex 地址;
- dex 解压缩后存放的目录;
- C/C++ 依赖的本地库文件目录;
- 上一级的类加载器;
小结:通过继承 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