可能是全網(wǎng)講最細的安卓resources.arsc解析教程(一)

aapt工具在編譯資源會將一些資源或者資源索引打包成resources.arsc。這個文件以二進制數(shù)據(jù)的形式記錄數(shù)據(jù),c/c++加載起來特別方便。

了解resources.arsc的結(jié)構(gòu)對理解安卓的資源加載原理有很重要的幫助。

這幾天寫resources.arsc解析工具時候在網(wǎng)上搜到了不少的資料、博客,但是它們寫的都不是特別的詳細,都會漏掉一些東西沒有提。導致在實現(xiàn)的時候遇到了很多的坑。這里我希望盡量把自己總結(jié)出來的東西一步步都列出來,盡量做到只需要看這篇博客就能自己實現(xiàn)一個resources.arsc的解析器。

這個工具已經(jīng)在github上開源(使用C++11,已經(jīng)在mac和ubuntu上報make編譯通過正常運行,Windows的同學就只好說聲抱歉了),感興趣的同學也可以直接下載下來玩玩。

總體結(jié)構(gòu)

resources.arsc是以一個個Chunk塊的形式組織的,Chunk的頭部信息記錄了這個Chunk的類型、長度等數(shù)據(jù)。

從整體上來看,其結(jié)構(gòu)為:資源索引表頭部+字符串資源池+N個Package數(shù)據(jù)塊:

1.png

頭部數(shù)據(jù)解析

整個resources.arsc就是一個Chunk塊,所以文件的開頭就是這個Chunk的頭部信息.不過需要注意的是resources.arsc文件采用小端編碼方式.所以數(shù)據(jù)應(yīng)該按字節(jié)從后往前讀。

頭部的結(jié)構(gòu)如下:

頭部類型(兩個字節(jié)),頭部大小(兩個字節(jié)),Chunk塊大小(四個字節(jié))

我們找一個apk文件,將它后綴改成.zip直接解壓,就可以得到resources.arsc,這里實際舉個例子,用編輯工具查看到resources.arsc的前8個字節(jié)的數(shù)據(jù),這里以16進制顯示:

0200 0c00 acb6 0300

首先頭部類型的兩個字節(jié)是0200(每兩個16進制數(shù)字代表了一個字節(jié)),但是這里是小端序,所以要從后往前讀,得到實際的值0x0002。

02 00 -> 00 02

resources.arsc內(nèi)部記錄的數(shù)據(jù)類型在ResourceTypes.h里面定義,我們找到頭部類型的枚舉,可以查到0x0002對應(yīng)的類型是RES_TABLE_TYPE。返回去看上面的圖,可以看到類型的確是RES_TABLE_TYPE。

enum {                                                                                                          
    RES_NULL_TYPE               = 0x0000,                                                                       
    RES_STRING_POOL_TYPE        = 0x0001,                                                                       
    RES_TABLE_TYPE              = 0x0002,                                                                       
    RES_XML_TYPE                = 0x0003,                                                                       

    // Chunk types in RES_XML_TYPE                                                                              
    RES_XML_FIRST_CHUNK_TYPE    = 0x0100,                                                                       
    RES_XML_START_NAMESPACE_TYPE= 0x0100,                                                                       
    RES_XML_END_NAMESPACE_TYPE  = 0x0101,                                                                       
    RES_XML_START_ELEMENT_TYPE  = 0x0102,                                                                       
    RES_XML_END_ELEMENT_TYPE    = 0x0103,                                                                       
    RES_XML_CDATA_TYPE          = 0x0104,                                                                       
    RES_XML_LAST_CHUNK_TYPE     = 0x017f,                                                                       
    // This contains a uint32_t array mapping strings in the string              
    // pool back to resource identifiers.  It is optional.                       
    RES_XML_RESOURCE_MAP_TYPE   = 0x0180,                                                                       

    // Chunk types in RES_TABLE_TYPE                                                                            
    RES_TABLE_PACKAGE_TYPE      = 0x0200,                                                                       
    RES_TABLE_TYPE_TYPE         = 0x0201,                                                                       
    RES_TABLE_TYPE_SPEC_TYPE    = 0x0202,                                                                       
    RES_TABLE_LIBRARY_TYPE      = 0x0203                                                                        
};

