Android的資源管理器的建立過程

渣渣008發表於2017-12-13

最近在研究Android的外掛化,外掛化需要解決的問題大概有這樣的幾個,為什麼需要外掛化技術這個就不說啦。

  • 資源訪問
  • 元件的生命週期的管理

參考: Android資源管理框架(Asset Manager)簡要介紹和學習計劃 Android應用程式的Activity啟動過程簡要介紹和學習計劃 Android原始碼分析-Activity的啟動過程

這篇文章淺解一下Android的資源管理器的建立過程。這裡說一下,其實很多人對外掛化的資源載入有一個誤區呀,就是一般別人說外掛化需要解決一個資源衝突的問題,這裡有一個很重要的問題。

為什麼會有資源衝突的問題?

這個問題其實不應該放在最前面說的,但是很多人都有一個誤區,感覺必須先說一下這個了。 首先看Android的資源分類,Android的資源可分為兩大類。分別是Asserts和Res。

  • Asserts assets類資源放在工程根目錄的Assets子目錄下,這些檔案最終會被原裝不動地打包在apk檔案中。如果我們要在程式中訪問這些檔案,那麼就需要指定檔名來訪問。
  • Res res資源比較多,放一張圖吧,基本一看就明白。
    res資源型別.png

res資源大概是這樣的啦,當然還有raw以及xml等資源啦。在編譯打包的過程中,會把資原始檔打包成二進位制檔案(.xml檔案打包成二進位制檔案,png檔案進行優化等)。會對除了assets資源之外所有的資源賦予一個資源ID常量,並且會生成一個資源索引表resources.arsc。

這個resources.arsc檔案記錄了所有的應用程式資源目錄的資訊,包括每一個資源名稱、型別、值、ID以及所配置的維度資訊。我們可以將這個resources.arsc檔案想象成是一個資源索引表,這個資源索引表在給定資源ID和裝置配置資訊的情況下,能夠在應用程式的資源目錄中快速地找到最匹配的資源。

這些資源ID被終會被定義為Java常量值,儲存在一個R.java檔案中,與應用程式的其它原始檔一起被編譯到程式中,這樣我們就可以在程式或者資原始檔中通過這些ID常量來訪問指定的資源。

資源ID的最終的格式是:0xPPTTNNNN 資源ID是一個4位元組的無符號整數,其中,最高位元組表示Package ID,次高位元組表示Type ID,最低兩位元組表示Entry ID。

  • PP Package ID相當於是一個名稱空間,限定資源的來源。 Android系統當前定義了兩個資源命令空間,其中一個系統資源命令空間(比如我們可以直接引用系統提供好的主題等),它的Package ID等於0x01,另外一個是應用程式資源命令空間,它的Package ID等於0x7F。系統資源包的Package ID就等於0x01,而我們在應用程式中定義的資源的Package ID的值都等於0x7F,上圖就可以看出來的。
  • TT Type ID是指資源的型別ID。 資源的型別有animator、anim、color、string和xml等等若干種,每一種都會被賦予一個ID。上圖也可以看出來的。
  • NNNN Entry ID是指每一個資源在其所屬的資源型別中所出現的次序。 注意,不同型別的資源的Entry ID有可能是相同的,但是由於它們的型別不同,我們仍然可以通過其資源ID來區別開來。

上面說這麼多就是想說,我們寫的App通常情況下資源等的ID都是0x7F開始的,外掛化的時候,開發外掛是當作一個App來開發的,打包的時候資源ID也是0x7F開始的,所以呢,這個外掛與外掛,外掛與宿主的資源ID很有可能是一樣的。

下面說的是重點: 在外掛化的開發過程中,載入外掛的資源可以單獨建立了用於訪問外掛資源的AssertManager和Resource物件,即外掛獨立使用一個資源管理器,*這樣宿主訪問宿主的資源,外掛訪問外掛的資源,這樣子是不會出現資源衝突問題的。*然而這個(外掛使用單獨的資源管理器)在現實中是不切實際的,一般都會將外掛的資源路徑新增到宿主的AssetManager中,這樣做的原因是為了外掛與宿主之間的資源共享,資源共享的原因主要是為了減少外掛的體積。 現階段對資源衝突的解決方案:

  • 修改aapt原始碼,定製aapt工具編譯期間修改PP段。 例如:atlas
  • 修改aapt的產物,即編譯後期重新整理外掛Apk的資源,編排ID。 例如:Small 這兩種方案都可以,修改aapt的原始碼,雖然說比較麻煩,但是其實需要修改的程式碼是比較少的,不過需要你有個Android的原始碼的環境才能編譯出aappt可執行檔案,修改aapt的產物,一般是寫gradle外掛來實現,因為Android的原始碼是知道的,所以我們能知道aapt最後生成的二進位制檔案的格式,然後自己整理資源的ID。

