1. 程式人生 > >Android外掛化學習之路(四)之使用外掛中的R資源

Android外掛化學習之路(四)之使用外掛中的R資源

res裡的每一個資源都會在R.java裡生成一個對應的Integer型別的id,APP啟動時會先把R.java註冊到當前的上下文環境,我們在程式碼裡以R檔案的方式使用資源時正是通過使用這些id訪問res資源,然而外掛的R.java並沒有註冊到當前的上下文環境,所以外掛的res資源也就無法通過id使用了。

如何使用外掛中的R資源

一種解決方式是外掛裡需要用到的新資源都通過純Java程式碼的方式建立(包括XML佈局、動畫、點九圖等),蛋疼但有效。

但這種方式太麻煩了,於是有了以下的解決方式:
記得我們平時怎麼使用res資源的嗎,就是“getResources().getXXX(resid)”,看看“getResources()”

  @Override
    public Resources getResources() {
        if (mResources != null) {
            return mResources;
        }
        if (mOverrideConfiguration == null) {
            mResources = super.getResources();
            return mResources;
        } else {
            Context resc = createConfigurationContext(mOverrideConfiguration);
            mResources = resc.getResources();
            return
mResources; } }

看起來像是通過mResources例項獲取res資源的,在找找mResources例項是怎麼初始化的,看看上面的程式碼發現是使用了super類ContextThemeWrapper裡的“getResources()”方法,看進去

 Context mBase;

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

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

看樣子又呼叫了Context的“getResources()”方法,看到這裡,我們知道Context只是個抽象類,其實際工作都是在ContextImpl完成的,趕緊去ContextImpl裡看看“getResources()”方法吧

    @Override
    public Resources getResources() {
        return mResources;
    }

到這裡並沒有mResources的建立過程啊!mResources是ContextImpl的成員變數,可能是在構造方法中建立的,於是我們看一下構造方法(這裡只給出關鍵程式碼)。

resources=mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
        packageInfo.getSplitResDirs(),packageInfo.getOverlayDirs(),
        packageInfo.getApplicationInfo().sharedLibraryFiles,displayId,
        overrideConfiguration,compatInfo);
        mResources=resources;

看樣子是在ResourcesManager的“getTopLevelResources”方法中建立的,看進去

  Resources getTopLevelResources(String resDir, String[] splitResDirs,
                                   String[] overlayDirs, String[] libDirs, int displayId,
                                   Configuration overrideConfiguration, CompatibilityInfo compatInfo) {
        Resources r;
        AssetManager assets = new AssetManager();
        if (libDirs != null) {
            for (String libDir : libDirs) {
                if (libDir.endsWith(".apk")) {
                    if (assets.addAssetPath(libDir) == 0) {
                        Log.w(TAG, "Asset path '" + libDir +
                                "' does not exist or contains no resources.");
                    }
                }
            }
        }
        DisplayMetrics dm = getDisplayMetricsLocked(displayId);
        Configuration config;
        ……
        r = new Resources(assets, dm, config, compatInfo);
        return r;
    }

看來這裡是關鍵了,看樣子就是通過這些程式碼從一個APK檔案載入res資源並建立Resources例項,經過這些邏輯後就可以使用R檔案訪問資源了。具體過程是,獲取一個AssetManager例項,使用其“addAssetPath”方法載入APK(裡的資源),再使用DisplayMetrics、Configuration、CompatibilityInfo例項一起建立我們想要的Resources例項。

於是,我們可以通過以下程式碼載入外掛APK裡res資源

 try {  
        AssetManager assetManager = AssetManager.class.newInstance();  
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);  
        addAssetPath.invoke(assetManager, mDexPath);  
        mAssetManager = assetManager;  
    } catch (Exception e) {  
        e.printStackTrace();  
    }  
    Resources superRes = super.getResources();  
    mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),  
            superRes.getConfiguration());  

