您好,登錄后才能下訂單哦!
本篇文章為大家展示了Android插件化是怎樣的,內(nèi)容簡(jiǎn)明扼要并且容易理解,絕對(duì)能使你眼前一亮,通過這篇文章的詳細(xì)介紹希望你能有所收獲。
插件化技術(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 中的代碼呢?
插件化讓 Apk
中的代碼(主要是指 Android 組件)能夠免安裝運(yùn)行,這樣能夠帶來很多收益:
減少安裝Apk的體積、按需下載模塊
動(dòng)態(tài)更新插件
宿主和插件分開編譯,提升開發(fā)效率
解決方法數(shù)超過65535
的問題
想象一下,你的應(yīng)用擁有 Native 應(yīng)用一般極高的性能,又能獲取諸如 Web 應(yīng)用一樣的收益。
嗯,理想很美好不是嘛?
組件化:是將一個(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。
想讓插件的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
是插件化中必須要掌握的,因?yàn)槲覀冎?code>Android 應(yīng)用本身是基于魔改的 Java 虛擬機(jī)的,而由于插件是未安裝的 apk,系統(tǒng)不會(huì)處理其中的類,所以需要使用 ClassLoader
加載 Apk,然后反射里面的代碼。
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 包和目錄
在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
。
每一個(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; }
要加載插件中的類,我們首先要?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) }
通過反射來執(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)程中使用來自于插件的組件類了。
我們之前說到 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ū)別了。
這里的不能直接交互的含義有兩個(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)行了。
由于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)。
該方式雖然能夠很好的實(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é)束了。
最后要說的是資源注入,其實(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
資源,再復(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; } }
這樣就完成了資源的注入。
上述內(nèi)容就是Android插件化是怎樣的,你們學(xué)到知識(shí)或技能了嗎?如果還想學(xué)到更多技能或者豐富自己的知識(shí)儲(chǔ)備,歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。