1. 程式人生 > >詳解Android外掛化開發-資源訪問

詳解Android外掛化開發-資源訪問

    動態載入技術(也叫外掛化技術),當專案越來越龐大的時候,我們通過外掛化開發不僅可以減輕應用的記憶體和CPU佔用,還可以實現熱插拔,即在不釋出新版本的情況下更新某些模組。
    通常我們把安卓資原始檔製作成外掛的形式,無外乎有一下幾種:

zip、jar、dex、APK(未安裝APK、安裝APK)

    對於使用者來講未安裝的APK才是使用者所需要的,不安裝、不重啟,無聲無息的載入資原始檔,這正是我們開發者追求的結果。
    但是,開發中宿主程式調起未安裝的外掛apk,一個很大的問題就是資源如何訪問,這些資原始檔的ID都對映在gen資料夾下的R.java中,而外掛中凡是以R開頭的資源都不能訪問。究其原因是因為宿主程式中並沒有外掛的資源,所以通過R來載入外掛的資源是行不通的,程式會丟擲異常:無法找到某某id所對應的資源。
    那麼開發中該怎麼辦呢,今天我們來一起探討一下外掛化開發中資原始檔訪問的解決方案。
    想必大家在開發中都寫過類似程式碼,例如,在主程式訪問字串檔案

this.getResources().getString(R.string.app_name);

    這裡的this,其實就是Context,上下文物件。通常我們的的APK安裝路徑為:

/data/apk/packagename~1/base.apk

    APK啟動,Context通過類載入器載入完畢後,會去APK中載入資原始檔。想必大家都知道,Activity的工作主要是通過ContextImpl來完成的, Activity中有一個叫mBase的成員變數,它的型別就是ContextImpl。注意到Context中有如下兩個抽象方法,看起來是和資源有關的,實際上Context就是通過它們來獲取資源的。這兩個抽象方法的真正實現在ContextImpl中,也就是說,只要實現這兩個方法,就可以解決資源問題了。

/** Return an AssetManager instance for your application's package. */

public abstract AssetManager getAssets();

/** Return a Resources instance for your application's package. */

public abstract Resources getResources();

    我們若是想使用這兩個方法,需要例項化Context物件,通常我們可以根據APK中的包名完成Context物件的建立:

Context pluginContext = this
.createPackageContext("com.castiel.demo",flags);

    但是這樣做有個前提,必須要求初始化時載入的是自己APK,如果我們載入的是未安裝的外掛APK,這麼做肯定就不可取了。為啥呢,看原始碼:

Resources resources = packageInfo.getResources(mainThread);
        if (resources != null) {
            if (activityToken != null
                    || 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, activityToken);
            }
        }
        mResources = resources;

Resources在這裡被賦值,我們再去程式碼中第一行的packageInfo,它來自LoadedApk類,其中的getResources方法如下:


public Resources getResources(ActivityThread mainThread) {
        if (mResources == null) {
            mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
                    mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
        }
        return mResources;
    }

該方法採用單例模式,注意其中的getTopLevelResources()方法中的第一個引數mResDir,我們繼續找其源頭,在ActivityThread類中,發現了:

    /**
     * Creates the top level resources for the given package.
     */
    Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
            String[] libDirs, int displayId, Configuration overrideConfiguration,
            LoadedApk pkgInfo) {
        return mResourcesManager.getTopLevelResources(resDir, splitResDirs, overlayDirs, libDirs,
                displayId, overrideConfiguration, pkgInfo.getCompatibilityInfo(), null);
    }

