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

思考android

通常狀況下,咱們在設計一個插件化框架的時候,要解決的無非是下面幾個問題:緩存

  1. 四大組件的動態註冊bash

  2. 組件相關的類的加載數據結構

  3. 資源的動態加載app

實際上從目前的主流插件化框架來看,都是知足了以上的特色,固然由於Activity是你們最經常使用到的,所以一些插件化框架便只考慮了對Activity的支持,好比Small框架,從原理上來看,基本都差很少,Hook了系統相關的API來接管本身的加載邏輯,特別是Hook 了AMS(ActivityManagerService)以及ClassLoader這2個,由於這2個控制着四大組件的加載以及運行邏輯,這裏的Hook指的是Hook了遠端服務在本地進程的代理對象而已,因爲進程隔離的存在,是沒辦法直接Hook遠端進程(Xposed能夠Hook掉系統服務,暫時不討論這個),但根據Binder原理,只須要Hook掉遠端進程在本地進程的代理對象便可爲咱們服務,從而實現咱們想要的邏輯,而資源的動態加載僅僅是本地進程的事情,今天咱們來簡單討論一下。框架

動態加載資源例子ide

下面咱們首先經過一個例子來講說,很簡單的例子,就是動態加載圖片,文本和佈局,首先新建一個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
複製代碼

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

今天的文章就寫到這裏,感謝你們閱讀。

相關文章
相關標籤/搜索