Android应用程序资源的编译和打包过程分析 (转自老罗的博客)
我们知道,在一个APK文件中,除了有代码文件之外,还有很多资源文件。这些资源文件是通过Android资源打包工具aapt(Android Asset Package Tool)打包到APK文件里面的。在打包之前,大部分文本格式的XML资源文件还会被编译成二进制格式的XML资源文件。在本文中,我们就详细分析 XML资源文件的编译和打包过程,为后面深入了解Android系统的资源管理框架打下坚实的基础。
在前面Android资源管理框架(Asset Manager)简要介绍和学习计划一 文中提到,只有那些类型为res/animator、res/anim、res/color、res/drawable(非Bitmap文件,即 非.png、.9.png、.jpg、.gif文件)、res/layout、res/menu、res/values和res/xml的资源文件均会从 文本格式的XML文件编译成二进制格式的XML文件,如图1所示:
图1 Android应用程序资源的编译和打包过程
这些XML资源文件之所要从文本格式编译成二进制格式,是因为:
1. 二进制格式的XML文件占用空间更小。这是由于所有XML元素的标签、属性名称、属性值和内容所涉及到的字符串都会被统一收集到一个字符串资源池中去,并 且会去重。有了这个字符串资源池,原来使用字符串的地方就会被替换成一个索引到字符串资源池的整数值,从而可以减少文件的大小。
2. 二进制格式的XML文件解析速度更快。这是由于二进制格式的XML元素里面不再包含有字符串值,因此就避免了进行字符串解析,从而提高速度。
将XML资源文件从文本格式编译成二进制格式解决了空间占用以及解析效率的问题,但是对于Android资源管理框架来说,这只是完成了其中的一部分工作。Android资源管理框架的另外一个重要任务就是要根据资源ID来快速找到对应的资源。
在前面Android资源管理框架(Asset Manager)简要介绍和学习计划一 文中提到,为了使得一个应用程序能够在运行时同时支持不同的大小和密度的屏幕,以及支持国际化,即支持不同的国家地区和语言,Android应用程序资源 的组织方式有18个维度,每一个维度都代表一个配置信息,从而可以使得应用程序能够根据设备的当前配置信息来找到最匹配的资源来展现在UI上,从而提高用 户体验。
由于Android应用程序资源的组织方式可以达到18个维度,因此就要求Android资源管理框架能够快速定位最匹配设备当前配置信息的资源来展现在 UI上,否则的话,就会影响用户体验。为了支持Android资源管理框架快速定位最匹配资源,Android资源打包工具aapt在编译和打包资源的过 程中,会执行以下两个额外的操作:
1. 赋予每一个非assets资源一个ID值,这些ID值以常量的形式定义在一个R.java文件中。
2. 生成一个resources.arsc文件,用来描述那些具有ID值的资源的配置信息,它的内容就相当于是一个资源索引表。
有了资源ID以及资源索引表之后,Android资源管理框架就可以迅速将根据设备当前配置信息来定位最匹配的资源了。接下来我们在分析Android应 用程序资源的编译和打包过程中,就主要关注XML资源的编译过程、资源ID文件R.java的生成过程以及资源索引表文件resources.arsc的 生成过程。
Android资源打包工具在编译应用程序资源之前,会创建一个资源表。这个资源表使用一个ResourceTable对象来描述,当应用程序资源编 译完成之后,它就会包含所有资源的信息。有了这个资源表之后, Android资源打包工具就可以根据它的内容来生成资源索引表文件 resources.arsc了。
接下来,我们就通过ResourceTable类的实现来先大概了解资源表里面都有些什么东西,如图2所示:
图2 ResourceTable的实现
ResourceTable类用来总体描述一个资源表,它的重要成员变量的含义如下所示:
--mAssetsPackage:表示当前正在编译的资源的包名称。
--mPackages:表示当前正在编译的资源包,每一个包都用一个Package对象来描述。例如,一般我们在编译应用程序资源时,都会引用系统预先编译好的资源包,这样当前正在编译的资源包除了目标应用程序资源包之外,就还有预先编译好的系统资源包。
--mOrderedPackages:和mPackages一样,也是表示当前正在编译的资源包,不过它们是以Package ID从小到大的顺序保存在一个Vector里面的,而mPackages是一个以Package Name为Key的DefaultKeyedVector。
--mAssets:表示当前编译的资源目录,它指向的是一个AaptAssets对象。
Package类用来描述一个包,这个包可以是一个被引用的包,即一个预先编译好的包,也可以是一个正在编译的包,它的重要成员变量的含义如下所示:
--mName:表示包的名称。
--mTypes:表示包含的资源的类型,每一个类型都用一个Type对象来描述。资源的类型就是指animimator、anim、color、drawable、layout、menu和values等。
--mOrderedTypes:和mTypes一样,也是表示包含的资源的类型,不过它们是Type ID从小到大的顺序保存在一个Vector里面的,而mTypes是一个以Type Name为Key的DefaultKeyedVector。
Type类用来描述一个资源类型,它的重要成员变量的含义如下所示:
--mName:表示资源类型名称。
--mConfigs:表示包含的资源配置项列表,每一个配置项列表都包含了一系列同名的资源,使用一个ConfigList来描述。例如,假设有main.xml和sub.xml两个layout类型的资源,那么main.xml和sub.xml都分别对应有一个ConfigList。
--mOrderedConfigs:和mConfigs一样,也是表示包含的资源配置项,不过它们是以Entry ID从小到大的顺序保存在一个Vector里面的,而mConfigs是以Entry Name来Key的DefaultKeyedVector。
--mUniqueConfigs:表示包含的不同资源配置信息的个数。我们可以将mConfigs和mOrderedConfigs看作是按照名称的不同来划分资源项,而将mUniqueConfigs看作是按照配置信息的不同来划分资源项。
ConfigList用来描述一个资源配置项列表,它的重要成员变量的含义如下所示:
--mName:表示资源项名称,也称为Entry Name。
--mEntries: 表示包含的资源项,每一个资源项都用一个Entry对象来描述,并且以一个对应的ConfigDescription为Key保存在一个 DefaultKeyedVector中。例如,假设有一个名称为icon.png的drawable资源,有三种不同的配置,分别是ldpi、mdpi 和hdpi,那么以icon.png为名称的资源就对应有三个项。
Entry类用来描述一个资源项,它的重要成员变量的含义如下所示:
--mName:表示资源名称。
--mItem:表示资源数据,用一个Item对象来描述。
Item类用来描述一个资源项数据,它的重要成员变量的含义如下所示:
--value:表示资源项的原始值,它是一个字符串。
--parsedValue:表示资源项原始值经过解析后得到的结构化的资源值,使用一个Res_Value对象来描述。例如,一个整数类型的资源项的原始值为“12345”,经过解析后,就得到一个大小为12345的整数类型的资源项。
ConfigDescription类是从ResTable_config类继承下来的,用来描述一个资源配置信息。ResTable_config 类的成员变量imsi、locale、screenType、input、screenSize、version和screenConfig对应的实际上 就是在前面Android资源管理框架(Asset Manager)简要介绍和学习计划一文提到的18个资源维度。
前面提到,当前正在编译的资源目录是使用一个AaptAssets对象来描述的,它的实现如图3所示:
图3 AaptAssets类的实现
AaptAssets类的重要成员变量的含义如下所示:
--mPackage:表示当前正在编译的资源的包名称。
--mRes:表示所包含的资源类型集,每一个资源类型都使用一个ResourceTypeSet来描述,并且以Type Name为Key保存在一个KeyedVector中。
--mHaveIncludedAssets:表示是否有引用包。
--mIncludedAssets:指向的是一个AssetManager,用来解析引用包。引用包都是一些预编译好的资源包,它们需要通过AssetManager来解析。事实上,Android应用程序在运行的过程中,也是通过AssetManager来解析资源的。
--mOverlay: 表示当前正在编译的资源的重叠包。重叠包是什么概念呢?假设我们正在编译的是Package-1,这时候我们可以设置另外一个Package-2,用来告 诉aapt,如果Package-2定义有和Package-1一样的资源,那么就用定义在Package-2的资源来替换掉定义在Package-1的 资源。通过这种Overlay机制,我们就可以对资源进行定制,而又不失一般性。
ResourceTypeSet类实际上描述的是一个类型为AaptGroup的KeyedVector,并且这个KeyedVector是以 AaptGroup Name为Key的。AaptGroup类描述的是一组同名的资源,类似于前面所描述的ConfigList,它有一个重要的成员变量mFiles,里面 保存的就是一系列同名的资源文件。每一个资源文件都是用一个AaptFile对象来描述的,并且以一个AaptGroupEntry为Key保存在一个 DefaultKeyedVector中。
AaptFile类的重要成员变量的含义如下所示:
--mPath:表示资源文件路径。
--mGroupEntry:表示资源文件对应的配置信息,使用一个AaptGroupEntry对象来描述。
--mResourceType:表示资源类型名称。
--mData:表示资源文件编译后得到的二进制数据。
--mDataSize:表示资源文件编译后得到的二进制数据的大小。
AaptGroupEntry类的作用类似前面所描述的ResTable_config,它的成员变量mcc、mnc、locale、vendor、 screenLayoutSize、screenLayoutLong、orientation、uiModeType、uiModeNight、 density、tounscreen、keysHidden、keyboard、navHidden、navigation、screenSize和 version对应的实际上就是在前面Android资源管理框架(Asset Manager)简要介绍和学习计划一文提到的18个资源维度。
了解了ResourceTable类和AaptAssets类的实现之后,我们就可以开始分析Android资源打包工具的执行过程了,如图4所示:
图4 Android资源打包工具的执行过程
假设我们当前要编译的应用程序资源目录结构如下所示:
public final class R { // ... public static final class string { public static final int string1=0x7f040000; public static final int string3=0x7f040001; } }
这时候第三方应用程序就会认为0x7f040001引用的永远是字符串“String 3”。
假设将来的某一天,我们需要在strings.xml文件中增加一个新的字符串,如下所示:
public final class R { // ... public static final class string { public static final int string1=0x7f040000; public static final int string2=0x7f040001; public static final int string3=0x7f040002; // New ID! Was 0x7f040001 } }
这就完蛋了,这时候第三方应用程序通过0x7f040001引用到的字符串变成了“String 2”。
如果我们使用上述的public.xml文件将字符串“String 3”固定为0x7f040001,那么Android资源打包工具aapt为字符串资源项string1、 string2和string3分配的资源ID就会如下所示:
public final class R { // ... public static final class string { public static final int string1=0x7f040000; public static final int string2=0x7f040002; public static final int string3=0x7f040001; // Resource ID from public.xml } }
这样第三方应用程序通过0x7f040001引用到的字符串仍然是“String 3”。
注意,我们在开发应用程序时,一般是不需要用到public.xml文件的,因为我们的资源基本上都是在内部使用的,不会导出来给第三方应用程序使用。只 在内部使用的资源,不管它的ID如何变化,我们都可以通过R.java文件定义的常量来正确地引用它们。只有系统定义的资源包才会使用到 public.xml文件,因为它定义的资源是需要提供给第三方应用程序使用的。
Step 2. 写入类型字符串资源池
在前面的第1个操作中,我们已经将每一个Package用到的类型字符串收集起来了,因此,这里就可以直接将它们写入到Package资源项元信息数据块头部后面的那个数据块去。
Step 3. 写入资源项名称字符串资源池
在前面的第2个操作中,我们已经将每一个Package用到的资源项名称字符串收集起来了,这里就可以直接将它们写入到类型字符串资源池后面的那个数据块去。
Step 4. 写入类型规范数据块
类型规范数据块用来描述资源项的配置差异性。通过这个差异性描述,我们就可以知道每一个资源项的配置状况。知道了一个资源项的配置状况之 后,Android资源管理框架在检测到设备的配置信息发生变化之后,就可以知道是否需要重新加载该资源项。类型规范数据块是按照类型来组织的,也就是 说,每一种类型都对应有一个类型规范数据块。
类型规范数据块的头部是用一个ResTable_typeSpec来定义的。ResTable_typeSpec定义在文件frameworks/base/include/utils/ResourceTypes.h中,如下所示:
public final class R { ...... public static final class layout { public static final int main=0x7f030000; public static final int sub=0x7f030001; } ...... }
注意,每一个资源类型在R.java文件中,都有一个对应的内部类,例如,类型为layout的资源项在R.java文件中对应的内部类为layout,而类型为string的资源项在R.java文件中对应的内部类就为string。
十二. 打包APK文件
所有资源文件都编译以及生成完成之后,就可以将它们打包到APK文件去了,包括:
1. assets目录。
2. res目录,但是不包括res/values目录, 这是因为res/values目录下的资源文件的内容经过编译之后,都直接写入到资源项索引表去了。
3. 资源项索引文件resources.arsc。
当然,除了这些资源文件外,应用程序的配置文件AndroidManifest.xml以及应用程序代码文件classes.dex,还有用来描述应用程 序的签名信息的文件,也会一并被打包到APK文件中去,这个APK文件可以直接拿到模拟器或者设备上去安装。
至此,我们就分析完成Android应用程序资源的编译和打包过程了,其中最重要的是要掌握以下四个要点:
1. Xml资源文件从文本格式编译为二进制格式的过程。
2. Xml资源文件的二进制格式。
3. 资源项索引表resources.arsc的生成过程。
4. 资源项索引表resources.arsc的二进制格式。
理解了Android应用程序资源的编译和打包过程之后,接下来我们就可以分析Android应用程序在运行时查找索引的过程了,敬请关注!
老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!