resource.arsc解析之 Dynamic package reference

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 加载Theme出错这是一篇补充文章,在做动态替换resId的过程中,我发现bag类型的ResTable_entry在使用过程中存在问题。比如style,其parent解析一直有问题

1、加载Theme出错


这是一篇补充文章,在做动态替换resId的过程中,我发现bag类型的ResTable_entry在使用过程中存在问题。比如style,其parent解析一直有问题,日志如下:

W/ResourceType: Failed resolving bag parent id 0x7d090062

W/ResourceType: Attempt to retrieve bag 0x7d090114 which is invalid or in a cycle.


经过一些粗暴的尝试,发现解决不了问题,看来需要祭出绝招了——看源码。

因为是与theme有关,所以我们追踪theme是怎么加载出来的,过程不说了,最后追踪到AssetManager.applyThemeStyle(),这个函数则是一个native函数,这就需要去底层代码看,源码地址:


android_frameworks_base/android_util_AssetManager.cpp at master · hushnymous/android_frameworks_base · GitHub


在源码里可以看到引用了ResourceType,这个源码的地址:

github.com/aosp-mirror…

在ResourceType.cpp源码中通过搜索上面日志里的内容,我们找到了如下代码:


ssize_t ResTable::getBagLocked(uint32_t resID, const bag_entry** outBag,
uint32_t* outTypeSpecFlags) const
{
    ...
    status_t err = grp->dynamicRefTable.lookupResourceId(&resolvedParent);
    if (err != NO_ERROR) {
        ALOGE("Failed resolving bag parent id 0x%08x", parent);
        return UNKNOWN_ERROR;
    }
    ...
}
复制代码


可以看到关键函数是lookupResourceId,如果这个函数返回的不是NO_ERROR就会报错。那么来看这个函数的源码:


status_t DynamicRefTable::lookupResourceId(uint32_t* resId) const {
    uint32_t res = *resId;
    size_t packageId = Res_GETPACKAGE(res) + 1;
    if (packageId == APP_PACKAGE_ID && !mAppAsLib) {
        // No lookup needs to be done, app package IDs are absolute.
        return NO_ERROR;
    }
    if (packageId == 0 || (packageId == APP_PACKAGE_ID && mAppAsLib)) {
        *resId = (0xFFFFFF & (*resId)) | (((uint32_t) mAssignedPackageId) << 24);
        return NO_ERROR;
    }
    // Do a proper lookup.
    uint8_t translatedId = mLookupTable[packageId];
    if (translatedId == 0) {
        ALOGW("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) {
                ALOGW("e[0x%02x] -> 0x%02x", (uint8_t)i, mLookupTable[i]);
            }
        }
        return UNKNOWN_ERROR;
    }
    *resId = (res & 0x00ffffff) | (((uint32_t) translatedId) << 24);
    return NO_ERROR;
}
复制代码


先从resId中获取packageId,然后经过有三个if语句,我们一个个来看。


第一个,如果packageId是APP_PACKAGE_ID且不是lib,return NO_ERROR,这里APP_PACKAGE_ID是一个常量0x7F,因为我们动态替换成了0x7D,所以不执行这里。


第二个,如果packageId是0 或 packageId是APP_PACKAGE_ID且是lib,return NO_ERROR,这里重新组合了resId,将原packageId替换成了mAssignedPackageId,mAssignedPackageId即是resource.arsc文件中Package头部中的那个PackageId,我们已经动态替换成了0x7D。但是由于不满足if条件,所以这里也不执行。


第三个,从mLookupTable中获取一个translatedId,如果translatedId是0,则return UNKNOWN_ERROR;否则重新组合了resId,将原packageId替换成了translatedId。

当我们执行到这里,获取的translatedId一定是0,所以才会报出上面的错误。

这样我们找到原因所在,即mLookupTable中缺少translatedId的数据。

