插件化理解與實現 —— 加載 Activity「類加載篇」 [復制鏈接]

2019-7-11 15:02
jjcodecode 閱讀:424 評論:0 贊:0
Tag:  插件化 類加載

1 前言

插件化算是比較復雜的一個話題。剛一接觸的時候,我是一臉懵逼的,網上看了很多博客,一直是似懂非懂,不得其要領。期間也嘗試看了Small,也是知其然不知其所以然。

就此擱置一段時間,直到真正拿出勇氣,嘗試自己實現插件化,成功加載了四大組件之一Activity。這才明白它的背后究竟做了什么,以及為什么這么做。

希望借著這篇文章,談談自己的理解。也希望通過我的小 Demo,能幫大家更輕松的理解諸如SmallVirtualApkAtlas之類的大型框架。如有紕漏,請留言指出。

2 效果預覽

主apk[com.fashare.app.MainActivity]喚起sd卡上的插件apk[com.fashare.testapk.PluginActivity] :

preview

3 源碼

https://github.com/fashare2015/Dynamic-Load-Learning

4 原理與實現

Activity 的加載,可以分為「類加載」和「資源加載」兩個主題。

考慮到篇幅比較長,本文主要討論「類加載」。

「資源加載」將放到下一篇文章中探討。

4.1 類加載

根據 Java 的類加載機制,我們知道,JVM 通過ClassLoader來加載 jar 包中的 .class 文件。并且我們需要注意兩點:

  • 一個 .class 文件 + 一個 ClassLoader 唯一確定一個 java 類
  • 請遵循 雙親委派模型

「雙親委派模型」通俗的講就是,系統類諸如java.*全都委派給 JVM 默認的 Bootstrap ClassLoader 加載,其他的由用戶自定義的 ClassLoader 加載。這樣可以保護java.*下的類,并確保系統類的唯一性。否則,會出現 yourClassLoader.loadClass("java.lang.String") != String.class

于是,Android 提供了兩類自定義的 ClassLoader 來加載 dex 中的 .class 文件。

  • PathClassLoader 只能加載已安裝(data/app/目錄下)的apk
  • DexClassLoader 可以加載外部(sd卡上)的apk

如下圖,打開我們的 demo 可以看到,它被安裝到data/app/下。然后由PathClassLoader來加載其中的所有類。

AS 3.0 可以預覽手機上的文件,非常方便。

4.1.1 創建插件專用的 DexClassLoader

由于要加載外部的插件 apk,你大概猜到了,我們得用 new 一個DexClassLoader來加載插件 apk 中的類。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package dalvik.system;

/**
* A class loader that loads classes from {@code .jar} and {@code .apk} files
* containing a {@code classes.dex} entry. This can be used to execute code not
* installed as part of an application.
*/
public class DexClassLoader extends BaseDexClassLoader {
/**
* Creates a {@code DexClassLoader} that finds interpreted and native
* code. Interpreted classes are found in a set of DEX files contained
* in Jar or APK files.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory directory where optimized dex files
* should be written; must not be {@code null}
* @param librarySearchPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}

看一眼它的注釋,大意是說「它可以加載 .jar/.apk 中未安裝的代碼作為 app 的一部分」。其中四個參數分別為:

  • dexPath:插件 apk 的路徑
  • optimizedDirectory: 需指定一個緩存目錄
  • librarySearchPath: native lib,暫時不太需要,直接給 null
  • parent: 根據「雙親委派模型」,應該給它宿主 apk 默認的PathClassLoader

于是,我們這樣先 new 一個 dexClassLoader 備用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.fashare.dl

/**
* 動態加載(門面類)
*/
object DL {
lateinit var dexClassLoader: DexClassLoader

/**
* 加載 sdcard 上的 未安裝的 apk
*/
fun loadApk(context: Context, pluginUri: Uri) {
dexClassLoader = DexClassLoader(
pluginUri.path,
context.cacheDir.path,
null,
context.classLoader)
}

...
}

然后在主 app 的 Application 里調用一下:

1
2
3
4
5
6
7
8
9
10
11
package com.fashare.app

class App: Application(){
override fun onCreate() {
super.onCreate()

val pluginFile = File(Environment.getExternalStorageDirectory(), "testapk-with-res.apk")
copyTestApkToSdcard(pluginFile)
DL.loadApk(this, Uri.fromFile(pluginFile))
}
}

4.1.2 用 dexClassLoader 加載插件中的 Activity

已經有了 dexClassLoader,我們需要在合適的時機加載插件里的 Activity。

研究一發 Activity 啟動流程,我們會發現 Activity 從創建到銷毀都會經過Instrumentation這個類。它非常重要,后面會經常和它打交道。

我們平常在 Manifest 里注冊的 Activity 類名,最終會走到Instrumentation.newActivity,然后反射出 Activity 實例。

所以,我們只需 hack 掉這個方法,new 出我們想要的 Activity 即可。

1
2
3
4
5
6
7
8
9
package android.app;

public class Instrumentation {
...
public Activity newActivity(ClassLoader cl, String className, Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return (Activity)cl.loadClass(className).newInstance();
}
}

4.1.3 hack Instrumentation

我們一起來找這個 hack 點。

我們知道,ActivityThread,也就是我們常說的主線程,它是進程級別的單例。

我們發現,它剛好有一個 Instrumentation 的成員變量,且參與到startActivity等各種重要的流程中,把它換掉就行啦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package android.app;

public final class ActivityThread {
// 單例
private static volatile ActivityThread sCurrentActivityThread;
public static ActivityThread currentActivityThread() {
return sCurrentActivityThread;
}

Instrumentation mInstrumentation;

public static void main(String[] args) {
...
// 我們常說的 Looper 的初始化
Looper.prepareMainLooper();

ActivityThread thread = new ActivityThread();
// 在 attach() 里,new 出了 Instrumentation,也是進程級別唯一的
thread.attach(false);

...
Looper.loop();
}

private void attach(boolean system) {
sCurrentActivityThread = this;

if (!system) {
...
} else {
...
mInstrumentation = new Instrumentation();
}
...
}
}

于是,我們用反射搞一下,換成我們自定義的InstrumentationProxy。用它來代理原來的 Instrumentation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.fashare.dl

object DL {
lateinit var dexClassLoader: DexClassLoader
internal lateinit var instrumentation: Instrumentation

fun loadApk(context: Context, uri: Uri) {...}

/**
* 替換 ActivityThread.mInstrumentation
*/
fun replaceInstrumentation() {
// 反射偽代碼:
val base = ActivityThread.currentActivityThread().mInstrumentation

ActivityThread.currentActivityThread().mInstrumentation = InstrumentationProxy(base)
}
}

InstrumentationProxy 其他不動,僅僅 override (hack) 掉 newActivity 這個方法。

1
2
3
4
5
6
7
8
9
10
11
12
package com.fashare.dl

/**
* Instrumentation 代理類
*/
internal class InstrumentationProxy(val base: Instrumentation) : Instrumentation() {

override fun newActivity(cl: ClassLoader?, className: String?, intent: Intent?): Activity? {
// 不用默認的 cl,而是用事先準備好的 DL.dexClassLoader 來加載 Activity。
return base.newActivity(DL.dexClassLoader, className, intent)
}
}

哈哈,到此為止,我們的 Activity 類加載已經完成啦。

注意此時的插件 Activity 只能是空 Activity,不能訪問 R 資源(此時還沒實現),但可以打 Log 以及 Toast。在主 app 里試用一下:

