1. 程式人生 > >Android外掛化資源的使用及動態載入 附demo

Android外掛化資源的使用及動態載入 附demo

上一篇我們已經完成了一個真正可執行的外掛化demo,而且demo中也解決了外掛中不可以使用資源的問題,但是由於篇幅的問題我們並沒有對原理講解,所以這一篇是對上一篇的一個收尾,如果沒有看過上一篇建議先看Android外掛化完美實現程式碼資源載入及原理講解 附可執行demo.

我們的宿主應用呼叫一個未安裝的外掛apk,正常的情況下是不能訪問外掛中的資源的,例如R.,因為我們宿主中根本就不存在這個資源id,所以就會崩潰。還有另一種情況,基於我們上一篇的demo中,我們使用了佔坑的方式載入了外掛中apk中的Activity,上一篇我們也分析了建立Activity的時候需要Classloader,這個Classloader是通過 r.packageInfo.getClassloader()來獲取的,而 r.packageInfo是一個LoadedApk型別的物件,這個物件是一個apk在記憶體中的標示, Apk檔案的相關資訊,諸如Apk檔案的程式碼和資源,甚至程式碼裡面的Activity,Service等四大元件的資訊我們都可以通過此物件獲取。我們再看一下這個LoadedApk物件怎麼建立

private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
        ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
        boolean registerPackage) {
    synchronized (mResourcesManager) {
        WeakReference<LoadedApk> ref;
        if (includeCode) {
            ref = mPackages.get(aInfo.packageName);
        } else {
            ref = mResourcePackages.get(aInfo.packageName);
        }
        LoadedApk packageInfo = ref != null ? ref.get() : null;
        if (packageInfo == null || (packageInfo.mResources != null
                && !packageInfo.mResources.getAssets().isUpToDate())) {
            if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package "
                    : "Loading resource-only package ") + aInfo.packageName
                    + " (in " + (mBoundApplication != null
                            ? mBoundApplication.processName : null)
                    + ")");
                    //這裡建立,看一下他的引數
            packageInfo 
                new LoadedApk(this, aInfo, compatInfo, baseLoader,
                        securityViolation, includeCode &&
                        (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);

          。。。
        }
        return packageInfo;
    }
}

我們看一下構造引數的第二個引數,aInfo,這是一個ApplicationInfo型別的物件,我們上一篇並沒有構造我們自己的LoadedApk物件,而只是將我們的dex和宿主的合併了而已,那麼也就是說了,這個LoadedApk裡傳入的ApplicationInfo其實是我們宿主的,並不是外掛apk中的。那麼也就是說如果我們在外掛apk中直接使用資源,等到外掛apk被宿主呼叫器後,使用的是宿主的資源庫,而宿主的資源中並沒有我們外掛apk中的資源,所有一執行的時候就會報錯。那麼要解決這個問題我們就得想辦法,讓外掛apk執行的時候使用自己的資源才行,下面我們分析。

我們在程式碼中使用資源的時候都是通過R.,或者是Context.getResources(),這兩種方式,其實R.也是通過Context.getResources()查詢對應id的。那麼我們直接分析Context.getResources()就好了,那麼Context的實現類是ContextImpl,我們去看看。
platform_frameworks_base-master\core\java\android\app\ContextImpl.java

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

我們再看一下這個mResources怎麼建立的,5.1原始碼,在這裡說一下

private ContextImpl(ContextImpl container, ActivityThread mainThread,
        LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
        Display display, Configuration overrideConfiguration, int createDisplayWithId) {

  ...

    mPackageInfo = packageInfo;

    //這裡拿到了一個ResourcesManager,單例的,說明我們應用當中使用的都是同一套資源
    mResourcesManager = ResourcesManager.getInstance();

   ...

    //LoadedApk物件中得到Resources物件
    Resources resources = packageInfo.getResources(mainThread);
     Resources resources = packageInfo.getResources(mainThread);
    if (resources != null) {
        if (activityToken != null
                || displayId != Display.DEFAULT_DISPLAY
                || overrideConfiguration != null
                || (compatInfo != null && compatInfo.applicationScale
                        != resources.getCompatibilityInfo().applicationScale)) {
            //給resource賦值
            resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
                    packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
                    packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
                    overrideConfiguration, compatInfo, activityToken);
        }
    }
    //給mResources賦值
    mResources = resources;

    ...

}

ResourcesManager.getInstance()是單例的這樣保證了我們每個Context獲取的都是同樣的資源,resources通過getTopLevelResources方法賦值,我們看看getTopLevelResources方法幹了什麼

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物件
        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();
        }'
        //建立一個Resource物件
        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;
        }
    }

方法有點長,但其實主要就3步,這裡說明一下,每個版本Resource的賦值過程可能不太一樣,但是最終都是通過一下三步來建立的Resource

