1.Instant Run资源热修复
- 构造一个新的AssetManager,并通过反射调用addAssetPath,把这个完整的新资源包加入到AssetManager中。
- 找到所有之前引用到原有AssetManager的地方,通过反射,把引用处替换为AssetManager。
Instant Run需要处理兼容性问题和找到所有AssetManager的引用处,真正是实现逻辑其实很简单。
重点是addAssetPath函数,java层的AssetManager只是个包装,真正实现是C++层的AssetManager。
执行addAssetPath就是解析格式,然后构造出底层数据结构的过程。调用链路是:
public final int addAssetPath(String path)
android_content_AssetManager_addAssetPath
AssetManager::addAssetPath
AssetManager::appendPathToResTable
ResTable::add
ResTable::addInternal
ResTable::parsePackage
class AssetManager : public AAssetManager {
......
mutable ResTable* mResources;
};
class ResTable
{
mutable Mutex mLock;
mutable Mutex mFilteredConfigLock;
status_t mError;
ResTable_config mParams;
// Array of all resource tables.
Vector mHeaders;
// Array of packages in all resource tables.
Vector mPackageGroups;
// Mapping from resource package IDs to indices into the internal
// package array.
uint8_t mPackageMap[256];
uint8_t mNextPackageId;
};
通过传入的资源包路径,先得到其中的resources.arsc,然后解析它的格式,存放在底层的AssetManager的mResources中。
一个android进程只包含一个ResTable,mPackageGroups就是所有解析过的资源包的集合。resources.arsc记录了所有资源的id分配情况以及资源中的所有字符串。底层的AssetManager做的事就是解析resources.arsc把相关信息存储到mPackageGroups里。
2.资源格式简介
资源编号是一个32位数字,用16进制表示就是0xPPTTEEEE。PP为package id,TT为type id,EEEE为entry id。
例如activity_main.xml这个资源编号是0x7f040019。它的package id是0x7f,资源类型id为0x04,Type String Pool里的第四个字符串正是layout类型,而0x04类型的第0x0019个资源项就是activity_main这个资源。
3.运行时资源的解析
appt打包,其资源包的package id就是0x7f。
系统资源包framework-res.apk,package id为0x01。
在走到app的第一行代码之前,系统已经帮我们构造好了一个AssetManager,包含系统资源和apk安装包资源。
如果直接调用addAssetPath加载补丁包,补丁包的package id也是0x7f,同一个package id的包被加载两次。
- android L之后:会把后来的包添加到之前的包的同一个PackageGroup下面。get的时候从第一个type开始遍历,也就是说会先取得安装包的资源,然后才是补丁包的。会造成补丁无效。
- android 4.4及以下:addAssetPath只是把补丁包的路径添加到mAssetPath中,而真正解析资源包的逻辑在app第一次执行AssetManager::getResTable的时候。而在执行加载补丁代码之前,getResTable可能已执行过无数次了。
4.另辟蹊跷的资源修复
如果不采用类似Instant Run的方案,市面上许多实现,是自己修改aapt,在打包时将补丁包资源进行重新编号。这样涉及修改SDK工具包,不利于集成和aapt升级。
我们可以构造一个package id为0x66的资源包,只包含改变了的资源项,然后直接在原有的AssetManager中addAssetPath这个包。
资源的改变包含增加、减少、修改:
- 新增资源:直接加入补丁包,新代码里直接引用即可。
- 减少资源:不使用它就行了,因此不考虑这种情况。
- 修改资源:视为新增资源,在打入补丁的时候,代码在引用处也会做相应修改,把原来使用的旧资源id变为新id。
绿线表示新增资源;红线表示修改的资源;黑线表示内容没有变化,但id发生改变的资源;x表示删除了的资源。
问题1:新增的资源及其导致id偏移
新增的资源导致了它们所属的type中跟在它们之后的资源id发生了位移。新资源插入的位置是随机的,与每次aapt打包解析xml的顺序有关。发生位移的资源不会加入patch,但是在patch的代码中会调整id的引用处。
imageView.setImageResource(R.drawble.holo_light)
//R.drawble.holo_light是一个int值
imageView.setImageResource(0x7f020002)
holo_light的图片内容没有变,代码引用处也没变。但是新资源的插入导致id改变,R.drawble.holo_light已变为如下,因此做差异对比前,需要把代码里修正回0x7f020002。
imageView.setImageResource(0x7f020003)
问题2:内容发生改变的资源
修改了文件内容,会被加入到patch中,并重新编号为新id。
setContentView(R.layout.activity_main);
//实际就是
setContentView(0x7f030000);
把新包里的这行代码变为0x66的id,相应的代码修复会在运行时发生,这样就引用到了正确的新资源。
setContentView(0x66020000);
问题3:对type的影响
上图由于type 0x01的所有资源项都没有变化,所以整个type 0x01都没有加入到patch中。这也使得后面的type的id都往前移了一位。因此Type String Pool中的字符串也要进行修正,这样才能使得0x01的type指向drawable,而不是原来的attr。
5.更优雅地替换AssetManager
Instant Run做了一大堆兼容版本和反射替换工作。
0x66 patch包方案,对于android L之后的版本,直接在原有AssetManager上应用patch就行了。但android KK及以下,addAssetPath不会加载资源,必须重新构造一个新的AssetManager并加入patch,再换掉原来的。
其实AssetManager的源码里,有一个destroy函数来销毁native层的AssetManager并释放资源。
framework/base/core/java/android/content/res/AssetManager.java
public final class AssetManager {
private native final void destroy();
}
frameworks/base/core/jni/android_util_AssetManager.cpp
static void android_content_AssetManager_destroy(JNIEnv* env, jobject clazz)
{
AssetManager* am = (AssetManager*)
(env->GetLongField(clazz, gAssetManagerOffsets.mObject));
ALOGV("Destroying AssetManager %p for Java object %p\n", am, clazz);
if (am != NULL) {
delete am;
env->SetLongField(clazz, gAssetManagerOffsets.mObject, 0);
}
}
首先它析构了native层的AssetManager,然后把java层的AssetManager对native层的AssetManager的引用设为空。
native层的AssetManager的析构函数会析构它的所有成员,这样就会释放之前加载了的资源。
frameworks/base/libs/androidfw/AssetManager.cpp
AssetManager::~AssetManager(void)
{
int count = android_atomic_dec(&gCount);
//ALOGI("Destroying AssetManager in %p #%d\n", this, count);
delete mConfig;
delete mResources;
// don't have a String class yet, so make sure we clean up
delete[] mLocale;
delete[] mVendor;
}
而现在java层的AssetManager已经成为了空壳,我们可以调用它的init方法,对它重新进行初始化。
执行init,会在native层创建一个没有添加过资源并且mResources没有初始化的AssetManager。然后我们再对它进行addAssetPath,之后由于mResources没有初始化过,就可以正常走到解析mResources的逻辑,加载所有此时add进去的资源了。
其中还要注意mStringBlocks,它记录了之前加载过的所有资源包的String Pool,要调用ensureStringBlocks()进行重新构造,不然后面使用它时会导致崩溃。