接著是頭部大小的兩個字節(jié)0c00(每兩個16進制數(shù)字代表了一個字節(jié)),從后往前讀得到實際值0x000c,也就是說這個頭部大小有12個字節(jié)

0c 00 -> 00 0c

再接著的是Chunk塊大小的四個字節(jié)acb6 0300(每兩個16進制數(shù)字代表了一個字節(jié)),從后往前讀得到實際值0x0003b6ac,轉(zhuǎn)換回十進制是243372,也就是說resources.arsc文件的大小是243372字節(jié)。

ac b6 03 00 -> 00 03 b6 ac

我們可以用wc命令查看這個文件的大小:

wc resources.arsc
277   2932 243372 resources.arsc

resources.arsc文件這樣組織的原因是可以很方便的使用c/c++去加載。

我們在源碼里面找到Chunk頭部信息的ResChunk_header結(jié)構(gòu)體:

/**                                                                                                             
* Header that appears at the front of every data chunk in a resource.            
*/                                                                                                              
struct ResChunk_header                                                                                          
{                                                                                                               
   // Type identifier for this chunk.  The meaning of this value depends         
   // on the containing chunk.                                                                                  
   uint16_t type;                                                                                               

   // Size of the chunk header (in bytes).  Adding this value to                 
   // the address of the chunk allows you to find its associated data            
   // (if any).                                                                                                 
   uint16_t headerSize;                                                                                         

   // Total size of this chunk (in bytes).  This is the chunkSize plus           
   // the size of any data associated with the chunk.  Adding this value         
   // to the chunk allows you to completely skip its contents (including         
   // any child chunks).  If this value is the same as chunkSize, there is       
   // no data associated with the chunk.                                                                        
   uint32_t size;                                                                                               
};  

下面就是show time了,由于普通機器設(shè)備上使用的都是小端序號,所以用c語言解析resources.arsc文件的頭部信息只要直接填充ResChunk_header結(jié)構(gòu)體就好了:

#include <stdio.h>                                                               
#include <stdlib.h>                                                              
#include <stdint.h>                                                              

struct ResChunk_header {                                                         
    uint16_t type;                                                               
    int16_t headerSize;                                                          
    uint32_t size;                                                               
};                                                                               

int main(int argc, char *argv[]) {                                               
    struct ResChunk_header header;                                               

    FILE* pFile = fopen(argv[1], "rb");                                          
    fread((void*)&header, sizeof(struct ResChunk_header), 1, pFile);             
    fclose(pFile);                                                               

    printf("type:%u, headSize:%u, size:%u\n", header.type, header.headerSize, header.size);
    return 0;                                                                    
}

編譯后運行,查看打印:

./a.out resources.arsc
type:2, headSize:12, size:243372

頭部類型

每個頭部類型都會有一個具體的結(jié)構(gòu)體和它對應(yīng),例如我們的RES_TABLE_TYPE類型對應(yīng)的就是ResTable_header:

struct ResTable_header                                                           
{                                                                                
    struct ResChunk_header header;                                               

    uint32_t packageCount;                                                       
};

這些結(jié)構(gòu)體的第一個成員都是是ResChunk_header,然后后面才是這個類型的特有數(shù)據(jù)。所以我們改下代碼去讀取完整的頭部信息:

#include <stdio.h>                                                               
#include <stdlib.h>                                                              
#include <stdint.h>                                                              

struct ResChunk_header {                                                         
    uint16_t type;                                                               
    int16_t headerSize;                                                          
    uint32_t size;                                                               
};                                                                               

struct ResTable_header{                                                          
    struct ResChunk_header header;                                               
    uint32_t packageCount;                                                       
};                                                                               

int main(int argc, char *argv[]) {                                               
    uint16_t type;                                                               

    FILE* pFile = fopen(argv[1], "rb");                                          
    fread((void*)&type, sizeof(type), 1, pFile);                                 
    if(type == RES_TABLE_TYPE) {                                                          
        struct ResTable_header header = {0x002};                                 
        fread((void*)(((char*)&header)+2), sizeof(struct ResTable_header)-2, 1, pFile);
        printf("type:%u, headSize:%u, size:%u, packageCount:%u\n",           
                header.header.type,                                              
                header.header.headerSize,                                        
                header.header.size,                                              
                header.packageCount);                                            
    }                                                                            
    fclose(pFile);                                                               
    return 0;                                                                    
}

運行得到:

./a.out resources.arsc
type:2, headSize:12, size:243372, packageCount:1

這里的packageCount指的是resources.arsc里面包含了多少個package的資源,一般只有一個。

到這里,RES_TABLE_TYPE的頭部信息我們就解析完成了。

全局字符串池

3.png

我們看回上面的圖,頭部之后緊接著的是Global String Pool。它其實也是一個Chunk,所以也有頭部。我們可以用同樣的方法去解析:

struct ResStringPool_header readResStringPoolHeader(FILE* pFile) {               
    struct ResStringPool_header header;                                          
    uint16_t type;                                                               
    fread((void*)&header, sizeof(struct ResStringPool_header), 1, pFile);        
    printf("type:%u, headSize:%u, size:%u, stringCount:%u, stringStart:%u, styleCount:%u, styleStart:%u\n",
                header.header.type,                                              
                header.header.headerSize,                                        
                header.header.size,                                              
                header.stringCount,                                              
                header.stringsStart,                                             
                header.styleCount,                                               
                header.stylesStart);                                             
    return header;                                                               
}

打印如下,這個字符串池里面有1971個字符串:

./a.out resources.arsc
type:2, headSize:12, size:243372, packageCount:1
type:1, headSize:28, size:72732, stringCount:1971, stringStart:7912, styleCount:0, styleStart:0

接著看圖,ResStringPool_header后面跟著的是

字符串偏移數(shù)組+style偏移數(shù)組+字符串+style

resources.arsc會把所有的應(yīng)用里面出現(xiàn)的字符串都放到這個全局字符串池里面,不過style的話我現(xiàn)在還沒有理解它的作用,遇到的值都是0,所以這里先忽略。我們只講字符串。

這個字符串偏移數(shù)組其實是一個uint32_t的數(shù)組,記錄了每個字符串距離stringStart的偏移,而stringStart就是具體存放字符串的內(nèi)存的位置距離ResStringPool_header起始地址的字節(jié)數(shù)。

需要注意的是,字符串的前兩個字節(jié)記錄了字符串的長度,而且字符串的編碼格式由ResStringPool_header::flags指定(utf-8或者utf-16)

可能這么講有些抽象,可以結(jié)合下面的示意圖還有代碼理解一下:

2.png
unsigned char* readStringsFromStringPool(FILE* pFile,  struct ResStringPool_header header) {
    uint32_t size = header.header.size - sizeof(struct ResStringPool_header);    
    unsigned char* pData = (unsigned char*)malloc(size);                         
    fread((void*)pData, size, 1, pFile);                                         
    uint32_t* pOffsets = (uint32_t*)pData;                                       

    //stringsStart指的是header的起始地址到字符串起始地址的距離                   
    //pData已經(jīng)是header末尾的地址了,所以要減去header的大小                      
    char* pStringsStart = pData + header.stringsStart - sizeof(struct ResStringPool_header);

    for(int i = 0 ; i < header.stringCount ; i++) {                              
        //前面兩個字節(jié)是長度,要跳過                                              
        char* str = pStringsStart + *(pOffsets + i) + 2;                         
        if(header.flags & UTF8_FLAG) {                                           
            printf("%s\n", str);                                                 
        } else {                                                                 
            printUtf16String(str);                                               
        }                                                                        
    }                                                                            

    return pData;                                                                
}                                                                         

這里的會把應(yīng)用里面用到的字符串資源都打印出來,值得注意的是這里的字符串資源指的不僅是我們定義的string標簽里的值:

...
Delete 鍵
查看全部
瀏覽首頁
空格鍵
與「%s」分享
選擇分享對象
...

還包括了資源的路徑等其他字符串資源:

...
res/anim/abc_fade_in.xml                                                         
res/anim/abc_fade_out.xml                                                        
res/anim/abc_grow_fade_in_from_bottom.xml                                        
res/anim/abc_popup_enter.xml                                                     
res/anim/abc_popup_exit.xml                                                      
res/anim/abc_shrink_fade_out_from_bottom.xml                                     
res/anim/abc_slide_in_bottom.xml                                                 
res/anim/abc_slide_in_top.xml                                                    
res/anim/abc_slide_out_bottom.xml                                                
res/anim/abc_slide_out_top.xml                                                   
res/anim/abc_tooltip_enter.xml                                                   
res/anim/abc_tooltip_exit.xml                                                    
res/color-v23/abc_btn_colored_borderless_text_material.xml                       
res/color-v23/abc_btn_colored_text_material.xml                                  
res/color-v23/abc_color_highlight_material.xml                                   
res/color-v23/abc_tint_btn_checkable.xml          
...