Android framework層的資源查詢

1.我們正常的使用資源的過程

在Android系統中,每一個應用程式一般都會配置很多資源,用來適配不同密度、大小和方向的螢幕,以及適配不同的國家、地區和語言等等。這些資源是在應用程式執行時自動根據裝置的當前配置資訊進行適配的。這也就是說,給定一個相同的資源ID,在不同的裝置配置之下,查詢到的可能是不同的資源。 這個查詢過程對應用程式來說,是完全透明的,這個過程主要是靠Android資源管理框架來完成的,而Android資源管理框架實際是由AssetManager和Resources兩個類來實現的。其中,Resources類可以根據ID來查詢資源,而AssetManager類根據檔名來查詢資源。事實上,如果一個資源ID對應的是一個檔案,那麼Resources類是先根據ID來找到資原始檔名稱,然後再將該檔名稱交給AssetManager類來開啟對應的檔案的。 注:上面這段話出自美團Android資源混淆保護實踐 一般我們查詢Assert資原始碼如下:

        try {
            getAssets().open("name");
        } catch (IOException e) {
            e.printStackTrace();
        }
複製程式碼

查詢Res資原始碼如下:

        Resources resources = getResources();
        String name = resources.getString(R.string.app_name);
複製程式碼

繼續跟蹤resources.getString的實現:

Res.getString.png
可以看到確實是使用的AssetManager來處理的。

2.上下文環境Context的建立

前面說了,查詢資源使用的是ResourcesAssetManager,那我們來跟蹤一下這兩個類的建立生成吧。

mBase.getResource.png
getResources.png

我們會很清楚的發現這兩個類全部是由Context建立的,所以現在需要找到mBaseContext的生成過程,從上圖清晰可見的是mBase的生成時機是在attachBaseContext這個方法中,找到哪裡呼叫這個方法,最後在子類Activity中找到了呼叫的時機。

Activity.attach.png
通過這個方法名,我們大概就知道,這個方法是Activity建立的時候會呼叫的,現在我們應該看看,一個Activity是怎樣建立出來的咯。

說到Activity的建立,首先應該想到Activity#startActivity方法的,從上往下看,顯然最後都是呼叫的Activity#startActivityForResult來實現的。

startActivityForResult.png
可以發現真正開啟Activity的實現在InstrumentationexecStartActivity方法中,我們去看他的實現:
Instrumentation#execStartActivity.png
然後觀察,發現最後呼叫的是: int result = ActivityManagerNative.getDefault().startActivity(whoThread, who.getBasePackageName(),intent,intent.resolveTypeIfNeeded(who.getContentResolver()),token, target, requestCode, 0, null, options); 這裡的ActivityManagerNative.getDefault返回IActivityManager物件,由於需要啟動的Activity不一定是我們當前程式的,比如Launcher在桌面啟動一個應用就會新開個程式的。這裡就會有IPC互動,這裡返回的IActivityManager,如果是和當前程式在同一個程式就是ActivityManagerNative的子類,因為IActivityManager介面裡面的方法它都沒有實現的,如果不在同一個程式這個IActivityManager物件就是ActivityManagerProxy物件。

IActivityManagerBinder的Server端的實現是ActivityManagerService。所以最後startActivity呼叫的是ActivityManagerService的startActivity方法。

public final class ActivityManagerService extends ActivityManagerNative
        implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {
    @Override
    public final int startActivity(IApplicationThread caller, String callingPackage,
            Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode,
            int startFlags, ProfilerInfo profilerInfo, Bundle options) {
        return startActivityAsUser(caller, callingPackage, intent, resolvedType, resultTo,
            resultWho, requestCode, startFlags, profilerInfo, options,
            UserHandle.getCallingUserId());
    }

    @Override
    public final int startActivityAsUser(IApplicationThread caller, String callingPackage,
            Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode,
            int startFlags, ProfilerInfo profilerInfo, Bundle options, int userId) {
        enforceNotIsolatedCaller("startActivity");
        userId = handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(), userId,
                false, ALLOW_FULL_ONLY, "startActivity", null);
        return mStackSupervisor.startActivityMayWait(caller, -1, callingPackage, intent,
                resolvedType, null, null, resultTo, resultWho, requestCode, startFlags,
                profilerInfo, null, null, options, false, userId, null, null);
    }
}
複製程式碼