那么如何才能让数据正确?由于mLookupTable使用地方很多,而且还存储这其他数据,所以调查起来有些困难。


2、aapt中的特殊处理


这样就又走进死胡同了?我想到另外一个方案,即直接修改aapt源码来修改resId,那么那个方案既然可以,我们是不是可以看看差别在哪里?

那么方案很简单,修改代码也不多,没有看到什么特殊处理,那么一定是aapt源码中有一些处理。在aapt源码中搜索0x7F

(为什么要搜索0x7F,因为aapt一定是判断resId的packageId是不是0x7F,如果不是有一些特殊处理)

可以找到如下代码:

地址: github.com/aosp-mirror…


status_t ResourceTable::flatten(Bundle* bundle, const sp<const ResourceFilter>& filter,
        const sp<AaptFile>& dest,
        const bool isBase)
{
    ...
    // The libraries this table references.
    Vector<sp<Package> > libraryPackages;
    const ResTable& table = mAssets->getIncludedResources();
    const size_t basePackageCount = table.getBasePackageCount();
    for (size_t i = 0; i < basePackageCount; i++) {
        size_t packageId = table.getBasePackageId(i);
        String16 packageName(table.getBasePackageName(i));
        if (packageId > 0x01 && packageId != 0x7f &&
                packageName != String16("android")) {
            libraryPackages.add(sp<Package>(new Package(packageName, packageId)));
        }
    }
    ...
    if (isBase) {
            status_t err = flattenLibraryTable(data, libraryPackages);
            if (err != NO_ERROR) {
                fprintf(stderr, "ERROR: failed to write library table\n");
                return err;
            }
      }
     ...
}
复制代码


可以看到当packageId不是0x7F,会新建一个Package结构存入libraryPackages,那么

这个libraryPackages有啥用?

我们继续往下看发现flattenLibraryTable函数使用了它,这个函数源码如下:


status_t ResourceTable::flattenLibraryTable(const sp<AaptFile>& dest, const Vector<sp<Package> >& libs) {
    // Write out the library table if necessary
    if (libs.size() > 0) {
        if (kIsDebug) {
            fprintf(stderr, "Writing library reference table\n");
        }
        const size_t libStart = dest->getSize();
        const size_t count = libs.size();
        ResTable_lib_header* libHeader = (ResTable_lib_header*) dest->editDataInRange(
                libStart, sizeof(ResTable_lib_header));
        memset(libHeader, 0, sizeof(*libHeader));
        libHeader->header.type = htods(RES_TABLE_LIBRARY_TYPE);
        libHeader->header.headerSize = htods(sizeof(*libHeader));
        libHeader->header.size = htodl(sizeof(*libHeader) + (sizeof(ResTable_lib_entry) * count));
        libHeader->count = htodl(count);
        // Write the library entries
        for (size_t i = 0; i < count; i++) {
            const size_t entryStart = dest->getSize();
            sp<Package> libPackage = libs[i];
            if (kIsDebug) {
                fprintf(stderr, "  Entry %s -> 0x%02x\n",
                        String8(libPackage->getName()).string(),
                        (uint8_t)libPackage->getAssignedId());
            }
            ResTable_lib_entry* entry = (ResTable_lib_entry*) dest->editDataInRange(
                    entryStart, sizeof(ResTable_lib_entry));
            memset(entry, 0, sizeof(*entry));
            entry->packageId = htodl(libPackage->getAssignedId());
            strcpy16_htod(entry->packageName, libPackage->getName().string());
        }
    }
    return NO_ERROR;
}
复制代码


可以看到向resource.arsc文件中写入了一个RES_TABLE_LIBRARY_TYPE类型的数据结构。

这个RES_TABLE_LIBRARY_TYPE在网上的resource.arsc解析相关文章中几乎没有被提到,而且网上的那张神图中也看不到这个结构。


3、RES_TABLE_LIBRARY_TYPE


那么它是什么?来看看我找到的一些蛛丝马迹:  

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


