Android 资源加载机制详解

Android提供了一种非常灵活的资源系统,可以根据不同的条件提供可替代资源。因此,系统基于很少的改造就能支持新特性,比如Android N中的分屏模式。这也是Android强大部分之一。本文主要讲述Android资源系统的实现原理,以及在应用开发中需要注意的事项。

1.定义资源

Android使用XML文件描述各种资源,包括字符串、颜色、尺寸、主题、布局、甚至是图片(selector,layer-list)。

资源可分为两部分,一部分是属性,另一部分是值。对于android:text="hello,world"text就是属性,hello,world就是值。

1.1 属性的定义

在APK程序中,属性定义在res/values/attrs.xml中,在系统中属性位于framework/base/core/res/res/values/attrs.xml文件中。具体定义如下所示:

1
2
3
4
5
6
<declare-styleable name="Window">
<attr name="windowBackground" format="reference"/>
<attr name="windowContentOverlaly" />
<attr name="windowFrame" />
<attr name="windowTitle" />
</declare-styleable>

styleable相当于一个属性集合,其在R.java文件中对应一个int[]数组,aapt为styleable中的每个attr(属性)分配一个id值,int[]中的每个id对应着styleable中的每一个attr。

对于<declare-styleable name="Window">,Window相当于属性集合的名称。
对于<attr name="windowBackground">,windowBackground相当于属性的名称;属性名称在应用程序范围内必须唯一,既无论定义几个资源文件,无论定义几个styleable,windowBackground必须唯一。

在Java代码中,变量在一个作用域内只能声明一次,但可以多次使用。attr也是一样,只能声明一次,但可以多处引用。如上代码所示,在Window中声明了一个名为windowBackground的attr,在Window中引用了一个名为windowTitle的attr

如果一个attr后面仅仅有一个name,那么这就是引用;如果不光有name还有format那就是声明。windowBackground是属性的声明,其不能在其他styleable中再次声明;windowTitle则是属性的引用,其声明是在别的styleable中。

1.2 值的定义

常见的值一般有以下几种:

  • String,Color,boolean,int类型:在res/values/xxx.xml文件中指定
  • Drawable类型:在res/drawable/xxx中指定
  • layout(布局):在res/layout/xxx.xml中指定
  • style(样式):在res/values/xxx.xml中指定

值的类型大致分为两类,一类是基本类型,一类是引用类型;对于int,boolean等类型在声明属性时使用如下方式:
<attr name="width" format="integer"/>
<attr name="text" format="string" />
<attr name="centerInParent"="boolean"/>
对于Drawable,layout等类型在声明属性时:
<attr name="background" format="reference"/>

2.解析资源

资源解析主要涉及到两个类,一个是AttributeSet,另一个是TypedArray。

2.1 AttributeSet

该类位于android.util.AttributeSet,纯粹是一个辅助类,当从XML文件解析时会返回AttributeSet对象,该对象包含了解析元素的所有属性及属性值。并且在解析的属性名称与attrs.xml中定义的属性名称之间建立联系。AttributeSet还提供了一组API接口从而可以方便的根据attrs.xml中已有的名称获取相应的值。

如果使用一般的XML解析工具,则可以通过类似getElementById()等方法获取属性的名称和属性值,然而这样并没有在获取的属性名称与attrs.xml定义的属性名称之间建立联系。

Attribute对象一般作为View的构造函数的参数传递过来,例如:

1
publlic TextView(Context context,AttributeSet attrs,int defStyle)

AttributeSet中的API可按功能分为以下几类,假定TextView定义如下所示:

1
2
3
4
5
6
<TextView
android:id="@+id/tv"
android:layout_width="@dimen/width"
android:layout_height="wrap_content"
style="@stylel/text"
/>

第一类,操作特定属性:

  • public String getIdAttribute(),获取id属性对应的字符串,此处返回”@+id/tv”
  • public String getStyleAttribute(),获取style属性对应的字符串,返回”@style/text”
  • public int getIdAttributeResourceValue(int defaultValue),返回id属性对应的int值,此处对应R.id.tv。

第二类,操作通用属性:

  • public int getAttributeCount(),获取属性的数目,本例中返回4
  • public String getAttributeName(int index),根据属性所在位置返回相应的属性名称。例如,id=0,layout_width=1,layout_height=2,style=3,如果getAttributeName(2),则返回android:layout_height
  • public String getAttributeValue(int index),根据位置返回值。本例中,getAttributeValue(2)则返回”wrap_content”。
  • public String getAttributeValue(String namespace,String name),返回指定命名空间,指定名称的属性值,该方法说明AttributeSet允许给一个XML Element的属性增加多个命名空间的属性值。
  • public int getAttributeResource(int index),返回指定位置的属性id值。本例中,getAttributeResource(2)返回R.attr.layout_width。前面也说过,系统会为每一个attr分配一个唯一的id。

