前言
上一篇文章,講到了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
函式的呼叫。
@Nullable
Drawable 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<jlong>(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<mAssetPaths.size(); i++) {
if (mAssetPaths[i].path == ap.path) {
if (cookie) {
*cookie = static_cast<int32_t>(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<int32_t>(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<jint>(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<AssetManager*>(this)->loadFileNameCacheLocked();
}
//建立ResTable
mResources = new ResTable();
updateResourceParamsLocked();
bool onlyEmptyResources = true;
const size_t N = mAssetPaths.size();
//遍歷Asset路徑,對於路徑呼叫appendPathToResTable
for (size_t i=0; i<N; i++) {
bool empty = appendPathToResTable(mAssetPaths.itemAt(i));
onlyEmptyResources = onlyEmptyResources && empty;
}
//如果為空,則證明沒有找到resources.arsc檔案
if (required && onlyEmptyResources) {
ALOGW("Unable to find resources file resources.arsc");
delete mResources;
mResources = NULL;
}
return mResources;
}
複製程式碼
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資源的裝載。