  1. 在 App.onCreate() 里調用 DL.replaceInstrumentation() —— 做 hack
  2. 在 App.onCreate() 里調用 DL.loadApk(this, Uri.fromFile(pluginFile)) —— 指定插件 apk 路徑
  3. 在主 app 的 Manifest 里注冊 com.fashare.testapk.PluginActivity
  4. 在主 app 的 MainActivity 里調用
    startActivity(Intent(this, DL.dexClassLoader.loadClass("com.fashare.testapk.PluginActivity")))

應該可以看到,已經成功喚起插件 Activity 了,生命周期也照常被調用。

4.1.4 還沒有結束

且看前面的第 3 步,如果使用插件還得把所有插件里的 Activity 事先在宿主 apk 里注冊一遍,那一點也不動態啊。所以,我們看到的市面上的插件化框架都是不需要注冊 Activity 的,我們也想辦法優化掉這一步。

直接上結論吧,startActivity() 走到 AMS 的時候,它會檢查目標 Activity 是否注冊過,并攔截掉未注冊的 Activity。說實話,這一段還沒有仔細的去跟過。

于是呢,我們可以事先注冊一個空 Activity,把 Intent 的目標 Activity 換成它,用它騙過 AMS 的檢查。然后在 newActivity 的時候,new 我們真正需要的插件 Activity。(有點 ViewStub 的感覺?)

代碼實現主要是 hack 掉 Instrumentation.execStartActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.fashare.dl

/**
* Intent 代理類,替換 component.className 為 StubActivity
*/
internal class IntentProxy(var base: Intent?) : Intent() {
init {
component = ComponentName(base?.component?.packageName, StubActivity::class.java.name)
}
}

/**
* 占坑用 Activity
*/
internal class StubActivity : AppCompatActivity()

internal class InstrumentationProxy(val base: Instrumentation) : Instrumentation() {
var mIntentProxy: IntentProxy? = null

/**
* new 之前,取出原始的 intent 來 new Activity實例
*/
override fun newActivity(cl: ClassLoader?, className: String?, intent: Intent?): Activity? {
return (mIntentProxy?.base ?: intent)?.let {
mIntentProxy = null
base.newActivity(DL.dexClassLoader, it.component?.className, it)
}
}

/**
* start 之前,替換 Intent 為 已注冊的 StubActivity, 以繞過 系統對 manifest 的檢查
*
* 注: 這里其實也是 override, 只是 super.execStartActivity(...) 為 @hide, 所以看起來比較奇怪.
*/
fun execStartActivity(
who: Context?, contextThread: IBinder?, token: IBinder?, target: Activity?,
intent: Intent?, requestCode: Int, options: Bundle?): ActivityResult? {

mIntentProxy = IntentProxy(intent)

return try {
Reflect.on(base).call("execStartActivity",
who, contextThread, token, target,
mIntentProxy, requestCode, options).get<ActivityResult?>()
} catch (e: Exception) {
null
}
}
}

4.1.5 類加載小結

類加載告一段落,弄透重要的幾點,再去看那些成熟的框架,會輕松很多:

  • DexClassLoader
  • hack Instrumentation
  • Actiivty(四大組件)占坑

我來說兩句
您需要登錄后才可以評論 登錄 | 立即注冊
facelist
所有評論(0)
領先的中文移動開發者社區
18620764416
7*24全天服務
意見反饋:[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粵ICP備15117877號 )

两码中特期期