第三类,获取特定类型的值:

  • public XXXType getAttributeXXXType(int index,XXXType defaultValue),其中XXXType包括int、unsigned int、boolean、float类型。使用该方法时,必须明确知道某个位置(index)对应的数据类型,否则会返回错误。而且该方法仅适用于特定的类型,如果某个属性值为一个style类型,或者为一个layout类型,那么返回值都将无效。

2.2 TypedArray

程序员在开发应用程序时,在XML文件中引用某个变量通常是android:background=”@drawable/background”,该引用对应的元素一般为某个View/ViewGroup,而View/ViewGroup的构造函数中会通过obatinStyledAttributes方法返回一个TypedArray对象,然后再调用对象中的getDrawable()方法获取背景图片。

TypedArray是对AttributeSet数据类的某种抽象。对于andorid:layout_width="@dimen/width",如果使用AttributeSet的方法,仅仅能获取”@dimen/width”字符串。而实际上该字符串对应了一个dimen类型的数据。TypedArray可以将某个AttributeSet作为参数构造TypedArray对象,并提供更方便的方法直接获取该dimen的值。

1
TypedArray a = context.obtainStyledAttributes(attrs,com.android.internal.R.styleable.XXX,defStyle,0);

方法obtainStyledAttributes()的第一个参数是一个AttributeSet对象,它包含了一个XML元素中定义的所有属性。第二个参数是前面定义的styleable,appt会把一个styleable编译成一个int[]数组,该数组的内部实现正是通过遍历AttributeSet中的每一个属性,找到用户感兴趣的属性,然后把值和属性经过重定位,返回一个TypedArray对象。想要获取某个属性的值则调用相关的方法即可,比如TypedArray.getDrawbale(),TypedArray.getString()等。getDrawable(),getString()方法内部均通过Resources获取属性值。

3.加载资源

在使用资源时首先要把资源加载到内存。Resources的作用主要就是加载资源,应用程序需要的所有资源(包括系统资源)都是通过此对象获取。一般情况下每个应用都会仅有一个Resources对象。

要访问资源首先要获取Resources对象。获取Resources对象有两种方法,一种是通过Context,一种是通过PackageManager。

3.1 使用Context获取Resources

抽象类Context内部个有getResources()方法,一般是在Activity对象或者Service对象中调用,因为Activity或者Service的本质是一个Context,而真正实现Context接口的是ContextImpl类。

获取Resources对象流程图

ContextImpl对象是在ActivityThread类中创建,所以getResources()方法实际上是调用ContextImpl.getResources()方法。在ContextImpl类中,该方法仅仅是返回内部的mResources变量,而对该变量赋值是在init()方法中。在创建ContextImpl对象后,一般会调用init()方法对ContextImpl对象内部变量初始化,其中就包括mResources变量,如以下代码所示:

1
2
3
4
final void init(ActivityThread.PackageInfo packageInfo, IBinder activityToken, ActivityThread mainThread, Resources container){
mPackageInfo = packageInfo;
mResources = mPackageInfo.getResources(mainThread);
}

从以上代码可以看出,mResources又是调用mPackageInfo的getResources()方法进行赋值。一个应用程序中可以有多个ContextImpl,但多个ContextImpl对象共享一个PackageInfo对象。所以多个ContextImpl对象中的mResources变量实际上是同一个Resources对象。

PackageInfo.getResources()方法如下所示:

1
2
3
4
5
public Resources getResources(ActivityThread mainThread){
if(mResources == null){
mResources = mainThread.getTopLevelResources(mResDir,this);
}
}

以上代码中,参数mainThread指的就是ActivityThread对象,每个应用程序只有一个ActivityThread对象。getTopLevelResources()方法就是获取本应用程序中的Resources对象。

在ActivityThread对象中,使用HashMap<ResourcesKey,WeakReference<Resources>> mActiveResources保存该应用程序所有的Resources对象,并且这些Resources都是以一个弱引用保存起来的,这样在内存紧张时可以释放Resources所占的内存。

在mActiveResources中,使用ResourcesKey映射Resources类,ResourcesKey仅仅是一个数据类,其创建方式如下所示:

1
ResourcesKey key = new ResourcesKey(resDir,compInfo.applicatioScale);

resDir变量代表资源文件所在路径,实际是指APK程序所在路径,例如 /data/app/xxx.apk。该APK会对应/data/dalvik-cache目录下的data@app@xxx.apk@classes.dex文件,这两个文件也是应用程序安装后自动生成的文件。

