资源管理框架概述

资源管理框架概述

功能

Android资源管理模块的主要功能包括对资源的分类、组织、编译、打包、加载、引用等。这些功能主要分布在AAPT和AssetManager两个模块中。AAPT负责资源的组织、编译、打包这些工作,而资源的加载和引用则主要由AssetManager来实现。

使用示例

我们在Android开发的过程中跟UI相关的操作,一定是R.id、R.string等内容
例如,我们设置一个TextView的内容时会使用如下代码:

TextView textView = findViewById(R.id.text);
textView.setText(R.string.next);
String string = getResources().getString(R.string.next);
textView.setText(string);

不过,在我们编译后class文件,却发现代码变成了:

TextView textView = (TextView)this.findViewById(2131230960);
textView.setText(2131492907);
String string = this.getResources().getString(2131492907);
textView.setText(string);

我们看到

getResources().getString(R.string.next)在编译后在文件中会成为getResources().getString(2131492907)

实际上这涉及到R文件的编译和资源替换。先看下资源查找大致行为:

在查找此资源时,系统会先将其转为十六进制: 2131492907 = 0x7f0C002B

这时的资源索引就是为0x7f0C002B

资源索引具有固定的格式:「0xPPTTEEEE」

PackageId(2位) + TypeId(2位) + EntryId(4位)

PP:Package ID,包的命名空间,取值范围为[0x01, 0x7f],第三方应用均为7f。

TT:资源类型,有anim、layout、mipmap、string、style等资源类型。

EEEE:代表某一类资源在偏移数组中的值

所以,0x7f0C002B中 PackageId = 0x7f、TypeId = 0x0C、EntryId = 0x002B

我们发现 后面只有四位,也就是最多有 65535个值,我们翻看源码,其实aapt会有一个检查,
// TableFlattener.cpp

CHECK(num_total_entries != 0);
CHECK(num_total_entries <= std::numeric_limits<uint16_t>::max());

如果超出了这些值,则会报错,如下

最简单的我们可以将arsc函数想象成一个含有多个Pair数组的文件,且每个资源类型(TypeId)对应一个Pair[](或多个,为了便于理解先只认为是一个)。因此在arsc中查找0x7f0C002B元素的值,就是去设法找到TypeId=0x0C所对应的数组,然后找到其中的第0X002B号元素。这个元素恰好就是"next => 下一个",左边是资源名称,右边是资源的值,有了这个字符串程序便可以访问到对应的资源了。

当然实际的arsc文件在结构上要稍微复杂一点,我们会在后续的内容中分析 arsc文件的生成和组织结构

资源的分类

Android资源的总体来说可以分为两大类:Assets和res。

Assets资源

Assets类资源放在工程根目录的assets子目录下,它里面保存的是一些原始的文件,可以以任何方式来进行组织。这些文件最终会被原装不动地打包在apk文件中。AAPT在编译打包的时候不会对它做任何的处理,当然也不会为它生成资源ID,如果我们要在程序中访问这些文件,那么就需要指定文件名来访问。


InputStream is = getResources().getAssets().open( "learn_assets.txt" ) ;

