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

stormWen發表於2018-01-11

思考

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

  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>
複製程式碼

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

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

然後寫一個佈局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卡許可權就好,

Android資源動態載入以及相關原理分析
好了,已經把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資源動態載入以及相關原理分析

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

動態載入資源原理分析

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

在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一下,如圖:

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

通過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
複製程式碼

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

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

相關文章