# Android #Plugin
一傻粘、認(rèn)識(shí)插件化
1.1 插件化起源
插件化技術(shù)最初源于免安裝運(yùn)行 Apk
的想法巷查,這個(gè)免安裝的 Apk
就可以理解為插件,而支持插件的 app
我們一般叫 宿主抹腿。
想必大家都知道,在 Android
系統(tǒng)中旭寿,應(yīng)用是以 Apk
的形式存在的警绩,應(yīng)用都需要安裝才能使用。但實(shí)際上 Android
系統(tǒng)安裝應(yīng)用的方式相當(dāng)簡(jiǎn)單盅称,其實(shí)就是把應(yīng)用 Apk
拷貝到系統(tǒng)不同的目錄下肩祥、然后把 so
解壓出來而已。
常見的應(yīng)用安裝目錄有:
-
/system/app
:系統(tǒng)應(yīng)用 -
/system/priv-app
:系統(tǒng)應(yīng)用 -
/data/app
:用戶應(yīng)用
那可能大家會(huì)想問缩膝,既然安裝這個(gè)過程如此簡(jiǎn)單混狠,Android
是怎么運(yùn)行應(yīng)用中的代碼的呢,我們先看 Apk
的構(gòu)成疾层,一個(gè)常見的 Apk
會(huì)包含如下幾個(gè)部分:
-
classes.dex
:Java
代碼字節(jié)碼 -
res
:資源文件 -
lib
:so
文件 -
assets
:靜態(tài)資產(chǎn)文件 -
AndroidManifest.xml
:清單文件
其實(shí) Android
系統(tǒng)在打開應(yīng)用之后将饺,也只是開辟進(jìn)程,然后使用 ClassLoader
加載 classes.dex
至進(jìn)程中痛黎,執(zhí)行對(duì)應(yīng)的組件而已予弧。
那大家可能會(huì)想一個(gè)問題,既然 Android
本身也是使用類似反射的形式加載代碼執(zhí)行湖饱,憑什么我們不能執(zhí)行一個(gè) Apk
中的代碼呢掖蛤?
1.2 插件化優(yōu)點(diǎn)
插件化讓 Apk
中的代碼(主要是指 Android
組件)能夠免安裝運(yùn)行,這樣能夠帶來很多收益:
- 減少安裝
Apk
的體積井厌、按需下載模塊 - 動(dòng)態(tài)更新插件
- 宿主和插件分開編譯蚓庭,提升開發(fā)效率
- 解決方法數(shù)超過65535的問題
想象一下,你的應(yīng)用擁有 Native
應(yīng)用一般極高的性能仅仆,又能獲取諸如 Web
應(yīng)用一樣的收益器赞。
嗯,理想很美好不是嘛蝇恶?
1.3 與組件化的區(qū)別
組件化:是將一個(gè)
App
分成多個(gè)模塊拳魁,每個(gè)模塊都是一個(gè)組件(module),開發(fā)過程中可以讓這些組件相互依賴或獨(dú)立編譯撮弧、調(diào)試部分組件潘懊,但是這些組件最終會(huì)合并成一個(gè)完整的Apk
去發(fā)布到應(yīng)用市場(chǎng)。插件化:是將整個(gè)App拆分成很多模塊贿衍,每個(gè)模塊都是一個(gè)Apk(組件化的每個(gè)模塊是一個(gè)lib)授舟,最終打包的時(shí)候?qū)⑺拗鰽pk和插件Apk分開打包,只需發(fā)布宿主Apk到應(yīng)用市場(chǎng)贸辈,插件Apk通過動(dòng)態(tài)按需下發(fā)到宿主Apk释树。
二、插件化的技術(shù)難點(diǎn)
想讓插件的Apk真正運(yùn)行起來,首先要先能找到插件Apk的存放位置奢啥,然后我們要能解析加載Apk里面的代碼秸仙。
但是光能執(zhí)行Java代碼是沒有意義的,在Android系統(tǒng)中有四大組件是需要在系統(tǒng)中注冊(cè)的桩盲,具體來說是在 Android
系統(tǒng)的 ActivityManagerService (AMS)
和 PackageManagerService (PMS)
中注冊(cè)的寂纪,而四大組件的解析和啟動(dòng)都需要依賴 AMS
和 PMS
,如何欺騙系統(tǒng)赌结,讓他承認(rèn)一個(gè)未安裝的 Apk
中的組件捞蛋,如何讓宿主動(dòng)態(tài)加載執(zhí)行插件Apk中 Android
組件(即 Activity
、Service
柬姚、BroadcastReceiver
拟杉、ContentProvider
、Fragment
)等是插件化最大的難點(diǎn)量承。
另外搬设,應(yīng)用資源引用(特指 R
中引用的資源,如 layout
撕捍、values
等)也是一大問題焕梅,想象一下你在宿主進(jìn)程中使用反射加載了一個(gè)插件 Apk
,代碼中的 R
對(duì)應(yīng)的 id
卻無法引用到正確的資源卦洽,會(huì)產(chǎn)生什么后果贞言。
總結(jié)一下,其實(shí)做到插件化的要點(diǎn)就這幾個(gè):
- 如何加載并執(zhí)行插件
Apk
中的代碼(ClassLoader Injection
) - 讓系統(tǒng)能調(diào)用插件
Apk
中的組件(Runtime Container
) - 正確識(shí)別插件
Apk
中的資源(Resource Injection
)
當(dāng)然還有其他一些小問題阀蒂,但可能不是所有場(chǎng)景下都會(huì)遇到该窗,我們后面再單獨(dú)說。
三蚤霞、ClassLoader Injection
ClassLoader 是插件化中必須要掌握的酗失,因?yàn)槲覀冎?code>Android 應(yīng)用本身是基于魔改的 Java
虛擬機(jī)的,而由于插件是未安裝的 apk昧绣,系統(tǒng)不會(huì)處理其中的類规肴,所以需要使用 ClassLoader
加載 Apk
,然后反射里面的代碼夜畴。
3.1 java 中的 ClassLoader
- BootstrapClassLoader 負(fù)責(zé)加載 JVM 運(yùn)行時(shí)的核心類拖刃,比如 JAVA_HOME/lib/rt.jar 等等
- ExtensionClassLoader 負(fù)責(zé)加載 JVM 的擴(kuò)展類,比如 JAVA_HOME/lib/ext 下面的 jar 包
- AppClassLoader 負(fù)責(zé)加載 classpath 里的 jar 包和目錄
3.2 android 中的 ClassLoader
在Android系統(tǒng)中ClassLoader
是用來加載dex
文件的贪绘,有包含 dex 的 apk 文件以及 jar 文件兑牡,dex 文件是一種對(duì)class文件優(yōu)化的產(chǎn)物,在Android中應(yīng)用打包時(shí)會(huì)把所有class文件進(jìn)行合并税灌、優(yōu)化(把不同的class文件重復(fù)的東西只保留一份)均函,然后生成一個(gè)最終的class.dex文件
- PathClassLoader 用來加載系統(tǒng)類和應(yīng)用程序類亿虽,可以加載已經(jīng)安裝的 apk 目錄下的 dex 文件
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
- DexClassLoader 用來加載 dex 文件,可以從存儲(chǔ)空間加載 dex 文件苞也。
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
我們?cè)诓寮幸话闶褂玫氖?DexClassLoader洛勉。
3.3 雙親委派機(jī)制
每一個(gè) ClassLoader 中都有一個(gè) parent 對(duì)象,代表的是父類加載器如迟,在加載一個(gè)類的時(shí)候坯认,會(huì)先使用父類加載器去加載,如果在父類加載器中沒有找到氓涣,自己再進(jìn)行加載,如果 parent 為空陋气,那么就用系統(tǒng)類加載器來加載劳吠。通過這樣的機(jī)制可以保證系統(tǒng)類都是由系統(tǒng)類加載器加載的。 下面是 ClassLoader 的 loadClass 方法的具體實(shí)現(xiàn)巩趁。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 先從父類加載器中進(jìn)行加載
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 沒有找到痒玩,再自己加載
c = findClass(name);
}
}
return c;
}
3.4 如何加載插件中的類
要加載插件中的類,我們首先要?jiǎng)?chuàng)建一個(gè) DexClassLoader议慰,先看下 DexClassLoader 的構(gòu)造函數(shù)需要那些參數(shù)蠢古。
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
// ...
}
}
構(gòu)造函數(shù)需要四個(gè)參數(shù): dexPath 是需要加載的 dex / apk / jar 文件路徑 optimizedDirectory 是 dex 優(yōu)化后存放的位置,在 ART 上别凹,會(huì)執(zhí)行 oat 對(duì) dex 進(jìn)行優(yōu)化草讶,生成機(jī)器碼,這里就是存放優(yōu)化后的 odex 文件的位置 librarySearchPath 是 native 依賴的位置 parent 就是父類加載器炉菲,默認(rèn)會(huì)先從 parent 加載對(duì)應(yīng)的類
創(chuàng)建出 DexClassLaoder 實(shí)例以后堕战,只要調(diào)用其 loadClass(className) 方法就可以加載插件中的類了。具體的實(shí)現(xiàn)在下面:
// 從 assets 中拿出插件 apk 放到內(nèi)部存儲(chǔ)空間
private fun extractPlugin() {
var inputStream = assets.open("plugin.apk")
File(filesDir.absolutePath, "plugin.apk").writeBytes(inputStream.readBytes())
}
private fun init() {
extractPlugin()
pluginPath = File(filesDir.absolutePath, "plugin.apk").absolutePath
nativeLibDir = File(filesDir, "pluginlib").absolutePath
dexOutPath = File(filesDir, "dexout").absolutePath
// 生成 DexClassLoader 用來加載插件類
pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
}
3.5 執(zhí)行插件類的方法
通過反射來執(zhí)行類的方法
val loadClass = pluginClassLoader.loadClass(activityName)
loadClass.getMethod("test",null).invoke(loadClass)
我們稱這個(gè)過程叫做 ClassLoader
注入拍霜。完成注入后嘱丢,所有來自宿主的類使用宿主的 ClassLoader
進(jìn)行加載,所有來自插件 Apk
的類使用插件 ClassLoader
進(jìn)行加載祠饺,而由于 ClassLoader
的雙親委派機(jī)制越驻,實(shí)際上系統(tǒng)類會(huì)不受 ClassLoader
的類隔離機(jī)制所影響,這樣宿主 Apk
就可以在宿主進(jìn)程中使用來自于插件的組件類了道偷。
四缀旁、Runtime Container
我們之前說到 Activity 插件化最大的難點(diǎn)是如何欺騙系統(tǒng),讓他承認(rèn)一個(gè)未安裝的 Apk
中的組件勺鸦。 因?yàn)椴寮莿?dòng)態(tài)加載的诵棵,所以插件的四大組件不可能注冊(cè)到宿主的 Manifest 文件中,而沒有在 Manifest 中注冊(cè)的四大組件是不能和系統(tǒng)直接進(jìn)行交互的祝旷。 如果直接把插件的 Activity 注冊(cè)到宿主 Manifest 里就失去了插件化的動(dòng)態(tài)特性履澳,因?yàn)槊看尾寮行略?Activity 都要修改宿主 Manifest 并且重新打包嘶窄,那就和直接寫在宿主中沒什么區(qū)別了。
4.1 為什么沒有注冊(cè)的 Activity 不能和系統(tǒng)交互
這里的不能直接交互的含義有兩個(gè)
- 系統(tǒng)會(huì)檢測(cè) Activity 是否注冊(cè) 如果我們啟動(dòng)一個(gè)沒有在 Manifest 中注冊(cè)的 Activity距贷,會(huì)發(fā)現(xiàn)報(bào)如下 error:
android.content.ActivityNotFoundException: Unable to find explicit activity class {com.zyg.commontec/com.zyg.plugin.PluginActivity}; have you declared this activity in your AndroidManifest.xml?
這個(gè) log 在 Instrumentation 的 checkStartActivityResult 方法中可以看到:
public class Instrumentation {
public static void checkStartActivityResult(int res, Object intent) {
if (!ActivityManager.isStartResultFatalError(res)) {
return;
}
switch (res) {
case ActivityManager.START_INTENT_NOT_RESOLVED:
case ActivityManager.START_CLASS_NOT_FOUND:
if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
throw new ActivityNotFoundException(
"Unable to find explicit activity class "
+ ((Intent)intent).getComponent().toShortString()
+ "; have you declared this activity in your AndroidManifest.xml?");
throw new ActivityNotFoundException(
"No Activity found to handle " + intent);
...
}
}
}
- Activity 的生命周期無法被調(diào)用柄冲,其實(shí)一個(gè) Activity 主要的工作,都是在其生命周期方法中調(diào)用了忠蝗,既然上一步系統(tǒng)檢測(cè)了 Manifest 注冊(cè)文件现横,啟動(dòng) Activity 被拒絕,那么其生命周期方法也肯定不會(huì)被調(diào)用了阁最。從而插件 Activity 也就不能正常運(yùn)行了戒祠。
4.2 運(yùn)行時(shí)容器技術(shù)
由于Android中的組件(Activity,Service速种,BroadcastReceiver和ContentProvider)是由系統(tǒng)創(chuàng)建的姜盈,并且由系統(tǒng)管理生命周期。 僅僅構(gòu)造出這些類的實(shí)例是沒用的配阵,還需要管理組件的生命周期馏颂。其中以Activity最為復(fù)雜,不同框架采用的方法也不盡相同棋傍。插件化如何支持組件生命周期的管理救拉。 大致分為兩種方式:
- 運(yùn)行時(shí)容器技術(shù)(ProxyActivity代理)
- 預(yù)埋StubActivity,hook系統(tǒng)啟動(dòng)Activity的過程
我們的解決方案很簡(jiǎn)單瘫拣,即運(yùn)行時(shí)容器技術(shù)亿絮,簡(jiǎn)單來說就是在宿主 Apk
中預(yù)埋一些空的 Android
組件,以 Activity
為例麸拄,我預(yù)置一個(gè) ContainerActivity extends Activity
在宿主中壹无,并且在 AndroidManifest.xml
中注冊(cè)它。
它要做的事情很簡(jiǎn)單感帅,就是幫助我們作為插件 Activity
的容器斗锭,它從 Intent
接受幾個(gè)參數(shù),分別是插件的不同信息失球,如:
pluginName
pluginApkPath
pluginActivityName
等岖是,其實(shí)最重要的就是 pluginApkPath
和 pluginActivityName
,當(dāng) ContainerActivity
啟動(dòng)時(shí)实苞,我們就加載插件的 ClassLoader
豺撑、Resource
,并反射 pluginActivityName
對(duì)應(yīng)的 Activity
類黔牵。當(dāng)完成加載后聪轿,ContainerActivity
要做兩件事:
* 轉(zhuǎn)發(fā)所有來自系統(tǒng)的生命周期回調(diào)至插件 Activity
* 接受 Activity
方法的系統(tǒng)調(diào)用,并轉(zhuǎn)發(fā)回系統(tǒng)
我們可以通過復(fù)寫 ContainerActivity
的生命周期方法來完成第一步猾浦,而第二步我們需要定義一個(gè) PluginActivity
陆错,然后在編寫插件 Apk
中的 Activity
組件時(shí)灯抛,不再讓其集成 android.app.Activity
,而是集成自我們的 PluginActivity
音瓷。
public class ContainerActivity extends Activity {
private PluginActivity pluginActivity;
@Override
protected void onCreate(Bundle savedInstanceState) {
String pluginActivityName = getIntent().getString("pluginActivityName", "");
pluginActivity = PluginLoader.loadActivity(pluginActivityName, this);
if (pluginActivity == null) {
super.onCreate(savedInstanceState);
return;
}
pluginActivity.onCreate();
}
@Override
protected void onResume() {
if (pluginActivity == null) {
super.onResume();
return;
}
pluginActivity.onResume();
}
@Override
protected void onPause() {
if (pluginActivity == null) {
super.onPause();
return;
}
pluginActivity.onPause();
}
// ...
}
public class PluginActivity {
private ContainerActivity containerActivity;
public PluginActivity(ContainerActivity containerActivity) {
this.containerActivity = containerActivity;
}
@Override
public <T extends View> T findViewById(int id) {
return containerActivity.findViewById(id);
}
// ...
}
// 插件 `Apk` 中真正寫的組件
public class TestActivity extends PluginActivity {
// ......
}
是不是感覺有點(diǎn)看懂了对嚼,雖然真正搞的時(shí)候還有很多小坑,但大概原理就是這么簡(jiǎn)單绳慎,啟動(dòng)插件組件需要依賴容器纵竖,容器負(fù)責(zé)加載插件組件并且完成雙向轉(zhuǎn)發(fā),轉(zhuǎn)發(fā)來自系統(tǒng)的生命周期回調(diào)至插件組件杏愤,同時(shí)轉(zhuǎn)發(fā)來自插件組件的系統(tǒng)調(diào)用至系統(tǒng)靡砌。
4.3 字節(jié)碼替換
該方式雖然能夠很好的實(shí)現(xiàn)啟動(dòng)插件Activity的目的,但是由于開發(fā)式侵入性很強(qiáng)珊楼,插件中的Activity必須繼承PluginActivity通殃,如果想把之前的模塊改造成插件需要很多額外的工作。
class TestActivity extends Activity {}
->
class TestActivity extends PluginActivity {}
有沒有什么辦法能讓插件組件的編寫與原來沒有任何差別呢亥曹?
Shadow
的做法是字節(jié)碼替換插件,這是一個(gè)非常棒的想法恨诱,簡(jiǎn)單來說媳瞪,Android
提供了一些 Gradle
插件開發(fā)套件,其中有一項(xiàng)功能叫 Transform Api
照宝,它可以介入項(xiàng)目的構(gòu)建過程蛇受,在字節(jié)碼生成后、dex
文件生成前厕鹃,對(duì)代碼進(jìn)行某些變換兢仰,具體怎么做的不說了,可以自己看文檔剂碴。
實(shí)現(xiàn)的功能嘛把将,就是用戶配置 Gradle
插件后,正常開發(fā)忆矛,依然編寫:
class TestActivity extends Activity {}
然后完成編譯后察蹲,最后的字節(jié)碼中,顯示的卻是:
class TestActivity extends PluginActivity {}
到這里基本的框架就差不多結(jié)束了催训。
五洽议、Resource Injection
最后要說的是資源注入,其實(shí)這一點(diǎn)相當(dāng)重要漫拭,Android
應(yīng)用的開發(fā)其實(shí)崇尚的是邏輯與資源分離的理念亚兄,所有資源(layout
、values
等)都會(huì)被打包到 Apk
中采驻,然后生成一個(gè)對(duì)應(yīng)的 R
類审胚,其中包含對(duì)所有資源的引用 id
匈勋。
資源的注入并不容易,好在 Android
系統(tǒng)給我們留了一條后路菲盾,最重要的是這兩個(gè)接口:
-
PackageManager#getPackageArchiveInfo
:根據(jù)Apk
路徑解析一個(gè)未安裝的Apk
的PackageInfo
-
PackageManager#getResourcesForApplication
:根據(jù)ApplicationInfo
創(chuàng)建一個(gè)Resources
實(shí)例
我們要做的就是在上面 ContainerActivity#onCreate
中加載插件 Apk
的時(shí)候颓影,用這兩個(gè)方法創(chuàng)建出來一份插件資源實(shí)例。具體來說就是先用 PackageManager#getPackageArchiveInfo
拿到插件 Apk
的 PackageInfo
懒鉴,有了 PacakgeInfo
之后我們就可以自己組裝一份 ApplicationInfo
诡挂,然后通過 PackageManager#getResourcesForApplication
來創(chuàng)建資源實(shí)例,大概代碼像這樣:
PackageManager packageManager = getPackageManager();
PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(
pluginApkPath,
PackageManager.GET_ACTIVITIES
| PackageManager.GET_META_DATA
| PackageManager.GET_SERVICES
| PackageManager.GET_PROVIDERS
| PackageManager.GET_SIGNATURES
);
packageArchiveInfo.applicationInfo.sourceDir = pluginApkPath;
packageArchiveInfo.applicationInfo.publicSourceDir = pluginApkPath;
Resources injectResources = null;
try {
injectResources = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);
} catch (PackageManager.NameNotFoundException e) {
// ...
}
拿到資源實(shí)例后临谱,我們需要將宿主的資源和插件資源 Merge
一下璃俗,編寫一個(gè)新的 Resources
類,用這樣的方式完成自動(dòng)代理:
public class PluginResources extends Resources {
private Resources hostResources;
private Resources injectResources;
public PluginResources(Resources hostResources, Resources injectResources) {
super(injectResources.getAssets(), injectResources.getDisplayMetrics(), injectResources.getConfiguration());
this.hostResources = hostResources;
this.injectResources = injectResources;
}
@Override
public String getString(int id, Object... formatArgs) throws NotFoundException {
try {
return injectResources.getString(id, formatArgs);
} catch (NotFoundException e) {
return hostResources.getString(id, formatArgs);
}
}
// ...
}
然后我們?cè)?ContainerActivity
完成插件組件加載后悉默,創(chuàng)建一份 Merge
資源城豁,再?gòu)?fù)寫 ContainerActivity#getResources
,將獲取到的資源替換掉:
public class ContainerActivity extends Activity {
private Resources pluginResources;
@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
pluginResources = new PluginResources(super.getResources(), PluginLoader.getResources(pluginApkPath));
// ...
}
@Override
public Resources getResources() {
if (pluginActivity == null) {
return super.getResources();
}
return pluginResources;
}
}
這樣就完成了資源的注入抄课。