1. 程式人生 > >Android資源動態載入以及相關原理分析

Android資源動態載入以及相關原理分析

思考

一般情況下,我們在設計一個外掛化框架的時候,要解決的無非是下面幾個問題:

  1. 四大元件的動態註冊

  2. 元件相關的類的載入

  3. 資源的動態載入

實際上從目前的主流外掛化框架來看,都是滿足了以上的特點,當然因為Activity是大家最常用到的,因此一些外掛化框架便只考慮了對Activity的支援,比如Small框架,從原理上來看,基本都差不多,Hook了系統相關的API來接管自己的載入邏輯,特別是Hook 了AMS(ActivityManagerService)以及ClassLoader這2個,因為這2個控制著四大元件的載入以及執行邏輯,這裡的Hook指的是Hook了遠端服務在本地程序的代理物件而已,由於程序隔離的存在,是沒辦法直接Hook遠端程序(Xposed可以Hook掉系統服務,暫時不討論這個),但根據Binder原理,只需要Hook掉遠端程序在本地程序的代理物件即可為我們服務,從而實現我們想要的邏輯,而資源的動態載入僅僅是本地程序的事情,今天我們來簡單討論一下。

動態載入資源例子

下面我們首先通過一個例子來說說,很簡單的例子,就是動態載入圖片,文字和佈局,首先新建一個application的Model,

我們在string.xml加入一個文字,比如:

<resources>
    <string name="app_name">ResourcesProject</string>

    <string name="dynamic_load">動態載入文字測試</string>
</resources>
複製程式碼

然後弄一個支付寶的圖片用來測試,

然後寫一個佈局activity_text.xml用來動態載入,程式碼如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:gravity="center"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/text"
        android:text="動態載入佈局"
        android:layout_width="wrap_content"
        android:textSize="20sp"
        android:layout_height="wrap_content" />
</LinearLayout>
複製程式碼

我們將這個專案打包成一個apk檔案,命名為plugin.apk,打包檔案放在assets目錄下面,最後放到SD卡目錄下面的plugin目錄下面就好,程式碼如下

