Android資源加載源碼分析

在android開發過程中,我們會用到/res目錄下的文件(圖片,顏色,布局文件等),通過getResource()方法我們能很方便的使用這個資源。這些資源會被一起打包到apk文件中,如下圖:

1493133495(1).png

真正的資源放在res目錄下,Android應用程序資源的編譯和打包之后就生成一個資源索引文件resources.arsc,通過resources.arsc能過準確的找到對應的資源文件,關于resources.arsc的詳細解釋,可參考這般文章http://blog.csdn.net/beyond702/article/details/49228115

那這些資源是怎么加載到,又是何時加載到應用程序中的呢?這篇文章想跟大家分享的就是Android中資源的加載和匹配。

在app啟動過程中,在什么時候進行app資源的加載(圖片,布局文件,顏色等)? 如何將資源與應用關聯在一起?

代碼中通過getResources()獲得Resoures對象,然后通過Resource的相關方法進行獲取相關資源,所以我們看Resoures對象是在什么時候創建的就能推斷出app資源加載的節點,看源碼:

@Override
public Resources getResources() {
    return mBase.getResources();
}

我看到我們在Activity中調用的getResources()方法其實是mBase.getResources(),也就是說調用的mBase中的getResources()方法,而mBase的數據類型其實是Context,是一個抽象類。代碼如下:

public class ContextWrapper extends Context {
Context mBase;

public ContextWrapper(Context base) {
    mBase = base;
}

/**
 * Set the base context for this ContextWrapper.  All calls will then be
 * delegated to the base context.  Throws
 * IllegalStateException if a base context has already been set.
 * 
 * @param base The new base context for this wrapper.
 */
protected void attachBaseContext(Context base) {
    if (mBase != null) {
        throw new IllegalStateException("Base context already set");
    }
    mBase = base;
}
什么是Context?

說到Context,作為一個android工程師肯定很熟悉,在開發過程中,Context是一個非常重要的的類型,Context意為上下文,也就是程序的運行環境。它分裝了很多重要的操作,如startActiviy()、sendBroadcast()、bindService()等,因此,Context對開發者來說最重要的高層接口。Context只是一個定義了很多接口的抽象類,這些接口的真正實現其實是通過Context的子類ContextImpl類中,這種設計模式叫做外光模式(感興趣的同學可以去研究一下什么是“外觀模式”)。

先看看ContextImpl的構造方法,看源碼:

private ContextImpl(ContextImpl container, ActivityThread mainThread,
    LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
    Display display, Configuration overrideConfiguration, int createDisplayWithId) {
mOuterContext = this;
//獲取包的信息
mPackageInfo = packageInfo;
mResourcesManager = ResourcesManager.getInstance();
 /代碼省略/
if (compatInfo == null) {
    compatInfo = (displayId == Display.DEFAULT_DISPLAY)
            ? packageInfo.getCompatibilityInfo()
            : CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;
}
//獲取對應資源
Resources resources = packageInfo.getResources(mainThread);
if (resources != null) {
    if (displayId != Display.DEFAULT_DISPLAY
            || overrideConfiguration != null
            || (compatInfo != null && compatInfo.applicationScale
                    != resources.getCompatibilityInfo().applicationScale)) {

        if (container != null) {
            // This is a nested Context, so it can't be a base Activity context.
            // Just create a regular Resources object associated with the Activity.
            resources = mResourcesManager.getResources(
                    activityToken,
                    packageInfo.getResDir(),
                    packageInfo.getSplitResDirs(),
                    packageInfo.getOverlayDirs(),
                    packageInfo.getApplicationInfo().sharedLibraryFiles,
                    displayId,
                    overrideConfiguration,
                    compatInfo,
                    packageInfo.getClassLoader());
        } else {
            // This is not a nested Context, so it must be the root Activity context.
            // All other nested Contexts will inherit the configuration set here.
            resources = mResourcesManager.createBaseActivityResources(
                    activityToken,
                    packageInfo.getResDir(),
                    packageInfo.getSplitResDirs(),
                    packageInfo.getOverlayDirs(),
                    packageInfo.getApplicationInfo().sharedLibraryFiles,
                    displayId,
                    overrideConfiguration,
                    compatInfo,
                    packageInfo.getClassLoader());
        }
    }
}
mResources = resources;
}

在ContextImpl的構造方法中會初始化化該進程的各個字段,例如資源、包信息、屏幕配置等。通過packageInfo可以得到Resources。所以我們發現要知道app資源的加載時間節點取決于ContextImpl的創建時間。

我們來看看 ContextImpl是什么時候賦值給Activity的mBase的?

先來講講app的啟動,app在啟動是,首先會fork一個子進程,并且調用ActivityThread.mian方法啟動該進程。ActivityThread又會構建Application對象,然后和Activity、ContextImpl關聯起來,最后會調用Activity的onCreate()、onStart()、onResume()函數使Activity運行起來 ,此時app的界面就出現在我們面前了。main函數會間接地調用ActivityThread中的handleLaunchActivity函數啟動默認的Activity,handleLaunchActivity代碼如下:

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
//代碼省略
 Activity a = performLaunchActivity(r, customIntent);
//代碼省略
}

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
// 代碼省略
Activity activity = null;
try {
    java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
//創建Activity
    activity = mInstrumentation.newActivity(
            cl, component.getClassName(), r.intent);
    StrictMode.incrementExpectedActivityCount(activity.getClass());
    r.intent.setExtrasClassLoader(cl);
    r.intent.prepareToEnterProcess();
    if (r.state != null) {
        r.state.setClassLoader(cl);
    }
} catch (Exception e) {
    if (!mInstrumentation.onException(activity, e)) {
        throw new RuntimeException(
            "Unable to instantiate activity " + component
            + ": " + e.toString(), e);
    }
}
try {
//創建Application
    Application app = r.packageInfo.makeApplication(false, mInstrumentation)

    if (activity != null) {
   //構建ContextImpl   createBaseContextForActivity方法返回的是ContextImpl對象
        Context appContext = createBaseContextForActivity(r, activity);
        CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
        Configuration config = new Configuration(mCompatConfiguration);
        
 //建立Activity與ContextImpl、Application的關聯 
        activity.attach(appContext, this, getInstrumentation(), r.token,
                r.ident, app, r.intent, r.activityInfo, title, r.parent,
                r.embeddedID, r.lastNonConfigurationInstances, config,
                r.referrer, r.voiceInteractor, window); 
//代碼省略
       
//回調Activity的onCreate方法
        if (r.isPersistable()) {
            mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
        } else {
            mInstrumentation.callActivityOnCreate(activity, r.state);
        }
} catch (SuperNotCalledException e) {
    throw e;
} catch (Exception e) {
    if (!mInstrumentation.onException(activity, e)) {
        throw new RuntimeException(
            "Unable to start activity " + component
            + ": " + e.toString(), e);
    }
}
return activity;
}

