对于integer、bool等values类型的资源还好,对于比较大的资源比如Drawable等,如果我们每次去获取,都要从resources.arsc中查寻资源相关的信息(对于Drawable来说就是资源路径),然后再根据资源信息(路径)去加载图片,那么这个过程将会非常耗时。所以,为了加快资源的获取速度,同时减少不必要的加载次数,android在资源管理过程中存在着大量的缓存机制,并且还会在Zygote进程起来后就会preload许多系统资源。本文我们挑选一些典型的代码来做分析。
ResourcesManager
中对Resources
的缓存 ResourcesManager
是Android用来统一管理同一个进程中的所有资源,也就是Resources
对象的,应用进程中每一个Resources
对象的创建和获取都应该经过ResourcesManager
之手。它会对它自己创建的每一个Resources
对象做缓存,我们在获取Resources
对象的时候,如果缓存中已经存在并且没有过时,那么ResourcesManager
就会直接返回缓存中的Resources
;如果不存在则会创建对应的Resources
,然后把它加入缓存并返回:
//frameworks/base/core/java/android/app/ResourcesManager.java
public class ResourcesManager {
private static ResourcesManager sResourcesManager;
//...
/**
* 缓存在mActiveResources里面,注意这里使用了若引用,
* 也是为了当进程中没有地方使用这个Resources对象的时候能够
* 在gc的同时释放掉资源,节省内存
*/
final ArrayMap<ResourcesKey, WeakReference<Resources> > mActiveResources
= new ArrayMap<ResourcesKey, WeakReference<Resources> >();
//非常典型的一个单例模式
public static ResourcesManager getInstance() {
synchronized (ResourcesManager.class) {
if (sResourcesManager == null) {
sResourcesManager = new ResourcesManager();
}
return sResourcesManager;
}
}
public Resources getTopLevelResources(String resDir, String[] splitResDirs,
String[] overlayDirs, String[] libDirs, int displayId,
Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {
//根据这些参数构造key
ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token);
Resources r;
/**
* 根据key去缓存中查找
*/
WeakReference<Resources> wr = mActiveResources.get(key);
r = wr != null ? wr.get() : null;
/**
* 如果查到了并且没有过时,就可以返回了
* 这里的没有过时是指,AssetManager加载完APK后,这个APK没有再被修改过
*/
if (r != null && r.getAssets().isUpToDate()) {
return r;
}
//如果缓存中没有,那么就要创建新的对象了
//创建AssetManager对象
AssetManager assets = new AssetManager();
//添加APK本身这个资源包
if (resDir != null) {
if (assets.addAssetPath(resDir) == 0) {
return null;
}
}
//添加分片资源包
if (splitResDirs != null) {
for (String splitResDir : splitResDirs) {
if (assets.addAssetPath(splitResDir) == 0) {
return null;
}
}
}
//添加Runtime Resources Overlay资源包
if (overlayDirs != null) {
for (String idmapPath : overlayDirs) {
assets.addOverlayPath(idmapPath);
}
}
//添加资源共享库
if (libDirs != null) {
for (String libDir : libDirs) {
if (assets.addAssetPath(libDir) == 0) {
Slog.w(TAG, "Asset path '" + libDir +
"' does not exist or contains no resources.");
}
}
}
//.......
//创建Resources对象
r = new Resources(assets, dm, config, compatInfo, token);
//......
//放入缓存
mActiveResources.put(key, new WeakReference<Resources>(r));
//可以返回了
return r;
}
}
另外需要注意的是,不论是Resources
对象还是AssetManager
对象,都会有一个静态的实例,代表系统资源,这就是说,在同一进程中,代表系统资源的Resources
对象和AssetManager
对象只有一个,大家共享之,避免重复:
//frameworks/base/core/java/android/content/res/Resources.java
public class Resources {
static Resources mSystem = null;
//典型的单例模式,线程安全
public static Resources getSystem() {
synchronized (sSync) {
Resources ret = mSystem;
if (ret == null) {
//构造方法会调用AssetManager.getSystem()
//并为它设置配置信息,创建StringBlock对象
ret = new Resources();
mSystem = ret;
}
return ret;
}
}
}
//frameworks/base/core/java/android/content/res/AssetManager.java
public final class AssetManager implements AutoCloseable {
static AssetManager sSystem = null;
private static void ensureSystemAssets() {
synchronized (sSync) {
if (sSystem == null) {
//创建代表系统资源的AssetManager对象,构造方法中会加载
//framework-res.apk中的resources.arsc
AssetManager system = new AssetManager(true);
//创建StringBlock对象,一个StringBlock代表一个Global String Pool
system.makeStringBlocks(null);
sSystem = system;
}
}
}
public static AssetManager getSystem() {
//加载framework-res.apk中的resources.arsc
ensureSystemAssets();
return sSystem;
}
}
AssetManager
中的相关逻辑在Android资源管理框架-------之Android中的资源包(二)中已经有所描述,这里就不重复了。
resources.arsc
以及ResTable
的缓存 当我们给AssetManager
添加资源包的时候,会调用其AddAssetPath
方法,AddAssetPath
方法调用appendPathToResTable
方法,这个方法会根据这个路径去加载压缩包,也就是我们的APK包,然后从中取出resources.arsc,然后再加载resources.arsc中的内容创建ResTable
对象。Android对APK包、resources.arsc和系统的ResTable
(非应用的)都有缓存机制。先说一下它们缓存相关的数据结构吧:一个APK包对应缓存中的一个ZipFileRO
对象、一个resources.arsc对应缓存中的一个Asset
对象,而ResTable
的缓存还是一个ResTable
对象。它们的缓存机制在appendPathToResTable
方法中都有体现。这段代码注释得比较多,但对本文而言关键点仅有四处,我们在注释里有*****关键代码(X)*****
这样的标记,其它的注释,只是为了帮助大家理解这个方法,比较熟悉的可以只看*****关键代码(X)*****
标记的部分。
//frameworks/base/libs/androidfw/AssetManager.cpp
bool AssetManager::appendPathToResTable(const asset_path& ap) const {
/*
* 在这里ass表示我们的资源包中的resources.arsc,
* sharedRes则是用来存放资源包中的resources.arsc中的具体数据
*/
Asset* ass = NULL;
ResTable* sharedRes = NULL;
bool shared = true;
bool onlyEmptyResources = true;
MY_TRACE_BEGIN(ap.path.string());
//idmap 是RRO相关的概念,这里不再多说
Asset* idmap = openIdmapLocked(ap);
/*
* 这里要先看一下已经加载过多少个资源包了,
* 如果没有加载过,那么就认为默认加载的头一个资源包是framework-res.apk
* 也就是Android的系统资源包
*/
size_t nextEntryIdx = mResources->getTableCount();
/*
* 我们要加载resources.arsc,大多数情况这个文件位于压缩包也就是apk中
* 对应我们这个if分支
* 但AssetManager也是支持对解压出来的resources.arsc的
* 这种情况对应下面的else分支
*/
//resources.arsc在apk中
if (ap.type != kFileTypeDirectory) {
if (nextEntryIdx == 0) {// *****关键代码(一)*****
//加载的是framework-res.apk,也就是Google的Android系统资源包
//先去缓存中查找系统资源包及其overlay包是否已经加载过,如果加载过,同时会创建
//ResTable对象,并缓存,也就是sharedRes得到的值肯定不为空
sharedRes = const_cast<AssetManager*>(this)->
mZipSet.getZipResourceTable(ap.path);
if (sharedRes != NULL) {
/**
* 如果已经创建过了,那么要添加的这个资源包,就应该放到它们后面
* 这种情况出现在在同一个进程创建多个不同的AssetManager或者Resources对象的时候,
* 每个AssetManager中都会添加系统资源包,这时候就用上这个缓存了
*/
nextEntryIdx = sharedRes->getTableCount();
}
}
//如果当前要加载的不是系统资源或者是系统资源,但是之前并未加载过,走这里
if (sharedRes == NULL) {
// *****关键代码(二)*****
//查缓存,看看之前是否加载并创建过Asset对象,这里的Asset对象对应的就是加载到
//内存中的resources.arsc
ass = const_cast<AssetManager*>(this)->
mZipSet.getZipResourceTableAsset(ap.path);
//如果没有找到,则加载之,即打开resources.arsc,并将得到的ass放入缓存
if (ass == NULL) {
ALOGV("loading resource table %s\n", ap.path.string());
// *****关键代码(三)*****
/**
* 去压缩包中打开resources.arsc
* 具体步骤为:先去缓存中查找压缩包(也就是APK)是否已经加载过了,
* 如果已经加载过,则直接从缓存中取出APK;否则,先从加载APK。
* 然后再从APK中解压出resources.arsc
*/
ass = const_cast<AssetManager*>(this)->
openNonAssetInPathLocked("resources.arsc",
Asset::ACCESS_BUFFER,
ap);
//放入缓存
if (ass != NULL && ass != kExcludedAsset) {
ass = const_cast<AssetManager*>(this)->
mZipSet.setZipResourceTableAsset(ap.path, ass);
}
}
//如果要加载的是系统资源,但这时候还从未加载过,则连同系统资源包的overlay package一并加载
if (nextEntryIdx == 0 && ass != NULL) {
// If this is the first resource table in the asset
// manager, then we are going to cache it so that we
// can quickly copy it out for others.
ALOGV("Creating shared resources for %s", ap.path.string());
//创建ResTable对象
sharedRes = new ResTable();
//加入资源包中的resources.arsc以及idmap
sharedRes->add(ass, idmap, nextEntryIdx + 1, false);
#ifdef HAVE_ANDROID_OS
const char* data = getenv("ANDROID_DATA");
LOG_ALWAYS_FATAL_IF(data == NULL, "ANDROID_DATA not set");
String8 overlaysListPath(data);
overlaysListPath.appendPath(kResourceCache);
overlaysListPath.appendPath("overlays.list");
//加载系统资源包的overlay package,并将这些资源包也加入sharedRes,
addSystemOverlays(overlaysListPath.string(), ap.path, sharedRes, nextEntryIdx);
#endif
// *****关键代码(四)*****
//缓存sharedRes
sharedRes = const_cast<AssetManager*>(this)->
mZipSet.setZipResourceTable(ap.path, sharedRes);
}
}
} else {//resources.arsc不在apk中,而在某个路径下
//加载到内存,创建Asset对象
ass = const_cast<AssetManager*>(this)->
openNonAssetInPathLocked("resources.arsc",
Asset::ACCESS_BUFFER,
ap);
//不是apk中的不缓存,不共享,添完就删
shared = false;
}
if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) {
if (sharedRes != NULL) {
ALOGV("Copying existing resources for %s", ap.path.string());
//如果是系统资源包,则将系统资源包连同它的overlay package一起添加到我们的
//mResources对象当中去
mResources->add(sharedRes);
} else {
ALOGV("Parsing resources for %s", ap.path.string());
//否则只添加我们要加载的这个资源包(其实仅仅是它的resources.arsc和idmap文件而已)
mResources->add(ass, idmap, nextEntryIdx + 1, !shared);
}
onlyEmptyResources = false;
//不共享的话,就没必要留着了
if (!shared) {
delete ass;
}
} else {
//没找到资源
ALOGV("Installing empty resources in to table %p\n", mResources);
mResources->addEmpty(nextEntryIdx + 1);
}
if (idmap != NULL) {
delete idmap;
}
MY_TRACE_END();
return onlyEmptyResources;
}
先看*****关键代码(一)*****
,Android会对framework-res.apk
创建一个单独的ResTable
对象,并将其缓存。在我们构造新的 java层的AssetManager
对象时,每次系统都会往里面添加系统的framework-res.apk
资源包。根据我们前面文章的分析,很明显每个Android应用进程或者system_server进程默认都会有代表系统资源的mSystem
变量,这个变量是java层Resources
类的一个静态变量,在Zygote进程preload资源的时候就会构造。也就是说,在我们的应用程序中,如果我们构造了自己的Resources
或者AssetManager
对象,系统同样会将framework-res.apk
这个资源包添加进去,不过流程将不再是加载framework-res.apk
,然后解压出resources.arsc,最后添加到ResTable
中;而是从缓存中取出framework-res.apk
对应的ResTable
对象,然后将其合并到我们新创建的AssetManager
对象的ResTable
中。我们看sharedRes = const_cast
这句,其实现为:
//frameworks/base/libs/androidfw/AssetManager.cpp
/**
* ZipSet类用来管理一个AssetManager中添加的所有APK文件
* AssetManager中有一个它的实例 mZipSet
*/
ResTable* AssetManager::ZipSet::getZipResourceTable(const String8& path)
{
//ZipSet中有两个缓存Vector,一个存APK文件,一个存路径,根据路径,找是否加载过APK文件
int idx = getIndex(path);
sp<SharedZip> zip = mZipFile[idx];
//如果没有加载过APK文件,那就先加载再把它放入缓存Vector
if (zip == NULL) {
//等下介绍其实现
zip = SharedZip::get(path);
mZipFile.editItemAt(idx) = zip;
}
/**
* 接下来的处理就要分开说了
* 如果是之前并没有缓存过对应的ResTable对象,那么下面这句就会返回空
* 如果之前已经缓存过ResTable对象,那么会将缓存返回
*/
return zip->getResourceTable();
}
那么什么时候缓存系统的ResTable
对象呢?看*****关键代码(四)*****
的实现:
//frameworks/base/libs/androidfw/AssetManager.cpp
ResTable* AssetManager::ZipSet::setZipResourceTable(const String8& path,
ResTable* res)
{
int idx = getIndex(path);
sp<SharedZip> zip = mZipFile[idx];
return zip->setResourceTable(res);
}
这里的zip->getResourceTable()
和zip->setResourceTable(res)
方法就是简单的getter和setter方法,就不贴代码了。也就是说在*****关键代码(四)*****
的位置,也就是Zygote preload系统资源的时候,就会将系统资源包对应的ResTable
对象缓存到SharedZip
即里面去,后面再用,就会直接从里面取。到这里,系统资源包的ResTable
对象的缓存就说完了。
我们再说压缩包,也就是APK文件的缓存机制。一个APK文件对应的数据结构是一个ZipFileRO
对象(这里的RO很明显就是read only了)。不过AssetManager
为了方便使用,又对它做了封装,这个封装就是`SharedZip:
//frameworks/base/include/androidfw/AssetManager.h
class SharedZip : public RefBase {
//省略其方法的声明
private:
SharedZip(const String8& path, time_t modWhen);
SharedZip(); // <-- not implemented
//APK的路径
String8 mPath;
//APK文件,本质是一个ZIP包
ZipFileRO* mZipFile;
//修改时间,主要用来判断我们的ResTable中的资源是否过时
time_t mModWhen;
//代表一个resources.arsc
Asset* mResourceTableAsset;
//缓存framework-res.apk对应的ResTable对象
ResTable* mResourceTable;
//RRO相关,overlay包的路径
Vector<asset_path> mOverlays;
static Mutex gLock;
//已经加载过的APK包
static DefaultKeyedVector<String8, wp<SharedZip> > gOpen;
};
需要注意的是gOpen
成员是static
的,也就是说SharedZip
是会缓存每一个打开过的APK的。前面我们在分析AssetManager::ZipSet::getZipResourceTable
的实现时,涉及到了zip = SharedZip::get(path);
这句,我们看其实现:
//frameworks/base/libs/androidfw/AssetManager.cpp
sp<AssetManager::SharedZip> AssetManager::SharedZip::get(const String8& path,
bool createIfNotPresent)/*createIfNotPresent 默认是true*/
{
AutoMutex _l(gLock);
time_t modWhen = getFileModDate(path);
//先去查缓存
sp<SharedZip> zip = gOpen.valueFor(path).promote();
//查到了,并且没有问题则返回
if (zip != NULL && zip->mModWhen == modWhen) {
return zip;
}
if (zip == NULL && !createIfNotPresent) {
return NULL;
}
//没查到在去加载,并加入缓存,下次就可以直接用了
zip = new SharedZip(path, modWhen);
gOpen.add(path, zip);
return zip;
}
然后,我们再来看看resources.arsc
的缓存,这个体现在*****关键代码(二)*****
和*****关键代码(三)*****
。这句ass = const_cast
的实现:
//frameworks/base/libs/androidfw/AssetManager.cpp
Asset* AssetManager::ZipSet::getZipResourceTableAsset(const String8& path)
{
//先拿到APK包,可能走缓存哦
int idx = getIndex(path);
sp<SharedZip> zip = mZipFile[idx];
if (zip == NULL) {
zip = SharedZip::get(path);
mZipFile.editItemAt(idx) = zip;
}
//再从SharedZip的缓存里找resources.arsc对应的缓存
return zip->getResourceTableAsset();
}
//frameworks/base/libs/androidfw/AssetManager.cpp
Asset* AssetManager::SharedZip::getResourceTableAsset()
{
ALOGV("Getting from SharedZip %p resource asset %p\n", this, mResourceTableAsset);
return mResourceTableAsset;
}
在*****关键代码(二)*****
那里,先去查缓存,如果没查到,则进入*****关键代码(三)*****
,去加载APK,解压出resources.arsc,构造其对应的Asset
对象:
//frameworks/base/libs/androidfw/AssetManager.cpp
Asset* AssetManager::openNonAssetInPathLocked(const char* fileName, AccessMode mode,
const asset_path& ap)
{
//......
String8 path(fileName);
//这里还是会走AssetManager::ZipSet::getZip方法,还是会去查APK是否缓存过
ZipFileRO* pZip = getZipFileLocked(ap);
if (pZip != NULL) {
//printf("GOT zip, checking NA '%s'\n", (const char*) path);
//resources.arsc
ZipEntryRO entry = pZip->findEntryByName(path.string());
if (entry != NULL) {
//printf("FOUND NA in Zip file for %s\n", appName ? appName : kAppCommon);
//解压出来,并创建Asset对象
pAsset = openAssetFromZipLocked(pZip, entry, mode, path);
pZip->releaseEntry(entry);
}
}
}
//......
}
在*****关键代码(三)*****
后面还会调用mZipSet.setZipResourceTableAsset(ap.path, ass);
将resources.arsc
对应的Asset
存入缓存:
//frameworks/base/libs/androidfw/AssetManager.cpp
Asset* AssetManager::ZipSet::setZipResourceTableAsset(const String8& path,
Asset* asset)
{
int idx = getIndex(path);
sp<SharedZip> zip = mZipFile[idx];
// doesn't make sense to call before previously accessing.
//存入对应的SharedZip对象中缓存起来
return zip->setResourceTableAsset(asset);
}
对于带有Bag的资源,比如说一个style,它的Bag(可以理解为item)可能会有许多,在同一个AssetManager
对象中,当我们获取过其中的某一个Bag后,也会被缓存起来,下次再获取的时候就不必再去resources.arsc
中查找解析了。这个缓存在PackageGroup
中:
struct ResTable::PackageGroup
{
//......省略无关的代码
// Computed attribute bags, first indexed by the type and second
// by the entry in that type.
ByteBucketArray<bag_set**>* bags;
DynamicRefTable dynamicRefTable;
}
bags
成员就是用来做缓存的,一个bag_set
就代表一个资源项的所有Bag,比如一个R.style.Widget_CompoundButton
的所有item都会按照bag的id升序排列的。bags
的组织方式大概如下:
我们看在获取bag时是怎么利用缓存的:
//frameworks/base/libs/androidfw/ResourceTypes.cpp
ssize_t ResTable::getBagLocked(uint32_t resID, const bag_entry** outBag,
uint32_t* outTypeSpecFlags) const
{
if (mError != NO_ERROR) {
return mError;
}
//拿到PackageGroup、Type、Entry的索引
const ssize_t p = getResourcePackageIndex(resID);
const int t = Res_GETTYPE(resID);
const int e = Res_GETENTRY(resID);
//printf("Get bag: id=0x%08x, p=%d, t=%d\n", resID, p, t);
//得到PackageGroup对象,合法性检查略去
PackageGroup* const grp = mPackageGroups[p];
//拿到TypeList,合法性检查略去
const TypeList& typeConfigs = grp->types[t];
//对entry的index做合法性检查略去
const size_t NENTRY = typeConfigs[0]->entryCount;
// First see if we've already computed this bag...
/**
* 在PackageGroup中,是有对Bag资源做缓存的,先去查缓存
*/
if (grp->bags) {
//bags根据资源的type、entry的index做了分类
//根据type、entry的index找到对应的bag_set
bag_set** typeSet = grp->bags->get(t);
if (typeSet) {
bag_set* set = typeSet[e];
if (set) {
//缓存里面有
if (set != (bag_set*)0xFFFFFFFF) {
if (outTypeSpecFlags != NULL) {
*outTypeSpecFlags = set->typeSpecFlags;
}
//返回缓存里的数据,bag_set后面跟着所有的bag_entry
*outBag = (bag_entry*)(set+1);
//ALOGI("Found existing bag for: %p\n", (void*)resID);
//返回bag的数量
return set->numAttrs;
}
ALOGW("Attempt to retrieve bag 0x%08x which is invalid or in a cycle.",
resID);
return BAD_INDEX;
}
}
}
// Bag not found, we need to compute it!
/**
* 如果缓存中木有查到,那么说明我们是第一次获取,
* 我们就只能自己去解析resources.arsc中对应的数据块来获取了
*/
bag_set* set = NULL
//去resources.arsc里加载解析,非本文重点,略去,结果会存在set里
//把查找到的结果存入缓存
typeSet[e] = set;
//...
}
这里我们不再详细介绍getBagLocked
方法,因为其非常复杂,感兴趣的同学可以看这里:Android资源管理框架-------之Bag资源信息的获取(七)。
在Resources
类中,对Drawable
和ColorStateList
以及Animator
等都有缓存,我们这里就以Drawable
为例来稍作说明。
//frameworks/base/core/java/android/content/res/Resources.java
public class Resources {
//......
/**
* 存储预加载的Drawable(xml、PNG等)
* 这个数组只有两个元素,和layoutdirection对应
* 因为要支持系统根据不同的布局方向,选择不同的图片资源
* 所以要分开存
*/
private static final LongSparseArray<ConstantState>[] sPreloadedDrawables;
/**
* 存储预加载的ColorDrawable(color)
* ColorDrawable就无所谓布局方向了,不需要用数组
*/
private static final LongSparseArray<ConstantState> sPreloadedColorDrawables
= new LongSparseArray<ConstantState>();
/**
* Drawable(xml、PNG等)的缓存
* 这个缓存是按照theme分类的,其中
* key 也就是这个String表示theme的ID
* value表示同一theme下的所有Drawable
*/
private final ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> mDrawableCache =
new ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>>();
/**
* Drawable(color)的缓存
* 这个缓存是按照theme分类的,其中
* key 也就是这个String表示theme的ID
* value表示同一theme下的所有Drawable
*/
private final ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> mColorDrawableCache =
new ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>>();
//......
}
需要说明的是,LongSparseArray
我们可以简单认为它是一个KEY为long
类型的Map
(网上介绍SparseArray
的文章很多,这里就不多说了)。在这里所谓的long
类型的KEY具体是什么呢?确切地说,它是cookie值+资源的路径字符串在Global String Pool中的索引(图片)或者是颜色值(Color),也就是说,它代表一个资源的信息;而VALUE则是ConstantState
对象的弱引用,它就是最终的资源本身了。ConstantState
则是一个抽象类,它的子类是Drawable
中非常关键的成员,比如BitmapDrawable
中的BitmapState
、ColorDrawable
中的ColorState
等等。还有,我们看到preload的Drawable
使用的都是强引用,而cache的Drawable
则使用的是弱引用,这也就是说,gc的时候,如果这些cache的资源没有被引用,则会被回收掉,以便节省内存。另外,preload的时候只会preload系统的资源,也就是framework-res.apk里的资源,应用自己的资源在preload的时候,系统是不知道的。我们了解了这些基本的数据结构后在来看getDrawable
的实现:
//frameworks/base/core/java/android/content/res/Resources.java
public Drawable getDrawable(int id) throws NotFoundException {
final Drawable d = getDrawable(id, null);
if (d != null && d.canApplyTheme()) {
Log.w(TAG, "Drawable " + getResourceName(id) + " has unresolved theme "
+ "attributes! Consider using Resources.getDrawable(int, Theme) or "
+ "Context.getDrawable(int).", new RuntimeException());
}
return d;
}
调用了一个重载方法,加了一个参数theme。这里大家可能不解,Drawable还跟theme有关系? 答案是确实有关系,我们可以在theme里指定Drawable
的某些特定属性,比如是否抗锯齿等。
//frameworks/base/core/java/android/content/res/Resources.java
public Drawable getDrawable(int id, @Nullable Theme theme) throws NotFoundException {
TypedValue value;
synchronized (mAccessLock) {
value = mTmpValue;
if (value == null) {
value = new TypedValue();
} else {
mTmpValue = null;
}
/**
* 我们知道TypedValue和Res_value基本对应(多了cookie)
* value.data 里放的值可能是:
* 如果类型是COLOR,那value.data里放的将会是color值
* 如果是图片,那value.data里放的将会是cookie值和一个字符串在global string pool中的索引,
* 这个字符串表示图片的路径
*/
getValue(id, value, true);
}
//根据资源信息去加载资源
final Drawable res = loadDrawable(value, id, theme);
synchronized (mAccessLock) {
if (mTmpValue == null) {
mTmpValue = value;
}
}
return res;
}
接下来才是关键,缓存相关的逻辑都在loadDrawable
方法里:
//frameworks/base/core/java/android/content/res/Resources.java
Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException {
//是colorDrawable还是png、xml
final boolean isColorDrawable;
final ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> caches;
final long key;
//表示是color,而不是xml、png
if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
&& value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
isColorDrawable = true;
//指向mColorDrawableCache
caches = mColorDrawableCache;
//key是一个颜色值
key = value.data;
} else {//png、xml
isColorDrawable = false;
//指向mDrawableCache
caches = mDrawableCache;
//cookie值 + 字符串索引
key = (((long) value.assetCookie) << 32) | value.data;
}
// First, check whether we have a cached version of this drawable
// that was inflated against the specified theme.
/**
* 我们知道preload资源的时候是资源的第一次加载,
* 也就没必要查缓存了
*/
if (!mPreloading) {
//去缓存里查,查到了直接返回
final Drawable cachedDrawable = getCachedDrawable(caches, key, theme);
if (cachedDrawable != null) {
return cachedDrawable;
}
}
// Next, check preloaded drawables. These are unthemed but may have
// themeable attributes.
final ConstantState cs;
//去preload的资源里查
if (isColorDrawable) {
cs = sPreloadedColorDrawables.get(key);
} else {
//根据布局方向不同,分开的
cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
}
final Drawable dr;
//如果查到里了,则根据查到到的ConstantState对象创建对应的drawable对象
if (cs != null) {
final Drawable clonedDr = cs.newDrawable(this);
if (theme != null) {
dr = clonedDr.mutate();
dr.applyTheme(theme);
dr.clearMutated();
} else {
dr = clonedDr;
}
//没查到,类型是颜色
} else if (isColorDrawable) {
dr = new ColorDrawable(value.data);
} else {
/**
* 没查到,类型是xml、png
* loadDrawableForCookie则会先根据value中的cookie
* 和value.data拿到资源(xml或者png)的路径,剩下的就是根据
* 路径加载资源,创建drawable了,这里就不作详细介绍了,有兴趣的
* 同学可以自己看
*/
dr = loadDrawableForCookie(value, id, theme);
}
// If we were able to obtain a drawable, store it in the appropriate
// cache (either preload or themed).
//放入缓存
if (dr != null) {
//这个changingConfigurations,对应于resources.arsc中ResTable_typeSpec后面跟的
//typeSpecFlags,表明该项资源都会随着哪些配置的不同而不同
dr.setChangingConfigurations(value.changingConfigurations);
cacheDrawable(value, theme, isColorDrawable, caches, key, dr);
}
return dr;
}
这个方法总结起来也是非常简单,查缓存,查到了返回;查不到则创建资源,然后缓存资源,最后返回。但这里面有两个方法值得一说:getCachedDrawable
、cacheDrawable
。我们一个一个看:
//frameworks/base/core/java/android/content/res/Resources.java
private Drawable getCachedDrawable(
ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> caches,
long key, Theme theme) {
synchronized (mAccessLock) {
//preload的资源,theme是空的
final String themeKey = theme != null ? theme.mKey : "";
//先根据theme的key取出
final LongSparseArray<WeakReference<ConstantState>> themedCache = caches.get(themeKey);
if (themedCache != null) {
/**
* 从LongSparseArray中根据key取出元素,
* 由于是弱引用,还需要判断是否被回收等处理
* 都是常规操作,这里就不贴代码了。
*/
final Drawable themedDrawable = getCachedDrawableLocked(themedCache, key);
if (themedDrawable != null) {
return themedDrawable;
}
}
// No cached drawable, we'll need to create a new one.
return null;
}
}
再看cacheDrawable
方法,顾名思义,它是用来缓存Drawable的,但是在preloading的时候,它的处理有些不同:
//frameworks/base/core/java/android/content/res/Resources.java
private void cacheDrawable(TypedValue value, Theme theme, boolean isColorDrawable,
ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> caches,
long key, Drawable dr) {
//先判空
final ConstantState cs = dr.getConstantState();
if (cs == null) {
return;
}
if (mPreloading) {//preloading的时候走这里
// Preloaded drawables never have a theme, but may be themeable.
final int changingConfigs = cs.getChangingConfigurations();
if (isColorDrawable) {
/**
* 判断该ColorDrawable是否需要保存
* 对于ColorDrawable,如果它会随着字体大小和density外的其它配置
* 的不同而不同的话,就没必要保存了。
*/
if (verifyPreloadConfig(changingConfigs, 0, value.resourceId, "drawable")) {
sPreloadedColorDrawables.put(key, cs);
}
} else {
/**
* 判断该Drawable是否需要保存
* 对于Drawable,如果它会随着字体大小和density以及布局方向外的其它配置
* 的不同而不同的话,就没必要保存了。
*/
if (verifyPreloadConfig(
changingConfigs, LAYOUT_DIR_CONFIG, value.resourceId, "drawable")) {
if ((changingConfigs & LAYOUT_DIR_CONFIG) == 0) {
// If this resource does not vary based on layout direction,
// we can put it in all of the preload maps.
sPreloadedDrawables[0].put(key, cs);
sPreloadedDrawables[1].put(key, cs);
} else {
// Otherwise, only in the layout dir we loaded it for.
sPreloadedDrawables[mConfiguration.getLayoutDirection()].put(key, cs);
}
}
}
} else {//非preloading
synchronized (mAccessLock) {
//preload的theme为空
final String themeKey = theme == null ? "" : theme.mKey;
//先根据theme的key取出对应的LongSparseArray
LongSparseArray<WeakReference<ConstantState>> themedCache = caches.get(themeKey);
//如果是缓存该theme的第一个资源,先创建LongSparseArray
if (themedCache == null) {
themedCache = new LongSparseArray<WeakReference<ConstantState>>(1);
caches.put(themeKey, themedCache);
}
//放进去
themedCache.put(key, new WeakReference<ConstantState>(cs));
}
}
}
我们看看到底是如何判断是否保存资源的:
private boolean verifyPreloadConfig(@Config int changingConfigurations,
@Config int allowVarying, @AnyRes int resourceId, @Nullable String name) {
// We allow preloading of resources even if they vary by font scale (which
// doesn't impact resource selection) or density (which we handle specially by
// simply turning off all preloading), as well as any other configs specified
// by the caller.
if (((changingConfigurations&~(ActivityInfo.CONFIG_FONT_SCALE |
ActivityInfo.CONFIG_DENSITY)) & ~allowVarying) != 0) {
String resName;
try {
resName = getResourceName(resourceId);
} catch (NotFoundException e) {
resName = "?";
}
// This should never happen in production, so we should log a
// warning even if we're not debugging.
Log.w(TAG, "Preloaded " + name + " resource #0x"
+ Integer.toHexString(resourceId)
+ " (" + resName + ") that varies with configuration!!");
return false;
}
return true;
}
也就是说除了CONFIG_FONT_SCALE
、CONFIG_DENSITY
以及我们指定的配置外,如果这个drawabe还会随着其它配置的不同而不同,那就不保存了。这里我们明白,对于资源的preload,也并不是说preload完了的所有资源我们都会保存,毕竟一个资源可能会随着配置项的不同而不同,如果都保存,那就太多了。
我们知道,Android应用进程以及system_server进程都是由Zygote进程fork出来的。Zygote进程在起来后,会preload许多东西,这样做的好处是,fork出来的每一个进程都已经加载好preload的东西了;否则,每创建一个应用进程都要把这些东西都加载一遍,那就影响启动速度了,这些preload的东西中,就包括资源。其调用序列为:main–>preload–>preloadResources:
//frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
private static void preloadResources() {
final VMRuntime runtime = VMRuntime.getRuntime();
Debug.startAllocCounting();
try {
System.gc();
runtime.runFinalizationSync();
/**
* 这句会触发对framework-res.apk的加载
* Resources、AssetManager、ResTable的创建等等
*/
mResources = Resources.getSystem();
mResources.startPreloading();
if (PRELOAD_RESOURCES) {//true
Log.i(TAG, "Preloading resources...");
long startTime = SystemClock.uptimeMillis();
//预加载图片资源
TypedArray ar = mResources.obtainTypedArray(
com.android.internal.R.array.preloaded_drawables);
int N = preloadDrawables(runtime, ar);
ar.recycle();
Log.i(TAG, "...preloaded " + N + " resources in "
+ (SystemClock.uptimeMillis()-startTime) + "ms.");
addBootEvent(new String("Zygote:Preload "+ N + " obtain resources in " +
(SystemClock.uptimeMillis() - startTime) + "ms"));
startTime = SystemClock.uptimeMillis();
ar = mResources.obtainTypedArray(
com.android.internal.R.array.preloaded_color_state_lists);
//预加载ColorStateList资源
N = preloadColorStateLists(runtime, ar);
ar.recycle();
Log.i(TAG, "...preloaded " + N + " resources in "
+ (SystemClock.uptimeMillis()-startTime) + "ms.");
/// M: Added for BOOTPROF @{
addBootEvent(new String("Zygote:Preload "+ N + " resources in " +
(SystemClock.uptimeMillis() - startTime) + "ms"));
/// @}
}
mResources.finishPreloading();
} catch (RuntimeException e) {
Log.w(TAG, "Failure preloading resources", e);
} finally {
Debug.stopAllocCounting();
}
}
mResources.startPreloading();
和mResources.finishPreloading();
最关键的就是控制mPreloading
的值,前者把值设置为true,后者设置为false。我们可以看一下preloadDrawables
方法,preloadColorStateLists
就不再介绍了:
//frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
private static int preloadDrawables(VMRuntime runtime, TypedArray ar) {
int N = ar.length();
//遍历TypedArray
for (int i=0; i<N; i++) {
if (Debug.getGlobalAllocSize() > PRELOAD_GC_THRESHOLD) {
if (false) {
Log.v(TAG, " GC at " + Debug.getGlobalAllocSize());
}
System.gc();
runtime.runFinalizationSync();
Debug.resetGlobalAllocSize();
}
//拿到资源id
int id = ar.getResourceId(i, 0);
if (false) {
Log.v(TAG, "Preloading resource #" + Integer.toHexString(id));
}
if (id != 0) {
//getDrawable流程上面已经介绍过,第二个参数为theme,preload的时候是null
if (mResources.getDrawable(id, null) == null) {
throw new IllegalArgumentException(
"Unable to find preloaded drawable resource #0x"
+ Integer.toHexString(id)
+ " (" + ar.getString(i) + ")");
}
}
}
//返回preload的个数
return N;
}
本篇就到这里啦,下一篇介绍AssetManager2
。