然後呼叫的是 ActivityStackSupervisor#startActivityMayWait --->ActivityStackSupervisor#startActivityLocked --->ActivityStackSupervisor#startActivityUncheckedLocked --->ActivityStack#startActivityLocked --->ActivityStackSupervisor#resumeTopActivitiesLocked --->ActivityStackSupervisor#resumeTopActivityLocked --->ActivityStack#resumeTopActivityInnerLocked --->ActivityStackSupervisor#startSpecificActivityLocked --->ActivityStackSupervisor#realStartActivityLocked 過程非常複雜,在最後的方法裡面呼叫了 app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken,System.identityHashCode(r), r.info, newConfiguration(mService.mConfiguration),new Configuration(stack.mOverrideConfig), r.compat, r.launchedFromPackage,task.voiceInteractor, app.repProcState, r.icicle, r.persistentState, results,newIntents, !andResume, mService.isNextTransitionForward(), profilerInfo); ,這裡的app.thread物件是IApplicationThread介面型別的物件。 IApplicationThread物件的Server端的實現是ApplicationThreadNative的子類ApplicationThread(它是ActivityThread的內部類),Proxy本地的代理實現是ApplicationThreadProxy。 最後呼叫的是下面的方法:

public final class ActivityThread {
    private class ApplicationThread extends ApplicationThreadNative {

        @Override
        public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
                ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
                CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
                int procState, Bundle state, PersistableBundle persistentState,
                List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
                boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {
               // 省略
               sendMessage(H.LAUNCH_ACTIVITY, r);
        }
  }
}
複製程式碼

函式中的處理就是Android的訊息系統的正常處理流程了,因為這個是ActivityThread的內部類,那麼對應的Handler應該在ActivityThread裡面的,最後可以找到是H.handleMessage處理的啦。 下面呼叫的是ActivityThread的:

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent);
--->
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent);
複製程式碼

來看看這個方法:

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        // System.out.println("##### [" + System.currentTimeMillis() + "] ActivityThread.performLaunchActivity(" + r + ")");
        ActivityInfo aInfo = r.activityInfo;
        // 省略
        ComponentName component = r.intent.getComponent();
        // 省略
        Activity activity = null;
        try {
            // 建立Activity
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            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) {
        }

        try {
            // 建立Applicatipn
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);
            if (activity != null) {
                // 建立Context即上面說的mBase。
                Context appContext = createBaseContextForActivity(r, activity);
                CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
                Configuration config = new Configuration(mCompatConfiguration);
                if (customIntent != null) {
                    activity.mIntent = customIntent;
                }
                r.lastNonConfigurationInstances = null;
                activity.mStartedActivity = false;
                int theme = r.activityInfo.getThemeResource();
                if (theme != 0) {
                    activity.setTheme(theme);
                }
                activity.mCalled = false;
                // 呼叫onCreate
                if (r.isPersistable()) {
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                } else {
                    mInstrumentation.callActivityOnCreate(activity, r.state);
                }
        }   
            r.paused = true;
            mActivities.put(r.token, r);
        } catch (SuperNotCalledException e) {
            throw e;
        } catch (Exception e) {
        }
        return activity;
    }
複製程式碼

看到這裡,我們終於找到了Activity裡面的mBase變數是怎麼生成的啦。現在看createBaseContextForActivity這個方法就知道,Context的真正實現了一些我們想知道方法的類是哪個啦。

    private Context createBaseContextForActivity(ActivityClientRecord r, final Activity activity) {
        int displayId = Display.DEFAULT_DISPLAY;
        try {
            displayId = ActivityManagerNative.getDefault().getActivityDisplayId(r.token);
        } catch (RemoteException e) {
        }
        // 建立Context
        ContextImpl appContext = ContextImpl.createActivityContext(
                this, r.packageInfo, displayId, r.overrideConfig);
        appContext.setOuterContext(activity);
        Context baseContext = appContext;
        // 省略。。。
        return baseContext;
    }