從代碼中我可以看到,每次創建Activiy都會創建ContextImpl與這個activity關聯起來,當然關聯起來的還有Application,只是Application只會創建一次。ContextImpl最終會被ContentWrapper類的mBase字段引用。

總結一下,獲取資源的操作實際上是由ContextImpl來完成的,Activity、Service等組件的getResource方法最終都會轉發給ContextImpl類型的mBase字段。也就是調用了ContextImpl的getResource函數,而這個Resource在ContextImpl關聯到 Activity之前就會初始化Resource對象。

Android資源是如何進行匹配的

根據上面的內容我們知道在ContextImpl構造函數中進行Resource的初始化,那我們看看 Resource是如何進行初始化的。

private ContextImpl(ContextImpl container, ActivityThread mainThread,
    LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
    Display display, Configuration overrideConfiguration, int createDisplayWithId) {
mOuterContext = this;
mPackageInfo = packageInfo;
//代碼省略
Resources resources = packageInfo.getResources(mainThread);
if (resources != null) {
if (displayId != Display.DEFAULT_DISPLAY
        || overrideConfiguration != null
        || (compatInfo != null && compatInfo.applicationScale
                != resources.getCompatibilityInfo().applicationScale)) {

    if (container != null) {
        // This is a nested Context, so it can't be a base Activity context.
        // Just create a regular Resources object associated with the Activity.
//根據設備配置獲取對應的資源 
        resources = mResourcesManager.getResources(
                activityToken,
                packageInfo.getResDir(),
                packageInfo.getSplitResDirs(),
                packageInfo.getOverlayDirs(),
                packageInfo.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfiguration,
                compatInfo,
                packageInfo.getClassLoader());
    } else {
        // This is not a nested Context, so it must be the root Activity context.
        // All other nested Contexts will inherit the configuration set here.
        resources = mResourcesManager.createBaseActivityResources(
                activityToken,
                packageInfo.getResDir(),
                packageInfo.getSplitResDirs(),
                packageInfo.getOverlayDirs(),
                packageInfo.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfiguration,
                compatInfo,
                packageInfo.getClassLoader());
    }
}
}
mResources = resources;
}