如果一个应用程序没有访问该应用程序以外的资源,那么mActivieResources变量中就仅有一个Resources对象。当应用程序想要访问其他应用程序的资源则需要构建不同的ResourcesKey,也就是需要不同的resDir,毕竟每一个ResourcesKey对应一个Resources对象,这样该应用程序就可以访问其他应用程序中的资源。

如果mActiveResources中还没有包含所要的Resources对象,那就需要重新创建一个:

1
2
3
4
5
6
AssetManager assets = new AssetManager();
if(assets.addAssetPath(resDir) == 0){
return null;
}
DisplayMetrics metrics = getDisplayMetricsLocked(false);
r = new Resources(assets,metrics,getConfiguration(),compInfo);

创建Resources需要一个AssetManager对象。在开发应用程序时,使用Resources.getAssets()获取的就是这里创建的AssetManager对象。AssetManager其实并不只是访问res/assets目录下的资源,而是可以访问res目录下的所有资源。

AssetManager在初始化的时候会被赋予两个路径,一个是应用程序资源路径 /data/app/xxx.apk,一个是Framework资源路径/system/framework/framework-res.apk(系统资源会被打包到此apk中)。所以应用程序使用本地Resources既可访问应用程序资源,又可访问系统资源。

AssetManager中很多获取资源的关键方法都是native实现,当使用getXXX(int id)访问资源时,如果id小于0x1000 0000时表示访问系统资源,如果id都大于0x7000 0000则表示应用资源。aapt在对系统资源进行编译时,所有资源id都被编译为小于0x1000 0000。

当创建好Resources后就把该对象放到mActivieResources中以便以后继续使用。

3.2 使用PackageManager获取Resources

该方法主要是用来访问其他应用程序中的资源,最典型的就是切换主题,但这种主题一般仅限于一个应用程序内部。获取Resources的过程如下所示:
使用PackageManager获取Resources对象流程

使用PackageManager获取Resources对象:

1
2
PackageManager pm = mContext.getPackageManager();
pm.getResourcesForApplication("com.android...your package name");

其中getPackageManager()返回一个PackageManager对象,PackageManager本身是一个abstract类,其真正实现类是ApplicationPackageManager。其内部方法一般调用远程PackageManagerService。ApplicationPackageManager在构造时传入一个远程服务的引用IPackageManager,该对象是通过调用getPackageManager()静态方法获取的。这种获取远程服务的方法和大多数获取远程服务的方法类似:

1
2
3
4
5
6
7
8
public static IPackageManager getPackageManager(){
if(sPackageManager !=null){
return sPackageManager;
}
IBinder b = ServiceManager.getService("package");
sPackageManager = IPackageManager.Stub.asInterface(b);
return sPackageManager;
}

获得了PackageManager对象后,接着调用getResourcesForApplication()方法,该方法位于ContextImpl.ApplicationPackageManager中:

1
2
3
4
5
6
7
8
9
10
11
@Override
public Resources getResourcesForApplication(ApplicationInfo app) throws NameNotFoundException{
if(app.packageName.equals("system")){
return mContext.mMainThread.getSystemContext().getResources();
}
Resources r = mContext.mMainThread.getTopLevelResources(app.uid == Process.myUid() ? app.sourceDir : app.publicSourceDir,mContext.mPackageInfo);
if(r != null){
return r;
}
throw new NameNotFoundException("Unable to open " + app.publicSourceDir);
}

以上代码内部调用mMainThread.getTopLevelResources()方法,又回到了使用Context获取Resources对象的过程中。注意,此处调用参数的含义:如果目标资源程序和当前程序是同一个uid,那么就使用目标程序的sourceDir作为路径,否则就使用目标程序的publicSourceDir目录,该目录可以在AndroidManifest.xml中指定。在大多数情况下,目标程序和当前程序不属于同一个uid,因此,多为publicSourceDir,而该值默认情况下和sourceDir的值相同。

当进入mMainThread.getTopLevelResources()方法后,ActivityThread对象就会在mActivieResources变量中保存一个新的Resources对象,其键值对应目标程序的包名。

3.3 加载应用程序资源

应用程序打包的最终文件是xxx.apk。APK本身是一个zip文件,可以使用压缩工具解压。系统在安装应用程序时首先解压,并将其中的文件放到指定目录。其中有一个文件名为resources.arsc,APK所有的资源均在其中定义。