複製程式碼

這裡我們終於知道,Context的最終的實現類是ContextImpl啦。 Android應用程式視窗的執行上下文環境是通過ContextImpl類來描述的,即每一個Activity元件都關聯有一個ContextImpl物件。ContextImpl類繼承了Context類,它與Activity元件的關係如圖所示: 圖片取自[Android應用程式視窗(Activity)的執行上下文環境(Context)的建立過程分析](http://blog.csdn.net/luoshengyang/article/details/8201936)

ContextImpl類與Activity類的關係圖.jpg
這裡我們解決了Activity裡面的Context是怎麼生成的問題。

3. AssetManager的建立過程

上面我們已經知道了,Activity裡面的Context的建立了,那麼那兩個獲取資源的方法在ContextImpl裡面是怎樣的呢。

    public class ContextImpl {
    @Override
    public AssetManager getAssets() {
        return getResources().getAssets();
    }
    @Override
    public Resources getResources() {
        return mResources;
    }
    }
複製程式碼

ContextImpl類的成員函式getResources返回的是一個Resources物件,有了這個Resources物件之後,我們就可以通過資源ID來訪問那些被編譯過的應用程式資源了。ContextImpl類的成員函式getAssets返回的是一個AssetManager物件,有了這個AssetManager物件之後,我們就可以通過檔名來訪問那些被編譯過或者沒有被編譯過的應用程式資原始檔了。事實上,Resources類也是通過AssetManager類來訪問那些被編譯過的應用程式資原始檔的,不過在訪問之前,它會先根據資源ID查詢得到對應的資原始檔名。

首先看ContextImpl的Resources物件的產生過程:

    private ContextImpl(ContextImpl container, ActivityThread mainThread,
            LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
            Display display, Configuration overrideConfiguration, int createDisplayWithId) {
        mResourcesManager = ResourcesManager.getInstance();
        Resources resources = packageInfo.getResources(mainThread);
        if (resources != null) {
            if (displayId != Display.DEFAULT_DISPLAY
                    || overrideConfiguration != null
                    || (compatInfo != null && compatInfo.applicationScale
                            != resources.getCompatibilityInfo().applicationScale)) {
                resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
                        packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
                        packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
                        overrideConfiguration, compatInfo);
            }
        }
        mResources = resources;
    }
複製程式碼

引數packageInfo指向的是一個LoadedApk物件,這個LoadedApk物件描述的是當前正在啟動的Activity組所屬的Apk。用來訪問應用程式資源的Resources物件是通過呼叫引數packageInfo所指向的是一個LoadedApk物件的成員函式getResources來建立的。這個Resources物件建立完成之後,由於應用程式的一些其他設定可能改變,需要重新生成Resource,最終生成的物件,就會儲存在ContextImpl類的成員變數mResources中。這兩處生成Resources的方法最終都會呼叫到ResourcesManagergetTopLevelResources方法。

    Resources getTopLevelResources(String resDir, String[] splitResDirs,
            String[] overlayDirs, String[] libDirs, int displayId,
            Configuration overrideConfiguration, CompatibilityInfo compatInfo) {
        final float scale = compatInfo.applicationScale;
        Configuration overrideConfigCopy = (overrideConfiguration != null)
                ? new Configuration(overrideConfiguration) : null;
        ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfigCopy, scale);
        Resources r;
        synchronized (this) {
        // 省略
        AssetManager assets = new AssetManager();
        // resDir can be null if the 'android' package is creating a new Resources object.
        // This is fine, since each AssetManager automatically loads the 'android' package
        // already.
        if (resDir != null) {
            if (assets.addAssetPath(resDir) == 0) {
                return null;
            }
        }
        if (splitResDirs != null) {
            for (String splitResDir : splitResDirs) {
                if (assets.addAssetPath(splitResDir) == 0) {
                    return null;
                }
            }
        }
        if (overlayDirs != null) {
            for (String idmapPath : overlayDirs) {
                assets.addOverlayPath(idmapPath);
            }
        }
        if (libDirs != null) {
            for (String libDir : libDirs) {
                if (libDir.endsWith(".apk")) {
                    // Avoid opening files we know do not have resources,
                    // like code-only .jar files.
                    if (assets.addAssetPath(libDir) == 0) {
                    }
                }
            }
        }

        //Log.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics);
        DisplayMetrics dm = getDisplayMetricsLocked(displayId);
        Configuration config;
        final boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY);
        final boolean hasOverrideConfig = key.hasOverrideConfiguration();
        if (!isDefaultDisplay || hasOverrideConfig) {
            config = new Configuration(getConfiguration());
            if (!isDefaultDisplay) {
                applyNonDefaultDisplayMetricsToConfigurationLocked(dm, config);
            }
            if (hasOverrideConfig) {
                config.updateFrom(key.mOverrideConfiguration);
                if (DEBUG) Slog.v(TAG, "Applied overrideConfig=" + key.mOverrideConfiguration);
            }
        } else {
            config = getConfiguration();
        }
        r = new Resources(assets, dm, config, compatInfo);
        // 省略
            return r;
        }
    }
