Android apk瘦身最佳实践(三):资源混淆原理

通常我们开发时,为了不让资源名重复,可能会定义名字很长的资源名,这其实也会增大 apk 包的体积。接下来我们讲讲如何做资源混淆,先从其原理开始。

1. R.java文件以及资源id

众所周知,R.java文件是 aapt 对资源文件进行编译后生成的一个资源 id 映射文件,每个资源文件都对应一个 int 型的 id 值,我们先创建一个 demo 工程,然后截取里面的部分代码可以看到如下代码:

public final class R {
    public static final class anim {
        public static int abc_fade_in = 0x7f010001;
        public static int abc_fade_out = 0x7f010002;
        public static int abc_grow_fade_in_from_bottom = 0x7f010003;
        ...
    }
    
    public static final class attr {
        public static int actionBarDivider = 0x7f040001;
        public static int actionBarItemBackground = 0x7f040002;
        public static int actionBarPopupTheme = 0x7f040003;
        ...

每个资源 id 都是一个4字节的无符号整数,其格式为 0xpptteeee,其中:

  • pp:表示 package id,相当于资源的命名空间,其中 0x01 表示系统资源的 package id,0x7f 则表示应用程序的 package id。从上面代码中可以看到,应用程序的所有资源 id 的 package id 值都为 0x7f,而系统资源的 id,我们可以从 android.R 文件中看到,其 package id 值都为 0x01。理论上,合法的 package id 取值范围为[0x01, 0x7f],其他都是非法的,我们可以修改 aapt 等打包工具来生成不同的 package id 值。
  • tt:表示 type id,用来表示不同的资源类型,以此来区分 drawable、string、anim、attr、layout、xml 等等,如上面例子中 anim 的 type id 值为 0x01,attr 的 type id值为 0x04。
  • eeee:表示 entry id,即该资源在它所属的资源类型里的编号,一般都是从 0x0001 开始,同类型里的资源的 entry id 值都互不相同。

以这个资源 id 为例,我们看看能得出哪些信息:

public static int abc_fade_in = 0x7f010001;

首先 package id 值为 0x7f,表示是应用程序的资源,type id 值为 0x01,表示是一个 anim 类型的资源,最后该资源的编号为 0x0001。

2. 资源混淆压缩的可行性

apk包实质上是一个 zip 文件,我们可以直接解压,可以看到其文件结构如下所示:

apk包结构

Android 应用程序在打包时,最终会生成一个 resources.arsc 文件,简单来讲它是一个资源 id 映射表,通过这个映射表,系统可以通过资源 id 的值查找到对应的资源,例如:通过一个字符串资源 id,找到对应的字符串值;通过图片资源的 id,找到对应的图片资源路径以及名称。

从图中可以看到,编译后所有的资源文件都在 res 文件夹下面,通过 resources.arsc 资源映射表,可以根据资源 id 找到 res 文件下面的具体某个资源。也就是说,我们可以修改 res 文件夹下面所有的资源文件路径以及名称,将其修改为短路径以及短名称,同时相对应的修改 resources.arsc 资源表,这样就可以做到对资源的名称混淆以及压缩了,最后以此达到减小 apk 包大小的目的了。

3. resources.arsc文件格式解析

对 res 文件夹下面的的资源文件进行重命名是简单的,比较复杂的是资源名改了以后,必须同步修改 resources.arsc 资源映射表,这就需要我们搞清楚该文件的具体格式了。resources.arsc 文件对应的数据结构的定义在 Android 源码里有定义,我找到的一份源码路径为:base/libs/androidfw/include/androidfw/ResourceTypes.h,在 ResourceTypes.h 文件里详细定义了 resources.arsc 文件的数据结构。

3.1 data chunk

首先,有个很重要的概念叫 data chunk,这个不好翻译成中文,我个人的理解是,每个 data chunk 就是一个数据块,这个数据块有特定的格式,用来表示特定的含义,每个 chunk 还可能嵌套包含多个小的 chunk 。最终,可以把一个文件当做是一个大的 data chunk,这个大的 data chunk 又嵌套包含了若干个其他小的 data chunk。

源代码里定义,每个 data chunk 都包含了一个固定的头信息,所以每个 data chunk 是由“头信息 + 数据内容”组成的,头信息表明了该 chunk 包含什么样的数据,数据内容则是该 chunk 真正包含的数据信息。

所有的头信息,都包含一个基本的数据结构体,其定义如下:

/** 
 * 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;
};

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
};

从中可以看到,这个头信息里定义了 chunk 的类型,头信息的字节数,这个 chunk 总的字节数大小。而枚举类则定义了所有的 data chunk 类型,每一种 data chunk 类型都会有自己特定的数据结构,下面我们将按照 resources.arsc 的结构一一解析,随便创建一个 demo 工程,打完包后解压 zip 文件,查看 resources.arsc 文件的字节码。

3.2 资源表header

resources.arsc 文件最先出现的是资源表头部,其结构体定义如下:

/**
 * Header for a resource table.  Its data contains a series of
 * additional chunks:
 *   * A ResStringPool_header containing all table values.  This string pool
 *     contains all of the string values in the entire resource table (not
 *     the names of entries or type identifiers however).
 *   * One or more ResTable_package chunks.
 *
 * Specific entries within a resource table can be uniquely identified
 * with a single integer as defined by the ResTable_ref structure.
 */
struct ResTable_header
{
    struct ResChunk_header header;