//读取文本内容
private String getAssetsString(String fileName) {
    StringBuilder stringBuilder = new StringBuilder();
    try {
        BufferedReader bf = new BufferedReader(new InputStreamReader(
                getAssets().open(fileName), "UTF-8") );
        String line;
        while ((line = bf.readLine()) != null) {
            stringBuilder.append(line);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return stringBuilder.toString();
}

res资源

res类资源放在工程根目录的res子目录下,它里面保存的文件大多数都会被编译,AAPT在编译的时候,都会被赋予资源ID。这样我们就可以在程序中通过ID来访问res类的资源,当然,我们也可以通过资源名称来访问,但是Android并不推荐这样做,因为这样要遍历整个key StringPool,并且对每个字符串做比较,效率极低。这里多说一句,所谓的资源ID,也就是我们经常使用的R文件,其本质是资源的一个索引。使用索引来访问,效率当然比遍历查询要高太多了。

// raw
InputStream inputStream = getResources().openRawResource(R.raw.learn_assets);                                       
// 根据资源id获取资源
String stringValue = getResources().getString(R.string.app_name);

// 根据资源名获取资源
int drawableId = getResources().getIdentifier("ic_launcher", "drawable", getPackageName());
Drawable drawable = getResources().getDrawable(drawableId);

res类资源按照不同的用途可以进一步划分为以下11种子类型:参考google 官方文档

animator

这类资源以XML文件保存在res/animator目录下,用来描述属性动画。属性动画通过改变对象的属性来实现动画效果,比如渐变效果等。

anim

这类资源以XML文件保存在res/anim目录下,用来描述补间动画。补间动画和属性动画不同,它不是通过修改对象的属性来实现,而是在对象的原来形状或者位置的基础上实现一个变换来得到的。

color

这类资源以XML文件保存在res/color目录下,用描述对象颜色状态Selector,我们可以定义一个Selector,规定一个对象在不同状态下显示不同的颜色。对象的状态可以划分为pressed、focused、selected、checkable、checked、enabled和window_focused等7种。

drawable

这类资源以XML或者Bitmap文件保存在res/drawable目录下,用来描述可绘制对象。可以在里面放置一些图片(.png, .9.png, .jpg, .gif),来作为程序界面视图的背景图。注意,保存在这个目录中的Bitmap文件在打包的过程中,可能会被优化的。例如,一个不需要多于256色的真彩色PNG文件可能会被转换成一个只有8位调色板的PNG面板,这样就可以无损地压缩图片,以减少图片所占用的内存资源。 文件有如下几种类型: Bitmap files Nine-Patches (re-sizable bitmaps) State lists Shapes Animation drawables Other drawables 有兴趣可以查看 官方文档

mipmap

mipmap仅仅是用来放不同分辨率下的启动器图标,其他绘制图片还是要放到drawable中去。(敲黑板,注意与drawable的区分)

layout

这类资源以XML文件保存在res/layout目录下,用来描述应用程序界面布局。

menu

这类资源以XML文件保存在res/menu目录下,用来描述应用程序菜单,例如,Options Menu、Context Menu和Sub Menu。

raw

这类资源以任意格式的文件保存在res/raw目录下,它们和assets类资源一样,都是原装不动地打包在apk文件中的,不过它们会被赋予资源ID,这样我们就可以在程序中通过ID来访问它们。例如,假设在res/raw目录下有一个名称为filename的文件,并且它在编译的过程,被赋予的资源ID为R.raw.filename,那么就可以使用以下代码来访问它:

Resources res = getResources();
InputStream is = res.openRawResource(R.raw.filename);
values

这类资源以XML文件保存在res/values目录下,用来描述一些简单值,例如,数组、颜色、尺寸、字符串和样式值等,一般来说,这六种不同的值分别保存在名称为arrays.xml、colors.xml、dimens.xml、strings.xml和styles.xml文件中。

xml

这类资源以XML文件保存在res/xml目录下,一般就是用来描述应用程序的配置信息。不常用 ,调用方式可以使用

Resources res = getResources();
InputStream is = res.getXML(R.xml.xxx);
font

这类资源以xml方式或者文件方式保存在res/font目录下,比如.ttf,.otf或.ttc文件后缀或者是包含<font-family>标签元素的xml

这些二进制格式的XML文件分别有一个字符串资源池,用来保存文件中引用到的每一个字符串,包括XML元素标签、属性名称、属性值,以及其它的一切文本值所使用到的字符串。这样原来在文本格式的XML文件中的每一个放置字符串的地方在二进制格式的XML文件中都被替换成一个索引到字符串资源池的整数值。

这里你可能有个疑问,这些xml的资源为啥要被编译成二进制的格式,xml文本格式不香吗?

这样做有两个好处:

  • A. 文件占用更小。例如,假设在原来的文本格式的XML文件中,有四个地方使用的都是同一个字符串,那么在最终编译出来的二进制格式的XML文件中,字符串资源池只有一份字符串值,而引用它的四个地方只占用一个整数值,比如可以节约空间,在xml文件中android这个namespace会引用非常多次,但在字符串池里,只有一次。
  • B. 解析速度更快。由于在二进制格式的XML文件中,所有的XML元素标签和属性等值都是使用整数来描述的,因此,在解析的过程中,就不再需要进行字符串解析和比较,这样就可以提高解析速度。

对于values类型的资源,比如string、interger等,AAPT会直接把他们编译入Global String Pool,这也就是我们解压APK后,看不到res/values/目录的原因了。

另外一个地方需要注意的是,每一个res资源在编译的打包完成之后,都会被分配一个资源ID,这些资源ID被终会被定义为Java常量值,保存在一个R.java文件中,与应用程序的其它源文件一起被编译到程序中,这样我们就可以在程序或者资源文件中通过这些ID常量来访问指定的资源。

总结起来就是,AAPT会对不同的资源类型做不同程度的处理,以达到节约空间,节省时间的目的。

资源的组织

我们会根据设备的屏幕大小或者语言等设置不同的资源,这就是我们要提到的资源组织维度,在android中一共有21个组织维度(旧版本应该是18个?暂未考证)。这21个组织维度分别为:

维度示例备注/说明
MCC and MNCExamples: mcc310
mcc310-mnc004mcc208-mnc00 etc.MCC(mobile country code),MNC(mobile network code ) 有时候是两个码组合在一起
Language and regionExamples:enfren-rUSfr-rFRfr-rCAb+enb+en+USb+es+419语言和地区,具体code可以参照国际标准 xx-rMM 格式,xx表示语言,r为region的首字母,MM为对应的 地区
Layout Directionldrtl/ldltrldrtl means "layout-direction-right-to-left". ldltr means "layout-direction-left-to-right" ldltr为默认值,表示从左向右读 比如有的layout结构为
res/
  layout/
     main.xml (Default layout)
  layout-ar/
     main.xml (Specific layout for Arabic)  layout-ldrtl/
     main.xml (Any "right-to-left" language, because the "ar" language
Screen sizesmallnormallargexlargesmall对应的最小值为 320x426 dp normal对应的屏幕分辨率最小值为320x470 dp large对应的屏幕分辨率最小值为480x640 dp xlarge 对应的屏幕分辨率最小值为 720x960 dp
Screen aspectlongnotlong指屏幕的宽高比,而不是屏幕的放置顺序(纵向和横向)。其值有两个:long(表示宽屏幕)、notlong(表示非宽屏幕)。
UI modecardesktelevisionappliancewatchvrheadsetui类型,车载、桌面、电视、家电、手表、vr头戴设备
Night modenight/notnight暗夜模式
smallestWidthsw<N>dp 比如sw320dp限定屏幕执行时使用的最小宽度。例如:sw320dp 表示目标设备的高和宽最小是320dp。
Round screenroundnotround屏幕是否是圆的,比如手表设备等,手机和平板默认为notround,api 23 新增
High Dynamic Range (HDR)highdrlowdr是否高动态光照渲染 API >= 26
Wide Color Gamutwidecgnowidecg是否是广色域,widecg:Display P3 or AdobeRGB ,nowidecg:sRGB API >=26
Available widthw<N>dp 比如w720dp屏幕最小宽度,格式:w<N>dp,其中N表示与像素无关的宽度。当用户旋转屏幕时会改变该值。例如:w720dp 表示目标设备的最小宽度为720dp
Available heighth<N>dp 比如h720dp屏幕最小高度,格式:h<N>dp,其中N表示与像素无关的宽度。当用户旋转屏幕时会改变该值。例如:h720dp 表示目标设备的最小高度为720dp
Screen orientationportland屏幕方向,垂直或水平
Screen pixel density (dpi)ldpimdpihdpixhdpixxhdpixxxhdpinodpitvdpianydpinnndpi物理屏幕的像素数,通常用dpi表示。可能的值有:ldpi – 低分辨率屏幕 120dpi左右mdpi – 中等分辨率屏幕160dpi 左右hdpi – 高分辨率屏幕 240dpi左右xhdpi – 超高分辨率屏幕 320dpi左右xxhdpi – 超超高分辨率屏幕 480dpi左右xxxhdpi – 超超超高分辨率屏幕 640dpi左右 (仅供launch icon)nodpi – 资源不可缩放 tvdpi – 在mdpi和hdpi之间 213dpi左右 anydpi – 任意分辨率nnndpi – 非标准分辨率,一般不用There is a 3:4 6:8 12:16 scaling ratio between the six primary densities (ignoring the tvdpi density). So, a 9x9 bitmap in ldpi is 12x12 in mdpi, 18x18 in hdpi, 24x24 in xhdpi and so on.
Touchscreen typenotouch/finger是否触屏
Keyboard availabilitykeysexposed/keyshidden/keyssoft是否有硬件键盘或软件键盘
Navigation key availabilitynavexposed/navhidden是否有功能键
Primary text input methodnokeys/qwerty/12key输入键盘类型,无硬件键盘,全键盘,九宫格键盘
Primary non-touch navigation methodnonav/dpad/trackball/wheel无、十字方向键/轨迹球/滚轮
Platform Version (API level)比如 v3platform 版本

需要说明一点,以上21个维度是按照优先级从最大到最小排列的,系统根据这个优先级找到最合适的资源来使用

具体的匹配算法流程可以参照如下,不过具体还是参考代码最为详细资源的筛选

示例

我们来看下官方提供的示例,看下实际的命中过程是怎么样的

假设一个应用程序的drawable资源按照以下方式来配置的:

drawable/
drawable-en/
drawable-fr-rCA/
drawable-en-port/
drawable-en-notouch-12key/
drawable-port-ldpi/
drawable-port-notouch-12key/

并且该应用程序所运行在的设置的配置情况如下所示:

Locale = en-GB
Screen orientation = port
Screen pixel density = hdpi
Touchscreen type = notouch
Primary text input method = 12key

根据上图的算法,Android assetManager将会按照如下的步骤进行选择drawable

1、消除与设备配冲突的目录,也就是drawable-fr-rCA/ 因为设备设置的是 en-GB
剩余

drawable/
drawable-en/
drawable-fr-rCA/
drawable-en-port/
drawable-en-notouch-12key/
drawable-port-ldpi/
drawable-port-notouch-12key/

2、选择一个资源组织维度来过滤上一步中剩下的目录(从MMC开始的,然后一直按照优先级往下)

3、检查第二步选择的维度是否有对应的资源目录。如果没有,就返回到第二步继续处理。如果有,那么就继续往下执行第四步。在我们这个例子中,要一直重复执行第二步,直到检查到language这个维度时。
首先从MMC开始,
没有目录跟MCC相关,则继续第二步,下一个配置为language,此时language为en,有文件夹命中

drawable/
drawable-en/
drawable-fr-rCA/
drawable-en-port/
drawable-en-notouch-12key/
drawable-port-ldpi/
drawable-port-notouch-12key/

此时剩余三个目录可用,继续回到2,继续找下一个维度,这时候来到screen orientation 时才命中了目录,中间的都没有命中,都pass,
此时设备的orientation为port,则做一次筛选

drawable/
drawable-en/
drawable-fr-rCA/
drawable-en-port/
drawable-en-notouch-12key/
drawable-port-ldpi/

drawable-port-notouch-12key/
这时候就只剩下 drawable-en-port 目录 此目录唯一,则算法结束。

其实算法的第一步,android框架已经优化过,默认就会剔除冲突的配置目录。
此时有个降级策略,如果有一个名为xxx的图片只在drawable-fr-rCA文件夹中有,由于此时用户命中的是 drawable-en-port目录,此目录下没有,则会报异常crash。

资源的编译和打包

资源的编译,这是AAPT的重头戏,主要包括了,对values资源的编译、对XML资源的编译(包括AndroidManifest.xml)、R文件的生成,.d(依赖文件)的生成、resources.arsc(资源索引表)的生成等。另外,我们还会重点讨论这些过程中涉及到的一些不常见的概念及其相关处理过程,比如overlayPackage、Bag资源、privateSymbolPackage、Res_lib等。一般情况下,Android源生的AAPT足以满足我们对应用资源的编译需求,当我们需要定制自己的系统资源的时候才可能涉及到对AAPT的修改。

我们会在接下来的内容中讲解资源的编译打包流程。

资源的加载和引用

资源的加载和引用,是由AssetManager来完成的。当Zygote创键完虚拟机,进入java世界后,fork出其它子进程前,它会加载许多系统的东西,系统资源也会在这个时候被加载。当这些资源被加载后,它们会被缓存起来,当需要再次引用时,就可以不用重新创键了。其实,fork出来的每一个子进程,都是已经加载过系统资源的了。

我们会在接下来的内容中讲解资源是怎么被加载和引用的。

最后修改:2021 年 01 月 12 日 21 : 28

发表评论

  • 小星星变奏曲 - 莫扎特
  • Moon River - Audrey Hepburn