实现Android插件化的核心技术(二):Android dynamic load resources



Android resources are loaded by AssetManager.

安卓资源由AssetManager加载。

AssetManager instance and add resources search paths by addAssetPath.

By default, it adds:

应用启动时,系统会为其创建一个AssetManager实例,并由addAssetPath方法添加资源搜索路径,默认添加:

  1. "/framework/base.apk" - Android base resources (base)
  2. "/data/app/*.apk" - The launching apk resources (host)

As we know, android resources are accessed by an unique index with format 0xPPTTNNNN.

所有的资源需要通过一个唯一的id来访问

  • PP - the resource package id (00-02: system, 7f: application, 03-7e: reserved)
  • TT - the resource type id (types are 'attr', 'layout', 'string' and etc.)
  • NNNN - the resource entry id

To avoid id conflicts, the resources ids in host and plugins should be partitioned.

为避免id冲突,宿主以及各个插件之间的资源id需要分段处理。

There are some ways to do this:

  1. Create a self-contained asset manager for each plugin. 

插件独立使用一个资源管理器

  • ❌Plugins can not access resource to each other. 

插件间无法访问资源

  1. Add all plugins' asset path to host's asset manager. 

添加所有资源路径到宿主

  1. Arrange the entry ids (NNNN) by public.xml 

分配NN段

  • ⚠️ High cost of maintenance. 字段有限,不好维护
  1. Arrange the type ids (TT) by public.xml 

分配TT段

  • ❌Plugins can not access resource to each other. 插件间无法访问资源
  1. Arrange the package ids (PP) 

分配PP段

  1. Modify aapt source code 修改aapt源码

    • ⚠️ High cost of maintenance. 不好维护
  2. Modify the products of aapt (binary resources.arsc and *.xml) 修改aapt生成产物

    • ✅Seamless, and the ability of ultimate slicing. 无缝连接,支持极致剪裁

Now, out final plan is to repack android asset package and reset package id of it's resources.

我们的最终方案是对资源包进行重新打包,重设资源id

Repack android asset package

First of all, unzip the package file resources.ap_, get files:

首先,解压resources.ap_文件,可以得到:

AndroidManifest.xml
resources.arsc
res
  `-- layout
       `-- activity_main.xml

They are all binary files, data structs are in ResourcesType.h

这些文件都是二进制的,数据结构参见ResourcesType.h

Repack resources.arsc

chunk

type

note

Table header

RES_TABLE_TYPE = 0x002

 

Resource strings

RES_STRING_POOL_TYPE = 0x001

e.g. 'Hello World!'

Package header

RES_TABLE_PACKAGE_TYPE = 0x200

Rewrite entry: package id

Type strings

RES_STRING_POOL_TYPE = 0x001

e.g. 'attr', 'layout', etc.

Key strings

RES_STRING_POOL_TYPE = 0x001

e.g. 'activity_main', etc.

DynamicRefTable

RES_TABLE_LIBRARY_TYPE = 0x0203

Insert entry for Android 5.0+

Type spec

RES_TABLE_TYPE_SPEC_TYPE = 0x0202

 

Type info

RES_TABLE_TYPE_TYPE = 0x0201

Rewrite entry: resource entry value

  1. Reset package id
  2. Filter types
  3. Dynamic package reference
    In Android 5.0+ needs to set the dynamicRefTable for lookup bag parent.

5.0以上需要对二进制文件加入“资源包id映射”数据段,以使得能正确查找到主题等bag的parent。

  1. What's the bag and bag parent?
    For example, we made a plugin with package id 0x34, and defined themes in it's styles.xml:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">..</style>
<style name="AppTheme.NoActionBar">..</style>

And then after aapt executed, we got a bag tree as following: (run aapt d --values resources *.apk to see more)

Theme.AppCompat.Light.DarkActionBar
    `-- AppTheme <bag> (id: 0x34050001)
          `-- AppTheme.NoActionBar <bag> (id: 0x34050000)

The AppTheme.NoActionBar's parent is AppTheme, but in ResourcesType.cpp, If has not pre-define dynamicRefTable (declare what 0x34 is), while looking up the parent ofAppTheme.NoActionBar (0x34050000) it would abort and never find AppTheme andTheme.AppCompat.Light.DarkActionBar.

The result is that, if your activity is an instance of AppCompatActivity but cannot apply theAppCompat Theme, then a crash raised. Let's see the codes:

ssize_t ResTable::getBagLocked(uint32_t resID, const bag_entry** outBag,
        uint32_t* outTypeSpecFlags) const
{
    ...
        bag_entry* cur = entries+curEntry;

        cur->stringBlock = entry.package->header->index;
        cur->map.name.ident = newName;
        cur->map.value.copyFrom_dtoh(map->value);
        status_t err = grp->dynamicRefTable.lookupResourceValue(&cur->map.value);
        if (err != NO_ERROR) {
            ALOGE("Reference item(0x%08x) in bag could not be resolved.", cur->map.value.data);
            return UNKNOWN_ERROR;
        }
    ...
}
status_t DynamicRefTable::lookupResourceValue(Res_value* value) const {
    ...
    status_t err = lookupResourceId(&value->data);
    if (err != NO_ERROR) {
        return err;
    }
    ...
}
status_t DynamicRefTable::lookupResourceId(uint32_t* resId) const {
    uint32_t res = *resId;
    size_t packageId = Res_GETPACKAGE(res) + 1;

    if (packageId == APP_PACKAGE_ID) {
        // No lookup needs to be done, app package IDs are absolute.
        return NO_ERROR;
    }

    if (packageId == 0) {
        // The package ID is 0x00. That means that a shared library is accessing
        // its own local resource, so we fix up the resource with the calling
        // package ID.
        *resId |= ((uint32_t) mAssignedPackageId) << 24;
        return NO_ERROR;
    }

    // Do a proper lookup.
    uint8_t translatedId = mLookupTable[packageId];
    if (translatedId == 0) {
        //--------------------------------------------------------
        // Lookup Code Block
        // [0x03, 0x7e] goes here
        //--------------------------------------------------------
        ALOGV("DynamicRefTable(0x%02x): No mapping for build-time package ID 0x%02x.",
                (uint8_t)mAssignedPackageId, (uint8_t)packageId);
        for (size_t i = 0; i < 256; i++) {
            if (mLookupTable[i] != 0) {
                ALOGV("e[0x%02x] -> 0x%02x", (uint8_t)i, mLookupTable[i]);
            }
        }
        return UNKNOWN_ERROR;
    }

    *resId = (res & 0x00ffffff) | (((uint32_t) translatedId) << 24);
    return NO_ERROR;
}
Repack *.xml

chunk

type

note

Xml header

RES_XML_TYPE = 0x0003

 

Attr ids

RES_XML_RESOURCE_MAP_TYPE = 0x180

Rewrite entry: each attr id

Xml node

RES_XML_START_ELEMENT_TYPE = 0x102

Rewrite entry: node attribute value



实现Android插件化的核心技术(二):Android dynamic load resources



Android resources are loaded by AssetManager.

安卓资源由AssetManager加载。

AssetManager instance and add resources search paths by addAssetPath.

By default, it adds:

应用启动时,系统会为其创建一个AssetManager实例,并由addAssetPath方法添加资源搜索路径,默认添加:

  1. "/framework/base.apk" - Android base resources (base)
  2. "/data/app/*.apk" - The launching apk resources (host)

As we know, android resources are accessed by an unique index with format 0xPPTTNNNN.

所有的资源需要通过一个唯一的id来访问

  • PP - the resource package id (00-02: system, 7f: application, 03-7e: reserved)
  • TT - the resource type id (types are 'attr', 'layout', 'string' and etc.)
  • NNNN - the resource entry id

To avoid id conflicts, the resources ids in host and plugins should be partitioned.

为避免id冲突,宿主以及各个插件之间的资源id需要分段处理。

There are some ways to do this:

  1. Create a self-contained asset manager for each plugin. 

插件独立使用一个资源管理器

  • ❌Plugins can not access resource to each other. 

插件间无法访问资源

  1. Add all plugins' asset path to host's asset manager. 

添加所有资源路径到宿主

  1. Arrange the entry ids (NNNN) by public.xml 

分配NN段

  • ⚠️ High cost of maintenance. 

字段有限,不好维护

  1. Arrange the type ids (TT) by public.xml 

分配TT段

  • ❌Plugins can not access resource to each other. 

插件间无法访问资源

  1. Arrange the package ids (PP) 

分配PP段

  1. Modify aapt source code 

修改aapt源码

  • ⚠️ High cost of maintenance. 不好维护
  1. Modify the products of aapt (binary resources.arsc and *.xml) 

修改aapt生成产物

  • ✅Seamless, and the ability of ultimate slicing. 无缝连接,支持极致剪裁

Now, out final plan is to repack android asset package and reset package id of it's resources.

我们的最终方案是对资源包进行重新打包,重设资源id

Repack android asset package

First of all, unzip the package file resources.ap_, get files:

首先,解压resources.ap_文件,可以得到:

AndroidManifest.xml
resources.arsc
res
  `-- layout
       `-- activity_main.xml

They are all binary files, data structs are in ResourcesType.h

这些文件都是二进制的,数据结构参见ResourcesType.h

