思考android
通常狀況下,咱們在設計一個插件化框架的時候,要解決的無非是下面幾個問題:緩存
四大組件的動態註冊bash
組件相關的類的加載數據結構
資源的動態加載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
複製代碼
解析的細節比較繁瑣,就不細細說明了,有興趣的能夠一層層研究下去。
今天的文章就寫到這裏,感謝你們閱讀。