https://juejin.cn/post/6973888932572315678
https://zhaomenghuan.js.org/blog/android-plugin-framework-proxy-hook.html
https://juejin.cn/post/7028196921143459870
前言
溫度爬升茄唐,蚊蟲也開始猖狂了起來川梅。燥熱的空氣里圾笨,穿梭著幾只置身死于度外的飛蟲,全然沒有在意我這個執(zhí)掌著生死的巨人赁温,讓人欽佩坛怪。
一直在小公司徘徊淤齐,在小團隊里面摸爬滾打股囊。面試中往往會被面試官對于一些平時用不到的技能細致追問,想要跳高只能開始學習工作中用不到的技能更啄,不然就會陷入死循環(huán)怪圈
- 你沒這塊工作經驗稚疹,單位不要。
- 又因為沒單位要祭务,所以就沒有工作經驗内狗。
所以只能自己偷偷默默學習,以demo當作經驗义锥。
名詞解釋
雙親委派機制
:在類加載器中柳沙,指的是當一個類加載器收到了類加載的請求的時候,他不會直接去加載指定的類拌倍,而是把這個請求委托給自己的父加載器去加載赂鲤。 只有父加載器無法加載這個類的時候噪径,才會由當前這個加載器來負責類的加載。
正文
首先附上demo 源碼鏈接
插件化一直都沒有嘗試過数初。聽起來高大上找爱,重要的是面試中被問到的概率也是居高不下。
在開發(fā)過程中泡孩,不用安裝app就能運行新的apk是多么美妙的事情车摄,插件化對于我們的工程應用也有實際意義。本文就以compose Demo項目完整實現(xiàn)一下插件化仑鸥。完成插件的生成裝載吮播,并成功跳轉展示對應的插件頁面。
一眼俊,插件化的方案
在Android中薄料,真正安裝一個apk的過程很簡單,就只是將apk文件拷貝到對應的目錄中泵琳,并且解壓出對應的so文件就好了摄职,其余就是一些解析,掃描組件获列,校驗谷市,dex優(yōu)化的操作,用戶安裝包都是拷貝存放在data/app
中击孩。
啟動app之后迫悠,會從zygote fork一個新的進程,并使用ClassLoader加載apk巩梢。
而我們要動態(tài)加載创泄,也是利用了DexClassLoader來加載我們的插件apk。
但是比起真正的安裝apk括蝠,我們也有以下問題鞠抑。
- 如何利用ClassLoader加載apk?
- 沒有注冊對應manifest忌警,在啟動activity的時候怎么繞過Activity限制搁拙?
- 沒有加載資源文件,要如何獲确唷箕速?
接下來的內容中,以解決以上問題為主要任務朋譬,實現(xiàn)完整的插件app Activity啟動盐茎,展示頁面的過程。
本文是對于compose項目的插件化徙赢,所以暫不涉及xml布局資源的加載獲取字柠。compose布局由代碼完成滑进。
二,DexClassLoader加載apk
如果需要將apk放置到緩存文件夾之外募谎,需要配置好存儲權限扶关,并動態(tài)請求,否則利用DexClassLoader加載會報錯:No original dex files found for dex location
接下來進入正題数冬。
我們先創(chuàng)建一個工程节槐。目錄如下所示
plugin_pjoject
- app //主工程app
- plugin_app //一個插件app
- plugin_base //基礎類庫,包含插件宿主Activity拐纱,加載插件工具類铜异,和基礎插件接口類
接下來在plugin_base
中編寫加載代碼
插件apk的生成只需要將plugin_app
直接打包出apk就行,不需要簽名秸架,不會進行簽名校驗揍庄。
方便起見,我們將插件 apk
直接放到assets
里面东抹。
代碼邏輯如下:
- 在app啟動的時候蚂子,先拷貝assets中的插件
plugin.apk
到緩存目錄中 - 生成一個對應的
DexClassLoader
,專門用來后續(xù)加載插件中的類缭黔。
object PluginLoader {
fun loadPlugin() {
val inputStream = Utils.getApp().assets.open("plugin.apk")
val filesDir = Utils.getApp().externalCacheDir
val apkFile = File(filesDir?.absolutePath, "plugin.apk")
apkFile.writeBytes(inputStream.readBytes())
val dexFile = File(filesDir, "dex")
FileUtils.createOrExistsDir(dexFile)
"輸出dex路徑${dexFile}".logI()
pluginClassLoader = DexClassLoader(apkFile.absolutePath, dexFile.absolutePath, null, this.javaClass.classLoader)
}
}
///插件的類加載器
lateinit var pluginClassLoader: DexClassLoader;
這樣我們就得到pluginClassLoader
食茎。
DexClassLoader源碼分析
1. 關聯(lián)android dalvik
源碼
要想對源碼進行分析,肯定要先能看到源碼馏谨,我們默認在AndroidStudio中無法查看DexClassLoader源碼,在manager中下載了對應source也一樣别渔。所以我們需要自己下載對應源碼和as關聯(lián),具體做法參考下面文章學習:
http://www.reibang.com/p/9af6d2fadcb1
2. 雙親委派機制
在關聯(lián)好源碼后惧互,我們先看CLassLoader類中最關鍵的loadClass方法
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 檢查是否被加載過
Class<?> c = findLoadedClass(name);
if (c == null) {
if (parent != null) {
///使用父加載器先加載類
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
if (c == null) {
// 如果仍然沒找到哎媚,就執(zhí)行findClass尋找
c = findClass(name);
}
}
return c;
}
}
從該版本加載器代碼中看到,類加載的默認機制就是先從祖先類加載開始加載喊儡,如果找不到就一層層子加載器加載拨与。其中findClass只是個空方法,交由子類自己實現(xiàn)具體代碼管宵。
其中findBootstrapClassOrNull
方法更是直接私有化截珍,并返回空,看起來并沒有實際意義箩朴。
值得注意的是,里面有個靜態(tài)方法
createSystemClassLoader
秋度,其方法返回了一個PathClassLoader
作為系統(tǒng)類的加載器炸庞。
重點:在我們的需求中,我們只需要加載宿主或者系統(tǒng)中不存在的類荚斯,所以只需要創(chuàng)建一個加載器埠居,并將原來的加載器作為父加載器就行了查牌。
這樣既能加載插件新類,又不影響原來的舊類滥壕。
從中也能看到纸颜,如果我們想要優(yōu)先讓自己的類加載器加載,就需要實現(xiàn)子類覆寫loadClass方法绎橘,自定義加載邏輯胁孙。
3. DexClassLoader 對于apk的加載實現(xiàn)
我們先來看下DexClassLoader
代碼。
/**
一個類加載器称鳞,它從包含classes.dex條目的.jar和.apk文件加載類涮较。這可用于執(zhí)行未作為應用程序的一部分安裝的代碼。
在 API 級別 26 之前冈止,此類加載器需要一個應用程序私有的可寫目錄來緩存優(yōu)化的類狂票。使用Context.getCodeCacheDir()創(chuàng)建這樣一個目錄:
File dexOutputDir = context.getCodeCacheDir();
不要在外部存儲上緩存優(yōu)化的類。外部存儲不提供保護您的應用程序免受代碼注入攻擊所必需的訪問控制熙暴。
*/
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
DexClassLoader
代碼很簡單闺属,本來它和PathClassLoader
的區(qū)別就是可以自定義optimizedDirectory
,但從注釋中可知周霉,為了安全起見屋剑,該參數在api 26也就是android 8.0之后就已經失效了,優(yōu)化輸出地址不再可以自由配置诗眨,而有系統(tǒng)統(tǒng)一設置唉匾。該加載器的所有的實現(xiàn)都在BaseDexClassLoader
中,只是進行了optimizedDirectory
的屏蔽操作匠楚。
接下來看下BaseDexClassLoader
的加載方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 首先巍膘,檢查該類是否存在于我們的共享庫中。
//...省略
//檢查該類加載器操作的 dexPath 中是否存在相關類芋簿。
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
//...省略異常處理
return c;
}
其中關鍵就是通過**pathList**``.findClass
通過DexPathList集合來查找峡懈。再定位到DexPathList.findClass
代碼
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
findClass最后是移交給dexElements處理了。
dexElements
初始化代碼在makeDexElements
方法中与斤,其中掃描了我們給的apk路徑所有文件(給的路徑可以是多個肪康,以pathSeparator分割),并將dex后綴的文件加載為DexFile撩穿,并和file一起組裝為Element對象放置到Element數組中磷支,給dexELements賦值。
這就完成了apk內所有dex文件的加載食寡。
接下來的DexFile具體加載定義類的代碼都是native代碼雾狈,分析就到此為止。
4. 最后總結下流程
對于我們的插件apk加載的過程如下抵皱。
- 以apk為路徑善榛,構建DexClassLoader
- BaseDexClassLoader加載apk內的所有dex文件辩蛋,加載為Element
- 插件內的類在需要的時候,在經歷過雙親委托的父節(jié)點加載器加載后移盆,被DexClassLoader加載出來
所以對于加載插件悼院,我們只需要創(chuàng)建一個專用的DexClassLoader就行了,如下:
DexClassLoader(apkFile.absolutePath, dexFile.absolutePath, null, this.javaClass.classLoader)
三咒循,startActivity 流程初步分析
1. Activity注冊校驗
加載出類之后据途,正常的代碼我們已經可以執(zhí)行了。但是在安卓中剑鞍,我們想要顯示界面昨凡,還要面對安卓對activity的注冊校驗。沒有注冊過的activity直接是無法打開的蚁署。會報錯:
Unable to find explicit activity class **; have you declared this activity in your AndroidManifest.xml?*
這是因為在我們startActivity的時候便脊,Instrumentation
中會進行校驗」飧辏看源碼可知哪痰,startActivity的流程如下:
對于Activity
注冊的校驗,就是在Instumentation.checkStartActivityResult
中進行的久妆。
2. Instrumentation 代理
我們從源碼中得知晌杰,控制跳轉的代碼由Instumentation
處理,所以我們可以對他做文章筷弦。
我們想要正常打開我們的插件頁面肋演,用的是容器思想,建立一個容器activity烂琴,承載插件的頁面爹殊,而要兼容startActivity 直接配置容器Class,只需要代理Instumentation對象奸绷,進行容器替換處理就可以了梗夸。
我們先實現(xiàn)自己的Instrumentation
代理類:
class PluginInstrumentation(var instrumentation:Instrumentation): Instrumentation() {
@SuppressLint("DiscouragedPrivateApi")
fun execStartActivity(
who: Context?, contextThread: IBinder?, token: IBinder?, target: Activity?,
intent: Intent?, requestCode: Int, options: Bundle?
): ActivityResult? {
try {
val pluginClazz = pluginClassLoader.loadClass(intent?.component?.className)
var newIntent=intent;
if (pluginClazz.superclass == IPluginActivityInterface::class.java) {
newIntent=Intent(who,HostActivity::class.java)
intent?.extras?.let {
newIntent.putExtras(it)
}
newIntent.putExtra(HostActivity.ARG_PLUGIN_CLASS_NAME,pluginClazz.name)
}
val execStartActivity: Method = Instrumentation::class.java.getDeclaredMethod(
"execStartActivity",
Context::class.java,
IBinder::class.java,
IBinder::class.java,
Activity::class.java,
Intent::class.java,
Int::class.javaPrimitiveType,
Bundle::class.java
)
return execStartActivity.invoke(instrumentation, who, contextThread, token, target, newIntent, requestCode, options) as ActivityResult
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
}
在代理類中,我們重寫了execStartActivity
号醉。對于插件Activity的Intent意圖反症,替換成HostActivity
。其余的插件相關頁面邏輯畔派,我們在HostActivity
中進行調用铅碍。
這樣就能繞過注冊限制,展示插件中定義的頁面和布局了父虑。
需要注意的是该酗,對于其中的插件類,我們是用專門的pluginClassLoader加載的士嚎。
3. 替換instrumentation代理類
我們先看instumentation 的初始化代碼呜魄。
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
IBinder shareableActivityToken) {
attachBaseContext(context);
///...
mInstrumentation = instr;
}
可以看到mInstrumentation
在attach方法中被賦值,我們想要代理改變量莱衩,就只需要在attach
之后爵嗅,利用反射重新賦值代理類就行了。
我們從源碼中觀察到笨蚁,attach
在ActivityThread.performnLaunchActivity
中被調用睹晒,也在
mInstrumentation.callActivityOnCreate
的調用之前,而onCreate的執(zhí)行就在該調用鏈中括细。
所以我們只需要在Activity.onCreate中完成mInstrumentation
的代理類替換就行了伪很。
這一塊代碼我們通過對
Application
添加activity
生命周期監(jiān)聽實現(xiàn) ,代碼如下奋单,這一塊代碼的執(zhí)行通過實現(xiàn)自己的ContentProvider
實現(xiàn)锉试。利用框架對provider的初始化機制,實現(xiàn)xml注冊就無感注冊監(jiān)聽览濒,具體代碼可以看源碼中的PluginContentProvider
類呆盖。
override fun onCreate(): Boolean {
Utils.getApp().registerActivityLifecycleCallbacks(object :ActivityLifecycleCallbacks{
@SuppressLint("DiscouragedPrivateApi")
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
// 拿到原始的 mInstrumentation字段
val mInstrumentationField: Field = Activity::class.java.getDeclaredField("mInstrumentation")
mInstrumentationField.isAccessible = true
// 創(chuàng)建代理對象
val originalInstrumentation: Instrumentation = mInstrumentationField.get(activity) as Instrumentation
mInstrumentationField.set(activity, PluginInstrumentation(originalInstrumentation))
}
//... 省略其他生命周期代碼
})
return true
}
四,利用宿主Activity替換掉實際打開的Activity
在完成mInstrumentation
替換后贷笛,我們還需要完善我們的宿主容器HostActivtiy以及對應插件Activity
的接口应又,完成以下幾個目的:
- 像打開正常的activity一樣,打開插件activity
- 占位Activity到插件Activity的轉換
- 無侵入式插件Activity開發(fā)
完成插件Activity的抽象代理類(插件Activity只是寫法和正常Activity一樣乏苦,其實不是Activity的子類)株扛,模擬正常Activity的生命周期
簡單起見,我們就先只定義了setContentView
汇荐,onCreate
和getIntent
方法洞就,其他生命周期方法在后續(xù)需要的時候在完善。重點是registerHostActivity
方法拢驾,我們通過該方法注冊宿主Activity奖磁,并在其各個生命周期中的各個方法,都交給mHostActivity
代理完成繁疤。
abstract class IPluginActivityInterface {
lateinit var mHostActivity: HostActivity
fun registerHostActivity(hostActivity: HostActivity) {
mHostActivity = hostActivity;
}
fun getIntent() = mHostActivity.intent
open fun onCreate(savedInstanceState: Bundle?){}
fun setContentView(layoutResID: Int) {
mHostActivity.setContentView(layoutResID)
}
}
在HostActivity
中咖为,實例化插件IPluginActivityInterface
類
我們在HostActivity
的onCreate
方法中取出之前Instumentation中傳遞的ARG_PLUGIN_CLASS_NAME
,通過反射實例化出插件Activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent.getStringExtra(ARG_PLUGIN_CLASS_NAME)?.let {
val clazz= pluginClassLoader.loadClass(it)
pluginActivity = clazz.newInstance() as? IPluginActivityInterface
pluginActivity?.registerHostActivity(this)
}
pluginActivity?.onCreate(savedInstanceState)
}
這樣就能正常加載插件Activity了稠腊。
四躁染,compose ui開發(fā)和兼容
插件模塊中的ui開發(fā)使用的是compose。對于compose的兼容非常簡單架忌,我們簡單實現(xiàn)個setContent拓展方法吞彤,中轉調用宿主activity的setContent方法就行了,如下:
fun IPluginActivityInterface.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
mHostActivity.setContent(parent,content)
}
這樣我們就能很正常的開發(fā)插件App了。如圖所示
界面開發(fā)完成后饰恕,自然就是跳轉了挠羔。
跳轉到PluginActivity
的代碼,也和普通Activity沒有區(qū)別埋嵌,直接startActivity破加,代碼如下:
val intent = Intent().apply {
component = ComponentName(context, "com.example.plugin_app.PluginsActivity")
}
context.startActivity(intent)
至此,完整的app雹嗦,插件app開發(fā)已經實現(xiàn)范舀。更多的細節(jié)在源碼中
最后
吁了一口長氣。
成功的完成了這個小文章的撰寫了罪。
最后放上示例demo的gif吧