resources.arsc是一种二进制格式的文件。aapt在对资源文件进行编译时,会为每一个资源分配唯一的id值,程序在执行时会根据这些id值读取特定的资源,而resources.arsc文件正是包含了所有id值得一个数据集合。在该文件中,如果某个id对应的资源是String或者数值(包括int,long等),那么该文件会直接包含相应的值,如果id对应的资源是某个layout或者drawable资源,那么该文件会存入对应资源的路径地址。

事实上,当程序运行时,所需要的资源都要从原始文件中读取(APK在安装时都会被系统拷贝到/data/app目录下)。加载资源时,首先加载resources.arsc,然后根据id值找到指定的资源。

3.4 加载Framework资源

系统资源是在zygote进程启动时被加载的,并且只有当加载了系统资源后才开始启动其他应用进程,从而实现其他应用进程共享系统资源的目标。

启动第一步就是加载系统资源,加载完毕后再调用startSystemServer()启动系统进程,并最后调用runSelectLoopMode()开始监听Socket,并启动指定的应用进程。加载系统资源是通过preLoadResources()完成的,该方法关键代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
mResources = Resources.getSystem();
mResources.startPreLoading();
if(PRELOAD_RESOURCES){
long startTime = SystemClock.uptimeMillis();
TypeArray ar = mResources.obtainTypedArray(com.android.internal.R.array.preloadingdrawables);
int N = prelaodDrawables(runtime,ar);
Log.i(TAG,"...preloading " + N + "resources in " + (SystemClock.uptimeMillis()-startTime) + "ms.");
startTime = SystemClock.uptimeMillis();
ar = mResources.obtainTypedArray(com.android.internal.R.array.preloading_color_state_lists);
N = preloadingColorStateLists(runtime,ar);
Log.i(TAG,"...preloaded " + N + "resources in " + (SystemClock.uptimeMillis()-startTime) + "ms.");
}
mResources.finishPreloading();

在以上代码中使用Resources.getSystem()创建Resources对象,一般情况下应用程序不应该调用此方法,因为该方法返回的Resources仅能访问Framework资源。

当Resources对象创建完成后,调用preloadDrawables()和preloadColorStateLists()装在需要”预装载”的资源。这两个方法都需要传入一个TypeArray,其来源是res/values/arrays.xml中定义的一个array数组资源,例如:

1
2
3
4
5
6
7
8
9
<array name="preloaded_drawables">
<item>@drawable/sym_def_app_icon</item>
<item>@drawable/arrow_down_float</item>
</array>

<array name="preloaded_color_state_lists">
<item>@color/hint_foreground_dark</item>
<item>@color/hint_foreground_light</item>
</array>

在Resources类中,相关资源读取函数需要将读取到的资源缓冲起来,以便以后使用,Resources类中定义了四个静态变量缓冲这些资源:

1
2
3
4
private static final LongSparseArray<Drawable.ConstantState> sPreloadedDrawables = new LongSparseArray<Drawable.ConstantState>();
private static final LongSparseArray<ColorStateList> sPreloadedColorStateLists = new LongSparseArray<ColorStateList>();
private static final LongSparseArray<Drawable.ConstantState> sPreloadedColorDrawables = new LongSparseArray<Drawable.ConstantState>();
private static boolean mPreloaded;

其中前三个变量是列表类型,并且被static修饰,所有Resources对象均共享这三个变量。所以当应用程序创建新的Resources对象时可以访问系统资源。

第四个变量用来区分是zygote装在资源还是普通应用进程装在资源。因为zygote与普通进程装载资源的方式类似,所以增加mPreloaded变量进行区分。

mPreloaded在startPreloading()中被置为true,在finishPreloading()中被置为false,而startPreloading()和finishPreloading()正是在ZygoteInit.java的preloadResources()中被调用,这就区别了zygote调用和普通进程调用。

最后,在Resources的具体资源读取方法中,会判断mPreloaded变量,如果为true,则同时把读取到的资源存储到三个静态列表中,否则把资源放到非静态列表中,这些非静态列表的作用范围为调用者所在进程。

Resources.loadDrawable()方法代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if(mPreloading){
if(isColorDrawable){
sPreloadedColorDrawables.put(key,cs);
} else {
sPreloadedDrawables.put(key,cs);
}
} else {
synchronized(mTmpValue){
if(isColorDrawbale){
mColorDrawableCache.put(key,new WeakReference<ColorDrawable>(cs));
} else {
mDrawableCache.put(key,new WeakReference<Drawable>(cs));
}
}
}

上面所介绍的资源加载仅仅只是加载在res/values/arrays.xml中预先定义的资源值,Framework包含了更多的资源,zygote所加载的仅仅是一小部分。对于那些非”预装载”的系统资源则不会被缓冲到静态列表变量中,这时应用进程如果需要一个非预装载资源则会在各自进程中保持一个资源缓冲。