    // The number of ResTable_package structures.
    uint32_t packageCount;
};
  • header:标准的 chunk 头部信息;
  • packageCount:编译的资源包的个数,大部分情况下应该都只有1个;

具体实例:

资源表chunk header

需要注意的是,字节码是按照(低位在前,高位在后)的顺序排列的,所以 type 等于 0x0002 而不是 0x0200。

从图中可以看到:

  • 该 chunk 的类型为 0x0002,也就是 RES_TABLE_TYPE,表示该 chunk 是资源表;
  • 该 chunk header 大小为 12 个字节;
  • 整个 chunk 的大小为 0x0007e90c,换算成十进制为518412,表示整个文件的大小为 518412 字节,在磁盘上查看该 resources.arsc 文件的大小,可以发现它的大小正好为 518412 字节,这说明整个文件是一个类型为 RES_TABLE_TYPE 的 data chunk ;
  • package 的数量是 1 个。
3.3 资源字符串池

紧接着出现的是资源字符串池,其中会包含资源的路径名、类似 strings.xml 里定义的资源的值,其头部数据结构如下:

/**
 * Definition for a pool of strings.  The data of this chunk is an
 * array of uint32_t providing indices into the pool, relative to
 * stringsStart.  At stringsStart are all of the UTF-16 strings
 * concatenated together; each starts with a uint16_t of the string's
 * length and each ends with a 0x0000 terminator.  If a string is >
 * 32767 characters, the high bit of the length is set meaning to take
 * those 15 bits as a high word and it will be followed by another
 * uint16_t containing the low word.
 *
 * If styleCount is not zero, then immediately following the array of
 * uint32_t indices into the string table is another array of indices
 * into a style table starting at stylesStart.  Each entry in the
 * style table is an array of ResStringPool_span structures.
 */
struct ResStringPool_header
{
    struct ResChunk_header header;

    // Number of strings in this pool (number of uint32_t indices that follow
    // in the data).
    uint32_t stringCount;

    // Number of style span arrays in the pool (number of uint32_t indices
    // follow the string indices).
    uint32_t styleCount;

    // Flags.
    enum {
        // If set, the string index is sorted by the string values (based
        // on strcmp16()).
        SORTED_FLAG = 1<<0,

        // String pool is encoded in UTF-8
        UTF8_FLAG = 1<<8
    };
    uint32_t flags;

    // Index from header of the string data.
    uint32_t stringsStart;