Package資源

4.png

讓我們繼續(xù)往下讀,接下來的就是包內(nèi)的資源了。resources.arsc可以支持打入多個package的資源,但是一般只會有一個package。

每個package的資源同樣打包成一個Chunk,它的頭部信息由ResTable_package結(jié)構(gòu)體表示:

int readResTablePackageHeader(FILE* pFile, struct ResTable_package* pHeader) {
    if(fread((void*)pHeader, sizeof(struct ResTable_package), 1, pFile) == 0) {
        return 0;
    }
    printf("type:%u, headSize:%u, size:%u, id:%x, packageName:",
                pHeader->header.type,
                pHeader->header.headerSize,
                pHeader->header.size,
                pHeader->id);
    printUtf16String((char16_t*)pHeader->name);
    return 1;
}

頭部信息記錄了package的id和名字:

type:512, headSize:288, size:188068, id:7f, packageName:com.cvte.tv.myapplication

資源類型字符串池

5.png

緊接著頭部之后的又是一個字符串池,它和之前介紹的全局字符串資源池的結(jié)構(gòu)是一樣的,我們可以用同樣的方法去讀取:

struct ResTable_package packageHeader;
while(readResTablePackageHeader(pFile, &packageHeader)) {
    struct ResStringPool_header typeStringPoolHeader = readResStringPoolHeader(pFile);
    unsigned char* pTypeStrings = readStringsFromStringPool(pFile, typeStringPoolHeader);
    ...
}

這里記錄的是這個package里面存儲的資源的類型:

type:1, headSize:28, size:248, stringCount:12, stringStart:76, styleCount:0, styleStart:0
anim
attr
bool
color
dimen
drawable
id
integer
layout
mipmap
string
style

可以看到這個package里面有anim、attr、bool、color、dimen、drawable、id、integer、layout、mipmap、string、style這么多中類型的資源。

資源項名稱字符串池

6.png

接下來又是一個字符串資源池,再讀一次:

struct ResTable_package packageHeader;
while(readResTablePackageHeader(pFile, &packageHeader)) {
    struct ResStringPool_header typeStringPoolHeader = readResStringPoolHeader(pFile);
    unsigned char* pTypeStrings = readStringsFromStringPool(pFile, typeStringPoolHeader);

    struct ResStringPool_header keyStringPoolHeader = readResStringPoolHeader(pFile);
    unsigned char* pKeyStrings = readStringsFromStringPool(pFile, keyStringPoolHeader);
    ...
}

這里讀出來的就是我們的資源的key:

type:1, headSize:28, size:41208, stringCount:1221, stringStart:4912, styleCount:0, styleStart:0
abc_fade_in
abc_fade_out
abc_grow_fade_in_from_bottom
abc_popup_enter
abc_popup_exit
abc_shrink_fade_out_from_bottom
abc_slide_in_bottom
abc_slide_in_top
abc_slide_out_bottom
abc_slide_out_top
abc_tooltip_enter
abc_tooltip_exit
actionBarDivider
actionBarItemBackground
actionBarPopupTheme
actionBarSize
actionBarSplitStyle
actionBarStyle
actionBarTabBarStyle
...
activity_main       <- activity_main在這里
...
app_name            <- app_name在這里
...

比如我們的app_name字符串,就會生成R.string.app_name,而這個"app_name"就會出現(xiàn)在資源項名稱字符串池里面:

<string name="app_name">My Application</string>

又或者我們定義了activity_main.xml,就會生成R.layout.activity_main,而這個"activity_main"也會出現(xiàn)在資源項名稱字符串池里面。

資源

接下來的就是一系列的RES_TABLE_TYPE_SPEC_TYPE和RES_TABLE_TYPE_TYPE,不過上面的圖畫的不是很清晰。

Package資源剩下的部分是按資源類型分組的。一個RES_TABLE_TYPE_SPEC_TYPE跟著多個RES_TABLE_TYPE_TYPE為一組,記錄一個類型在不同配置下的資源。