在通過mPackageInfo得到對應的資源之后,最終都會調用ResourceManager的中的方法來根據設備配置等相關信息獲取對應的資源,也就是資源的適配。已ResourceManager中的getResources方法為例,代碼如下:

public @Nullable Resources getResources(@Nullable IBinder activityToken,
    @Nullable String resDir,
    @Nullable String[] splitResDirs,
    @Nullable String[] overlayDirs,
    @Nullable String[] libDirs,
    int displayId,
    @Nullable Configuration overrideConfig,
    @NonNull CompatibilityInfo compatInfo,
    @Nullable ClassLoader classLoader) {
try {
    Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
//已apk路徑、屏幕設備id、配置等構建一個資源key
    final ResourcesKey key = new ResourcesKey(
            resDir,
            splitResDirs,
            overlayDirs,
            libDirs,
            displayId,
            overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
            compatInfo);
    classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
//根據這個key和ativityToke在mActivityResourceReferences中查看是否加載過這個資源,如果     
//有直接返回,如果沒有加載過生成一個Resource返回并保存到 mActivityResourceReferences
//中。
    return getOrCreateResources(activityToken, key, classLoader);
} finally {
    Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
}
}

在android23之前版本ResourcesManager通過mActiveResources字段管理Resource,它的數據類型是ArrayMap<ResourcesKey, WeakReference<Resources> >,在android23之后ResourcesManager通過mActivityResourceReferences字段管理Resources,它的數據類型是WeakHashMap<IBinder, ActivityResources>,ActivityResources的結構代碼如下:

private static class ActivityResources {
public final Configuration overrideConfig = new Configuration();
public final ArrayList<WeakReference<Resources>> activityResources = new ArrayList<>();
}

已android23之前Resources的管理方式為例說明,代碼如下:

public Resources getTopLevelResources(String resDir, String[] splitResDirs,
    String[] overlayDirs, String[] libDirs, int displayId,
    Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {
final float scale = compatInfo.applicationScale;
ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token);
Resources r;
synchronized (this) {
//判斷是否加載過該資源
    WeakReference<Resources> wr = mActiveResources.get(key);
    r = wr != null ? wr.get() : null;
    if (r != null && r.getAssets().isUpToDate()) {
    //已加載過,直接返回
        return r;
    }
}
//沒有加載過,構建AssetManager對象
AssetManager assets = new AssetManager();
   //將APK路徑添加到AssetManager的資源路徑中
if (resDir != null) {
    if (assets.addAssetPath(resDir) == 0) {
        return null;
    }
}
//屏幕分辨率
DisplayMetrics dm = getDisplayMetricsLocked(displayId);
//設備配置
Configuration config;
//創建Resources
r = new Resources(assets, dm, config, compatInfo, token);
if (false) {
    Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "
            + r.getConfiguration() + " appScale="
            + r.getCompatibilityInfo().applicationScale);
}
synchronized (this) {
    //緩存資源 
    mActiveResources.put(key, new WeakReference<Resources>(r));
    return r;
}
}

首先會以APK路徑、屏幕設備id、配置等構建一個資源key,根據這個key到ResourcesManager類的mActiveResources中查詢是否 加載已經加載過該Apk資源,如果含有緩存那么直接使用緩存。這個mActiveResources維護了當前應用程序進程中加載的每一個APK文件及其對應的Resources對象的對應關系。如果沒有緩存,那么就會創建一個,并且保存在mActiveResources中。

在沒有資源緩存的情況下,ActivityThread會創建一個AssetManager對象,并且調用AssetManager對象的addAssetPath函數來將參數resDir作為它的資源目錄,這個Dir就是Apk文件的絕對路徑。創建了一個新的AssetManager對象之后,會將這個AssetManager對象作為Resource構造的第一個參數來構建一個新的Resources對象。這個新創建的Resources對象會以前面所創建的ResourcesKey對象為鍵值緩存在mActiveResources所描述的一個HashMap中,以便重復使用該資源時無需重復創建。

總結

以上就是關于android資源加載和匹配的源碼分析,通過對資源加載機制的學習,可以幫助我們重更深的角度理解一個app在android系統中的運行原理。據我說知,目前很多android插件技術也是基于android的資源加載機制實現的。最后希望我的文章能對大家有所幫助,文中有任何錯誤歡迎大家指出。

參考:
1.《Android源碼設計模式解析與實戰》
2.Android源碼

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容