resource.arsc格式简单介绍

示例应用

为方便分析,写了一个超简洁Demo,没集成AppCompat,就一个MainActivity,使用如下布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:textColor="@color/teal_200" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:layout_marginTop="100dp"
android:textColor="@android:color/holo_blue_bright" />

</FrameLayout>

values

1
2
3
<string name="app_name">Arsc</string>

<color name="teal_200">#FF03DAC5</color>

values-night

1
<color name="teal_200">#CC03DAC5</color>

整体格式介绍

网图镇楼

arsc.png

  1. 图中有一处错误,package名称长度是128,不是256
  2. Type Spec和Config List部分关系及结构不清晰,是每一个Spec后会跟多个Config,每一个Config后也会有多个Entry

整个arsc文件是一系列的Chunk,其中有些Chunk还可以包含一系列子Chunk

每个Chunk有一个统一的Header,包含这个Chunk的

  • 类型
  • Header大小
  • Header和关联的数据的总大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 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;
};

ResTable

最外层Chunk,整个文件是一个ResTable的Chunk

ResTable Chunk Header = ResChunk Header + package_count

1
2
3
4
5
6
struct ResTable_header
{
struct ResChunk_header header;
// The number of ResTable_package structures.
uint32_t packageCount;
};

ResTable Chunk = GlobalStringPool Chunk + package_count * Package Chunk

看AndResGuard的解析过程

1
2
3
4
5
6
7
8
9
10
11
12
private ResPackage[] readTable() throws IOException, AndrolibException {
nextChunkCheckType(Header.TYPE_TABLE);
int packageCount = mIn.readInt(); // package count
mTableStrings = StringBlock.read(mIn); // GlobalStringPool
ResPackage[] packages = new ResPackage[packageCount];
nextChunk();
for (int i = 0; i < packageCount; i++) {
packages[i] = readPackage(); // Read Packages
}
...
return packages;
}

ResStringPool

GlobalStringPool主要定义了全局的一些字符串,如我们定义的Strings,Layout文件路径等,格式为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
};

这里面的stringCount表示字符串常量数,styleCount表示这些字符串里的一些样式

AndResGuard的读取方法为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static StringBlock read(ExtDataInput reader) throws IOException {
reader.skipCheckChunkTypeInt(CHUNK_STRINGPOOL_TYPE, CHUNK_NULL_TYPE);
// 读取各种Header
int chunkSize = reader.readInt();
int stringCount = reader.readInt();
int styleCount = reader.readInt();
int flags = reader.readInt();
int stringsOffset = reader.readInt();
int stylesOffset = reader.readInt();
...
if (styleCount != 0) {
block.m_styleOffsets = reader.readIntArray(styleCount);
}
{
...
// 读取字符串字节数组
reader.readFully(block.m_strings);
}
if (stylesOffset != 0) {
int size = (chunkSize - stylesOffset);
// 读取style数组
block.m_styles = reader.readIntArray(size / 4);
}
return block;
}

以示例应用的resources.arsc分析,读取出的Global String为

Global string 0: Arsc
Global string 1: res/layout/activity_main.xml
Global string 2: res/mipmap-hdpi-v4/ic_launcher.png
Global string 3: res/mipmap-mdpi-v4/ic_launcher.png
Global string 4: res/mipmap-xhdpi-v4/ic_launcher.png
Global string 5: res/mipmap-xxhdpi-v4/ic_launcher.png
Global string 6: res/mipmap-xxxhdpi-v4/ic_launcher.png

可以看到全局的字符串就是资源文件的路径和定义的String

ResTable_package

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
};

文档中的typeIdOffset没见AndResGuard读取,没详细去看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private ResPackage readPackage() throws IOException, AndrolibException {
checkChunkType(Header.TYPE_PACKAGE);
int id = (byte) mIn.readInt(); // package id
String name = mIn.readNullEndedString(128, true); // package name
System.out.printf("reading packagename %s\n", name);

// 这些Header没用到
/* typeNameStrings */
mIn.skipInt();
/* typeNameCount */
mIn.skipInt();
/* specNameStrings */
mIn.skipInt();
/* specNameCount */
mIn.skipInt();
mCurrTypeID = -1;
// 资源类型字符串池
mTypeNames = StringBlock.read(mIn);
// 资源名称字符串池
mSpecNames = StringBlock.read(mIn);
mResId = id << 24;

mPkg = new ResPackage(id, name);
...
nextChunk();
while (mHeader.type == Header.TYPE_LIBRARY) {
// 引用的共享库的id与名称映射
readLibraryType();
}
while (mHeader.type == Header.TYPE_SPEC_TYPE) {
readTableTypeSpec();
}
return mPkg;
}

这里的很多Header里的字符AndResGuard都没用到,直接忽略了,其中有两个字符串池。

  • TypeName
  • SpecName

TypeNames表示字符串类型,如anim, layout等,以Demo包为例,TypeName字符串为

Type name 0: color
Type name 1: layout
Type name 2: mipmap
Type name 3: string

SpecName表示上述各类型下的字符串,如所有的id,图片资源名等,还是以Demo包为便,SpecName为