比如我們在前面的資源類型字符串池里面看到有anim、attr、bool、color、dimen、drawable、id、integer、layout、mipmap、string、style,十二種類型的資源,于是就有十二組的RES_TABLE_TYPE_SPEC_TYPE和RES_TABLE_TYPE_TYPE,而且順序也是按資源類型字符串池里面的順序排的:

7.png

可以看到每一組都是以一個RES_TABLE_TYPE_SPEC_TYPE開頭記錄該類型的資源的信息,然后跟著多個RES_TABLE_TYPE_TYPE記錄該類型在不同Config下的數(shù)據(jù)(如color、color-v21、color-v23或者我們更熟悉的string、string-en-US、string-zh-CN、string-zh-TW等)

我們可以實際看看打印:

struct ResChunk_header chunkHeader;
uint8_t id;
while(fread((void*)&chunkHeader, sizeof(struct ResChunk_header), 1, pFile)
        && chunkHeader.type != RES_TABLE_PACKAGE_TYPE) {
    fread((void*)&id, sizeof(uint8_t), 1, pFile);
    printf("0x%x, %d\n", chunkHeader.type, id);
    fseek(pFile, chunkHeader.size - sizeof(struct ResChunk_header) - sizeof(uint8_t), SEEK_CUR);
}

輸出:

0x202, 1
0x201, 1
0x202, 2
0x201, 2
0x202, 3
0x201, 3
0x201, 3
0x202, 4
0x201, 4
0x201, 4
0x201, 4
0x202, 5
0x201, 5
0x201, 5
0x201, 5
0x201, 5
0x201, 5
0x201, 5
0x201, 5
...

這里打印了type和id,type 0x202代表了RES_TABLE_TYPE_SPEC_TYPE,0x201代表了RES_TABLE_TYPE_TYPE。

而id則代表了資源類型字符串池里面的順序,例如1是anim,2是attr,3是bool...

翻譯過來就是:

RES_TABLE_TYPE_SPEC_TYPE,  anim
RES_TABLE_TYPE_TYPE,       anim
RES_TABLE_TYPE_SPEC_TYPE,  attr
RES_TABLE_TYPE_TYPE,       attr
RES_TABLE_TYPE_SPEC_TYPE,  bool
RES_TABLE_TYPE_TYPE,       bool
RES_TABLE_TYPE_TYPE,       bool
RES_TABLE_TYPE_SPEC_TYPE,  color
RES_TABLE_TYPE_TYPE,       color
RES_TABLE_TYPE_TYPE,       color
RES_TABLE_TYPE_TYPE,       color
RES_TABLE_TYPE_SPEC_TYPE,  dimen
RES_TABLE_TYPE_TYPE,       dimen
RES_TABLE_TYPE_TYPE,       dimen
RES_TABLE_TYPE_TYPE,       dimen
RES_TABLE_TYPE_TYPE,       dimen
RES_TABLE_TYPE_TYPE,       dimen
RES_TABLE_TYPE_TYPE,       dimen
RES_TABLE_TYPE_TYPE,       dimen
...

好的,因為信息量已經(jīng)有點大了,本節(jié)先到這里。接下來是整個resources.arsc中最重要的資源的具體定義會在下一篇筆記中介紹,完整的demo代碼也會在下一篇博客的末尾給出。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Apk中的resources.arsc是aapt工具編譯資源時生成的一個重要文件。App資源能根據(jù)配置的變化,索引...
    小爨閱讀 21,444評論 4 44
  • 寫在前面的話 從寫下這個標題開始,我就知道這篇文章需要幾天的時間才能真正完成。當然不是因為一切從零開始,只是因為要...
    nick_young閱讀 5,955評論 4 3
  • 文章中所使用軟件和代碼資源 示例apk示例代碼binary view二進制文件查看工具:android 6.0系統(tǒng)...
    第八區(qū)閱讀 3,688評論 0 3
  • 今天假期最后一天,忙忙碌碌的就這樣過去了,上午基本還是兒子的作業(yè)時間,今天寫的數(shù)學2到10的分成,還有語文...
    風鈴_a47a閱讀 143評論 0 0
  • 我的姥姥姥爺是一對善良的老人,本本分分活了一輩子,現(xiàn)在一個88一個87歲了,我這幾年越來越怕他們哪天突然離開了。春...
    我不是胡太后閱讀 150評論 0 0

友情鏈接更多精彩內(nèi)容