热修复原理3:资源

1.Instant Run资源热修复

  1. 构造一个新的AssetManager,并通过反射调用addAssetPath,把这个完整的新资源包加入到AssetManager中。
  2. 找到所有之前引用到原有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()进行重新构造,不然后面使用它时会导致崩溃。

你可能感兴趣的:(热修复原理3:资源)