Android(安卓)资源加载机制剖析
前言
上一篇文章,讲到了Android中进程的启动和一个Activity的创建到显示流程,现在本篇要分析的是在Android中资源的装载机制,例如字符串资源,图片资源是如何被装载的。这里将从字符串和图片两种类型资源展开分析,同时对于后面所利用的资源装载的内容也会做简单的分析。
Resources源码剖析
资源装载实现结构图对于资源的装载机制,这里核心的几个类是Resources,ResourcesImpl,AssetManager。Resources算是对于ResourcesImpl的一个代理,Resources的所有调用都是会调用到ResourcesImpl上,在ResourcesImpl的内部,具备对于资源的Cache和AssetManager,对于资源的装载会首先从其Cache中进行查找,当查找不到的时候,会调用AssetManager进行相应资源的装载,装载之后会在ResourcesImpl中将资源缓存下来。
Resource 中有一个内部静态变量
static Resources mSystem = null;
在getSystem方法中进行了初始化,作为对于内部变量的持有被保存着,其初次的调用是在zygote创建新进程的时候,预加载资源的时候被调用。
public static Resources getSystem() { synchronized (sSync) { Resources ret = mSystem; if (ret == null) { ret = new Resources(); mSystem = ret; } return ret; }}
Resrouce对象的创建,在Resrouce中的各种操作,最终真正的执行者是ResourcesImpl。
private Resources() { this(null); final DisplayMetrics metrics = new DisplayMetrics(); metrics.setToDefaults(); final Configuration config = new Configuration(); config.setToDefaults(); mResourcesImpl = new ResourcesImpl(AssetManager.getSystem(), metrics, config, new DisplayAdjustments());}
在Resources的构造函数中创建ResourcesImpl的实例。
public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics, @Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) { mAssets = assets; mMetrics.setToDefaults(); mDisplayAdjustments = displayAdjustments; updateConfiguration(config, metrics, displayAdjustments.getCompatibilityInfo()); mAssets.ensureStringBlocks();}
在创建ResoucesImpl实例的时候,获得了AssetManager的实例,其负责了应用层和资源文件的交互。Resource对象的获得,是通过ContextImpl方法中获得,获得方式是返回了其内部的变量mResource变量,
resources = mResourcesManager.getResources(activityToken,packageInfo.getResDir(),packageInfo.getSplitResDirs(),packageInfo.getOverlayDirs(),packageInfo.getApplicationInfo().sharedLibraryFiles,displayId,overrideConfiguration,compatInfo,packageInfo.getClassLoader());
调用了ResourcesManager的getOrCreateResources方法。其实现为从activityResources中查找,如果查找不到,则会重新创建一个,然后加入到activityResources中,并返回。
获取字符串资源
从一个获取资源文件的方法看起,这里从一个获取文字的方法入手。
@NonNull public CharSequence getText(@StringRes int id) throws NotFoundException { CharSequence res = mResourcesImpl.getAssets().getResourceText(id); if (res != null) { return res; } throw new NotFoundException("String resource ID #0x" + Integer.toHexString(id));}
public AssetManager getAssets() { return mAssets;}
调用AssetManager的getResourceText
final CharSequence getResourceText(@StringRes int resId) { synchronized (this) { final TypedValue outValue = mValue; if (getResourceValue(resId, 0, outValue, true)) { return outValue.coerceToString(); } return null; }}
首先根据id获得TypedValue,然后根据TypedValue获得我们需要的资源。
final boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue, boolean resolveRefs) { final int block = loadResourceValue(resId, (short) densityDpi, outValue, resolveRefs); if (block < 0) { return false; } if (outValue.type == TypedValue.TYPE_STRING) { outValue.string = mStringBlocks[block].get(outValue.data); } return true;}
对于字符串资源,其值就存在TypedValue中,所以在获得了TypedValue之后,就可以通过其来获得资源值。
获取图片资源
由于图片资源的特殊性,相比于字符串资源的获取,要复杂一些,这里从上层的获取方法开始进行分析。
public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme) throws NotFoundException { final TypedValue value = obtainTempTypedValue(); try { final ResourcesImpl impl = mResourcesImpl; impl.getValue(id, value, true); return impl.loadDrawable(this, value, id, theme, true); } finally { releaseTempTypedValue(value); }}
和对于字符串资源的装载类似,首先根据资源ID获取一个TypedValue对象,然后利用TypedValue实例,通过AssetManager进行装载。
void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs) throws NotFoundException { boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs); if (found) { return; }}
final boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue, boolean resolveRefs) { final int block = loadResourceValue(resId, (short) densityDpi, outValue, resolveRefs); if (block < 0) { return false; } if (outValue.type == TypedValue.TYPE_STRING) { outValue.string = mStringBlocks[block].get(outValue.data); } return true;}
Drawable资源的获取核心代码是在对于ResourcesImpl
的loadDrawable
函数的调用。
@NullableDrawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme, boolean useCache) throws NotFoundException { try { if (TRACE_FOR_PRELOAD) { if ((id >>> 24) == 0x1) { final String name = getResourceName(id); if (name != null) { Log.d("PreloadDrawable", name); } } } //判断是否为ColorDrawable final boolean isColorDrawable; final DrawableCache caches; final long key; if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { isColorDrawable = true; caches = mColorDrawableCache; key = value.data; } else { isColorDrawable = false; caches = mDrawableCache; key = (((long) value.assetCookie) << 32) | value.data; } //,是否存在查找的Drawable if (!mPreloading && useCache) { final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme); if (cachedDrawable != null) { return cachedDrawable; } } // 检查预加载的资源文件中,是否存在要查找的Drawable final Drawable.ConstantState cs; if (isColorDrawable) { cs = sPreloadedColorDrawables.get(key); } else { cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key); } //创建Drawable Drawable dr; if (cs != null) { dr = cs.newDrawable(wrapper); } else if (isColorDrawable) { dr = new ColorDrawable(value.data); } else { dr = loadDrawableForCookie(wrapper, value, id, null); } // 对Drawable的主题进行处理 final boolean canApplyTheme = dr != null && dr.canApplyTheme(); if (canApplyTheme && theme != null) { dr = dr.mutate(); dr.applyTheme(theme); dr.clearMutated(); } // 将装载的Drawable资源加入到缓存之中 if (dr != null && useCache) { dr.setChangingConfigurations(value.changingConfigurations); cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr); } return dr; } catch (Exception e) { ... }}
loadDrawableForCookie
根据TypedValue中存储的信息,从XML文件或者资源流中构建Drawable
private Drawable loadDrawableForCookie(Resources wrapper, TypedValue value, int id, Resources.Theme theme) { if (value.string == null) { throw new NotFoundException("Resource \"" + getResourceName(id) + "\" (" + Integer.toHexString(id) + ") is not a Drawable (color or path): " + value); } //解析值的文件名 final String file = value.string.toString(); if (TRACE_FOR_MISS_PRELOAD) { // Log only framework resources if ((id >>> 24) == 0x1) { final String name = getResourceName(id); if (name != null) { Log.d(TAG, "Loading framework drawable #" + Integer.toHexString(id) + ": " + name + " at " + file); } } } final Drawable dr; //如果文件后缀为xml,通过XmlResourceParser构建Drawable对象 try { if (file.endsWith(".xml")) { final XmlResourceParser rp = loadXmlResourceParser( file, id, value.assetCookie, "drawable"); dr = Drawable.createFromXml(wrapper, rp, theme); rp.close(); } else { //从文件流中构建Drawable对象 final InputStream is = mAssets.openNonAsset( value.assetCookie, file, AssetManager.ACCESS_STREAMING); dr = Drawable.createFromResourceStream(wrapper, value, is, file, null); is.close(); } } catch (Exception e) { ... } return dr;}
XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie, @NonNull String type) throws NotFoundException { if (id != 0) { try { synchronized (mCachedXmlBlocks) { final int[] cachedXmlBlockCookies = mCachedXmlBlockCookies; final String[] cachedXmlBlockFiles = mCachedXmlBlockFiles; final XmlBlock[] cachedXmlBlocks = mCachedXmlBlocks; // 检测缓存是否在我们需要的资源 final int num = cachedXmlBlockFiles.length; for (int i = 0; i < num; i++) { if (cachedXmlBlockCookies[i] == assetCookie && cachedXmlBlockFiles[i] != null && cachedXmlBlockFiles[i].equals(file)) { return cachedXmlBlocks[i].newParser(); } } // 如果资源不在缓存之中,这通过AssetManager去装载,然后加入到缓存中 final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file); if (block != null) { final int pos = (mLastCachedXmlBlockIndex + 1) % num; mLastCachedXmlBlockIndex = pos; final XmlBlock oldBlock = cachedXmlBlocks[pos]; if (oldBlock != null) { oldBlock.close(); } cachedXmlBlockCookies[pos] = assetCookie; cachedXmlBlockFiles[pos] = file; cachedXmlBlocks[pos] = block; return block.newParser(); } } } catch (Exception e) { .... } }}
图片资源装载流程 图片资源的装载流程是首先将根据ID获得TypedValue实例,然后根据TypedValue进行查找Drawable资源,首先检测缓存中是否有该资源,如果没有从预加载资源中查找,如果预加载资源中也没有,判断要加载的资源类型,如果为colorDrawable,这根据Typedvalue进行创建,否则通过加载xml或者文件输入流进行处理来获得Drawable对象。
资源的装载分为两步,一个是通过资源ID得到ID对应的TypedValue对象,对于简单的资源,通过TypedValue即可,对于复杂的资源,需要第二步来把资源文件装载到内存之中。
AssetManager
在创建Resources的构造函数,创建ResourcesImpl的时候调用了AssetManager的getSystem方法,该方法用来确保创建唯一的AssetManager实例。
public static AssetManager getSystem() { ensureSystemAssets(); return sSystem;}
保证全局只有一个AssetManager
private static void ensureSystemAssets() { synchronized (sSync) { if (sSystem == null) { AssetManager system = new AssetManager(true); system.makeStringBlocks(null); sSystem = system; } }}
private AssetManager(boolean isSystem) { if (DEBUG_REFS) { synchronized (this) { mNumRefs = 0; incRefsLocked(this.hashCode()); } } init(true);}
static void android_content_AssetManager_init(JNIEnv* env, jobject clazz, jboolean isSystem){ if (isSystem) { verifySystemIdmaps(); } AssetManager* am = new AssetManager(); if (am == NULL) { jniThrowException(env, "java/lang/OutOfMemoryError", ""); return; } am->addDefaultAssets(); ALOGV("Created AssetManager %p for Java object %p\n", am, clazz); env->SetLongField(clazz, gAssetManagerOffsets.mObject, reinterpret_cast(am));}
bool AssetManager::addDefaultAssets(){ const char* root = getenv("ANDROID_ROOT"); LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set"); String8 path(root); path.appendPath(kSystemAssets); return addAssetPath(path, NULL, false /* appAsLib */, true /* isSystemAsset */);}
设置资源路径,通过对这个值的修改可以实现动态加载。
public final int addAssetPath(String path) { return addAssetPathInternal(path, false);}
private final int addAssetPathInternal(String path, boolean appAsLib) { synchronized (this) { int res = addAssetPathNative(path, appAsLib); makeStringBlocks(mStringBlocks); return res; }}
添加资源路径
bool AssetManager::addAssetPath( const String8& path, int32_t* cookie, bool appAsLib, bool isSystemAsset){ AutoMutex _l(mLock); asset_path ap; String8 realPath(path); if (kAppZipName) { realPath.appendPath(kAppZipName); } ap.type = ::getFileType(realPath.string()); if (ap.type == kFileTypeRegular) { ap.path = realPath; } else { ap.path = path; ap.type = ::getFileType(path.string()); if (ap.type != kFileTypeDirectory && ap.type != kFileTypeRegular) { ALOGW("Asset path %s is neither a directory nor file (type=%d).", path.string(), (int)ap.type); return false; } } // Skip if we have it already. for (size_t i=0; i(i+1); } return true; } } ALOGV("In %p Asset %s path: %s", this, ap.type == kFileTypeDirectory ? "dir" : "zip", ap.path.string()); ap.isSystemAsset = isSystemAsset; mAssetPaths.add(ap); // new paths are always added at the end if (cookie) { *cookie = static_cast(mAssetPaths.size()); }#ifdef __ANDROID__ // Load overlays, if any asset_path oap; for (size_t idx = 0; mZipSet.getOverlay(ap.path, idx, &oap); idx++) { oap.isSystemAsset = isSystemAsset; mAssetPaths.add(oap); }#endif if (mResources != NULL) { appendPathToResTable(ap, appAsLib); } return true;}
APK文件中有一个文件resource.arsc。这个文件存放的是APK中资源的ID和资源类型,属性,文件名的读经关系表和所有的字符串,装载APK,就是解析该文件生成ResRTable对象,通过ResTable对象来解析资源ID。解压相应的路径,从中获得相应的资源表,然后加入到其中。每一次的调用都会调用,appendPathToResTable,将新增的路径的资源表添加到其中。
根据ID获取TypedValue
void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs) throws NotFoundException { boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs); if (found) { return; } throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id));}
final boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue, boolean resolveRefs) { final int block = loadResourceValue(resId, (short) densityDpi, outValue, resolveRefs); if (block < 0) { return false; } if (outValue.type == TypedValue.TYPE_STRING) { outValue.string = mStringBlocks[block].get(outValue.data); } return true;}
从AssetManager中根据ID获取Value值。
static jint android_content_AssetManager_loadResourceValue(JNIEnv* env, jobject clazz, jint ident, jshort density, jobject outValue, jboolean resolve){ if (outValue == NULL) { jniThrowNullPointerException(env, "outValue"); return 0; } AssetManager* am = assetManagerForJavaObject(env, clazz); if (am == NULL) { return 0; } const ResTable& res(am->getResources()); Res_value value; ResTable_config config; uint32_t typeSpecFlags; ssize_t block = res.getResource(ident, &value, false, density, &typeSpecFlags, &config); if (kThrowOnBadId) { if (block == BAD_INDEX) { jniThrowException(env, "java/lang/IllegalStateException", "Bad resource!"); return 0; } } uint32_t ref = ident; if (resolve) { block = res.resolveReference(&value, block, &ref, &typeSpecFlags, &config); if (kThrowOnBadId) { if (block == BAD_INDEX) { jniThrowException(env, "java/lang/IllegalStateException", "Bad resource!"); return 0; } } } if (block >= 0) { return copyValue(env, outValue, &res, value, ref, block, typeSpecFlags, &config); } return static_cast(block);}
对于资源的TypedValue的获取核心代码。
const ResTable& res(am->getResources());ssize_t block = res.getResource(ident, &value, false, density, &typeSpecFlags, &config);
const ResTable& AssetManager::getResources(bool required) const{ const ResTable* rt = getResTable(required); return *rt;}
根据AssetManager中设置的资源路径来查找资源Table
首先判断是否已经存在ResTable,如果不存在则创建,存在则会直接返回。然后根据路径去查找相应的Resources文件,然后将其转化为ResTable,方便后面的查找。
const ResTable* AssetManager::getResTable(bool required) const{ ResTable* rt = mResources;//已经存在 if (rt) { return rt; } AutoMutex _l(mLock); if (mResources != NULL) { return mResources; } if (mCacheMode != CACHE_OFF && !mCacheValid) { const_cast(this)->loadFileNameCacheLocked(); } //创建ResTable mResources = new ResTable(); updateResourceParamsLocked(); bool onlyEmptyResources = true; const size_t N = mAssetPaths.size(); //遍历Asset路径,对于路径调用appendPathToResTable for (size_t i=0; i
Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs, String[] libDirs, int displayId, LoadedApk pkgInfo) { return mResourcesManager.getResources(null, resDir, splitResDirs, overlayDirs, libDirs, displayId, null, pkgInfo.getCompatibilityInfo(), pkgInfo.getClassLoader());}
通过mResourcesManager获得Resources对象,如果不存在,则创建一个。
小结
资源加载对于资源的加载,大概可以通过上图进行概括,根据ID获取TypedValue,TypedValue的获取是在AssetManager添加资源路径的时候,通过对资源表的解析来构建的一个ResTable,通过该数据结构根据ID作为索引查找并构建TypedValue,然后再根据资源文件的类型,借助TypedValue内存储的关于资源的详细信息来获取资源,同时将加载的资源进行缓存。因此在插件化的方案中,通过创建新的Resource对象,为其添加新的Asset路径,从而构建出一个新的ResTable,实现通过ID进行非宿主App资源的装载。
参考资料
Android 资源ID生成规则
更多相关文章
- 从AIDL开始谈Android进程间Binder通信机制
- Android(安卓)7.1.1 通话记录数据库详解
- 关于真机测试的android选择图片在ImageView上显示,调用setImageBi
- 开发者必备的十二大Android开发资源
- android中Intent传值与Bundle传值的区别详解
- Android(安卓)Boot: After kernel stage
- Android(安卓)Demo:读取本地图库与调用摄像头拍摄
- Android(安卓)Fragment 详解
- Android头像上传(本地相册和调用系统相机)