1.建立AssetManager物件

2.通過addAssetPath方法將資源路徑新增給AssetManager,這個方法是hint的,我們要通過反射呼叫

  /**
 * 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;
    }
}

3.通過AssetManager物件建立一個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, null);
}

那麼我們通過上面的分析後,知道了Resource怎麼建立的,那麼我們就可以給外掛apk建立一個屬於自己的Resource物件,這樣他就可以自由的使用資源了。但是還要留意一點就是在我們的外掛apk預設拿的是宿主的Resource物件,如果想讓外掛apk可以自由的使用資源,那麼我們就必須要在宿主中提供一個返回外掛apk自己資源的方法,然後在外掛apk中我們要重寫Context的getResource方法,這樣才算真正的完成

1.在宿主程式中,一定要在呼叫外掛apk前執行下面程式碼,demo中在Application中執行

        //建立我們自己的Resource
        String apkPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/chajian_demo.apk";

        //建立AssetManager
        assetManager = AssetManager.class.newInstance();
        Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
        addAssetPathMethod.setAccessible(true);

        addAssetPathMethod.invoke(assetManager, apkPath);


        Method ensureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
        ensureStringBlocks.setAccessible(true);
        ensureStringBlocks.invoke(assetManager);

        Resources supResource = getResources();
        Log.e("Main", "supResource = " + supResource);
        newResource = new Resources(assetManager, supResource.getDisplayMetrics(), supResource.getConfiguration());

        mTheme = newResource.newTheme();
        mTheme.setTo(super.getTheme());

2.在宿主中提供給外掛apk返回自己Resource物件的方法,demo中為了簡單直觀,直接在Application中重寫了getResources和getAssets方法,這個可以根據自己的需求來定,只是提供一種思路

     @Override
public AssetManager getAssets() {
    return assetManager == null ? super.getAssets() : assetManager;
}

@Override
public Resources getResources() {
    return newResource == null ? super.getResources() : newResource;
}

3.在外掛apk中使用資源的Activity中一定要重新getResources和getAssets方法,因為我們的外掛apk預設使用的是宿主的Resource,而宿主中並沒有我們外掛中的資源id,一定要拿自己的Resource才可以使用。這裡說的使用資源包括R.和Context.getResource()。

 @Override
public AssetManager getAssets() {
    if(getApplication() != null && getApplication().getAssets() != null){
        return getApplication().getAssets();
    }
    return super.getAssets();
}

@Override
public Resources.Theme getTheme() {
    if(getApplication() != null && getApplication().getTheme() != null){
        return getApplication().getTheme();
    }
    return super.getTheme();
}

ok了,到這裡可以愉快的在外掛apk中使用資源了,以上demo程式碼為了簡單明瞭,所有很多地方需要自己根據需求改進,這裡只給出了實現的思路和方法。到這裡外掛化所有的東西全部說完了,研究這些東西也花了不少的時間,拿出來和大家分享,如果覺得不錯的話,還請給點個贊,專案上給個star,

相關推薦

Android外掛資源的使用動態載入 demo

上一篇我們已經完成了一個真正可執行的外掛化demo,而且demo中也解決了外掛中不可以使用資源的問題,但是由於篇幅的問題我們並沒有對原理講解,所以這一篇是對上一篇的一個收尾,如果沒有看過上一篇建議先看Android外掛化完美實現程式碼資源載入及原理講解 附可執行

Android外掛架構設計之載入資原始檔

開篇介紹 現在專案比較大 資源比較多,但是若希望動態來載入資原始檔,可以有以下幾種方式: 1. 通過下載資原始檔zip包然後解壓來載入 2. 通過外掛開發 本文通過外掛開發來實現載入外掛中的資原始檔. 程式演示 也可以開啟連結 效果演示 開

Android外掛完美實現程式碼資源載入原理講解 可執行demo

*本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出 。 我們通過前4篇的分解,分別將外掛化設計到的知識點全部梳理了一遍,如果沒有看過的,建議先看前面4篇 6. 外掛化資源的使用及動態載入 附demo 好了上面介紹了之

Android外掛探索(二)資源載入

前情提要 PathClassLoader和DexClassLoader的區別 DexClassLoader的原始碼如下: public class DexClassLoader extends BaseDexClassLoader {

Android外掛開發 第三篇 [載入外掛資源]

引言 本文講解宿主如何從外掛apk中獲取到資源,為啥要從外掛中獲取資源呢?這種需求可能來自於顯示外掛的名字啊,圖示之類的。比如宿主的一個按鍵上顯示“掃一掃”或者"搖一搖"之類的,這個字串是外掛提供的。 Demo建立 引入外掛的AssetManager private sta

Android外掛基礎之載入已安裝的apk資源

郵箱:[email protected] 我也是在進行學習和研究,有問題可以直接說 應該明確的問題 外掛化是什麼意思? 為什麼要使用外掛化的這種方式? 外掛化的優劣? 根據上述問題我們就可以進行初步的瞭解了 外掛顧名思義就是

Android外掛學習之路(一)之動態載入綜述

前段時間,公司專案完成了外掛化的開發,自己也因此學習了很多Android外掛化的知識,於是想把這些內容記錄下來,本次帶來Android外掛化的第一篇:動態載入綜述 背景知識 1.什麼是動態載入? 動態載入技術應由以下幾個部分組成: 1) 應用在執行

Android外掛原理和實踐 (四) 之 合併外掛中的資源

我們繼續來學習Android外掛化相關知識,還是要圍繞著三個根本問題來展開。在前面兩章中已經講解過第一個根本問題:在宿主中如何去載入外掛以及呼叫外掛中類和元件程式碼。Demo中使用了Service來演示,因為還沒有解決載入外掛中資源的問題,用Activity不好展示。所以本文將要從資源的載入機制

Android外掛開發篇之----動態載入Activity 免安裝執行程式

                一、前言又到週末了,時間過的很快,今天我們來看一下Android中外掛開發篇的最後一篇文章的內容:動態載入Activity(免安裝執行程式),在上一篇文章中說道了,如何動態載入資源(應用換膚原理解析),沒看過的同學,可以轉戰:當然,今天說道的內容還這這篇文章有關係。關於動態載入

unity Editor自動生成材質動態載入資源

最近這兩天在做一個專案,然後裡面有六十多個素材,還要全部生成材質球,差點人都廢了,然後去手冊上著了一下可以自動生成材質球的程式碼。然後自動生成材質球的過程中我還想要給材質球賦予一個預設的漫反射貼圖,貼圖是從網上下載來的,踩了數不清的坑之後終於搞定了。

Android外掛原理和實踐 (五) 之 解決合併資源資源Id衝突的問題

Android外掛化中,要解決資源的問題,有些外掛化框架會選擇不合並資源,這樣就得維護多套mResources變數,這樣的話難免開發上沒有那麼的靈活和方便。所以一般地都是選擇合併資源,也就是我們上一遍文章《Android外掛化原理和實踐 (四) 之 合併外掛中的資源》介紹的辦法。但是合併後資源i

Android外掛原理和實踐 (三) 之 載入外掛中的元件程式碼

我們在上一篇文章《Android外掛化原理和實踐 (二) 之 載入外掛中的類程式碼》中埋下了一個懸念,那就是通過構造一個DexClassLoader物件後使用反射只能反射出普通的類,而不能正常使用四大元件,因為會報出異常。今天我們就來解開這個懸念和提出解決方法。 1 揭開懸念 還記得《A

Android外掛原理和實踐 (二) 之 載入外掛中的類程式碼

我們在上一篇文章《Android外掛化原理和實踐 (一)之 外掛化簡介和基本原理簡述》中介紹了外掛化一些基本知識和歷史,最後還列出了三個根本問題。接下來我們打算圍繞著這三個根本問題展開對外掛化的學習。首先本章將介紹第一個根本問題:宿主和外掛中如何相互呼叫程式碼。要實現它們相互呼叫,就得要宿主先將

Android 熱修復與外掛 一】帶你入門Android外掛demo

本文為博主Colin原創文章,歡迎轉載。 https://blog.csdn.net/colinandroid/article/details/79431502   一. 背景 Android外掛化作為每個合格的Android程式設計師都必須會的技術,被各大廠廣泛使用。隨著各大廠對

Android 外掛技術 載入任意未安裝apk

轉載     http://www.eoeandroid.com/thread-562805-1-1.html  (一) 綜述    隨著智慧手機硬體效能的逐步提升,移動應用也做的越來越複雜,android平臺上應用的apk包體積也越來越大,然後同類產品開始比拼誰的

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

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

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

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

Android外掛學習之路(六)之動態建立Activity

靜態代理Activity模式的限制 我們在代理Activity模式一文裡談到啟動外掛APK裡的Activity的兩個難題嗎,由於外掛裡的Activity沒在主專案的Manifest裡面註冊,所以無法經歷系統Framework層級的一系列初始化過程,最終導致獲得

Android外掛原理解析--外掛載入機制

上文 Activity生命週期管理 中我們地完成了『啟動沒有在AndroidManifest.xml中顯式宣告的Activity』的任務;通過Hook AMS和攔截ActivityThread中H類對於元件排程我們成功地繞過了AndroidMAnifest.xml的限制。但是

Android 外掛原理解析——外掛載入機制

上文 Activity生命週期管理 中我們地完成了『啟動沒有在AndroidManifest.xml中顯式宣告的Activity』的任務;通過Hook AMS和攔截ActivityThread中H類對於元件排程我們成功地繞過了AndroidMAnifest.xml的限