Resource.arsc生成和结构

从资源ID获取字符串资源

String appName = getResources().getString(R.string.app_name);

image-20200908155244841

从资源ID获取drawable资源的过程

 Drawable drawable = getResources().getDrawable(R.drawable.background, getTheme());

image-20200908155600516

研究源码来源

tinker gradle plugin中的resourceParser

ResDiffDecoder–AbstractApkParser–ResourceTableParser

AbstractApkParser.parseResourceTable

private DexClass[] dexClasses;
 private ResourceTable resourceTable;
 private String manifestXml;
 private ApkMeta apkMeta;
 private Set<Locale> locales;
 private List<CertificateMeta> certificateMetaList;
 private static final Locale DEFAULT_LOCALE;
 private Locale preferredLocale;

public void parseResourceTable() throws IOException {
   byte[] data = this.getFileData("resources.arsc");
   if (data == null) {
     this.resourceTable = new ResourceTable();
     this.locales = Collections.emptySet();
   } else {
     this.resourceTable = new ResourceTable();
     this.locales = Collections.emptySet();
     ByteBuffer buffer = ByteBuffer.wrap(data);
     ResourceTableParser resourceTableParser = new ResourceTableParser(buffer);
     resourceTableParser.parse();
     this.resourceTable = resourceTableParser.getResourceTable();
     this.locales = resourceTableParser.getLocales();
   }
 }

AndroidParser.resourceTableLogicalChange

public static boolean resourceTableLogicalChange(Configuration config) throws IOException {
   ApkParser parser = new ApkParser(config.mOldApkFile);
   ApkParser newParser = new ApkParser(config.mNewApkFile);
   parser.parseResourceTable();
   newParser.parseResourceTable();
   return parser.getResourceTable().equals(newParser.getResourceTable());
 }

ResourceTable设计

graph LR
resourceTableParser-->resourceTable-->ResourcePackage1-->typeStringPool
resourceTable-->ResourcePackage2
resourceTable-->ResourcePackage3
ResourcePackage1-->keyStringPool
ResourcePackage1-->TypeSpec1
ResourcePackage1-->TypeSpec2
ResourcePackage1-->TypeSpec3
ResourcePackage1-->ListType1("ListTypeConfig1")
ResourcePackage1-->ListType2("ListTypeConfig2")
ResourcePackage1-->ListType3("ListTypeConfig3")
ListType1-->Type1
Type1-->ResourceEntry1-->ResourceValue1
Type1-->ResourceEntry2-->ResourceValue2
Type1-->ResourceEntry3-->ResourceValue3
ListType1-->Type2
ListType1-->Type3

ChunkHeader设计

classDiagram
class ChunkHeader {
-int chunkType;
-int headerSize;
-long chunkSize;
}
class ResourceTableHeader {
 -long packageCount;
}
class StringPoolHeader {
-long stringCount;
-long styleCount;
-long flags;
-long stringsStart;
-long stylesStart;
}
class PackageHeader {
 -long id;
 -String name;
 -long typeStrings;
 -long lastPublicType;
 -long keyStrings;
 -long lastPublicKey;
}

class TypeSpecHeader {
 -short id;
 -short res0;
 -int res1;
 -long entryCount;
}
class TypeHeader {
 -short id;
 -short res0;
 -int res1;
 -long entryCount;
 -long entriesStart;
 -ResTableConfig config;
 -long startPoint;
}

ChunkHeader<|--ResourceTableHeader
ChunkHeader<|--StringPoolHeader
ChunkHeader<|--PackageHeader
ChunkHeader<|--TypeSpecHeader
ChunkHeader<|--TypeHeader

ResourceTableParser

private ByteOrder byteOrder;
 private StringPool stringPool;
 private ByteBuffer buffer;
 private ResourceTable resourceTable;
 private List<ResourceTableParser.FlagsOffset> mFlagsOffsets;
 private Set<Locale> locales;

public void parse() {
   ResourceTableHeader resourceTableHeader = (ResourceTableHeader)this.readChunkHeader();//1

  this.stringPool = ParseUtils.readStringPool(this.buffer, (StringPoolHeader)this.readChunkHeader());//2
   this.resourceTable = new ResourceTable();
   this.resourceTable.setFileSize((long)this.buffer.array().length);
   this.resourceTable.setStringPool(this.stringPool);
   PackageHeader packageHeader = (PackageHeader)this.readChunkHeader();//3
   for(int i = 0; (long)i < resourceTableHeader.getPackageCount(); ++i) {
     Pair<ResourcePackage, PackageHeader> pair = this.readPackage(packageHeader);//main
     this.resourceTable.addPackage((ResourcePackage)pair.getLeft());
     packageHeader = (PackageHeader)pair.getRight();//下一个package的Header

  }
   this.resourceTable.setBuffers(this.buffer);
 }


