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

來源:互聯網
上載者:User

標籤:主程   toc   overlay   classname   lan   注意   static   net   manage   

 動態載入技術(也叫外掛程式化技術),當項目越來越龐大的時候,我們通過外掛程式化開發不僅可以減輕應用的記憶體和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開發藝術探索》

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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.