複製程式碼

getTopLevelResources來獲得一個Resources物件的時候,需要指定要獲取的Resources物件所對應的Apk檔案路徑,這個Apk檔案路徑就儲存在LoadedApk類的成員變數mResDir中,這裡還可能有其他的資源路徑,也可以新增。 這樣就建立出了Resources物件和AssertManager物件啦。

下面看AssetManager類的建構函式和成員函式addAssetPath的實現,接著再看Resources類的建構函式的實現。

    public AssetManager() {
        synchronized (this) {
            if (DEBUG_REFS) {
                mNumRefs = 0;
                incRefsLocked(this.hashCode());
            }
            init(false);
            if (localLOGV) Log.v(TAG, "New asset manager: " + this);
            ensureSystemAssets();
        }
    }
複製程式碼

AssetManager類的建構函式是通過呼叫另外一個成員函式init來執行初始化工作的。在初始化完成當前正在建立的AssetManager物件之後,AssetManager類的建構函式還會呼叫另外一個成員函式ensureSystemAssets來檢查當前程式是否已經建立了一個用來訪問系統資源的AssetManager物件。

如果用來訪問系統資源的AssetManager物件還沒有建立的話,那麼AssetManager類的成員函式ensureSystemAssets就會建立並且初始化它,並且將它儲存在AssetManager類的靜態成員變數sSystem中。注意,建立用來訪問系統資源和應用程式資源的AssetManager物件的過程是一樣的,區別只在於它們所要訪問的Apk檔案不一樣。 addAssetPath的C++實現在android_util_AssetManager.cpp C++實在太差。 通過搜尋java對應的方法即可找到C++對應的方法啦。

{ 
"addAssetPathNative", 
"(Ljava/lang/String;Z)I",
(void*) android_content_AssetManager_addAssetPath 
}
複製程式碼
static jint android_content_AssetManager_addAssetPath(JNIEnv* env, jobject clazz,
                                                       jstring path, jboolean appAsLib)
{
    ScopedUtfChars path8(env, path);
    if (path8.c_str() == NULL) {
        return 0;
    }
    AssetManager* am = assetManagerForJavaObject(env, clazz);
    if (am == NULL) {
        return 0;
    }
    int32_t cookie;
    bool res = am->addAssetPath(String8(path8.c_str()), &cookie, appAsLib);
    return (res) ? static_cast<jint>(cookie) : 0;
}
複製程式碼

額,然後呼叫到了AssetManageraddAssetPath方法。 這個檔案在AssetManager.cpp

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());
            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;
        }
    }
    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;
}
複製程式碼

如果全域性變數kAppZipName的值不等於NULL的話,那麼它的值一般就是被設定為“classes.jar”,這時候就表示應用程式的資原始檔是儲存在引數path所描述的一個目錄下的一個classes.jar檔案中。全域性變數kAppZipName的值一般被設定為NULL,並且引數path指向的是一個Apk檔案。 AssetManager類的成員函式addAssetPath首先是要檢查引數path指向的是一個檔案或者目錄,並且該檔案或者目錄存在,否則的話,它就會直接返回一個false值,而不會再繼續往下處理了。 如果已經新增過了,那麼AssetManager類的成員函式addAssetPath就不會再繼續往下處理了。如果達到條件就會把路徑新增到成員變數mAssetPaths所描述的一個Vector中去。

