安卓资源管理框架
参考博客:http://blog.csdn.net/luoshengyang/article/details/8744683
assets类资源放在工程根目录的assets子目录下,它里面保存的是一些原始的文件,可以以任何方式来进行组织。这些文件最终会被原装不动地打包在apk文件中。如果我们要在程序中访问这些文件,那么就需要指定文件名来访问。
例如,假设在 assets目录下有一个名称为filename的文件,那么就可以使用以下代码来访问它:
AssetManager am= getAssets();
InputStream is = assset.open("filename");
res类资源放在工程根目录的res子目录下,它里面保存的文件大多数都会被编译,并且都会被赋予资源ID。这样我们就可以在程序中通过ID来访问res类的资源。res类资源按照不同的用途可以进一步划分为以下9种子类型:
这类资源以XML文件保存在res/animator目录下,用来描述属性动画。属性动画通过改变对象的属性来实现动画效果,例如,通过不断地修改对象的坐标值来实现对象移动动画,又如,通过不断地修改对象的Alpha通道值来实现对象的渐变效果。
这类资源以XML文件保存在res/anim目录下,用来描述补间动画。补间动画和属性动画不同,它不是通过修改对象的属性来实现,而是在对象的原来形状或者位置的基础上实现一个变换来得到的,例如,对对象施加一个旋转变换,就可以获得一个旋转动画,又如,对对象实施一个缩放变换,就可以获得一个缩放动画。从数学上来讲,就是在对象的原来形状或者位置的基础上施加一个变换矩阵来实现动画效果。注意,在动画的执行过程中,对象的属性是始终保持不变的,我们看到的只不过是它的一个变形副本。
这类资源以XML文件保存在res/color目录下,用描述对象颜色状态选择子。例如,我们可以定义一个选择子,规定一个对象在不同状态下显示不同的颜色。对象的状态可以划分为pressed、focused、 selected、checkable、checked、enabled和window_focused等7种。
这类资源以XML或者Bitmap文件保存在res/drawable目录下,用来描述可绘制对象。例如,我们可以在里面放置一些图片(.png, .9.png, .jpg, .gif),来作为程序界面视图的背景图。注意,保存在这个目录中的Bitmap文件在打包的过程中,可能会被优化的。例如,一个不需要多于256色的真 彩色PNG文件可能会被转换成一个只有8位调色板的PNG面板,这样就可以无损地压缩图片,以减少图片所占用的内存资源。
这类资源以XML文件保存在res/layout目录下,用来描述应用程序界面布局。
这类资源以XML文件保存在res/menu目录下,用来描述应用程序菜单,例如,Options Menu、Context Menu和Sub Menu。
这类资源以任意格式的文件保存在res/raw目录下,它们和assets类资源一样,都是原装不动地打包在apk文件中的,不过它们会被赋予资源ID,这样我们就可以在程序中通过ID来访问它们。例如,假设在res/raw目录下有一个名称为filename的文件,并且它在编译的过程,被赋予的资源 Id为R.raw.filename,那么就可以使用以下代码来访问它:
Resources res = getResources();
InputStream is = res .openRawResource(R.raw.filename);
这类资源以XML文件保存在res/values目录下,用来描述一些简单值,例如,数组、颜色、尺寸、字符串和样式值等,一般来说,这六种不同的值分别保存在名称为arrays.xml、colors.xml、 dimens.xml、strings.xml和styles.xml文件中。
这类资源以XML文件保存在res/xml目录下,一般就是用来描述应用程序的配置信息。
上述9种类型的资源文件,除了raw类型资源,以及Bitmap文件的drawable类型资源之外,其它的资源文件均为文本格式的XML文件,它们在打包的过程中,会被编译成二进制格式的XML文件。这些二进制格式的XML文件分别有一个字符串资源池,用来保存文件中引用到的每一个字符串,包括XML元素标签、属性名称、属性值,以及其它的一切文本值所使用到的字符串。这样原来在文本格式的XML文件中的每一个放置字符串的地方在二进制格式的XML文件中都被替换成一个索引到字符串资源池的整数值。
A. 文件占用更小。例如,假设在原来的文本格式的XML文件中,有四个地方使用的都是同一个字符串,那么在最终编译出来的二进制格式的XML文件中,字符串资源池只有一份字符串值,而引用它的四个地方只占用一个整数值。
B. 解析速度更快。由于在二进制格式的XML文件中,所有的XML元素标签和属性等值都是使用整数来描述的,因此,在解析的过程中,就不再需要进行字符串解析,这样就可以提高解析速度。
还有另外一个地方需要注意的是,每一个res资源在编译的打包完成之后,都会被分配一个资源ID,这些资源ID被终会被定义为Java常量值,保存在一个R.java文件中,与应用程序的其它源文件一起被编译到程序中,这样我们就可以在程序或者资源文件中通过这些ID常量来访问指定的资源。
我们在编译和打包应用程序资源的过程中,会生成一个resources.arsc文件,这个文件记录了所有的应用程序资源目录的信息,包括每一个资源名称、类型、值、ID以及所配置的维度信息。我们可以将这个resources.arsc文件想象成是一个资源索引表,这个资源索引表在给定资源ID和设备配置信息的情况下,能够在应用程序的资源目录中快速地找到最匹配的资源。
最后,我们可以通过图3来总结应用程序资源的编译、打包以及查找过程:
分析上图:
A. 除了assets和res/raw资源被原装不动地打包进APK之外,其它的资源都会被编译或者处理。
B. 除了assets资源之外,其它的资源都会被赋予一个资源ID。
C. 打包工具负责编译和打包资源,编译完成之后,会生成一个resources.arsc文件和一个R.java,前者保存的是一个资源索引表,后者定义了各个资源ID常量。
D. 应用程序配置文件AndroidManifest.xml同样会被编译成二进制的XML文件,然后再打包到APK里面去。
E. 应用程序在运行时通过AssetManager来访问资源,或通过资源ID来访问,或通过文件名来访问。
资源文件是通过Android资源打包工具aapt(Android Asset Package Tool)打包到APK文件里面的。为了支持Android资源管理框架快速定位最匹配资源,Android资源打包工具aapt在编译和打包资源的过程中,会执行以下两个额外的操作:
a. 赋予每一个非assets资源一个ID值,这些ID值以常量的形式定义在一个R.java文件中。
b. 生成一个resources.arsc文件,用来描述那些具有ID值的资源的配置信息,它的内容就相当于是一个资源索引表。
Android资源打包工具在编译应用程序资源之前,会创建一个资源表。这个资源表使用一个ResourceTable对象来描述,当应用程序资源编译完成之后,它就会包含所有资源的信息。有了这个资源表之后, Android资源打包工具就可以根据它的内容来生成资源索引表文件 resources.arsc了。
解析AndroidManifest.xml是为了获得要编译资源的应用程序的包名称。我们知道,在AndroidManifest.xml文件中,manifest标签的package属性的值描述的就是应用程序的包名称。有了这个包名称之后,就可以创建资源表了,即创建一个ResourceTable对象。
Android系统定义了一套通用资源,这些资源可以被应用程序引用。例如,我们在XML布局文件中指定一个LinearLayout的android:orientation属性的值为“vertical”时,这个“vertical”实际上就是在系统资源包里面定义的一个值。
在编译应用程序资源之前,Android资源打包工具aapt会创建一个AaptAssets对象,用来收集当前需要编译的资源文件。这些需要编译的资源文件就保存在AaptAssets类的成员变量mRes中.不同类型的资源文件他们的ResourceTypeSet对象不一样.
类型为values的ResourceTypeSet只有一个AaptGroup,它的名称为strings.xml。
类型为drawable的ResourceTypeSet只有一个AaptGroup,它的名称为icon.png。
类型为layout的ResourceTypeSet有两个AaptGroup,它们的名称分别为main.xml和sub.xml。
前面收集到的资源只是保存在一个AaptAssets对象中,这一步需要将这些资源同时增加到一个资源表中去,即增加到前面所创建的一个ResourceTable对象中去,因为最后我们需要根据这个ResourceTable来生成资源索引表,即生成resources.arsc文件。
类型为values的资源描述的都是一些简单的值,如数组、颜色、尺寸、字符串和样式值等,这些资源是在编译的过程中进行收集的。
类型为values的资源除了是string之外,还有其它很多类型的资源,其中有一些比较特殊,如bag、style、plurals和array类的资源。这些资源会给自己定义一些专用的值,这些带有专用值的资源就统称为Bag资源。
前面的六步操作为编译Xml资源文件准备好了所有的素材,因此,现在就开始要编译Xml资源文件了。除了values类型的资源文件,其它所有的Xml资源文件都需要编译。这里我们只挑layout类型的资源文件来说明Xml资源文件的编译过程,也就是这篇文章中要用到的例子中的main.xml文件,
上图的前三步都是在为第四步准备资源,在第四步执行:
这里生成资源符号为后面生成R.java文件做好准备的。从前面的操作可以知道,所有收集到的资源项都按照类型来保存在一个资源表中,即保存在一个 ResourceTable对象。因此,Android资源打包工具aapt只要遍历每一个Package里面的每一个Type,然后取出每一个 Entry的名称,并且根据这个Entry在自己的Type里面出现的次序来计算得到它的资源ID,那么就可以生成一个资源符号了,这个资源符号由名称以及资源ID所组成。
我们首先总结一下,经过上述八个操作之后,所获得的资源列表如下图:
有了这些资源项之后,Android资源打包工具aapt就可以按照流程来生成资源索引表resources.arsc了;
经过前面的九个步骤之后,应用程序的所有资源项就编译完成了,这时候就开始将应用程序的配置文件AndroidManifest.xml也编译成二进制格式的Xml文件。之所以要在应用程序的所有资源项都编译完成之后,再编译应用程序的配置文件,是因为后者可能会引用到前者。
在前面的第八步中,我们已经将所有的资源项及其所对应的资源ID都收集起来了,因此,这里只要将直接将它们写入到指定的R.java文件去就可以了。
所有资源文件都编译以及生成完成之后,就可以将它们打包到APK文件去了;
包括:
1、assets目录。
2、资源项索引文件resources.arsc。
3、res目录,但是不包括res/values目录, 这是因为res/values目录下的资源文件的内容经过编译之后,都直接写入到资源项索引表去了。
每一个Activity组件都关联有一个ContextImpl对象,这个ContextImpl对象就是用来描述Activity组件的运行上下文环境的。Activity组件是从Context类继承下来的,而ContextImpl同样是从Context类继承下来的。我们在Activity组件调用的大部分成员函数都是转发给与它所关联的一个ContextImpl对象的对应的成员函数来处理的,其中就包括用来访问应用程序资源的两个成员函数getResources和getAssets。
ContextImpl类的成员函数getAssets返回的是一个AssetManager对象,有了这个AssetManager对象之后,我们就可以通过文件名来访问那些被编译过或者没有被编译过的应用程序资源文件了。
每一个Activity组件在进程的加载过程中,都会创建一个对应的ContextImpl,并且调用这个ContextImpl对象的成员函数init来执行初始化Activity组件运行上下文环境的工作。其中就包括创建用来访问应用程序资源的Resources对象和AssetManager对象的工作,接下来,我们就从ContextImpl类的成员函数init开始分析Resources对象和AssetManager对象的创建以及初始化过程。
Resources类的成员变量mConfiguration指向的是一个Configuration对象,用来描述设备当前的配置信息;
AssetManager对象的成员函数setConfiguration来将这些配置信息设置到与之关联的C++层的AssetManager对象中它的成员变量mResources指向的是一个ResTable对象,这个ResTable对象描述的就是一个资源索引表。
AssetManager类的JNI方法getNativeStringBlock实际上就是将每一个资源包里面的resources.arsc文件的资源项值字符串资源池数据块读取出来,并且封装在一个C++层的StringPool对象中,然后AssetManager类的成员函数 makeStringBlocks再将该StringPool对象封装成一个Java层的StringBlock中。
这里从contentView到填充布局总共的步骤是22步:
Activity类的成员变量mWindow指向的是一个PhoneWindow对象,Activity类的成员函数setContentView实际上是调用PhoneWindow类的成员函数setContentView来进一步操作。
PhoneWindow类的成员变量mContentParent用来描述一个类型为DecorView的视图对象,或者这个类型为DecorView的 视图对象的一个子视图对象,用作UI容器。当它的值等于null的时候,就说明当前正在处理的Activity组件的视图对象还没有创建。在这种情况下,就会调用成员函数installDecor来创建当前正在处理的Activity组件的视图对象。否则的话,就说明是要重新设置当前正在处理的 Activity组件的视图。在重新设置之前,首先调用成员变量mContentParent所描述的一个ViewGroup对象来移除原来的UI内容。
PhoneWindow类的成员变量mLayoutInflater指向的是一个PhoneLayoutInflater对象。PhoneLayoutInflater类是从LayoutInflater类继续下来的,同时它也继承了LayoutInflater类的成员函数 inflate。通过调用PhoneWindow类的成员变量mLayoutInflater所指向的一个PhoneLayoutInflater对象的成员函数inflate,也就是从父类继承下来的成员函数inflate,就可以将参数layoutResID所描述的一个UI布局设置到 mContentParent所描述的一个视图容器中去。这样就可以将当前正在处理的Activity组件的UI创建出来。
最后,PhoneWindow类的成员函数还会调用一个Callback接口的成员函数onContentChanged来通知当前正在处理的Activity组件,它的视图内容发生改变了。每一个Activity组件都实现了一个Callback接口,并且将这个Callback接口设置到了与它所关联的PhoneWindow的内部去,因此,最后调用的实际上是Activity类的成员函数onContentChanged。
layoutInflater类三个参数版本的成员函数inflate中,首先是获得用来描述当前运行上下文环境的一个Resources对象,然后接调用这个Resources对象的成员函数getLayout来查找参数resource所描述的UI布局文件。
Resources类的成员函数getLayout找到了指定的UI布局文件之后,就会打开它。由于Android系统的UI布局文件是一个Xml文件,因此,Resources类的成员函数getLayout打开它之后,得到的是一个XmlResourceParser对象。有了这个 XmlResourceParser对象之后,LayoutInflater类三个参数版本的成员函数inflate就将它传递给另外一个三个参数版本的成员函数inflate,以便后者可以通过它来创建一个UI界面。
Resources类的成员函数getLayout的实现很简单,它通过调用另外一个成员函数loadXmlResourceParser来查找并且打开由参数id所描述的一个UI布局文件。
类型为layout的资源ID对应的资源值即为一个UI布局文件名称。有了这个UI布局文件名称之后,Resources类的成员函数 loadXmlResourceParser接着再调用另外一个四个参数版本的成员函数loadXmlResourceParser来加载对应的UI布局文件,并且得到一个XmlResourceParser对象返回给调用者。
Resources类的成员变量mAssets指向的是一个AssetManager对象,Resources类的成员函数getValue通过调用它的成员函数getResourceValue来获得与参数id所对应的资源的值。注意,如果AssetManager类的成员函数 getResourceValue查找不到与参数id所对应的资源,那么Resources类的成员函数getValue就会抛出一个类型为 NotFoundException的异常。
当AssetManager类的成员函数loadResourceValue的返回值block大于等于0的时候,实际上就表示参数ident所描述的资源项在当前应用程序使用的第block个资源索引表中,而当参数ident所描述的资源项是一个字符串时,那么就可以在第block个资源索引表的资源项值字符串资源池中找到对应的字符串,并且保存在参数outValue所描述的一个TypedValue对象的成员变量string中,以便返回给调用者使用。注意,最终得到的字符串在第block个资源索引表的资源项值字符串资源池中的位置就保存在参数outValue所描述的一个TypedValue对象的成员变量data中。
setManager类的成员函数loadResourceValue是一个JNI方法,它是由C++层的函数android_content_AssetManager_loadResourceValue来实现的
函数android_content_AssetManager_loadResourceValue主要是执行以下五个操作:
1. 调用函数assetManagerForJavaObject来将参数clazz所描述的一个Java层的AssetManager对象的成员变量mObject转换为一个C++层的AssetManager对象。
2. 调用上述得到的C++层的AssetManager对象的成员函数getResources来获得一个ResTable对象,这个ResTable对象描述的是一个资源表。
3. 调用上述得到的ResTable对象的成员函数getResource来获得与参数ident所对应的资源项值及其配置信息,并且保存在类型为Res_value的变量value以及类型为ResTable_config的变量config中。
4. 如果参数resolve的值等于true,那么就继续调用上述得到的ResTable对象的成员函数resolveReference来解析前面所得到的资源项值。
5. 调用函数copyValue将上述得到的资源项值及其配置信息拷贝到参数outValue所描述的一个Java层的TypedValue对象中去,返回调用者可以获得与参数ident所对应的资源项内容。
接下来,我们就主要分析第2~4操作,即AssetManager对象的成员函数getResources以及ResTable类的成员函数getResource和resolveReference的实现,以便可以了解Android应用程序资源的查找过程。
AssetManager类的成员函数getResources的实现看起来比较复杂,但是它要做的事情就是解析当前应用程序所使用的资源包里面的resources.arsc文件;当前应用程序所使用的资源包有两个,其中一个是系统资源包,即/system/framework/framework-res.apk,另外一个就是自己的APK文件。这些APK文件的路径都分别使用一个asset_path对象来描述,并且保存在AssetManager类的成员变量mAssetPaths中。
AssetManager类的成员函数getResources接下来按照以下步骤来解析当前应用程序所使用的每一个资源包里面的resources.arsc文件:
1. 检查资源包里面的resources.arsc文件已经提取出来。如果已经提取出来的话,那么以当前正在处理的资源包路径为参数,调用当前正在处理的 AssetManager对象的成员变量mZipSet所指向的一个ZipSet对象的成员函数getZipResourceTableAsset就可以获得一个对应的Asset对象。
2. 如果资源包里面的resources.arsc文件还没有提取出来,那么就会调用当前正在处理的AssetManager对象的成员函数 openNonAssetInPathLocked来将该resources.arsc文件提取出来。提取的结果就是获得一个对应的Asset对象,保存在变量ass中。注意,如果当前提取出来的Asset对象的地址值不等于全局变量kExcludedAsset的值,那么就将该Asset对象设置为当前正在处理的AssetManager对象的成员变量mZipSet所指向的一个ZipSet对象中去,这是通过调用该ZipSet对象的成员函数 setZipResourceTableAsset来实现的。
Android系统的资源管理框架提供了一种idmap机制,用来个性化定制一个资源包里面已有的资源项,也就是说,每一个资源包都可能有一个对应的 idmap文件,用来描述它所个性化定制的资源项。在提取和解析资源包的过程中,如果该资源包存在idmap文件,那么该idmap文件也会被解析,并且解析得到的一个Asset对象也会同时被增加到变量rt所指向的一个ResTable对象中去。