In Android 5.0+ needs to set the dynamicRefTable for lookup bag parent.

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


根据上面的我们大概知道这个“资源包id映射”是5.0之后才加入的,所以网上那么比较陈旧的文章中没有提及。它的作用就是正确的查找bag类型Entry数据的parent。

(至于什么是bag类型,请阅读《...》)

这样我们就知道了,如果resId使用默认的0x7F,则不存在问题;如果不使用默认的0x7F,而且还缺少dynamicRefTable这个映射,则查找parent会出问题。

同时,当用aapt编译资源时,如果resId的packageId不是0x7F,才会添加dynamicRefTable。

由于我们是在resource.arsc生产后才去修改,所以aapt编译时并未加入dynamicRefTable,这就是问题所在,我们需要手动加入它。

那么我们就需要知道它的结构,如下:


struct ResTable_lib_header
{
    struct ResChunk_header header;
    // The number of shared libraries linked in this resource table.
    uint32_t count;
};
复制代码


首先header包括两部分,一个是ResChunk_header结构体,一个是包含的映射个数。

ResChunk_header结构体如下:


struct ResChunk_header  
 {  
     //当前这个chunk的类型  
     uint16_t type;  
     //当前这个chunk的头部大小  
     uint16_t headerSize;  
     //当前这个chunk的大小  
     uint32_t size;  
 };  
复制代码


这个结构体就不细说了,其中type就是RES_TABLE_LIBRARY_TYPE

紧接着header,就是映射表了,每一个映射都是一个ResTable_lib_entry结构,一共有count个


struct ResTable_lib_entry
{
    // The package-id this shared library was assigned at build time.
    // We use a uint32 to keep the structure aligned on a uint32 boundary.
    uint32_t packageId;
    // The package name of the shared library. \0 terminated.
    char16_t packageName[128];
};
复制代码


映射结构也很简单,packageId+packageName,但是注意packageId占固定4byte大小,而packageName则占固定256byte大小(2byte一个字符,一共128个字符),当然一般packageName都不会这么长,多余的用0补充。


4、dynamicRefTable位置


上面我们知道了dynamicRefTable的结构,那么它在文件中处于什么位置呢?又与哪些数据有联系?

根据网上的神图我重新做了一张更完整更准确的结构图,补充了dynamicRefTable,如下:

网络异常,图片无法展示
|


这样看到比较清晰了,dynamicRefTable是整个Package结构中的一部分,与Package中的其他结构是并列的,所以不会影响。但是加入dynamicRefTable会影响Package头部中的块大小,以及文件header中的文件大小。

那么如果下面还有另外一个Package数据块,则会有影响么?

答案是不会,因为与位置相关的数据,比如偏移量,一般都是相对于该数据块头部的。

 

5、验证dynamicRefTable


这样dynamicRefTable的结构就分析完了。那么我们如何知道resource.arsc文件中是否存在这种结构?

我们可以使用aapt反编译一下打包好的apk

./aapt d resources /Users/.../app-debug.apk

会得到如下数据:

Package Groups (1)