public static void copyFileToSD(Context context) {
        try {
            InputStream fis = context.getAssets().open("plugin.apk");
            String sdPath = Environment.getExternalStorageDirectory().getAbsolutePath();
            File file = new File(sdPath, "plugin");
            if (!file.exists()) {
                file.mkdirs();
            }
            OutputStream bos = new FileOutputStream(file.getAbsolutePath() + File.separator + "plugin.apk");
            byte[] buffer = new byte[1024];
            int readCount = 0;
            while ((readCount = fis.read(buffer)) != -1) {
                bos.write(buffer, 0, readCount);
            }
            bos.flush();
            fis.close();
            bos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
複製程式碼

當然6.0以上注意一下SD卡許可權就好,

好了,已經把apk檔案放在sd卡了,現在來載入測試一下吧,下面 是程式碼:

 private void loadPlugResources() {
        try {
            String resourcePath = Environment.getExternalStorageDirectory().toString() + "/plugin/plugin.apk";
            AssetManager mAsset=AssetManager.class.newInstance();
            Method method=mAsset.getClass().getDeclaredMethod("addAssetPath",String.class);
            method.setAccessible(true);
            method.invoke(mAsset,resourcePath);
            /**
             * 構建外掛的資源Resources物件
             */
            Resources pluginResources=new Resources(mAsset,getResources().getDisplayMetrics(),getResources().getConfiguration());
            /**
             * 根據apk的檔案路徑獲取外掛的包名資訊
             */
            PackageInfo packageInfo=getPackageManager().getPackageArchiveInfo(resourcePath, PackageManager.GET_ACTIVITIES);
            //獲取資源的id並載入
            int imageId=pluginResources.getIdentifier("alipay","mipmap",packageInfo.packageName);
            int strId = pluginResources.getIdentifier("dynamic_load", "string", packageInfo.packageName);
            int layoutID = pluginResources.getIdentifier("activity_test", "layout", packageInfo.packageName);
            //生成XmlResourceParser
            XmlResourceParser xmlResourceParser=pluginResources.getXml(layoutID);
            imageView.setImageDrawable(pluginResources.getDrawable(imageId));
            textView.setText(pluginResources.getString(strId));
            View view= LayoutInflater.from(this).inflate(xmlResourceParser,null);
            mView.addView(view,0);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
複製程式碼

我們簡單分析一下上面的流程:

1.首先是根據AssetManager 的原理,呼叫隱藏方法addAssetPath把外部apk檔案塞進一個AssetManager ,然後根據

   public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(assets, metrics, config, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO);
    }

複製程式碼

生成一個外掛的Resource物件。

2.根據Resources物件呼叫getIdentifier方法獲取了圖片,文字以及佈局的id,分別設定圖片和文字,再動態載入了一個佈局,呼叫Resources.getXml()方法獲取XmlResourceParser 來解析佈局,最後再載入佈局顯示,執行如圖;

可以看到已經成功載入顯示在介面上了。

動態載入資源原理分析

上面我們看了如何以外掛的形式載入外部的資源,實際上無論是載入外部資源,還是載入宿主本身的資源,它們的原理都是相同的,只要我們弄懂了宿主自身的資源是如何載入的,那麼對於上面的過程自然也就理解了.

在Android中,當我們需要載入一個資源時,一般都會先通過getResources()方法,得到一個Resources物件,再通過它提供的getXXX方法獲取到對應的資源,下面將分析一下具體的呼叫邏輯,首先是當我們呼叫在Activity/Service/Application中呼叫getResources()時,由於它們都繼承於ContextWrapper,該方法就會呼叫到ContextWrapper的getResources()方法,而該方法又會呼叫它內部的mBase變數的對應方法,

@Override
    public Resources getResources()
    {
        return mBase.getResources();
    }
複製程式碼

這裡的mBase是一個ContextImpl物件,因為Context是一個抽象類,真正的實現是在ContextIImpl裡面的,它的getResources()方法,返回的是其內部的成員變數mResources,如下程式碼:

@Override
    public Resources getResources() {
        return mResources;
    }
複製程式碼

可見是直接返回了一個mResources物件了,那麼這個mResources是怎麼來的呢,我們可以看到是在ContextImpl的建構函式裡面賦值的,程式碼如下:

private ContextImpl(ContextImpl container, ActivityThread mainThread,
            LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
            Display display, Configuration overrideConfiguration, int createDisplayWithId) {
        mOuterContext = this;

        mMainThread = mainThread;
        mActivityToken = activityToken;
        mRestricted = restricted;

        if (user == null) {
            user = Process.myUserHandle();
        }
        mUser = user;

        mPackageInfo = packageInfo;
        mResourcesManager = ResourcesManager.getInstance();

        final int displayId = (createDisplayWithId != Display.INVALID_DISPLAY)
                ? createDisplayWithId
                : (display != null) ? display.getDisplayId() : Display.DEFAULT_DISPLAY;

        CompatibilityInfo compatInfo = null;
        if (container != null) {
            compatInfo = container.getDisplayAdjustments(displayId).getCompatibilityInfo();
        }
        if (compatInfo == null) {
            compatInfo = (displayId == Display.DEFAULT_DISPLAY)
                    ? packageInfo.getCompatibilityInfo()
                    : CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;
        }
        mDisplayAdjustments.setCompatibilityInfo(compatInfo);
        mDisplayAdjustments.setConfiguration(overrideConfiguration);

        mDisplay = (createDisplayWithId == Display.INVALID_DISPLAY) ? display
                : ResourcesManager.getInstance().getAdjustedDisplay(displayId, mDisplayAdjustments);
		
		//resources 是由packageInfo(LoadedApk )的getResources()方法獲取;
        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是apk檔案在記憶體中的表示,它內部包含了所關聯的ActivityThread以及四大元件,我們在ContextImpl中賦值的其實就是它內部的mResources物件,程式碼如下: `

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

可以看到如果為null,那麼返回mainThread.getTopLevelResources方法,這個是主執行緒的方法,如果已經有了,那麼就直接返回mResources物件,我們來看看主執行緒的getTopLevelResources方法:

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

複製程式碼

這裡也是根據安裝的apk的目錄來獲取的,為了更加理解引數,我們來debug一下,如圖:

通過debug,我們可以清楚的看到構造Resource物件所必須的引數的來源,因此,只要具備了這些,就可以任意構造,而不管位置是在哪裡,因此最終呼叫的是mResourcesManager的getTopLevelResources方法,其實裡面也差不多,主要是建立資源,然後快取起來,也是利用了AssetManager原理:

 //建立ResourcesKey 
 ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfigCopy, scale);
//判斷快取,如果有快取,直接返回,否則才建立
Resources r;
        synchronized (this) {
            // Resources is app scale dependent.
            if (DEBUG) Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);

            WeakReference<Resources> wr = mActiveResources.get(key);
            r = wr != null ? wr.get() : null;
            //if (r != null) Log.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());
            if (r != null && r.getAssets().isUpToDate()) {
                if (DEBUG) Slog.w(TAG, "Returning cached resources " + r + " " + resDir
                        + ": appScale=" + r.getCompatibilityInfo().applicationScale
                        + " key=" + key + " overrideConfig=" + overrideConfiguration);
                return r;
            }
        }

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;
                }
            }
        }
  
 // 快取起來
 mActiveResources.put(key, new WeakReference<>(r));

複製程式碼

下面我們來分析一下資源的管理者ResourcesManager的一些程式碼:

private static ResourcesManager sResourcesManager;
    private final ArrayMap<ResourcesKey, WeakReference<Resources> > mActiveResources =
            new ArrayMap<>();
    private final ArrayMap<Pair<Integer, DisplayAdjustments>, WeakReference<Display>> mDisplays =
            new ArrayMap<>();

    CompatibilityInfo mResCompatibilityInfo;

    Configuration mResConfiguration;

    public static ResourcesManager getInstance() {
        synchronized (ResourcesManager.class) {
            if (sResourcesManager == null) {
                sResourcesManager = new ResourcesManager();
            }
            return sResourcesManager;
        }
    }
複製程式碼

我們可以看到是一個單例模式,並且有使用了mActiveResources 作為快取資源物件,sResourcesManager在整個應用程式中只有一個例項的存在,我們上面分析了在建立mResources的時候,是首先判斷是否有快取的,如果有快取了,則直接返回需要的mResources物件,沒有的時候再建立並且存入快取。

ResourcesKey 和ResourcesImpl 以及 Resources 和AssetManager的關係

上面建立資源的程式碼中都出現了他們,那他們到底是什麼關係呢?

●. Resources其實只是一個代理物件,只是暴露給開發者的一個上層介面,我們平時呼叫的getResources().getString(),getgetIdentifier方法等都是給開發者直接用的.對於資源的使用者來說,看到的是Resources介面,其實在構建Resources物件時,同時也會建立一個ResourcesImpl物件作為它的成員變數,Resources會呼叫它來去獲取資源,而ResourcesImpl訪問資源都是通過AssetManager來完成

●. ResourcesKey 是一個快取Resources的Key,也就是說對於一個應用程式,可以儲存不同的Resource,是否返回之前的Resources物件,取決於ResourcesKey的equals方法是否相等

@Override
    public boolean equals(Object obj) {
        if (!(obj instanceof ResourcesKey)) {
            return false;
        }
        ResourcesKey peer = (ResourcesKey) obj;

        if (!Objects.equals(mResDir, peer.mResDir)) {
            return false;
        }
        if (mDisplayId != peer.mDisplayId) {
            return false;
        }
        if (!mOverrideConfiguration.equals(peer.mOverrideConfiguration)) {
            return false;
        }
        if (mScale != peer.mScale) {
            return false;
        }
        return true;
    }
複製程式碼

● ResourcesImpl ,看到命名,我們已經基本明白了是Resources的實現類,其內部包含了一個AssetManager,所有資源的訪問都是通過它的Native方法來實現的

public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics,
            @Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) {
        mAssets = assets;
        mMetrics.setToDefaults();
        mDisplayAdjustments = displayAdjustments;
        mConfiguration.setToDefaults();
        updateConfiguration(config, metrics, displayAdjustments.getCompatibilityInfo());
        mAssets.ensureStringBlocks();
    }
複製程式碼

通過建構函式便可以得知mAssets的來源,所有的資源都是通過mAssets訪問的,比如:

int getIdentifier(String name, String defType, String defPackage) {
        if (name == null) {
            throw new NullPointerException("name is null");
        }
        try {
            return Integer.parseInt(name);
        } catch (Exception e) {
            // Ignore
        }
        return mAssets.getResourceIdentifier(name, defType, defPackage);
    }
複製程式碼

其他也是類似的。

● AssetManager:作為資源獲取的執行者,它是ResourcesImpl的內部成員變數。

通過上面的分析,我們已經知道了資源的訪問最終是由AssetManager來完成,在AssetManager的建立過程中我們首先告訴它資源所在的路徑,之後它就會去以下的幾個地方檢視資源,通過反射呼叫的addAssetPath。動態載入資源的關鍵,就是如何把包含資源的外掛路徑新增到AssetManager當中

public final int addAssetPath(String path) {
        synchronized (this) {
            int res = addAssetPathNative(path);
            makeStringBlocks(mStringBlocks);
            return res;
        }
    }
複製程式碼

可以看到Java層的AssetManager只是個包裝,真正關於資源處理的所有邏輯,其實都位於native層由C++實現的AssetManager。 執行addAssetPath就是解析這個格式,然後構造出底層資料結構的過程。整個解析資源的呼叫鏈是:

public final int addAssetPath(String path)

=jni=> android_content_AssetManager_addAssetPath

=> AssetManager::addAssetPath => AssetManager::appendPathToResTable => ResTable::add => ResTable::addInternal => ResTable::parsePackage
複製程式碼

解析的細節比較繁瑣,就不細細說明了,有興趣的可以一層層研究下去。

今天的文章就寫到這裡,感謝大家閱讀。
 

相關推薦

Android資源動態載入以及相關原理分析

思考 一般情況下,我們在設計一個外掛化框架的時候,要解決的無非是下面幾個問題: 四大元件的動態註冊 元件相關的類的載入 資源的動態載入 實際上從目前的主流外掛化框架來看,都是滿足了以上的特點,當然因為Activity是大家最常用到的,因此一些外

Android Apk資源載入機制原始碼分析以及資源動態載入實現系列文章

Android系統中執行Apk時是如何對包內的資源進行載入以及我們開發中設定相關資源後又是如何被加載出來,這個系列我們可以學習系統載入資源的機制原理,然後我們再巧妙的利用學習系統載入技巧來打造我們自己的動態資源載入機制實現。 這個系列主要分為如下3部分內容來講

Android apk動態載入機制的研究(二) 資源載入和activity生命週期管理

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

Android實現資源動態載入的兩種方式

這是Android Apk源載入機制原理分析以及動態載入實現系列文章 的最後一篇。經過前兩篇的介紹之後,相關基礎都講的差不多了,現在要實現自己專案中的資源載入框架,這裡提供兩種方式,區別在於由誰來載入資源。 1、利用系統載入資源Apk 2、主動手動實現資

jdk動態代理示例以及程式碼原理分析

相信很多人在剛剛學習Java時,會感覺【動態代理】晦澀難懂,只知道如何來呼叫它,卻不知道它的實現細節。本文通過根據JDK原始碼,展示這些細節,以期能對JDK的動態代理有深入的理解。 簡單示例程

[Android]使用HorizontalScrollView實現廣告欄Banner及相關原理分析

       現在的App中,廣告欄Banner的使用還是挺廣泛的,用於展示各種廣告、活動推薦等。使用HorizontalScrollView可以很簡單的實現一個可自動播放、可滑動、可點選的廣告欄Banner,這個也可以做為一個例子,來學習自定義控制元件的製作。

Android 權重正確解釋以及解釋誤區分析

1.首先宣告只有在Linearlayout中,layout_weight屬性才有效。 在這裡我們設定三個的權重比為 藍1:黃2:紅2那麼它的效果是不是 藍1:黃2:紅2呢 <TextView android:layout_weight="1" andr

android原生熱修復流程和原理分析實現

首先apk就是一個壓縮檔案,解壓apk檔案的內容如下圖: 安卓原生熱修復主要原理圖和流程圖如下,我花了好長時間才繪好,中間改了好幾次,應該來說是很直觀明白的,其中有截取了BaseDexClassLoader的關鍵原始碼,還有DexPathList的原始碼 a.現將打

Android控制元件TextView的實現原理分析

                        在前面一個系列的文章中,我們以視窗為單位,分析了WindowManagerService服務的實現。同時,在再前面一個系列的文章中,我們又分析了視窗的組成。簡單來說,視窗就是由一系列的檢視按照一定的佈局組織起來的。實際上,每一個檢視都是一個控制元件,這些控制可以

View繪製機制和LayoutInflater動態載入以及三種繪圖介面更新區別

View繪製流程及機制 流程研究 場景:最外層自定義MaxViewGroup繼承自LinearLayout+內層自定義ViewGroup繼承自LinearLayout+自定義View 注:1.LinearLayout的onMearsure過程為兩遍,每次呼叫Vi

JAVA類的靜態載入動態載入以及NoClassDefFoundError和ClassNotFoundException

我們都知道JAVA初始化一個類的時候可以用new 操作符來初始化,也可通過Class.forName的方式來得到一個Class型別的例項,然後通過這個Class型別的例項的newInstance來初始化.我們把前者叫做JAVA的靜態載入,把後者叫做動態載入.後者在很多框架中

Android動態載入技術

Android類動態載入技術     Android應用開發在一般情況下,常規的開發方式和程式碼架構就能滿足我們的普通需求。但是有些特殊問題,常常引發我們進一步的沉思。我們從沉思中產生頓悟,從而產生新的技術形式。    如何開發一個可以自定義控制元件的Android應用?

android動態載入webview,webview載入html資料,並且隱藏滾動條

 ScrollView layouts = (ScrollView) findViewById(R.id.web); WebView webviews = new WebView(DtDetailActivity.this);webviews.setVisibility(

Unity中資源動態載入的幾種方式比較

初學Unity的過程中,會發現打包釋出程式後,unity會自動將場景需要引用到的資源打包到安裝包裡,沒有到的不會跟進去。我們在編輯器裡看到的Asset中的檔案結構只是工作於編輯器環境下的,在遊戲中unity會重新組織資料庫。這是我們一定會遇到一個需求,即動態的載入我們自己的

【Unity】Unity中資源動態載入的兩種方式之AssetsBundle

首先要說的是,我們的工程中有2個指令碼,分別是:Build(編輯器類指令碼,無需掛載到任何物體),但是必須要把Build指令碼放到Editor資料夾中Load指令碼,掛載到攝像機上<pre name="code" class="csharp">using Uni

Android動態載入技術

{    Object* loader = (Object*) args[0];    StringObject* nameObj = (StringObject*) args[1];    const u1* data = (const u1*) args[2];    int offset = args[

Android效能優化之 App啟動原理分析及速度和時間優化

應用的啟動速度緩慢這是很多開發者都遇到的一個問題,比如啟動緩慢導致的黑屏,白屏問題,大部分的答案都是做一個透明的主題,或者是做一個Splash介面,但是這並沒有從根本上解決這個問題。那麼如何從根本上解決這個問題或者做到一定程度的緩解? 一、應用的啟動方式 1、冷啟動:

Android apk動態載入機制的研究

背景問題是這樣的:我們知道,apk必須安裝才能執行,如果不安裝要是也能執行該多好啊,事實上,這不是完全不可能的,儘管它比較難實現。在理論層面上,我們可以通過一個宿主程式來執行一些未安裝的apk,當然,實踐層面上也能實現,不過這對未安裝的apk有要求。我們的想法是這樣的,首先要

Android筆記:視訊直播的原理分析

最近一段時間,視訊直播可謂大火。在視訊直播領域,有不同的商家提供各種的商業解決方案,包括軟硬體裝置,攝像機,編碼器,流媒體伺服器等。本文要講解的是如何使用一系列免費工具,打造一套視訊直播方案。 視訊直播流程 視訊直播的流程可以分為如下幾步: 採集 —&g

Android之SharedPreferences詳解與原理分析

SharedPreferences作為Android儲存資料方式之一,主要特點是: 1. 只支援Java基本資料型別,不支援自定義資料型別; 2. 應用內資料共享; 3. 使用簡單. 使用方法 1、存資料 SharedPreferenc