Spec name 0: gray
Spec name 1: teal_200
Spec name 2: activity_main
Spec name 3: ic_launcher
Spec name 4: app_name
以上分别是定义的Color名,layout名,图片包,字符串名,可以看到,都是资源的名称,其实也就是R.[string|id|layout|color].xxxxx中xxxx部分的名称

在每一个Package Chunk的最后,是如下类型的Chunk

  • 0个或多个引用的共享库
  • 多个TypeSpec Chunk
  • 共享库下也可能有多个TypeSpec Chunk

Package
—-Library
—-Library
——–TypeSpec
—-TypeSpec[anim]
—-TypeSpec[layout]
—-config
—-config
——–Entry
——–Entry
——–Entry
—-TypeSpec[color]

TypeSpec

一个TypeSpec就是一种资源类型,如anim, layout等都有一个对应的TypeSpec。定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* A specification of the resources defined by a particular type.
*
* There should be one of these chunks for each resource type.
*
* This structure is followed by an array of integers providing the set of
* configuration change flags (ResTable_config::CONFIG_*) that have multiple
* resources for that configuration. In addition, the high bit is set if that
* resource has been made public.
*/
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,
};
};

Demo中的Type Spec为

Type (color) Entry count (2)
Type (color) Entry count (2)
Type (layout) Entry count (1)
Type (mipmap) Entry count (1)
Type (string) Entry count (1)

每一个TypeSpec后会有多个Config,这个Config就是对应对一个资源的多维度配置,如语言,运营商,图片倍数等。以Demo为例

Color spec: 2 config,default + night
Layout spec: 1 config
Mipmap: 5 config, mdpi + hdpi + xhdpi + xxhdpi + xxxhdpi
String: 1 config

每个Config下会有多个Entry,这个Entry,就是对应的每一个资源,Entry里包含这个资源的名称,如mipmap-xhdpi/ic_launcher.png。

每个Entry后,紧邻着这个Entry关联的ResTable_Value,表示这个资源的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/**
* 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;
};

/**
* 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' 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.
...
// ...end of integer flavors.
TYPE_LAST_COLOR_INT = 0x1f,
// ...end of integer flavors.
TYPE_LAST_INT = 0x1f
};
uint8_t dataType;
// Structure of complex data values (TYPE_UNIT and TYPE_FRACTION)
enum {
...
// TYPE_DIMENSION: Value is raw pixels.
COMPLEX_UNIT_PX = 0,
// TYPE_DIMENSION: Value is Device Independent Pixels.
COMPLEX_UNIT_DIP = 1,
// TYPE_DIMENSION: Value is a Scaled device independent Pixels.
COMPLEX_UNIT_SP = 2,
...
};
// Possible data values for TYPE_NULL.
enum {
// The value is not defined.
DATA_NULL_UNDEFINED = 0,
// The value is explicitly defined as empty.
DATA_NULL_EMPTY = 1
};
// The data for this item, as interpreted according to dataType.
typedef uint32_t data_type;
data_type data;
void copyFrom_dtoh(const Res_value& src);
};

ResTable_Value就是资源的值,会是一个对字符串池的索引,后续可以从字符串池中拿到类似res/drawable/saturn__user_chat_press.xml这样的字符串

混淆资源名

要混淆资源,都有哪些要改的

  • 文件重命名,也可以改变目录,如从res/layout/main.xml改为r/l/m.xml
  • 修改全局字符串池中的文件路径的字符串,从res/layout/main.xml改为r/l/m.xml等
  • TypeSpec下的每个资源的名称,如id名称,布局文件名称,也就是R文件下所有的字段,从main改为m等

至于xml及代码中对这些资源的引用,不用管,因为编译时已经把对这些资源的文件名引用改为常量数字了,如Demo中main.xml的编译结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="-1"
android:layout_height="-1">

<TextView
android:textColor="@ref/0x7f010000"
android:layout_width="-2"
android:layout_height="-2"
android:text="Hello World!" />

<TextView
android:textColor="@ref/0x0106001b"
android:layout_width="-2"
android:layout_height="-2"
android:layout_marginTop="dimension(25601)"
android:text="@ref/0x7f040000" />
</FrameLayout>

(这些XML基实也有对应的Chunk,以及每个节点的属性,也有相应的Attribute Chunk)

Demo分析

用xxd查看Demo的resource.arsc文件如下图所示,注意这个文件是小端排序,如果是多字节的数字,要从后往前读

看上述输出,第5-8字节是38070000,由于是小端,转成数字是0x00000738,转换成10进制是1848,正好是文件的大小

第21-24字节是07000000,对应数字7,表示字符串数为7,再往后可以看到对应的7个字符串:一个string,一个layout路径,5个mipmap图片路径。

再往后可以看到包名me.angeldevil.arsc,128字节,所以后面有很多0

以及后面的SpecName,TypeName都能看到

SpecName:color, layout, mipmap, string
TypeName: gray, teal_200, activity_main, ic_launcher, app_name

参考资料

  1. ResourceTypes.h
  2. AndroidGradlePlugin官方支持,AGP4.2官方提供了对资源文件名的混淆,但还在alpha阶段,还不知道支持到什么程度,如是否有白名单配置,没文档
  3. 深入浅出的理解Android resources.arsc文件
  4. AndResGuard源码
  5. 安装包立减1M–微信Android资源混淆打包工具
  6. 谷歌出品Arsc文件Dumper