private ChunkHeader readChunkHeader() {
   long begin = (long)this.buffer.position();
   int chunkType = Buffers.readUShort(this.buffer);
   int headerSize = Buffers.readUShort(this.buffer);
   long chunkSize = Buffers.readUInt(this.buffer);
   switch(chunkType) {
   case 0:
   default:
     throw new ParserException("Unexpected chunk Type: 0x" + Integer.toHexString(chunkType));
   case 1:
     StringPoolHeader stringPoolHeader = new StringPoolHeader(chunkType, headerSize, chunkSize);
     stringPoolHeader.setStringCount(Buffers.readUInt(this.buffer));
     stringPoolHeader.setStyleCount(Buffers.readUInt(this.buffer));
     stringPoolHeader.setFlags(Buffers.readUInt(this.buffer));
     stringPoolHeader.setStringsStart(Buffers.readUInt(this.buffer));
     stringPoolHeader.setStylesStart(Buffers.readUInt(this.buffer));
     this.buffer.position((int)(begin + (long)headerSize));//main
     return stringPoolHeader;
   case 2:
     ResourceTableHeader resourceTableHeader = new ResourceTableHeader(chunkType, headerSize, chunkSize);
     resourceTableHeader.setPackageCount(Buffers.readUInt(this.buffer));
     this.buffer.position((int)(begin + (long)headerSize));//main
     return resourceTableHeader;

  ............

ResourceTable

private Map<Short, ResourcePackage> packageMap = new HashMap();
 private Map<String, ResourcePackage> packageNameMap = new HashMap();
 private ByteBuffer buffer;
 private StringPool stringPool;
 private long fileSize;
 public static Map<Integer, String> sysStyle = ResourceLoader.loadSystemStyles();

ResourcePackage

private String name;
 private short id;
 private StringPool typeStringPool;
 private StringPool keyStringPool;
 private Map<Short, TypeSpec> typeSpecMap = new HashMap();
 private Map<String, TypeSpec> typeSpecNameMap = new HashMap();
 private Map<Short, List<Type>> typesMap = new HashMap();
 private Map<String, List<Type>> typesNameMap = new HashMap();

TypeSpec

private long[] entryFlags;
 private String name;
 private short id;

Type

private String name;
 private short id;
 private TypeHeader typeHeader;
 private Locale locale;
 private ResTableConfig config;
 private StringPool keyStringPool;
 private ByteBuffer buffer;
 private long[] offsets;
 private StringPool stringPool;
 private ResourceTable resourceTable;
 private HashMap<Integer, ResourceEntry> resourceEntryHashMap = new HashMap();
 private HashMap<String, ResourceEntry> resourceEntryNameHashMap = new HashMap();

ResourceEntry

private int size;
 private int flags;
 private String key;
 private ResourceValue value;
 private Type type;

ResourceValue

protected final int value;
 protected int size;
 protected short dataType;

AAPT Source

源文件:android\frameworks\base\tools\aapt(2)\main.cpp

int main(int argc, char* const argv[])
  
源文件:android\frameworks\base\tools\aapt(2)\AaptAssets.cpp

Parser

http://androidxref.com/7.0.0_r1/xref/frameworks/base/tools/aapt2/unflatten/BinaryResourceParser.cpp

http://androidxref.com/7.0.0_r1/xref/frameworks/base/tools/aapt2/unflatten/ResChunkPullParser.cpp#27

Android 9.0上位于 appt2/format/binary/.cpp

File Format

image

Chunk Header in every Chunk

Resources.arsc文件格式是由一系列的chunk构成,每一个chunk均包含如下结构的ResChunk_header,用来描述这个chunk的基本信息

type:是当前这个chunk的类型

headerSize:是当前这个chunk的头部大

size:是当前这个chunk的大小

RES_TABLE_TYPE

Resources.arsc文件的第一个结构是资源索引表头部。其结构如下,描述了Resources.arsc文件的大小和资源包数量。

**header:就是标准的Chunk头部信息格式****

**packageCount:被编译的资源包的个数****

Android中一个apk可能包含多个资源包,默认情况下都只有一个就是应用的包名所在的资源包

image-20200908141532873

图中蓝色高亮的部分就是资源索引表头部。通过解析,我们可以得到如下信息,这个chunk的类型为RES_TABLE_TYPE,头部大小为0XC,整个chunk的大小为1400252byte,有一个编译好的资源包。

RES_STRING_POOL_TYPE

紧跟着资源索引表头部的是资源项的值字符串资源池,这个字符串资源池包含了所有的在资源包里面所定义的资源项的值字符串

*header:标准的Chunk头部信息结构*

*stringCount:字符串的个数*

**styleCount:字符串样式的个数****

**flags:字符串的属性,可取值包括0x000(UTF-16),0x001(字符串经过排序)、0X100(UTF-8)和他们的组合值****

**stringStart:字符串内容块相对于其头部的距离****

**stylesStart:字符串样式块相对于其头部的距离****

*实例:*

图中绿色高亮的部分就是字符串资源池头部,通过解析,我们可以得到如下信息,这个chunk的类型为RES_STRING_POOL_TYPE,即字符串资源池。头部大小为0X1C,整个chunk的大小为369524byte,有8073条字符串,72个字符串样式,为UTF-8编码,无排序,字符串内容块相对于此chunk头部的偏移为0X7F60,字符串样式块相对于此chunk头部的偏移为0X5A054。 紧接着头部的的是两个偏移数组,分别是字符串偏移数组和字符串样式偏移数组。这两个偏移数组的大小分别等于stringCount和styleCount的值,而每一个元素的类型都是无符号整型。整个字符中资源池结构如下。

字符串资源池中的字符串前两个字节为字符串长度,长度计算方法如下。另外如果字符串编码格式为UTF-8则字符串以0X00作为结束符,UTF-16则以0X0000作为结束符。 len = (((hbyte & 0x7F) « 8)) | lbyte; 字符串与字符串样式有一一对应的关系,也就是说如果第n个字符串有样式,则它的样式描述位于样式块的第n个元素。 字符串样式的结构包括如下两个结构体,ResStringPool_ref和ResStringPool_span。 一个字符串可以对应多个ResStringPool_span和一个ResStringPool_ref。ResStringPool_span在前描述字符串的样式,ResStringPool_ref在后固定值为0XFFFFFFFF作为占位符。样式块最后会以两个值为0XFFFFFFFF的ResStringPool_ref作为结束。

*实例:*

图中蓝色高亮的部分就是样式内容块,按照格式解析可以得出,第一个字符串和第二字符串无样式,第三个字符串第4个字符到第7个字符的位置样式为字符串资源池中0X1F88的字符,以此类推。

RES_TABLE_PACKAGE_TYPE

接着资源项的值字符串资源池后面的部分就是Package数据块,这个数据块记录编译包的元数据

**header:Chunk的头部信息数据结构****

*id:包的ID,等于Package Id,一般用户包的值Package Id为0X7F,系统资源包的Package Id为0X01;这个值很重要的,在后面我们构建前面说到的那个public.xml中的id值的时候需要用到。*

*name:包名*

*typeString:类型字符串资源池相对头部的偏移*

*lastPublicType:最后一个导出的Public类型字符串在类型字符串资源池中的索引,目前这个值设置为类型字符串资源池的元素个数。在解析的过程中没发现他的用途*

*keyStrings:资源项名称字符串相对头部的偏移*

*lastPublicKey:最后一个导出的Public资源项名称字符串在资源项名称字符串资源池中的索引,目前这个值设置为资源项名称字符串资源池的元素个数。在解析的过程中没发现他的用途*

*实例:*

图中紫色高亮的部分就是ResTable_package,按照上面的格式解析数据,我们可以得出,此Chunk的Type为RES_TABLE_PACKAGE_TYPE,头部大小为0X120,整个chunk的大小为1030716byte,Package Id为0X7F,包名称为co.runner.app,类型字符串资源池距离头部的偏移是0X120,有15条字符串,资源项名称字符串资源池0X1EC,有6249条字符串。 Packege数据块的整体结构,可以用以下的示意图表示:

img

其中Type String Pool和Key String Pool是两个字符串资源池,结构和资源项的值字符串资源池结构相同,分别对应类型字符串资源池和资源项名称字符串资源池。 再接下来的结构体可能是类型规范数据块或者类型资源项数据块,我们可以通过他们的Type来识别,类型规范数据块的Type为RES_TABLE_TYPE_SPEC_TYPE,类型资源项数据块的Type为RES_TABLE_TYPE_TYPE。

RES_TABLE_TYPE_SPEC_TYPE

类型规范数据块用来描述资源项的配置差异性。通过这个差异性描述,我们就可以知道每一个资源项的配置状况。知道了一个资源项的配置状况之后,Android资源管理框架在检测到设备的配置信息发生变化之后,就可以知道是否需要重新加载该资源项。类型规范数据块是按照类型来组织的,也就是说,每一种类型都对应有一个类型规范数据块

**header:Chunk的头部信息结构****

*id:标识资源的Type ID,Type ID是指资源的类型ID。资源的类型有animator、anim、color、drawable、layout、menu、raw、string和xml等等若干种,每一种都会被赋予一个ID。*

*res0:保留,始终为0*

*res1:保留,始终为0*

**entryCount:等于本类型的资源项个数,指名称相同的资源项的个数。****

实例:

img

图中绿色高亮的部分就是ResTable_typeSpec,按照上面的格式解析数据,我们可以得出,此Chunk的Type为RES_TABLE_TYPE_SPEC_TYPE,头部大小为0X10,整个chunk的大小为564byte,资源ID为1,本类型资源项数量为137。 ResTable_typeSpec后面紧跟着的是一个大小为entryCount的uint32_t数组,每一个数组元素都用来描述一个资源项的配置差异性的。

RES_TABLE_TYPE_TYPE

类型资源项数据块用来描述资源项的具体信息, 这样我们就可以知道每一个资源项的名称、值和配置等信息。 类型资源项数据同样是按照类型和配置来组织的,也就是说,一个具有n个配置的类型一共对应有n个类型资源项数据块。

**header:Chunk的头部信息结构****

*id:标识资源的Type ID*

*res0:保留,始终为0*

*res1:保留,始终为0*

*entryCount:等于本类型的资源项个数,指名称相同的资源项的个数。*

*entriesStart:等于资源项数据块相对头部的偏移值。*

*resConfig:指向一个ResTable_config,用来描述配置信息,地区,语言,分辨率等*

*实例:*

img

图中红色高亮的部分就是ResTable_type,按照上面的格式解析数据,我们可以得出,RES_TABLE_TYPE_TYPE,头部大小为0X44,整个chunk的大小为4086byte,资源ID为1,本类型资源项数量为137,资源数据块相对于头部的偏移为0X268。 ResTable_type后接着是一个大小为entryCount的uint32_t数组,每一个数组元素都用来描述一个资源项数据块的偏移位置。 紧跟在这个偏移数组后面的是一个大小为entryCount的ResTable_entry数组,每一个数组元素都用来描述一个资源项的具体信息。

struct ResTable_entry

ResTable_entry根据flags的不同,后面跟随的数据也不相同,如果flags此位为1,则ResTable_entry是ResTable_map_entry,ResTable_map_entry继承自ResTable_entry,其结构如下。

struct ResTable_map_entry

ResTable_map_entry其后跟随则count个ResTable_map类型的数组,ResTable_map的结构如下:

struct ResTable_map

*实例:*

img

图中颜色由深到浅就是一个完整的flags为1的资源项,现在就一起来解读这段数据的含义,这个资源项头部的大小为0X10,flags为1所以后面跟随的是ResTable_map数组,名称没有在资源项引用池中,没有父map_entry,有一个ResTable_map。 如果flags此位为0,则ResTable_entry其后跟随的是一个Res_value,描述一个普通资源的值,Res_value结构如下。

struct Res_value

*size:ResValue的头部大小*

*res0:保留,始终为0*

*dataType:数据的类型,可以从上面的枚举类型中获取*

*data:数据对应的索引*

这里我们看到了有一个转化的方法,这个我们在解析AndroidManifest文件的时候也用到了这个方法。

*实例:*

图中画红线的部分就是一个ResTable_entry其后跟随的是一个Res_value的例子,从中我们可以得出以下信息,这个头部大小为8,flags等于0,所以后面跟随的是Res_value,在资源项名称字符串资源池中的索引为150,对应的值是badge_continue_months,Res_value的大小为8,数据的类型是TYPE_STRING,在资源项的值字符串资源池的索引为1912,对应的值是res/drawable-nodpi-v4/badge_continue_months.png。

参考

Android 应用资源表(resources.arsc)解析–aapt

Resource.arsc文件解析

Android逆向之旅—解析编译之后的Resource.arsc文件格式

通过ApkTool分析resources.arsc文件以及resources.arsc文件的格式

一文读懂resource.arsc文件结构

Android6.0之App中的资源查找过程