Repack resources.arsc

chunk

type

note

Table header

RES_TABLE_TYPE = 0x002

 

Resource strings

RES_STRING_POOL_TYPE = 0x001

e.g. 'Hello World!'

Package header

RES_TABLE_PACKAGE_TYPE = 0x200

Rewrite entry: package id

Type strings

RES_STRING_POOL_TYPE = 0x001

e.g. 'attr', 'layout', etc.

Key strings

RES_STRING_POOL_TYPE = 0x001

e.g. 'activity_main', etc.

DynamicRefTable

RES_TABLE_LIBRARY_TYPE = 0x0203

Insert entry for Android 5.0+

Type spec

RES_TABLE_TYPE_SPEC_TYPE = 0x0202

 

Type info

RES_TABLE_TYPE_TYPE = 0x0201

Rewrite entry: resource entry value

  1. Reset package id
  2. Filter types
  3. Dynamic package reference
    In Android 5.0+ needs to set the dynamicRefTable for lookup bag parent.

5.0以上需要对二进制文件加入“资源包id映射”数据段,以使得能正确查找到主题等bag的parent。

  1. What's the bag and bag parent?
    For example, we made a plugin with package id 0x34, and defined themes in it's styles.xml:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">..</style>
<style name="AppTheme.NoActionBar">..</style>

And then after aapt executed, we got a bag tree as following: (run aapt d --values resources *.apk to see more)

Theme.AppCompat.Light.DarkActionBar
    `-- AppTheme <bag> (id: 0x34050001)
          `-- AppTheme.NoActionBar <bag> (id: 0x34050000)

The AppTheme.NoActionBar's parent is AppTheme, but in ResourcesType.cpp, If has not pre-define dynamicRefTable (declare what 0x34 is), while looking up the parent ofAppTheme.NoActionBar (0x34050000) it would abort and never find AppTheme andTheme.AppCompat.Light.DarkActionBar.

The result is that, if your activity is an instance of AppCompatActivity but cannot apply theAppCompat Theme, then a crash raised. Let's see the codes:

ssize_t ResTable::getBagLocked(uint32_t resID, const bag_entry** outBag,
        uint32_t* outTypeSpecFlags) const
{
    ...
        bag_entry* cur = entries+curEntry;

        cur->stringBlock = entry.package->header->index;
        cur->map.name.ident = newName;
        cur->map.value.copyFrom_dtoh(map->value);
        status_t err = grp->dynamicRefTable.lookupResourceValue(&cur->map.value);
        if (err != NO_ERROR) {
            ALOGE("Reference item(0x%08x) in bag could not be resolved.", cur->map.value.data);
            return UNKNOWN_ERROR;
        }
    ...
}
status_t DynamicRefTable::lookupResourceValue(Res_value* value) const {
    ...
    status_t err = lookupResourceId(&value->data);
    if (err != NO_ERROR) {
        return err;
    }
    ...
}
status_t DynamicRefTable::lookupResourceId(uint32_t* resId) const {
    uint32_t res = *resId;
    size_t packageId = Res_GETPACKAGE(res) + 1;

    if (packageId == APP_PACKAGE_ID) {
        // No lookup needs to be done, app package IDs are absolute.
        return NO_ERROR;
    }

    if (packageId == 0) {
        // The package ID is 0x00. That means that a shared library is accessing
        // its own local resource, so we fix up the resource with the calling
        // package ID.
        *resId |= ((uint32_t) mAssignedPackageId) << 24;
        return NO_ERROR;
    }

    // Do a proper lookup.
    uint8_t translatedId = mLookupTable[packageId];
    if (translatedId == 0) {
        //--------------------------------------------------------
        // Lookup Code Block
        // [0x03, 0x7e] goes here
        //--------------------------------------------------------
        ALOGV("DynamicRefTable(0x%02x): No mapping for build-time package ID 0x%02x.",
                (uint8_t)mAssignedPackageId, (uint8_t)packageId);
        for (size_t i = 0; i < 256; i++) {
            if (mLookupTable[i] != 0) {
                ALOGV("e[0x%02x] -> 0x%02x", (uint8_t)i, mLookupTable[i]);
            }
        }
        return UNKNOWN_ERROR;
    }

    *resId = (res & 0x00ffffff) | (((uint32_t) translatedId) << 24);
    return NO_ERROR;
}
Repack *.xml

chunk

type

note

Xml header

RES_XML_TYPE = 0x0003

 

Attr ids

RES_XML_RESOURCE_MAP_TYPE = 0x180

Rewrite entry: each attr id

Xml node

RES_XML_START_ELEMENT_TYPE = 0x102

Rewrite entry: node attribute value