Android 資源加載機制剖析

前言

上一篇文章,講到了Android中進程的啓動和一個Activity的建立到顯示流程,如今本篇要分析的是在Android中資源的裝載機制,例如字符串資源,圖片資源是如何被裝載的。這裏將從字符串和圖片兩種類型資源展開分析,同時對於後面所利用的資源裝載的內容也會作簡單的分析。java

Resources源碼剖析

資源裝載實現結構圖

對於資源的裝載機制,這裏核心的幾個類是Resources,ResourcesImpl,AssetManager。Resources算是對於ResourcesImpl的一個代理,Resources的全部調用都是會調用到ResourcesImpl上,在ResourcesImpl的內部,具有對於資源的Cache和AssetManager,對於資源的裝載會首先從其Cache中進行查找,當查找不到的時候,會調用AssetManager進行相應資源的裝載,裝載以後會在ResourcesImpl中將資源緩存下來。android

Resource 中有一個內部靜態變量緩存

static Resources mSystem = null;
複製代碼

在getSystem方法中進行了初始化,做爲對於內部變量的持有被保存着,其初次的調用是在zygote建立新進程的時候,預加載資源的時候被調用。bash

public static Resources getSystem() {
    synchronized (sSync) {
        Resources ret = mSystem;
        if (ret == null) {
            ret = new Resources();
            mSystem = ret;
        }
        return ret;
    }
}
複製代碼

Resrouce對象的建立,在Resrouce中的各類操做,最終真正的執行者是ResourcesImpl。cookie

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變量,app

resources = mResourcesManager.getResources(
activityToken,
packageInfo.getResDir(),
packageInfo.getSplitResDirs(),
packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfiguration,
compatInfo,
packageInfo.getClassLoader());
複製代碼

調用了ResourcesManager的getOrCreateResources方法。其實現爲從activityResources中查找,若是查找不到,則會從新建立一個,而後加入到activityResources中,並返回。ide

獲取字符串資源

從一個獲取資源文件的方法看起,這裏從一個獲取文字的方法入手。函數

@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的getResourceTextui

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資源的獲取核心代碼是在對於ResourcesImplloadDrawable函數的調用。

@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資源的裝載。

參考資料

Android 資源ID生成規則

相關文章
相關標籤/搜索