注意,有的人擔心從外掛APK載入進來的res資源的ID可能與主專案裡現有的資源ID衝突,其實這種方式載入進來的res資源並不是融入到主專案裡面來,主專案裡的res資源是儲存在ContextImpl裡面的Resources例項,整個專案共有,而新加進來的res資源是儲存在新建立的Resources例項的,也就是說ProxyActivity其實有兩套res資源,並不是把新的res資源和原有的res資源合併了(所以不怕R.id重複),對兩個res資源的訪問都需要用對應的Resources例項,這也是開發時要處理的問題。(其實應該有3套,Android系統會載入一套framework-res.apk資源,裡面存放系統預設Theme等資源)

這裡你可能注意到了我們採用了反射的方法呼叫AssetManager的“addAssetPath”方法,而在上面ResourcesManager中呼叫AssetManager的“addAssetPath”方法是直接呼叫的,不用反射啊,而且看看SDK裡AssetManager的“addAssetPath”方法的原始碼(這裡也能看到具體APK資源的提取過程是在Native裡完成的),發現它也是public型別的,外部可以直接呼叫,為什麼還要用反射呢?

 /**
     * Add an additional set of assets to the asset manager.  This can be
     * either a directory or ZIP file.  Not for use by applications.  Returns
     * the cookie of the added asset, or 0 on failure.
     * {@hide}
     */
    public final int addAssetPath(String path) {
        synchronized (this) {
            int res = addAssetPathNative(path);
            makeStringBlocks(mStringBlocks);
            return res;
        }
    }

這裡有個誤區,SDK的原始碼只是給我們參考用的,APP實際上執行的程式碼邏輯在android.jar裡面(位於android-sdk\platforms\android-XX),反編譯android.jar並找到ResourcesManager類就可以發現這些介面都是對應用層隱藏的。

public final class AssetManager {
    AssetManager() {
        throw new RuntimeException("Stub!");
    }

    public void close() {
        throw new RuntimeException("Stub!");
    }

    public final InputStream open(String fileName) throws IOException {
        throw new RuntimeException("Stub!");
    }

    public final InputStream open(String fileName, int accessMode) throws IOException {
        throw new RuntimeException("Stub!");
    }

    public final AssetFileDescriptor openFd(String fileName) throws IOException {
        throw new RuntimeException("Stub!");
    }

    public final native String[] list(String paramString) throws IOException;

    public final AssetFileDescriptor openNonAssetFd(String fileName) throws IOException {
        throw new RuntimeException("Stub!");
    }

    public final AssetFileDescriptor openNonAssetFd(int cookie, String fileName) throws IOException {
        throw new RuntimeException("Stub!");
    }

    public final XmlResourceParser openXmlResourceParser(String fileName) throws IOException {
        throw new RuntimeException("Stub!");
    }

    public final XmlResourceParser openXmlResourceParser(int cookie, String fileName) throws IOException {
        throw new RuntimeException("Stub!");
    }

    protected void finalize() throws Throwable {
        throw new RuntimeException("Stub!");
    }

    public final native String[] getLocales();
}

載入外掛中的layout資源

我們使用LayoutInflate物件,一般使用方法如下:

View view = LayoutInflater.from(context).inflate(R.layout.main_fragment, null);

其中,R.layout.main_fragment我們可以通過上述方法獲取其ID,那麼關鍵的一步就是如何生成一個context?直接傳入當前的context是不行的。 解決方案有2個:
• 1.建立一個自己的ContextImpl,Override其方法。
• 2.通過反射,直接替換當前context的mResources私有成員變數。
當然,我們是使用第二種方案:

@Override
    protected void attachBaseContext(Context context) {
        replaceContextResources(context);
        super.attachBaseContext(context);
    }

    /**
     * 使用反射的方式,使用Bundle的Resource物件,替換Context的mResources物件
     * @param context
     */
    public void replaceContextResources(Context context){
        try {
            Field field = context.getClass().getDeclaredField("mResources");
            field.setAccessible(true);
            field.set(context, mBundleResources);
            System.out.println("debug:repalceResources succ");
        } catch (Exception e) {
            System.out.println("debug:repalceResources error");
            e.printStackTrace();
        }
    }

我們在Activity的attachBaseContext方法中,對Context的mResources進行替換,這樣,我們就可以載入離線apk中的佈局了。

這樣,載入外掛的R資源就解決了。