您好,登錄后才能下訂單哦!
本篇文章為大家展示了Android插件化是怎樣的,內容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。
插件化技術最初源于免安裝運行 Apk的想法,這個免安裝的 Apk 就可以理解為插件,而支持插件的 app 我們一般叫 宿主。
想必大家都知道,在 Android
系統中,應用是以 Apk 的形式存在的,應用都需要安裝才能使用。但實際上 Android 系統安裝應用的方式相當簡單,其實就是把應用 Apk 拷貝到系統不同的目錄下、然后把 so 解壓出來而已。
常見的應用安裝目錄有:
/system/app
:系統應用
/system/priv-app
:系統應用
/data/app
:用戶應用
那可能大家會想問,既然安裝這個過程如此簡單,Android
是怎么運行應用中的代碼的呢,我們先看 Apk
的構成,一個常見的 Apk 會包含如下幾個部分:
classes.dex
:Java 代碼字節碼
res
:資源文件
lib
:so 文件
assets
:靜態資產文件
AndroidManifest.xml
:清單文件
其實 Android
系統在打開應用之后,也只是開辟進程,然后使用 ClassLoader
加載 classes.dex
至進程中,執行對應的組件而已。
那大家可能會想一個問題,既然 Android
本身也是使用類似反射的形式加載代碼執行,憑什么我們不能執行一個 Apk 中的代碼呢?
插件化讓 Apk
中的代碼(主要是指 Android 組件)能夠免安裝運行,這樣能夠帶來很多收益:
減少安裝Apk的體積、按需下載模塊
動態更新插件
宿主和插件分開編譯,提升開發效率
解決方法數超過65535
的問題
想象一下,你的應用擁有 Native 應用一般極高的性能,又能獲取諸如 Web 應用一樣的收益。
嗯,理想很美好不是嘛?
組件化:是將一個App
分成多個模塊,每個模塊都是一個組件(module
),開發過程中可以讓這些組件相互依賴或獨立編譯、調試部分組件,但是這些組件最終會合并成一個完整的Apk去發布到應用市場。
插件化:是將整個App拆分成很多模塊,每個模塊都是一個Apk(組件化的每個模塊是一個lib),最終打包的時候將宿主Apk和插件Apk分開打包,只需發布宿主Apk到應用市場,插件Apk通過動態按需下發到宿主Apk。
想讓插件的Apk真正運行起來,首先要先能找到插件Apk的存放位置,然后我們要能解析加載Apk里面的代碼。
但是光能執行Java代碼是沒有意義的,在Android系統中有四大組件是需要在系統中注冊的,具體來說是在 Android 系統的 ActivityManagerService
(AMS) 和 PackageManagerService
(PMS) 中注冊的,而四大組件的解析和啟動都需要依賴 AMS 和 PMS,如何欺騙系統,讓他承認一個未安裝的 Apk 中的組件,如何讓宿主動態加載執行插件Apk中 Android 組件(即 Activity
、Service
、BroadcastReceiver
、ContentProvider
、Fragment
)等是插件化最大的難點。
另外,應用資源引用(特指 R 中引用的資源,如 layout、values 等)也是一大問題,想象一下你在宿主進程中使用反射加載了一個插件 Apk,代碼中的 R 對應的 id 卻無法引用到正確的資源,會產生什么后果。
總結一下,其實做到插件化的要點就這幾個:
如何加載并執行插件 Apk 中的代碼(ClassLoader Injection
)
讓系統能調用插件 Apk 中的組件(Runtime Container
)
正確識別插件 Apk 中的資源(Resource Injection
)
當然還有其他一些小問題,但可能不是所有場景下都會遇到,我們后面再單獨說。
ClassLoader
是插件化中必須要掌握的,因為我們知道Android
應用本身是基于魔改的 Java 虛擬機的,而由于插件是未安裝的 apk,系統不會處理其中的類,所以需要使用 ClassLoader
加載 Apk,然后反射里面的代碼。
BootstrapClassLoader
負責加載 JVM 運行時的核心類,比如 JAVA_HOME/lib/rt.jar 等等
ExtensionClassLoader
負責加載 JVM 的擴展類,比如 JAVA_HOME/lib/ext 下面的 jar 包
AppClassLoader
負責加載 classpath
里的 jar 包和目錄
在Android
系統中ClassLoader
是用來加載dex文件的,有包含 dex 的 apk 文件以及 jar 文件,dex 文件是一種對class文件優化的產物,在Android
中應用打包時會把所有class文件進行合并、優化(把不同的class文件重復的東西只保留一份),然后生成一個最終的class.dex
文件
PathClassLoader
用來加載系統類和應用程序類,可以加載已經安裝的 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 文件,可以從存儲空間加載 dex 文件。
public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), libraryPath, parent); } }
我們在插件化中一般使用的是 DexClassLoader
。
每一個 ClassLoader
中都有一個 parent 對象,代表的是父類加載器,在加載一個類的時候,會先使用父類加載器去加載,如果在父類加載器中沒有找到,自己再進行加載,如果 parent 為空,那么就用系統類加載器來加載。通過這樣的機制可以保證系統類都是由系統類加載器加載的。 下面是 ClassLoader
的 loadClass
方法的具體實現。
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) { // 先從父類加載器中進行加載 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; }
要加載插件中的類,我們首先要創建一個 DexClassLoader
,先看下 DexClassLoader
的構造函數需要哪些參數。
public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) { // ... } }
構造函數需要四個參數: dexPath
是需要加載的 dex / apk / jar 文件路徑 optimizedDirectory
是 dex 優化后存放的位置,在 ART 上,會執行 oat 對 dex 進行優化,生成機器碼,這里就是存放優化后的 odex 文件的位置 librarySearchPath
是 native 依賴的位置 parent
就是父類加載器,默認會先從 parent
加載對應的類
創建出 DexClassLaoder
實例以后,只要調用其 loadClass(className)
方法就可以加載插件中的類了。具體的實現在下面:
// 從 assets 中拿出插件 apk 放到內部存儲空間 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) }
通過反射來執行類的方法
val loadClass = pluginClassLoader.loadClass(activityName) loadClass.getMethod("test",null).invoke(loadClass)
我們稱這個過程叫做 ClassLoader
注入。完成注入后,所有來自宿主的類使用宿主的 ClassLoader
進行加載,所有來自插件 Apk 的類使用插件 ClassLoader
進行加載,而由于 ClassLoader
的雙親委派機制,實際上系統類會不受 ClassLoader 的類隔離機制所影響,這樣宿主 Apk 就可以在宿主進程中使用來自于插件的組件類了。
我們之前說到 Activity
插件化最大的難點是如何欺騙系統,讓他承認一個未安裝的 Apk 中的組件。 因為插件是動態加載的,所以插件的四大組件不可能注冊到宿主的 Manifest
文件中,而沒有在 Manifest 中注冊的四大組件是不能和系統直接進行交互的。 如果直接把插件的 Activity 注冊到宿主 Manifest
里就失去了插件化的動態特性,因為每次插件中新增 Activity
都要修改宿主 Manifest
并且重新打包,那就和直接寫在宿主中沒什么區別了。
這里的不能直接交互的含義有兩個
系統會檢測 Activity
是否注冊 如果我們啟動一個沒有在 Manifest
中注冊的 Activity,會發現報如下 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?
這個 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 的生命周期無法被調用,其實一個 Activity 主要的工作,都是在其生命周期方法中調用了,既然上一步系統檢測了 Manifest
注冊文件,啟動 Activity
被拒絕,那么其生命周期方法也肯定不會被調用了。從而插件 Activity 也就不能正常運行了。
由于Android
中的組件(Activity
,Service
,BroadcastReceiver
和ContentProvider
)是由系統創建的,并且由系統管理生命周期。 僅僅構造出這些類的實例是沒用的,還需要管理組件的生命周期。其中以Activity最為復雜,不同框架采用的方法也不盡相同。插件化如何支持組件生命周期的管理。 大致分為兩種方式:
運行時容器技術(ProxyActivity
代理)
預埋StubActivity
,hook系統啟動Activity的過程
我們的解決方案很簡單,即運行時容器技術,簡單來說就是在宿主 Apk 中預埋一些空的 Android 組件,以 Activity 為例,我預置一個 ContainerActivity extends Activity
在宿主中,并且在 AndroidManifest
.xml 中注冊它。
它要做的事情很簡單,就是幫助我們作為插件 Activity 的容器,它從 Intent 接受幾個參數,分別是插件的不同信息,如:
pluginName
pluginApkPath
pluginActivityName
等,其實最重要的就是 pluginApkPath
和 pluginActivityName
,當 ContainerActivity
啟動時,我們就加載插件的 ClassLoader
、Resource
,并反射 pluginActivityName
對應的 Activity 類。當完成加載后,ContainerActivity
要做兩件事:
轉發所有來自系統的生命周期回調至插件 Activity
接受 Activity
方法的系統調用,并轉發回系統
我們可以通過復寫 ContainerActivity
的生命周期方法來完成第一步,而第二步我們需要定義一個 PluginActivity
,然后在編寫插件 Apk 中的 Activity 組件時,不再讓其集成 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 { // ...... }
是不是感覺有點看懂了,雖然真正搞的時候還有很多小坑,但大概原理就是這么簡單,啟動插件組件需要依賴容器,容器負責加載插件組件并且完成雙向轉發,轉發來自系統的生命周期回調至插件組件,同時轉發來自插件組件的系統調用至系統。
該方式雖然能夠很好的實現啟動插件Activity
的目的,但是由于開發式侵入性很強,插件中的Activity
必須繼承PluginActivity
,如果想把之前的模塊改造成插件需要很多額外的工作。
class TestActivity extends Activity {} -> class TestActivity extends PluginActivity {}
有沒有什么辦法能讓插件組件的編寫與原來沒有任何差別呢?
Shadow
的做法是字節碼替換插件,這是一個非常棒的想法,簡單來說,Android
提供了一些 Gradle 插件開發套件,其中有一項功能叫 Transform Api
,它可以介入項目的構建過程,在字節碼生成后、dex 文件生成前,對代碼進行某些變換,具體怎么做的不說了,可以自己看文檔。
實現的功能嘛,就是用戶配置 Gradle 插件后,正常開發,依然編寫:
class TestActivity extends Activity {}
然后完成編譯后,最后的字節碼中,顯示的卻是:
class TestActivity extends PluginActivity {}
到這里基本的框架就差不多結束了。
最后要說的是資源注入,其實這一點相當重要,Android
應用的開發其實崇尚的是邏輯與資源分離的理念,所有資源(layout
、values
等)都會被打包到 Apk 中,然后生成一個對應的 R 類,其中包含對所有資源的引用 id。
資源的注入并不容易,好在 Android
系統給我們留了一條后路,最重要的是這兩個接口:
PackageManager#getPackageArchiveInfo
:根據 Apk 路徑解析一個未安裝的 Apk 的 PackageInfo
PackageManager#getResourcesForApplication
:根據 ApplicationInfo
創建一個 Resources
實例
我們要做的就是在上面 ContainerActivity#onCreate
中加載插件 Apk 的時候,用這兩個方法創建出來一份插件資源實例。具體來說就是先用 PackageManager#getPackageArchiveInfo
拿到插件 Apk 的 PackageInfo
,有了 PacakgeInfo 之后我們就可以自己組裝一份 ApplicationInfo
,然后通過 PackageManager#getResourcesForApplication
來創建資源實例,大概代碼像這樣:
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) { // ... }
拿到資源實例后,我們需要將宿主的資源和插件資源 Merge 一下,編寫一個新的 Resources
類,用這樣的方式完成自動代理:
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); } } // ... }
然后我們在 ContainerActivity
完成插件組件加載后,創建一份 Merge
資源,再復寫 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; } }
這樣就完成了資源的注入。
上述內容就是Android插件化是怎樣的,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。