Android资源管理及资源的编译和打包过程分析
前言
在工作和学习中,我们除了要写一些业务代码,还要对项目的编译和打包流程有一定的认识,才能在遇到相关问题的时候能有所头绪。在这个过程中,我们往往会忽略掉资源文件是如何被添加进去的,Android的资源管理框架是一个很庞大和复杂的框架,资源编译打包的过程也很复杂和繁琐,本文就来浅谈一下Android的资源文件是如何编译和打包的吧,除了当做一个自我总结,也希望能对看到本文的你有所帮助和启发。当然了文章比较长,希望你能耐心的看完。
编译打包流程
Android一个包中,除了代码以外,还有很多的资源文件,这些资源文件在apk打包的过程中,通过AAPT工具,打包到apk中。我们首先看一下apk的打包流程图,
概述一下这张图,打包主要有一下几个步骤:
- 打包资源文件:通过aapt工具将res目录下的文件打包生成R.java文件和resources.arsc资源文件,比如AndroidManifest.xml和xml布局文件等。
- 处理aidl files:如果有aidl接口,通过aidl工具打包成java接口类
- java Compiler:javac编译,将R.java,源码文件,aidl.java编译为class文件
- dex:源码.class,第三方jar包等class文件通过dx工具生成dex文件
- apkbuilder:apkbuilder将所有的资源编译过的和不需要编译的,dex文件,arsc资源文件打包成一个完整的apk文件
- jarsigner:以上生成的是一个没有签名的apk文件,这里通过jarsigner工具对该apk进行签名,从而得到一个带签名的apk文件
- zipalign:对齐,将apk包中所有的资源文件距离文件起始偏移为4的整数倍,这样运行时可以减少内存的开销
资源分类
asset目录
存放原始资源文件,系统在编译时不会编译该目录下的资源文件,所以不能通过id的方式访问,如果要访问这些文件,需要指定文件名来访问。可以通过AssetManager访问原始文件,它允许你以简单的字节流的形式打开和读取和应用程序绑定在一起的原始资源文件。以下是一个从assets中读取本地的json文件的实例:
StringBuilder sb = new StringBuilder();
AssetManager assets = getAssets();
try {
InputStream open = assets.open(“xxx.json”);
//使用一个转换流转换为字符流进行读取
InputStreamReader inputStreamReader = new InputStreamReader(open);
//缓冲字符流
BufferedReader reader = new BufferedReader(inputStreamReader);
String readLine;
while((readLine = reader.readLine())!=null){
sb.append(readLine);
}
String s = sb.toString();
} catch (IOException e) {
e.printStackTrace();
}
复制代码
来看看一般项目中asset目录下会放些什么东东吧
res目录
存放可编译的资源文件(raw除外),编译时,系统会自动在R.java文件中生成资源文件的id,访问这种资源可以通过R.xxx.id即可。
目录 | 资源类型 |
---|---|
animator/ | 用于定义属性动画的xml |
anim/ | 用于定义补间动画的xml(属性动画也可以在这里定义) |
color/ | 用于颜色状态列表的xml |
drawable/ | 位图文件(.9.png、.png、.jpg、.gif) |
mipmap/ | 适用于不同启动器图标密度的可绘制对象文件 |
layout/ | 用于定义用户界面布局的 XML 文件 |
menu/ | 用于定义应用菜单(如选项菜单、上下文菜单或子菜单)的 XML 文件 |
values/ | 包含字符串、整型数和颜色等简单值的 XML 文件 |
XML/ | 可在运行时通过调用 Resources.getXML() 读取的任意 XML 文件。各种 XML 配置文件(如可搜索配置)都必须保存在此处。 |
font/ | 带有扩展名的字体文件(如 .ttf、.otf 或 .ttc),或包含 元素的 XML 文件 |
raw/ | 需以原始形式保存的任意文件 |
编译资源文件的结果
好处
对资源进行编译有以下两点好处
- 空间占用小:二进制xml文件占用的空间更小,因为所有的xml文件的标签、属性名称、属性值和内容所涉及到的字符串都会被统一收集到一个字符串池中。有了这个字符串池,原来使用字符串的地方就可以使用一个整数索引代替,从而可以减少文件的大小
- 解析速度快:二进制的xml文件解析的速度更快,xml文件中不在包含字符串值,所以就省去了解析字符串的时间,从而提高了速度。
编译完成之后,除了assets资源之外,会给其他所有的资源生成一个id,根据这些id,打包工具会生成一个资源索引表resources.arsc以及R.java文件。资源索引表会记录所有资源的信息,根据资源id和设备信息,快速的匹配最合适的资源,R文件则记录各个资源的id常量。
生成资源索引表
首先来看一张图,这是resources.arsc的结构图
整个resources.arsc是由一系列的chunk组成的,每一个chunk都有一个头,用来描述chunk的元数据。
- header:每个chunk的头部用来描述该chunk的元信息,包括当前chunk的类型,头大小,块大小等
- Global String Pool:全局字符串池,将所有字符串放到这个池子中,大家都复用这个池子中的数据,什么样的字符串会放到这个池子中呢?所有资源的文件的路径名,以及资源文件中所定义的资源的值,所以这个池子也可以叫做资源项的值字符串资源池,包含了所有在资源包里定义的资源项的值字符串,比如下面代码中
ABC
就存放在这里 - package数据块:
- package header:记录包的元数据,包名、大小、类型等
- 资源类型字符串池:存储所有类型相关的字符串,如:attr、drawable、layout、anim等
- 资源项名称字符串池:存储应用所有资源文件中资源项名称相关的字符串,比如下边的
app_name
就存放在这里。 - Type Spec:类型规范数据块,用来描述资源项的配置差异性,通过这个差异性描述,我们就可以知道每一个资源项的配置状况。Android设备众多,为了使得应用程序支持不同的大小、密度、语言,Android将资源组织为18个维度,每一个资源类都对应一组配置列表,配置这个资源类的不同维度,最后再使用一套匹配算法来为应用程序在资源目录中选择最合适的资源。
- config list:上边说到,每个type spec是一个类型的描述,每个类型会有多个维度,config list就是由多个ResTable_type结构来描述的,每一个ResTable_type描述的就是一个维度。
<resources>
<string name="app_name">ABC</string>
</resources>
复制代码
生成R文件和资源id
首先看一下R文件的结构图,每一种资源文件都对应一个静态内部类,对照前面所说的res文件目录结构,其中每个静态内部类中的一个静态常量分别定义一条资源标识符
或者这样:
public static final class layout {
public static final int main=0x7f030000;
}
复制代码
public static final int main=0x7f030000;
就表示layout目录下的main.xml文件。id中最高字节代表package的id,次高字节代表type的id,最后的字节代表当前类型中出现的序号。
- package id:相当于一个命名空间,限定资源的来源,Android系统当前定义了两个资源命令空间,其中系统资源命令空间是
0x01
,另外一个应用程序资源命令空间为0x7f
,所有位于 0x01到0x7f 之间的packageid都是合法的。 - type id:指资源的类型id,如anim、color、layout、raw...等,每一种资源都对应一个type id
- entry id:指每一个资源在其所属资源类型中出现的次序,不同资源类型的entry id是有可能相同的,但是由于他们的type id不同,所以一样可以进行区分。
资源文件只能以小写字母和下划线作为首字母,随后的名字中只能出现a-z或者0-9或者_.
这些字符,否则会报错。
当我们在相应的res的资源目录中添加资源文件时,便会在相应的R文件中的静态内部类中自动生成一条静态的常量,对添加的文件进行索引。
在布局文件中当我们需要为组件添加id属性时,可以使用@+id/idname
,+
表示在R文件的名为id的内部类中添加一条记录。如果这个id不存在,则会首先生成它。
资源文件打包流程
说完了资源文件的一些基本信息以后,相信你对apk包内的资源文件有了一个更加明确的认识了吧,接下来我们就来讲一讲资源文件是如何打包到apk中的,这个过程非常复杂,需要好好的理解和记忆。
Android资源打包工具在编译应用程序资源之前,会创建资源表ResourceTable,当应用程序资源编译完之后,这个资源表就包含了资源的所有信息,然后就可以根据这个资源表来生成资源索引文件resources.arsc了。
解析AndroidManifest.xml
获取要编译资源的应用程序的包名、minSdkVersion等,有了包名就可以创建资源表了,也就是ResourceTable。
添加被引用的资源包
通常在编译一个apk包的时候,至少会涉及到两个资源包,一个是被引用的系统资源包,里面有很多系统级的资源,比如我们熟知的四大布局 LinearLayout、FrameLayout等
以及一些属性layout_width、layout_height、layout_oritation
等,另一个就是当前正在编译的应用程序的资源包。
收集资源文件
在编译应用程序资源之前,aapt会创建AaptAssets对象,用来收集当前需要编译的资源文件,这些资源文件被保存在AaptAssets类的成员变量mRes中。
将收集到的资源增加到资源表ResourceTable
之前将资源添加到了AaptAssets中,这一步将资源添加到ResourceTable中,我们最后要根据这个资源表来生成resources.arsc资源索引表,回头看看arsc文件的结构图,它也有一个resourceTable。
这一步收集到资源表的资源是不包括values的,因为values资源需要经过编译后,才能添加到资源表中
编译values资源
values资源描述的是一些比较简单的轻量级资源,如strings/colors/dimen等,这些资源是在编译的过程中进行收集的
给bag资源分配id
values资源下,除了string之外,还有其他的一些特殊资源,这些资源给自己定义一些专用的值,比如LinearLayout的orientation属性,它的取值范围为 vertical 和 horizontal,这就相当于定义了vertical和horizontal两个bag。
在编译其他非values资源之前,我们需要给之前收集到的bag资源分配资源id,因为它可能会被其它非values类资源所引用。
编译xml文件
之前的六步为编译xml文件做好了准备,收集到了xml所需要用到的所有资源,现在可以开始编译xml文件了,比如layout、anims、animators等。编译xml文件又可以分为四个步骤
解析xml文件
这一步会将xml文件转化为一系列树形结构的XMLNode,每一个XMLNode都表示一个xml元素,解析完成之后,就可以得到一个根节点XMLNode,然后就可以根据这个根节点来完成下边的操作
赋予属性名称id
这一步为每个xml元素的属性名称都赋予资源id,比如一个控件TextView,它有layout_width和layout_height两个属性,这里就要给这些属性名称赋予一个资源id。对系统资源包来说,这些属性名称都是它定义好的一些列bag资源,在编译的时候,就已经分配好了资源id了。
对于每一个xml文件都是从根节点开始给属性名称赋予资源id,然后再递归的给每一个子节点属性名称赋予资源id,一直到每一个节点的属性名称都有了资源id为止。
解析属性值
这一步是上一步的进一步深化,上一步为每个属性赋值id,这一步对属性对应的值进行解析,比如对于刚才的TextView,就会对其width和height的值进行解析,可能是match_parent也可能是warp_content.
压平xml文件
将xml文件进行扁平化处理,将其变为二进制格式,有如下几个步骤
- 收集有资源id的属性名称字符串,并将它们放在一个数组里。这些收集到的属性名称字符串保存在字符串资源池中,与收集到的资源id数组是一一对应的。
- 收集xml文件中其他所有的字符串,也就是没有资源id的字符串
- 写入xml文件头,最终编译出来的xml二进制文件是一系列的chunk组成的,每一个chunk都有一个头部,来描述元信息。
- 写入资源池字符串,将第一步和第二步收集到的内容写入Global String pool中,也就是之前所说的arsc文件结构里的全局字符串资源池中
- 写入资源id,将所有的资源id收集起来,生成package时要用到,对应arsc文件的结构的package。
- 压平xml文件,就是将各个xml元素中的字符串都替换掉,这些字符串或者被替换为到字符串资源池的一个索引,或者被替换为一个具有类型的其他值
给资源生成资源符号
这里生成资源符号为之后生成R文件做准备,之前的操作将所有收集到的资源文件都按照类型保存在资源表中,也就是ResourceTable对象。aapt在这里只需要遍历每一个package里面的type,然后取出每一个entry的名称,在根据其在相应的type中出现的次序,就可以计算出相应的资源id了,然后就能得到其资源符号。资源符号=名称+资源id
根据资源id生成资源索引表
在这里我们将生成resources.arsc,对其生成的步骤再次进行拆解
- 按照package收集类型字符串,如drawable、string、layout、id等,当前被编译的应用程序有几个package,就对应几组类型字符串,每一组类型字符串保存在其所属的package中。
- 收集资源型名称字符串,还是以package为单位,比如在string.xml中,
<resources> <string name="app_name">ABC</string> </resources>
就可以收集其中的属性app_name - 收集资源项值字符串,还是上面的string.xml就可以收集到ABC
- 生成package数据块,就是按照之前说的resources.arsc文件格式中package的格式进行一步步的解析和收集
- 写入资源索引表头部,也就是ResTable_header
- 写入资源项的值字符串资源池,上面的第3步,将所有的值字符串收集起来了,这里直接写入就好了
- 写入package数据块,将第4步收集到的package数据块写入到资源索引表中。
经过以上几步,资源项索引表resources.arsc就生成好了。
编译AndroidManifest.xml文件
经过以上的几个步骤,应用程序的所有资源就编译完成了,这里就将应用程序的配置文件AndroidManifest.xml也编译为二进制文件。
生成R文件
到这里,我们已经知道了所有的资源以及其对应的id,然后就可以愉快的写入到R文件了,根据不同的type写到不同的静态内部类中,就像之前所描述的R文件的格式那样。
打包到APK
所有的资源文件都编译以及生成完之后,就可以将其打包到apk中了
- assets目录
- res目录,除了values之外,因为values目录下的资源文件经过编译以后,已经直接写入到资源索引表中去了
- 资源索引表resources.arsc
- 除了资源文件之外的其他文件(dex、AndroidManifest.xml、签名信息等)
结语
终于捋完了,整个资源文件的编译打包过程真的是很复杂又很繁琐的一个过程,在阅读的过程中要时刻对照着那几张机构图才能更好地对这些文件有更清晰的认识。资源文件在Android的学习和工作中是非常重要的,很多时候这些知识会被忽略掉,但是如果有时间好好捋一捋这些知识对于自身是一个很大的提升。
画个流程图
最后再用一张流程图来回顾一个整个流程