Package Group 0 id=0x6d packageCount=1 name=com.example.bennu.testapp

 DynamicRefTable entryCount=1:

   0x6d -> com.example.bennu.testapp

 Package 0 id=0x6d name=com.example.bennu.testapp

   type 1 configCount=2 entryCount=12

     spec resource 0x6d020000 com.example.bennu.testapp:drawable/arrow: flags=0x00000000

     spec resource 0x6d020001 com.example.bennu.testapp:drawable/fio: flags=0x00000000

     spec resource 0x6d020002 com.example.bennu.testapp:drawable/fit: flags=0x00000000

     spec resource 0x6d020003 com.example.bennu.testapp:drawable/fith: flags=0x00000000

     spec resource 0x6d020004 com.example.bennu.testapp:drawable/fo: flags=0x00000000

     spec resource 0x6d020005 com.example.bennu.testapp:drawable/ft: flags=0x00000000

     spec resource 0x6d020006 com.example.bennu.testapp:drawable/fth: flags=0x00000000

     spec resource 0x6d020007 com.example.bennu.testapp:drawable/test_bg_java: flags=0x00000000

     spec resource 0x6d020008 com.example.bennu.testapp:drawable/test_bg_xml: flags=0x00000000

     spec resource 0x6d020009 com.example.bennu.testapp:drawable/xo: flags=0x00000000

     spec resource 0x6d02000a com.example.bennu.testapp:drawable/xt: flags=0x00000000

     spec resource 0x6d02000b com.example.bennu.testapp:drawable/xth: flags=0x00000000

     config (default):

       resource 0x6d020007 com.example.bennu.testapp:drawable/test_bg_java: t=0x03 d=0x00000000 (s=0x0008 r=0x00)

       resource 0x6d020008 com.example.bennu.testapp:drawable/test_bg_xml: t=0x03 d=0x00000001 (s=0x0008 r=0x00)

     config xhdpi-v4:

       resource 0x6d020000 com.example.bennu.testapp:drawable/arrow: t=0x03 d=0x00000003 (s=0x0008 r=0x00)

       resource 0x6d020001 com.example.bennu.testapp:drawable/fio: t=0x03 d=0x00000004 (s=0x0008 r=0x00)

       resource 0x6d020002 com.example.bennu.testapp:drawable/fit: t=0x03 d=0x00000005 (s=0x0008 r=0x00)

       resource 0x6d020003 com.example.bennu.testapp:drawable/fith: t=0x03 d=0x00000006 (s=0x0008 r=0x00)

       resource 0x6d020004 com.example.bennu.testapp:drawable/fo: t=0x03 d=0x00000007 (s=0x0008 r=0x00)

       resource 0x6d020005 com.example.bennu.testapp:drawable/ft: t=0x03 d=0x00000008 (s=0x0008 r=0x00)

       resource 0x6d020006 com.example.bennu.testapp:drawable/fth: t=0x03 d=0x00000009 (s=0x0008 r=0x00)

       resource 0x6d020009 com.example.bennu.testapp:drawable/xo: t=0x03 d=0x0000000a (s=0x0008 r=0x00)

       resource 0x6d02000a com.example.bennu.testapp:drawable/xt: t=0x03 d=0x0000000b (s=0x0008 r=0x00)

       resource 0x6d02000b com.example.bennu.testapp:drawable/xth: t=0x03 d=0x0000000c (s=0x0008 r=0x00)

 可以看到有dynamicRefTable的相关数据,如果没有则表示不存在这类数据。


6、总结


dynamicRefTable的存在很重要,影响了bag类数据的parent部分。但是有一个前提,即资源索引不使用默认的0x7F。所以在正常编译时我们不需要特别去注意它,但是如果我们手动干预或后期修改了资源索引,就不得不考虑它了。


目录
相关文章
|
iOS开发 HTML5 移动开发
iOS 基础函数解析 - Foundation Functions Reference
iOS 基础函数解析 - Foundation Functions Reference 太阳火神的美丽人生 (http://blog.csdn.net/opengl_es) 本文遵循“署名-非商业用途-保持一致”创作公用协议 转载请保留此句:太阳火神的美丽人生 -  本博客专注于 敏捷开发及移动和物联设备研究:iOS、Android、Html5、Arduino、pcDuino,否则,出自本博客的文章拒绝转载或再转载,谢谢合作。
1025 0
|
3天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
15 2
|
1月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
66 0
|
1月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
52 0
|
1月前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
60 0
|
1月前
|
安全 Java 程序员
Collection-Stack&Queue源码解析
Collection-Stack&Queue源码解析
80 0
|
3天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
16天前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
36 3
|
1月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
53 5
|
1月前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)
108 5

推荐镜像

更多