重點看裡面的resDir引數,我們再往上找原始碼,最終找到ResourcesManager類,找到getTopLevelResources()方法:

    /**
     * Creates the top level Resources for applications with the given compatibility info.
     *
     * @param resDir the resource directory.
     * @param overlayDirs the resource overlay directories.
     * @param libDirs the shared library resource dirs this app references.
     * @param compatInfo the compability info. Must not be null.
     * @param token the application token for determining stack bounds.
     */
    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) {
            // Resources is app scale dependent.
            if (false) {
                Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);
            }
            WeakReference<Resources> wr = mActiveResources.get(key);
            r = wr != null ? wr.get() : null;
            //if (r != null) Slog.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());
            if (r != null && r.getAssets().isUpToDate()) {
                if (false) {
                    Slog.w(TAG, "Returning cached resources " + r + " " + resDir
                            + ": appScale=" + r.getCompatibilityInfo().applicationScale);
                }
                return r;
            }
        }
        //if (r != null) {
        //    Slog.w(TAG, "Throwing away out-of-date resources!!!! "
        //            + r + " " + resDir);
        //}

        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 (assets.addAssetPath(libDir) == 0) {
                    Slog.w(TAG, "Asset path '" + libDir +
                            "' does not exist or contains no resources.");
                }
            }
        }

        //Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics);
        DisplayMetrics dm = getDisplayMetricsLocked(displayId);
        Configuration config;
        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);
            }
        } else {
            config = getConfiguration();
        }
        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) {
            WeakReference<Resources> wr = mActiveResources.get(key);
            Resources existing = wr != null ? wr.get() : null;
            if (existing != null && existing.getAssets().isUpToDate()) {
                // Someone else already created the resources while we were
                // unlocked; go ahead and use theirs.
                r.getAssets().close();
                return existing;
            }

            // XXX need to remove entries when weak references go away
            mActiveResources.put(key, new WeakReference<Resources>(r));
            return r;
        }
}

該方法的註釋中,明確指出@param resDir the resource directory,載入本地資源目錄,載入自己的APK。

通過以上的分析,我們知道getResources()方法通過AssetManager載入自己的APK,那麼我們要想載入未安裝的外掛APK,唯有自定義實現一個Resources類,專門用來載入未安裝的APK。但是我試過了,直接重寫不行,為啥,因為Android並沒有提供Resource構造方法中的AssetManager的構造方法,我們看下原始碼:

    /**
     * Create a new Resources object on top of an existing set of assets in an
     * AssetManager.
     *
     * @param assets Previously created AssetManager.
     * @param metrics Current display metrics to consider when
     *                selecting/computing resource values.
     * @param config Desired device configuration to consider when
     *               selecting/computing resource values (optional).
     */
    public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(assets, metrics, config, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO);
    }

接著再看一下Resource構造方法中的AssetManager引數原始碼

    /**
     * Create a new AssetManager containing only the basic system assets.
     * Applications will not generally use this method, instead retrieving the
     * appropriate asset manager with {@link Resources#getAssets}.    Not for
     * use by applications.
     * {@hide}
     */
    public AssetManager() {
        synchronized (this) {
            if (DEBUG_REFS) {
                mNumRefs = 0;
                incRefsLocked(this.hashCode());
            }
            init(false);
            if (localLOGV) Log.v(TAG, "New asset manager: " + this);
            ensureSystemAssets();
        }
    }

注意註釋中的{@hide},隱藏起來了,Android系統不讓我們使用。既然不讓我們直接使用,那我們可以採用反射的方式來拿到AssetManager。接下來我把自定義的實現類貼出來,給大家示例:

/**
 * 
 * @ClassName: MyPluginResources 
 * @Description: 自定義外掛資原始檔獲取工具類
 * @author 猴子搬來的救兵http://blog.csdn.net/mynameishuangshuai
 * @version
 */
public class MyPluginResources extends Resources{

    public MyPluginResources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        super(assets, metrics, config);
    }

    /**
     * 自定義返回外掛的資原始檔的Resource方法
     * @param resources
     * @param assets
     * @return
     */
    public static MyPluginResources getPluginResources(Resources resources,AssetManager assets){
        MyPluginResources pluginResources = new MyPluginResources(assets, resources.getDisplayMetrics(), resources.getConfiguration());
        return pluginResources;
    } 

    //自己定義載入外掛APK的AssetsManager
    public static AssetManager getPluginAssetsManager(File apkFile,Resources resources) throws ClassNotFoundException{
        // 由於系統沒有提供AssetManager的例項化方法,因此我們使用反射
        Class<?> forName = Class.forName("android.content.res.AssetManager");
        Method[] declaredMethods = forName.getDeclaredMethods();
        for(Method method :declaredMethods){
            if(method.getName().equals("addAssetPath")){
                try {
                    AssetManager assetManager = AssetManager.class.newInstance();
                    // 呼叫addAssetPath方法,引數為我們外掛APK的路徑
                    method.invoke(assetManager, apkFile.getAbsolutePath());
                    return assetManager;
                } catch (Exception e) {
                    e.printStackTrace();
                } 
            }
        }
        return null;
    }
}

這裡寫圖片描述

這樣,我們在專案中就可以使用我們自定義的AssetManager來獲取未安裝外掛APK中的資原始檔

AssetManager assetManager = PluginResources.getPluginAssetsManager(apkFile,
            this.getResources());

參考:《Android開發藝術探索》