    // Index from header of the style data.
    uint32_t stylesStart;
};
  • header:标准的 chunk 头部信息;
  • stringCount:字符串个数;
  • styleCount:字符串样式的个数(刚开始没有搞懂这是啥,后面看具体数据结构可以理解
  • flags:字符串标记,可取值有 0x01(表示经过排序),0x100(表示采用UTF-8编码),标记由他们组合而成;
  • stringsStart:字符串内容块相对于此头部的偏移字节数,也就是从该 chunk 的头部开始往后数 stringsStart 个字节后,就是真正的字符串内容数据了,该字段方便定位;
  • stylesStart:字符串样式块相对于此头部的偏移字节数;

具体实例:

资源字符串chunk header

从图中可以看到:

  • 该 chunk 的类型为 0x0001,也就是 RES_STRING_POOL_TYPE,表示资源字符串池;
  • 该 chunk header 大小为 28 个字节,正好与图中红线标注的地方相吻合;
  • 整个 chunk 的大小为 0x01e224(十进制为123428) 字节;
  • 字符串个数为 0x099c,等于 2460 个字符串;
  • 字符串样式个数为 0;
  • flags = 0x0100,表示字符串是 UTF-8 编码,没有排序;
  • 字符串数据块相对于此 chunk 头部的偏移字节数为 0x268c,也就是说从整个资源字符串 chunk 的第 0x268c 个字节起,是字符串内容数据块了。真正的字符串内容定义,是从这里才开始的。
  • 字符串样式数据块相对于此 chunk 头部的偏移为 0,这里是因为 styleCount = 0 的缘故;

紧跟着 ResStringPool_header 的是字符串偏移数组和字符串样式偏移数组,这两个数组的大小分别为 stringCount、styleCount,每个数组的元素都是无符号整型。再接着就是字符串数据块、字符串样式数据块了,所以整个 ResStringPool 的数据结构为:ResStringPool_header + 字符串偏移数组 + 字符串样式偏移数组 + 字符串数据块 + 字符串样式数据块。

在该例子中,stringCount = 2460,即字符串有 2460 个,那么字符串偏移数组的长度也为 2460,每个数组元素都是 int 型的数值,那么字符串偏移数组的字节数为 2460 * 4 = 9840 个字节,由于 styleCount = 0,所以紧接着字符串偏移数组的是字符串数据块了,前面讲到该 chunk header 大小为 28 字节,所以从该 chunk 的 9840 + 28 = 9868 个字节起,表示的是字符串数据块了,9868 换算成 16 进制为 0x268c,这与 stringStart 的值也是相吻合的。

何为字符串偏移数组呢?前面讲到共有 2460 个字符串,那么这里就为每个字符串设定了一个编号,从 0-2459,偏移数组的每个元素值表示对应编号的字符串相对于 stringsStart 的偏移。也就是说每个字符串都有一个编号,通过编号可以从字符串偏移数组里找到该字符串内容所在的位置。其数据结构定义为:

/**
 * Reference to a string in a string pool.
 */
struct ResStringPool_ref
{
    // Index into the string pool table (uint32_t-offset from the indices
    // immediately after ResStringPool_header) at which to find the location
    // of the string data in the pool.
    uint32_t index;
};

我们从中找出一个字符串值看看:

111.png

第一个字符串相对于 stringsStart 的偏移值为 0,第二个字符串相对于 stringsStart 的偏移值为 27,前面讲到 stringsStart = 9868,那么则表示从本 chunk 的第 9868 个字节之后开始是第一个字符串,从 9868 + 27 = 9895 个字节之后开始是第二个字符串,那么第一个字符串共有 27 个字节长度来表示。每个字符串的前2个字节为字符串长度,此外,UTF-8 编码的字符串以 0x00 结尾,UTF-16 编码的字符串以 0x0000结尾

从二进制文件中可以找出第一个字符串的 16 进制数据为:

1818 7265 732f 616e 696d 2f61 6263 5f66 6164 655f 696e 2e78 6d6c 00

前2个字节表示长度,字符串长度为 0x18 = 24,后面有24个字节是实际的字符串值,最后以 00 结尾
将16进制数据转换为字符串为:res/anim/abc_fade_in.xml

同样得到第二个字符串为:

1919 7265 732f 616e 696d 2f61 6263 5f66 6164 655f 6f75 742e 786d 6c00

实际字符串长度为:0x19 = 25 个字节
对应的字符串值为:��res/anim/abc_fade_out.xml

从上面也可以看到,每个字符串都以 0x00 结尾。

这个字符串的长度计算很费解,源码里有个算法如下:

//UTF-16 编码的字符串长度计算方法
/**
 * Strings in UTF-16 format have length indicated by a length encoded in the
 * stored data. It is either 1 or 2 characters of length data. This allows a
 * maximum length of 0x7FFFFFF (2147483647 bytes), but if you're storing that
 * much data in a string, you're abusing them.
 *
 * If the high bit is set, then there are two characters or 4 bytes of length
 * data encoded. In that case, drop the high bit of the first character and
 * add it together with the next character.
 */
static inline size_t
decodeLength(const uint16_t** str)
{
    size_t len = **str;
    if ((len & 0x8000) != 0) {
        (*str)++;
        len = ((len & 0x7FFF) << 16) | **str;
    }
    (*str)++;
    return len;
}

//UTF-8 编码的字符串长度计算方法
/**
 * Strings in UTF-8 format have length indicated by a length encoded in the
 * stored data. It is either 1 or 2 characters of length data. This allows a
 * maximum length of 0x7FFF (32767 bytes), but you should consider storing
 * text in another way if you're using that much data in a single string.
 *
 * If the high bit is set, then there are two characters or 2 bytes of length
 * data encoded. In that case, drop the high bit of the first character and
 * add it together with the next character.
 */
static inline size_t
decodeLength(const uint8_t** str)
{
    size_t len = **str;
    if ((len & 0x80) != 0) {
        (*str)++;
        len = ((len & 0x7F) << 8) | **str;
    }
    (*str)++;
    return len;
}

从注释中可以看到,如果是UTF-8编码格式的字符串,最多能存储的长度为 0x7FFF(32767个字节),超出这个长度的字符串则不支持了。

这个字符串长度的计算很费解,好多文章的介绍都是错的,都说前2个字节来表示长度,但实际并不是如此,折腾了好久,最终参考微信的 AndResGuard 源码,找到一个正确的计算方式:

 private static final int[] getUtf8(byte[] array, int offset) {
    int val = array[offset];
    int length;
    if ((val & 0x80) != 0) {
      offset += 2;
    } else {
      offset += 1;
    }
    // And we read only the utf-8 encoded length of the string
    val = array[offset];
    offset += 1;
    if ((val & 0x80) != 0) {
      int low = (array[offset] & 0xFF);
      length = ((val & 0x7F) << 8) + low;
      offset += 1;
    } else {
      length = val;
    }
    return new int[] { offset, length };
  }

字符串样式偏移数组与字符串偏移数组是一样的,通过同样的方法可以找到字符串样式,其数据结构定位如下:

/**
 * This structure defines a span of style information associated with
 * a string in the pool.
 */
struct ResStringPool_span
{
    enum {
        END = 0xFFFFFFFF
    };

    // This is the name of the span -- that is, the name of the XML
    // tag that defined it.  The special value END (0xFFFFFFFF) indicates
    // the end of an array of spans.
    ResStringPool_ref name;

    // The range of characters in the string that this span applies to.
    uint32_t firstChar, lastChar;
};
3.4 package header

紧跟着资源字符串后面的是 package 数据块了,package chunk 也有自己的 header,紧跟着 header 后面的是资源类型字符串池资源名称字符串池,这2个字符串池都有些什么呢?与前面介绍的字符串池存储的内容有什么差别呢?举个例来说明:

<string name="app_name">HM-ThinApk</string>
<color name="colorPrimary">#3F51B5</color>

其中 string、color 就是资源类型字符串,其他的还有 attr、anim、layout 等等;
其中 app_name、colorPrimary 就是资源名称字符串了;
其中 HM-ThinApk 这种值,会出现在前面的字符串池里;

package header 的数据结构定义如下:

/**
 * A collection of resource data types within a package.  Followed by
 * one or more ResTable_type and ResTable_typeSpec structures containing the
 * entry values for each resource type.
 */
struct ResTable_package
{
    struct ResChunk_header header;

    // If this is a base package, its ID.  Package IDs start
    // at 1 (corresponding to the value of the package bits in a
    // resource identifier).  0 means this is not a base package.
    uint32_t id;

    // Actual name of this package, \0-terminated.
    uint16_t name[128];

    // Offset to a ResStringPool_header defining the resource
    // type symbol table.  If zero, this package is inheriting from
    // another base package (overriding specific values in it).
    uint32_t typeStrings;

    // Last index into typeStrings that is for public use by others.
    uint32_t lastPublicType;

    // Offset to a ResStringPool_header defining the resource
    // key symbol table.  If zero, this package is inheriting from
    // another base package (overriding specific values in it).
    uint32_t keyStrings;

    // Last index into keyStrings that is for public use by others.
    uint32_t lastPublicKey;

    uint32_t typeIdOffset;
};
  • id:package id,前面介绍资源 id 的数据格式时说过,0x7F 表示应用程序,0x01 表示系统;
  • name:包名称;
  • typeStrings:资源类型字符串池相对于 package chunk 头部的偏移字节数;
  • lastPublicType:可以简单理解为资源类型字符串的个数;
  • keyStrings:资源名称字符串相对于 package chunk 头部的偏移字节数;
  • lastPublicKey:可以简单理解为资源名称字符串的个数;
  • typeIdOffset:没明白做什么

前面资源字符串池的 size = 0x01e224,加上整个资源表的头大小 0x0c,从文件的第 0x01e224 + 0x0c = 0x01e230 个字节起是 package 数据块了,具体看看实例:

  • chunk type = 0200,对应的类型是 RES_TABLE_PACKAGE_TYPE,即资源表包类型;
  • packageId = 0x7f,与前面介绍的资源 id 的高字节是对应的;
  • 包名以 0x00 结尾,长度不足全部补 0 ,图中红色框框标出来的就是包名数据块,去掉后面的 00 ,可以解析出包名字符串值为:com.hm.iou.professional,结果也是相吻合的;
  • typeStrings = 0x0120,资源类型字符串相对于头部的偏移为 0x0120,字符串个数为 0x11 = 17 个;
  • keyStrings = 0x020c,资源名称字符串相对于头部的偏移为 0x020c,字符串个数为 0x142e = 5166 个;
  • 最后从图中看到还有4个字节 0x000000 ,应该是表示 typeIdOffset 的,目前没明白什么意思;
3.5 资源类型字符串和资源名称字符串

package header 之后就是资源类型字符串和资源名称字符串了,这里的字符串的格式与前面介绍的全局资源字符串池的格式是一样的,同样举个列子来说明:

在 package data 从 header 开始的第 0x0120 = 288 字节之后,就是类型字符串池了。

从图中可以看到,字符串个数为 0x11 = 27,与 package header 里的 lastPublicType 值是一样的。这段字节码 0404 616e 696d 00,我们解析出来为:anim。依次解析出所有的字符串值可得到所有的类型有:anim、animator、array、attr、bool、color、dimen、drawable、id、integer、layout、mipmap、raw、string、style、<empty>、xml。同理可以解析出所有的资源名称字符串了。

3.6 TypeSpec(类型规范)

接下来是类型规范数据块了,每种资源类型都有一个这样的数据块。这也是同一个资源ID在不同配置下,找到不同资源文件的关键。先看其头部数据结构定义:

struct ResTable_typeSpec
{
    struct ResChunk_header header;

    // The type identifier this chunk is holding.  Type IDs start
    // at 1 (corresponding to the value of the type bits in a
    // resource identifier).  0 is invalid.
    uint8_t id;
    
    // Must be 0.
    uint8_t res0;
    // Must be 0.
    uint16_t res1;
    
    // Number of uint32_t entry configuration masks that follow.
    uint32_t entryCount;

    enum : uint32_t {
        // Additional flag indicating an entry is public.
        SPEC_PUBLIC = 0x40000000u,

        // Additional flag indicating an entry is overlayable at runtime.
        // Added in Android-P.
        SPEC_OVERLAYABLE = 0x80000000u,
    };
};

看看具体实例:

类型规范数据头信息
  • type = 0202,对应的是 RES_TABLE_TYPE_SPEC_TYPE,表示类型规范;
  • id = 0x01,前面介绍资源 id 的结构为 0xpptteeee,这个 id 就是资源的类型 id 值;
  • res0、res1 都是保留字段,目前好像没什么用;
  • entryConunt 表示本类型的资源项个数;

紧跟着 ResTable_typeSpec 之后的,是大小为 entryConunt(=39) 的 int 型数组,可以看到数组里每个元素的值都一样,都为 0x40000000,表示资源是公共的。图中 chunkSize = 172,从头开始往后172字节后,就可定位到下一个数据结构了。

3.7 资源类型 ResTable_type

在类型规范数据块之后,就是资源类型数据块了,其头部定义如下:

struct ResTable_type
{
    struct ResChunk_header header;

    enum {
        NO_ENTRY = 0xFFFFFFFF
    };
    
    // The type identifier this chunk is holding.  Type IDs start
    // at 1 (corresponding to the value of the type bits in a
    // resource identifier).  0 is invalid.
    uint8_t id;
    
    enum {
        // If set, the entry is sparse, and encodes both the entry ID and offset into each entry,
        // and a binary search is used to find the key. Only available on platforms >= O.
        // Mark any types that use this with a v26 qualifier to prevent runtime issues on older
        // platforms.
        FLAG_SPARSE = 0x01,
    };
    uint8_t flags;

    // Must be 0.
    uint16_t reserved;
    
    // Number of uint32_t entry indices that follow.
    uint32_t entryCount;

    // Offset from header where ResTable_entry data starts.
    uint32_t entriesStart;

    // Configuration this collection of entries is designed for. This must always be last.
    ResTable_config config;
};
  • header:头部信息结构;
  • id:对应前面资源类型字符串池中的索引值,但是从1开始,表示资源类型,例如:anim、string等;
  • flags:
  • reserved:保留字段,始终为0;
  • entryCount:本类型的资源项个数;
  • entriesStart:资源数据块相对于头部的偏移字节数;
  • resConfig:描述配置信息,例如地区、语言、分辨率等等;

最后面的 config 是个配置信息,它有特定数据结构,我们先看看前面的几个字段:

接下来看看 ResTable_config 的数据结构,这个数据结构比较复杂:

/**
 * Describes a particular resource configuration.
 */
struct ResTable_config
{
    // Number of bytes in this structure.
    uint32_t size;
    
    union {
        struct {
            // Mobile country code (from SIM).  0 means "any".
            uint16_t mcc;
            // Mobile network code (from SIM).  0 means "any".
            uint16_t mnc;
        };
        uint32_t imsi;
    };
    
    union {
        struct {            
            char language[2];          
            char country[2];
        };
        uint32_t locale;
    };
   
    union {
        struct {
            uint8_t orientation;
            uint8_t touchscreen;
            uint16_t density;
        };
        uint32_t screenType;
    };
       
    union {
        struct {
            uint8_t keyboard;
            uint8_t navigation;
            uint8_t inputFlags;
            uint8_t inputPad0;
        };
        uint32_t input;
    };

    union {
        struct {
            uint16_t screenWidth;
            uint16_t screenHeight;
        };
        uint32_t screenSize;
    };
       
    union {
        struct {
            uint16_t sdkVersion;
            // For now minorVersion must always be 0!!!  Its meaning
            // is currently undefined.
            uint16_t minorVersion;
        };
        uint32_t version;
    };
    
    union {
        struct {
            uint8_t screenLayout;
            uint8_t uiMode;
            uint16_t smallestScreenWidthDp;
        };
        uint32_t screenConfig;
    };
    
    union {
        struct {
            uint16_t screenWidthDp;
            uint16_t screenHeightDp;
        };
        uint32_t screenSizeDp;
    };

    // The ISO-15924 short name for the script corresponding to this
    // configuration. (eg. Hant, Latn, etc.). Interpreted in conjunction with
    // the locale field.
    char localeScript[4];

    // A single BCP-47 variant subtag. Will vary in length between 4 and 8
    // chars. Interpreted in conjunction with the locale field.
    char localeVariant[8];

    // An extension of screenConfig.
    union {
        struct {
            uint8_t screenLayout2;      // Contains round/notround qualifier.
            uint8_t colorMode;          // Wide-gamut, HDR, etc.
            uint16_t screenConfigPad2;  // Reserved padding.
        };
        uint32_t screenConfig2;
    };
   
}

这个数据结构,定义了运营商、locale、屏幕属性、输入属性、屏幕尺寸、系统版本、屏幕配置、扩展屏幕属性等等。看到这些,我们基本就能明白它的作用了。通常,我们定义图片时,为了适配不同的分辨率,会定义 mipmap-xdpi、mipmap-xxhdpi、mipmap-xxxhdpi 等不同的文件夹,在里面分别放入同名但不同分辨率的图片资源。通过 ResTable_config 数据块,系统会通过当前设备的各种不同属性,找到最适合某个资源 id 的资源。

ResTable_config 后接着是一个大小为 entryCount 偏移数组,每一个数组元素都是一个4字节数据,用来描述资源项数据块 ResTable_entry 相对头部的偏移位置。 从图中的例子中可以看到,ResTable_config 数据结构的大小为 0x40(64个字节),紧跟着 entryCount = 39 个 int 数据,ResTable_type 的头大小为 84 字节,从 84 + 39 * 4 = 240 个字节开始,是下一个数据结构 ResTable_entry了,240 与 ResTable_type 结构里的 entriesStart 的值也是相匹配的。

这里有个很重要的地方容易搞错:

一般情况下,偏移数组的大小为 entryCount,后面会跟着同样数目的 ResTable_entry 数据,但这里会有所不同。偏移数组中的元素数量可能比其后面的 ResTable_entry 数量多,对于没有对应 ResTable_entry 结构的偏移数组中元素,其值为0xffffffff。
3.8 资源项数据 ResTable_entry

ResTable_type 后面 跟着的是 entryCount 个 ResTable_entry 数据结构,它用来描述资源的具体信息,ResTable_entry 的数据结构定义如下:

/**
 * This is the beginning of information about an entry in the resource
 * table.  It holds the reference to the name of this entry, and is
 * immediately followed by one of:
 *   * A Res_value structure, if FLAG_COMPLEX is -not- set.
 *   * An array of ResTable_map structures, if FLAG_COMPLEX is set.
 *     These supply a set of name/value mappings of data.
 */
struct ResTable_entry
{
    // Number of bytes in this structure.
    uint16_t size;

    enum {
        // If set, this is a complex entry, holding a set of name/value
        // mappings.  It is followed by an array of ResTable_map structures.
        FLAG_COMPLEX = 0x0001,
        // If set, this resource has been declared public, so libraries
        // are allowed to reference it.
        FLAG_PUBLIC = 0x0002,
        // If set, this is a weak resource and may be overriden by strong
        // resources of the same name/type. This is only useful during
        // linking with other resource tables.
        FLAG_WEAK = 0x0004
    };
    uint16_t flags;
    
    // Reference into ResTable_package::keyStrings identifying this entry.
    struct ResStringPool_ref key;
};

从中可以看到,ResTable_entry 里根据 flags 值的不同,表示的数据也不同,如果 flags & 0x0001 != 0,则 ResTable_entry 是一个 ResTable_map_entry 数据结构,ResTable_map_entry 继承自 ResTable_entry,其数据结构如下定义:

/**
 *  This is a reference to a unique entry (a ResTable_entry structure)
 *  in a resource table.  The value is structured as: 0xpptteeee,
 *  where pp is the package index, tt is the type index in that
 *  package, and eeee is the entry index in that type.  The package
 *  and type values start at 1 for the first item, to help catch cases
 *  where they have not been supplied.
 */
struct ResTable_ref
{
    uint32_t ident;
};

/**
 * Extended form of a ResTable_entry for map entries, defining a parent map
 * resource from which to inherit values.
 */
struct ResTable_map_entry : public ResTable_entry
{
    // Resource identifier of the parent mapping, or 0 if there is none.
    // This is always treated as a TYPE_DYNAMIC_REFERENCE.
    //指向父ResTable_map_entry的资源ID,如果没有父ResTable_map_entry,则等于0。
    ResTable_ref parent;
    
    // Number of name/value pairs that follow for FLAG_COMPLEX.
    //等于后面ResTable_map的数量
    uint32_t count;
};

ResTable_map_entry 后面紧跟着的是 count 个 ResTable_map 类型的数组,ResTable_map 的数据结构定义如下:

/**
 * A single name/value mapping that is part of a complex resource
 * entry.
 */
struct ResTable_map
{
    // The resource identifier defining this mapping's name.  For attribute
    // resources, 'name' can be one of the following special resource types
    // to supply meta-data about the attribute; for all other resource types
    // it must be an attribute resource.
    //资源项id
    ResTable_ref name;
    
    // This mapping's value.
    //资源项值
    Res_value value;
};

/**
 * Representation of a value in a resource, supplying type
 * information.
 */
struct Res_value
{
    // Number of bytes in this structure.
    uint16_t size;

    // Always set to 0.
    uint8_t res0;
    
    // Type of the data value.
    enum : uint8_t {
        // The 'data' is either 0 or 1, specifying this resource is either
        // undefined or empty, respectively.
        TYPE_NULL = 0x00,
        // The 'data' holds a ResTable_ref, a reference to another resource
        // table entry.
        TYPE_REFERENCE = 0x01,
        // The 'data' holds an attribute resource identifier.
        TYPE_ATTRIBUTE = 0x02,
        // The 'data' holds an index into the containing resource table's
        // global value string pool.
        TYPE_STRING = 0x03,
        // The 'data' holds a single-precision floating point number.
        TYPE_FLOAT = 0x04,
        // The 'data' holds a complex number encoding a dimension value,
        // such as "100in".
        TYPE_DIMENSION = 0x05,
        // The 'data' holds a complex number encoding a fraction of a
        // container.
        TYPE_FRACTION = 0x06,
        // The 'data' holds a dynamic ResTable_ref, which needs to be
        // resolved before it can be used like a TYPE_REFERENCE.
        TYPE_DYNAMIC_REFERENCE = 0x07,
        // The 'data' holds an attribute resource identifier, which needs to be resolved
        // before it can be used like a TYPE_ATTRIBUTE.
        TYPE_DYNAMIC_ATTRIBUTE = 0x08,

        // Beginning of integer flavors...
        TYPE_FIRST_INT = 0x10,

        // The 'data' is a raw integer value of the form n..n.
        TYPE_INT_DEC = 0x10,
        // The 'data' is a raw integer value of the form 0xn..n.
        TYPE_INT_HEX = 0x11,
        // The 'data' is either 0 or 1, for input "false" or "true" respectively.
        TYPE_INT_BOOLEAN = 0x12,

        // Beginning of color integer flavors...
        TYPE_FIRST_COLOR_INT = 0x1c,

        // The 'data' is a raw integer value of the form #aarrggbb.
        TYPE_INT_COLOR_ARGB8 = 0x1c,
        // The 'data' is a raw integer value of the form #rrggbb.
        TYPE_INT_COLOR_RGB8 = 0x1d,
        // The 'data' is a raw integer value of the form #argb.
        TYPE_INT_COLOR_ARGB4 = 0x1e,
        // The 'data' is a raw integer value of the form #rgb.
        TYPE_INT_COLOR_RGB4 = 0x1f,

        // ...end of integer flavors.
        TYPE_LAST_COLOR_INT = 0x1f,

        // ...end of integer flavors.
        TYPE_LAST_INT = 0x1f
    };
    uint8_t dataType;

    // The data for this item, as interpreted according to dataType.
    typedef uint32_t data_type;
    data_type data;

};

如果 flags & 0x0001 = 0,它后面跟着的是一个 Res_value 数据。ResTable_entry 总共有2种数据结构,一种是 map 形式的,包含一系列的键值对数据,一种是非 map 形式的,只包含一个能访问的具体数据项。再来看个具体实例:

先看第一段绿色横线标注的数据,它是一个 ResTable_entry 数据:

  • size = 0x08,表示有8个字节;
  • flags = 0x0002,表示它后面紧跟着的是 Res_value 数据;
  • key = 0x00000000,字符串在 package 数据中的资源名称字符串池中的索引;

红色横线标注的是 Res_value 数据:

  • size = 0x08,表示有8个字节;
  • res0 = 0x00,保留字段;
  • dataType = 0x03,可以查看到其对应的类型为 TYPE_STRING,表示这是一个字符串资源项;
  • data = 0x00000000,表示该字符串对应的是全局字符传池中的索引;

一个 ResTable_typeSpec 数据后面会跟着若干个 ResTable_type 数据,每个 ResTable_type 数据后面又跟着若干个 ResTable_entry 数据,而整个资源表里会有若干个 ResTable_typeSpec 数据存在。

4. 文件解析demo

源码地址:https://github.com/houjinyun/AndroidResourcesParse

其中还有几个地方没有搞明白,里面的实现可能会有问题:

  1. 字符串样式的解析暂没有实现;
  2. 如果资源表里包含多个 package,需要解析多个 package 信息,当然大部分情况下只会有一个 package 信息,在某些插件化方式实现的包或者修改过 aapt 打包方式的包里,可能会有多个 package 信息;

通过自己解析一遍之后,对照着资源表的数据来看,可以逐步搞清楚初次接触很多不理解的地方。

例如:

//字符串定义, R.string.username
<stirng name="username">kaka </string>

//图片定义,R.mipmap.ic_launcher
res/mipmap-xhdpi/ic_launcher.png
res/mipmap-xxhdpi/ic_launcher.png

这里面资源类型字符串有:string、mipmap,资源名称字符串有:username、ic_launcher,而全局资源字符串则有 kaka、res/mipmap-xhdpi/ic_launcher.png、res/mipmap-xxhdpi/ic_launcher.png 共计3个。

一般有多少种资源类型就会有多少个 ResTable_typeSpec ,以上面这个 mipmap 为例,资源表中会有一个代表 mipmap 的 ResTable_typeSpec 数据,它包含 2 个 ResTable_type 数据,一种对应的是 xhdpi,一种对应的是 xxhdpi,也就是说资源配置类型有多少种就会有多少个 ResTable_type 数据。

5. 资源混淆的原理

了解 resources.arsc 的文件结构,可以更好地帮助理解资源混淆的原理。以 res/mipmap-xhdpi/ic_launcher.png 这个为例,它表示 ic_launcher.png 这个图片在资源文件夹中的路径,而该路径在资源表中是定义在全局资源字符串池当中的。如果我们将该资源的文件路径在打包时重命名为 r/a/a.png 之类的,并且相应的修改 resources.arsc 资源表,那么是不是就实现了资源混淆了呢。

此外,所有的资源名,我们也可以修改成一个很短的名字,例如 R.layout.activity_user_login 这个资源名,在资源表的名称字符串池中会存在 activity_user_login 这个字符串,同样我们可以将之修改为简单的类似 a 这样的短字符串,这样也能进一步压缩大小了。

6. 怎么通过资源id查找到对应的资源

以 mipmap 图片资源为例,假设在不同的分辨率目录下有不同的图片资源,举个栗子:
xhdpi: a.png、b.png、c.png
xxhdpi:a.png、c.png
xxxhdpi:b.png

在该例子中,mipmap 图片资源总共有3种配置 xhdpi、xxhdpi、xxxhdpi,每种资源配置下面的图片各不相同,那么基于这个例子,生成的资源表大概如下:

  • 1个表示 mipmap 资源类型的 ResTable_typeSpec(类型规范数据),它会有一个 type id(类型id),它的资源个数为3(entryCount = 3);
  • 在 ResTable_typeSpec 数据后面紧跟着 3 个 ResTable_type 数据块,每个 ResTable_type 分别对应着 xhdpi、xxhdpi、xxxhdpi 这 3 种资源配置中的一种;
  • 每个 ResTable_type 包含长度为 3 的 ResTable_entry 数组,数组的索引就是资源的 entry id;
  • ResTable_entry 包含了资源名称字符串索引、资源值信息等,也就是说可以从中找到它所代表的资源名称如 a.png,还可以找到 a.png 在资源文件夹中的文件路径如 res/mipmap-xhdpi/a.png;
  • xhdpi 包含了 3 张图片,xxhdpi 包含了 2 张图片,xxxhdpi 包含了 1 张图片,每种配置都对应了 1 个 ResTable_type 数据结构,但实际上每种资源配置所拥有的资源文件个数是不一样的,但每个 ResTable_type 的 entryCount 值是一样的。比方说 xhdpi 的 ResTable_entry 顺序为 [a, b, c],xxhdpi 的顺序则为 [a, null, c],xxxhdpi 的顺序则为 [null, b, null],意思是同一个名字的资源文件在不同的资源配置里的数组索引值是一样的;
  • 一个资源 id 的格式为 0xpptteeee,通过 pp 可以找到资源表里的 package 信息,通过 tt(也就是 type id) 可以找到对应的 ResTable_typeSpec 信息,通过 eeee (也就是 entry id)可以找到对应的 ResTable_entry,最终就能找到对应的资源值了;

系列文章
Android apk瘦身最佳实践(一):去除R.class
Android apk瘦身最佳实践(二):代码混淆和资源压缩
Android apk瘦身最佳实践(三):资源混淆原理
Android apk瘦身最佳实践(四):采用AndResGuard进行资源混淆
Android apk瘦身最佳实践(五):图片压缩
Android apk瘦身最佳实践(六):采用D8编译器

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容