android 资源管理相关分析(基于android-24)

来源:互联网 发布:苏联核弹攻击中国 知乎 编辑:程序博客网 时间:2024/06/16 03:51

一、资源文件、ID之间的关系

1、前言

android开发者都知道,对于android中的res下的资源大都会生成相应的id,保存在R.java文件中,之后既可以通过R.type.resource来访问相应的资源了,这样大大方便了开发者。对于这些资源文件同代码一样,最终都会被打包值apk中(通过aapt:Android Asset Package Tool),而且大部分文本格式(xml)的文件还会被编译成二进制格式的资源文件。

众所周知,android机型实在太多,不同大小和密度的屏幕、支持多种语言、屏幕的方向转换等等,android的提供的资源组织方式有很多种,应用程序可以根据这些配置信息,找到最匹配的资源,来进行ui展示。

针对这么多的资源组织方式(具体可参见https://developer.android.com/guide/topics/resources/providing-resources.html#BestMatch),android有自己的资源管理框架,本文主要是探索android资源管理相关的知识(本文中的源代码都是基于android-api-24来进行的,如果本地无法下载存储大量源代码,可以通过源码在线网站进行查阅http://androidxref.com)。

开发者能看到的android资源框架中几个重要的文件有R.java以及Resource.arsc(解压apk可以看到该文件)。其中:R.java保存着不同资源对应的ID值(除了assets中的资源),开发者可通过该文件中的id进行资源访问。该文件是通过aapt工具产生的;APK中的Resources.arsc文件是用来描述那些具有ID值的资源的配置信息,它的内容就相当于是一个资源索引表。该文件同样是由AAPT工具在打包过程中生成的,里面维护者资源ID、Name、Path或者Value的对应关系,AssetManager基于这个索引表,就可以通过资源的ID找到这个资源对应的文件或者数据。Resources的创建是基于ResourceTable对象来进行描述的,并在appt打包的时候依据此生成。

通过这Resources和R.java两个文件,即可快速获取相关资源。
2、R.java文件中的ID格式
我们经常见到R文件中的final常量值(0x7f00000001),这些值用来标识具体的资源ID。资源ID是一个4字节的无符号整数,格式为0xPPTTEEEE。其中,最高字节P表示Package ID,次高字节T表示Type ID,最低两字节E表示Entry ID。Package ID相当于是一个命名空间,限定资源的来源。Android系统当前定义了两个资源命令空间,其中一个系统资源命令空间,它的Package ID等于0x01,另外一个是应用程序资源命令空间,它的Package ID等于0x7f。所有位于[0x01, 0x7f]之间的Package ID都是合法的,而在这个范围之外的都是非法的Package ID。我们自己应用程序中定义的资源的Package ID的值都等于0x7f,这一点可以通过生成的R.java文件来验证。
Type ID是指资源的类型ID。资源的类型有animator、anim、color、drawable、layout、menu、raw、string和xml等等若干种,每一种都会被赋予一个ID。
Entry ID是指每一个资源在其所属的资源类型中所出现的次序。注意,不同类型的资源的Entry ID有可能是相同的,但是由于它们的类型不同,我们仍然可以通过其资源ID来区别开来。
对于一个apk文件,可以通过aapt -d resources xxx.apk来获取id及其附加信息,这里映射了id与包及对应的属性之间的关系,如下所示:
resource 0x7f0f06ef com.mogujie.littlestore:id/purse_index_header: t=0x12 d=0x00000000 (s=0x0008 r=0x00)
resource 0x7f0f06f0 com.mogujie.littlestore:id/purse_index_header_logo: t=0x12 d=0x00000000 (s=0x0008 r=0x00)
resource 0x7f0f06f1 com.mogujie.littlestore:id/purse_index_back_icon: t=0x12 d=0x00000000 (s=0x0008 r=0x00)
resource 0x7f0f06f2 com.mogujie.littlestore:id/purse_index_settings_icon: t=0x12 d=0x00000000 (s=0x0008 r=0x00)
resource 0x7f0f06f3 com.mogujie.littlestore:id/purse_index_assets_count: t=0x12 d=0x00000000 (s=0x0008 r=0x00)
Resources.arsc文件解析参考:http://blog.csdn.net/beyond702/article/details/51744082
二、Resources对象获取的源码解析
了解Resources能更好的理解android资源管理机制,同时也可以根据相关原理来实现一些功能(如主题替换等)。android中获取资源都是通过getResources进行的,本节对Resources的获取进行分析。
1、一般获取resources的方式是通过getResources来进行获取,事实上该实现是位于ContextImpl类中,具体代码如下:
packageInfo是一个LoadedApk类型对象,这个LoadedApk类型对象描述的是当前正在启动的Activity组所属的Apk。packageInfo所指向的是一个LoadedApk对象的成员函数getResources来创建的。这个Resources对象创建完成之后,就会保存在ContextImpl类的成员变量mResources中。
2、LoadedApk中的getResources,具体代码如下
参数mainThread指向了一个ActivityThread类型的对象,这个ActivityThread对象描述的是当前正在运行的应用程序进程。LoadedApk类的成员函数getResources首先检查其成员变量mResources的值是否等于null。如果,那么就不为null,则返回该Resources对象,如果为null则会调用参数mainThread所指向的一个ActivityThread对象的成员函数getTopLevelResources来获得这个Resources对象,然后再返回给调用者。
在调用ActivityThread类的成员函数getTopLevelResources来获得一个Resources对象的时候,需要指定要获取的Resources对象所对应的Apk文件路径,这个Apk文件路径就保存在LoadedApk类的成员变量mResDir中。例如,假设我们要获取的Resources对象是用来访问系统自带的音乐播放器的资源的,那么对应的Apk文件路径就为/system/app/Music.apk。
3、ActivityThread中的getTopLevelResources
这里,继续调用了ResourcesManager中的getResources方法。
4、ResourcesManager 中的getResources方法,这里主要是产生了ResourcesKey对象,该对象内部会保持一个依据入参产生的hash值,后续将会根据该key进行缓存或者查找。

5、接着,在该方法中,又调用了getOrCreateResources方法,具体实现如下

该方法需要三个入参,第一个代表与该资源相关联的activity,ResourcesKey前面已经提到,最后一个是classLoader,默认是SystemClassLoader。在该方法中主要关注的是ResourcesImpl类型对象的创建,该对象是Resources的核心,Resources实际上是该类的wrapper。因此ResourcesImpl其实是Resources的实际实现接口。接下来的讨论会以该类为参考点。

该方法大致可分为两个过程:一是ResourcesImpl查找过程(findResourcesImplForKeyLocked);二是ResourcesImpl的创建过程(createResourcesImpl)。第一个过程是确定ResourcesImpl对象是否已经生成,如果生成则以此为核心生成Resources对象,并返回。第二个过程则会根据ResourcesKey进行对象的创建。

首先,看findResourcesImplForKeyLocked方法:


其中,mResourceImpls的定义如下。

mResourceImpls指向了类型为ArrayMap的对象,从该对象的定义可以看出,该对象维持着ResourcesKey与ResourcesImpl的一个映射关系。而ResourcesKey又是基于当前应用程序进程中加载的APK文件路径产生的(在Resources的getResources方法中,传入了参数resDir),也就是说,给定一个Apk文件路径,ActivityThread类的成员函数getTopLevelResources可以在成员变量mResourceImpls中检查是否存在一个对应的ResourcesImpl对象。如果存在,并且这个ResourcesImpl对象里面包含的资源文件没有过时(即调用这个ResourcesImpl对象的成员函数getAssets所获得一个AssetManager对象的成员函数isUpToDate的返回值等于true),那么就会将ResourcesImpl对象返回。然后根据该对象来判断调用方需要的Resources对象是否已经存在。根据ResourcesImpl的状态可分为以下两种:

(1)ResourcesImpl不为null;
获取到ResourcesImpl后,首先会根据该对象进行调节,如果符合要求则会根据该对象来返回是否已经存在,如果是,则直接返回相应的Resources,否则会产生ResourcesImpl的wapper Resources,并将该Resources的弱引用缓存值activityResources中,方便下次获取。这样完成了Resources对象的获取。如下所示。
(2)ResourcesImpl为null,此时,系统会调用ResourcesManager的createResourcesImpl方法,代码如下所示。
如果不存在与参数resDir对应的ResourcesImpl对象,或者存在的这个ResourcesImpl对象是过时的,那么就会通过createResourcesImpl方法来创建一个新的ResourcesImpl对象。在ResourcesImpl的构造方法中,会创建一个AssetManager对象,并且调用这个新创建的AssetManager对象的成员函数addAssetPath来将参数resDir所描述的Apk文件路径作为它的资源目录。新的ResourcesImpl对象产生后,系统会以前面所创建的ResourcesKey对象为key,将该对象缓存在mResourceImpls所描述的一个ArrayMap中,以便以后可以获取回来使用,然后以此来构建Resources对象,并返回。不过,在新创建Resources对象之前,需要再次检查该mResourceImpls是否已经存在一个对应的ResourcesImpl对象了,这是因为当前线程在创建新的AssetManager对象和Resources对象的过程中,可能有其它线程抢先一步创建了与参数resDir对应的ResourcesImpl对象,并且将该ResourcesImpl对象保存到该ArrayMap中去了。
5、assetManager 的创建
该方法首先调用了AssetManager的构造方法,然后调用了addAssetPath方法,首先看AssetManager构造方法的实现:
AssetManager类的构造函数是通过调用另外一个成员函数init来执行初始化工作的。在初始化完成当前正在创建的AssetManager对象之后,AssetManager类的构造函数还会调用另外一个成员函数ensureSystemAssets来检查当前进程是否已经创建了一个用来访问系统资源的AssetManager对象。如果用来访问系统资源的AssetManager对象还没有创建的话,那么AssetManager类的成员函数ensureSystemAssets就会创建并且初始化它,并且将它保存在AssetManager类的静态成员变量sSystem中。注意,创建用来访问系统资源和应用程序资源的AssetManager对象的过程是一样的,区别只在于它们所要访问的Apk文件不一样,因此,接下来我们就只分析用来访问应用资源的AssetManager对象的创建过程以及初始化过程。在这里,init方法和addAssetPath方法最终都会去调用native方法。native中的init方法会通过环境变量ANDROID_ROOT获取android系统的路径(/system/framework/framework-res.apk”),然后将其添加至当前正在初始化的AssetManager对象中。
6、正如前文中提到的,在getOrCreateResourcesForActivityLocked中,最终会调用Resources的构造函数,具体如下:该处调用了updateConfiguration方法,config对应的是设备配置信息,updateConfiguration首先是根据参数config和metrics来更新设备的当前配置信息,例如,屏幕大小和密码、国家地区和语言、键盘配置情况等等。ensureStringBlocks()代码如下:

AssetManager类的成员变量mStringBlocks指向的是一个StringBlock数组,其中,每一个StringBlock对象都是用来描述一个字符串资源池。每一个资源表都包含有一个资源项值字符串资源池,AssetManager类的成员变量mStringBlocks就是用来保存所有的资源表中的资源项值字符串资源池的。AssetManager类的成员函数ensureStringBlocks首先检查成员变量mStringBlocks的值是否等于null。如果等于null的话,那么就说明当前应用程序使用的资源表中的资源项值字符串资源池还没有读取出来,这时候就会调用另外一个成员函数makeStringBlocks来进行读取。

至此Resources对象的初始化完成。依据该过程,可知,只要给定相应apk路径,开发者完全可以动态加载apk资源。但是由源码可知,这些机制很多都是native实现,因此需要采取反射手段来进行获取,然后进一步实现。

三、获取资源部分,以Resources.getDrawable为例。
本节主要介绍Resources如何根据id进行资源查找的。Android资源管理框架实际就是由AssetManager和Resources两个类来实现的。其中,Resources类可以根据ID来查找资源,而AssetManager类根据文件名来查找资源。事实上,如果一个资源ID对应的是一个文件,那么Resources类是先根据ID来找到资源文件名称,然后再将该文件名称交给AssetManager类来打开对应的文件的。
Resources类根据资源ID来查到资源名称实际上也是要通过AssetManager类来实现的,这是因为资源ID与资源名称的对应关系是由打包在APK里面的resources.arsc文件中的。当Resources类查找的资源对应的是一个文件的时候,它就会再次将资源名称交给AssetManager,以便后者可以打开对应的文件,否则的话,上一步找到的资源名称就是最终的查找结果。APK包里面的esources.arsc文件是在编译应用程序资源的时候生成的,然后连同其它被编译的以及原生的资源一起打包在一个APK包里面。
1、getResources.getDrawable
该方法调用了两个参数的重载方法,这个方法也是高版本提倡用的方法。
该方法中调用了getValue方法,改方法会根据id获取资源相关的数据(如资源路径等),并保存在value中,然后传入loadDrawable。重点看ResourcesImpl的loadDrawable方法:
从该方法中可以看出,系统在处理Drawable时会进行缓存,通过调用getInstance来进行判断,代码如下:
该方法中调用了get(key, theme)方法,改方法会通过key,从WeakReference<T>中获取是否有缓存的ConstantState,如果有则返回。
如果entry存在,就会调用newDrawable来实现,在Drawable中的newDrawalbe是个抽象方法,具体实现以BitmapDrawable为例:
可以看到在产生BitmapDrawable的时候讲this传入了。这样就可以达到共享。如果没有缓存资源,且不是预先加载的资源,则获取资源,具体代码如下:
假如现在要获取的不是以xml结尾的文件,而是普通的drawable资源文件,就调用AssetManager的openNonAsset来获取文件是输入流,之后就会调用Drawable中的createFromResourceStream来创建一个Drawable对象,创建完成之后,如果需要缓存,则调用cacheDrawable方法进行缓存(缓存的内容为ConstantState),代码如下:
我们只关心非预加载资源,则最终会调用caches.put(key, theme, cs, usesTheme);具体实现方法如下:
这里保存的地方,正式前文中通过key获取的缓存的entries。需要注意的是,这里保存的是ConstantState类型的实例。

至此,drawable资源获取和缓存分析完毕。

0 0
原创粉丝点击