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

2018-02-27 11:30:29来源:https://www.jianshu.com/p/813e36b321f8作者:GoileoLee人点击

分享



转载请注明出处: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() 传入了


自定义的补丁 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









最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台