下面是Resources的建立過程。

    public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config,
            CompatibilityInfo compatInfo) {
        mAssets = assets;
        mMetrics.setToDefaults();
        if (compatInfo != null) {
            mCompatibilityInfo = compatInfo;
        }
        updateConfiguration(config, metrics);
        assets.ensureStringBlocks();
    }
複製程式碼

Resources類的成員變數mConfiguration指向的是一個Configuration物件,用來描述裝置當前的配置資訊。 Resources類的成員函式updateConfiguration首先是根據引數config和metrics來更新裝置的當前配置資訊,例如,螢幕大小和密碼、國家地區和語言、鍵盤配置情況等等,接著再呼叫成員變數mAssets所指向的一個Java層的AssetManager物件的成員函式setConfiguration來將這些配置資訊設定到與之關聯的C++層的AssetManager物件中去。

    /*package*/ final void makeStringBlocks(StringBlock[] seed) {
        final int seedNum = (seed != null) ? seed.length : 0;
        final int num = getStringBlockCount();
        mStringBlocks = new StringBlock[num];
        for (int i=0; i<num; i++) {
            if (i < seedNum) {
                mStringBlocks[i] = seed[i];
            } else {
                mStringBlocks[i] = new StringBlock(getNativeStringBlock(i), true);
            }
        }
    }
複製程式碼

AssetManager類的成員變數mStringBlocks指向的是一個StringBlock陣列,其中,每一個StringBlock物件都是用來描述一個字串資源池AssetManager類的成員變數mStringBlocks就是用來儲存所有的資源表中的資源項值字串資源池的。 如果資源還沒讀取出來,那麼會先讀取的。也會將系統資源表裡面的資源項值字串資源池也一起拷貝到成員變數mStringBlokcs所描述的一個陣列中去。getStringBlockCount這個方法獲取的資源數,含有系統資源表的個數sysNum的。

            if (i < seedNum) {
                mStringBlocks[i] = seed[i];
複製程式碼

這裡如果有疑問的話,其實是這樣的: 用來訪問系統資源包的AssetManager物件就儲存在AssetManager類的靜態成員變數sSystem中,並且這個AssetManager物件是最先被建立以及初始化的。也就是說,當執行到這一步的時候,所有系統資源表的資源項值字串資源池已經讀取出來,它們就儲存在AssetManager類的靜態成員變數sSystem所描述的一個AssetManager物件的成員變數mStringBlocks中,因此,只將它們拷貝到當前正在處理的AssetManager物件的成員變數mStringBlokcs的前sysNum個位置上去就可以了。

這裡,就分析完成Android應用程式資源管理器的建立的初始化過程了,主要就是建立和初始化AssetManager和Resources,其中,初始化操作包括設定AssetManager物件的資原始檔路徑以及裝置配置資訊等。

所以我們想要載入一個外掛的資源,首先要確定是宿主和外掛是不是需要共享資源,需要共享的話,可能只是呼叫AssetManager.addAssetPath(),把外掛apk的地址傳遞進來,然後進行即可,但是這個方案的前提是已經解決 資源的衝突問題。 另外一種方案就是,外掛使用一個全新的Resources物件。

public class LoadResources {

    public static class PluginResource {
        public Resources resources;
        public AssetManager assetManager;
        public Resources.Theme theme;
    }

    public static PluginResource getPluginResources(String apkPath, Resources supResource, Resources.Theme supTheme) {
        try {
            PluginResource resource = new PluginResource();
            //建立AssetManager
            AssetManager newAssetManager = AssetManager.class.newInstance();
            Method addAssetPathMethod = newAssetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
            addAssetPathMethod.setAccessible(true);
            addAssetPathMethod.invoke(newAssetManager, apkPath);
            Method ensureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
            ensureStringBlocks.setAccessible(true);
            ensureStringBlocks.invoke(newAssetManager);
            //建立我們自己的Resource
            Resources newResource = new Resources(newAssetManager,
                    supResource.getDisplayMetrics(), supResource.getConfiguration());
            Resources.Theme newTheme = newResource.newTheme();
            newTheme.setTo(supTheme);
            resource.assetManager = newAssetManager;
            resource.resources = newResource;
            resource.theme = newTheme;
            return resource;
        } catch (Exception e) {
        }
        return null;
    }

}
複製程式碼

然後複寫Activity的三個對應的方法,在需要的時候返回想要的物件即可。

相關文章