博文出處:插件化框架android-pluginmgr全解析,歡迎大家關(guān)注我的博客踢星,謝謝统刮!
0x00 前言:插件化的介紹
閱讀須知:閱讀本文的童鞋最好是有過插件化框架使用經(jīng)歷或者對插件化框架有過了解的故源。前方高能污抬,大牛繞道。
最近一直在關(guān)注 Android 插件化方面,所以今天的主題就確定是 Android 中比較熱門的“插件化”了印机。所謂的插件化就是下載 apk 到指定目錄矢腻,不需要安裝該 apk ,就能利用某個已安裝的 apk (即“宿主”)調(diào)用起該未安裝 apk 中的 Activity 射赛、Service 等組件(即“插件”)多柑。
Android 插件化的發(fā)展到目前為止也有一段時間了,從一開始任主席的 dynamic-load-apk 到今天要分析的 android-pluginmgr 再到360的 DroidPlugin 楣责,也代表著插件化的思想從頂部的應(yīng)用層向下到 Framework 層滲入竣灌。最早插件化的思想是 dynamic-load-apk 實(shí)現(xiàn)的, dynamic-load-apk 在“宿主” ProxyActivity 的生命周期中利用接口回調(diào)了“插件” PluginActivity 的“生命周期”秆麸,以此來間接實(shí)現(xiàn) PluginActivity 的“生命周期”初嘹。也就是說,其實(shí)插件中的 “PluginActivity” 并不具有真正 Activity 的性質(zhì)蛔屹,實(shí)質(zhì)就是一個普通類削樊,只是利用接口回調(diào)了類中的生命周期方法而已。比接口回調(diào)更好的方案就是利用 ActivityThread 兔毒、Instrumentation 等去動態(tài)地 Hook 即將創(chuàng)建的 ProxyActivity ,也就是說表面上創(chuàng)建的是 ProxyActivity 甸箱,其實(shí)實(shí)際上是創(chuàng)建了 PluginActivity 育叁。這種思想相比于 dynamic-load-apk 而言,插件中 Activity 已經(jīng)是實(shí)質(zhì)上的 Activity 芍殖,具備了生命周期方法豪嗽。今天我們要解析的 android-pluginmgr 插件化框架就是基于這種思想的。最后就是像 DroidPlugin 這種插件化框架豌骏,改動了 ActivityManagerService 龟梦、 PackageManagerService 等 Android 源碼,以此來實(shí)現(xiàn)插件化窃躲〖品。總之,并沒有哪種插件化框架是最好的蒂窒,一切都是要根據(jù)自身實(shí)際情況而決定的躁倒。
熟悉插件化的童鞋都知道,插件化要解決的有三個基本難題:
- 插件中 ClassLoader 的問題洒琢;
- 插件中的資源文件訪問問題秧秉;
- 插件中 Activity 組件的生命周期問題。
基本上衰抑,解決了上面三個問題象迎,就可以算是一個合格的插件化框架了。但是要注意的是呛踊,插件化遠(yuǎn)遠(yuǎn)不止這三個問題砾淌,比如還有插件中 .so 文件加載啦撮,支持 Service 插件化等問題。
好了拇舀,講了這么多廢話逻族,接下來我們就來分析 android-pluginmgr 的源碼吧。
0x01 PluginManager.init
注:本文分析的 android-pluginmgr 為 master 分支骄崩,版本為0.2.2聘鳞;
android-pluginmgr的簡單用法
我們先簡單地來看一下 android-pluginmgr 框架的用法(來自于 android-pluginmgr 的 README.md ):
-
declare permission in your
AndroidManifest.xml
:<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-
regist an activity:
<activity android:name="androidx.pluginmgr.DynamicActivity" />
-
init PluginMgr in your application:
@Override public void onCreate(){ PluginManager.init(this); //... }
-
load plugin from plug apk:
PluginManager pluginMgr = PluginManager.getSingleton(); File myPlug = new File("/mnt/sdcard/Download/myplug.apk"); PlugInfo plug = pluginMgr.loadPlugin(myPlug).iterator().next();
-
start activity:
mgr.startMainActivity(context, plug);
基本的用法就像以上這五步,另外需要注意的是要拂,“插件”中所需要的權(quán)限都要在“宿主”的 AndroidManifest.xml 中進(jìn)行申明抠璃。
PluginManager.init(this)源碼
下面我們來分析下 PluginManager.init(this);
的源碼:
/**
* 初始化插件管理器,請不要傳入易變的Context,那將造成內(nèi)存泄露!
*
* @param context Application上下文
*/
public static void init(Context context) {
if (SINGLETON != null) {
Trace.store("PluginManager have been initialized, YOU needn't initialize it again!");
return;
}
Trace.store("init PluginManager...");
SINGLETON = new PluginManager(context);
}
可以看到在 init(Context context)
中主要創(chuàng)建了一個 SINGLETON
單例,所以我們就要追蹤 PluginManager
構(gòu)造器的源碼了:
/**
* 插件管理器私有構(gòu)造器
*
* @param context Application上下文
*/
private PluginManager(Context context) {
if (!isMainThread()) {
throw new IllegalThreadStateException("PluginManager must init in UI Thread!");
}
this.context = context;
File optimizedDexPath = context.getDir(Globals.PRIVATE_PLUGIN_OUTPUT_DIR_NAME, Context.MODE_PRIVATE);
dexOutputPath = optimizedDexPath.getAbsolutePath();
dexInternalStoragePath = context.getDir(
Globals.PRIVATE_PLUGIN_ODEX_OUTPUT_DIR_NAME, Context.MODE_PRIVATE
);
DelegateActivityThread delegateActivityThread = DelegateActivityThread.getSingleton();
Instrumentation originInstrumentation = delegateActivityThread.getInstrumentation();
if (!(originInstrumentation instanceof PluginInstrumentation)) {
PluginInstrumentation pluginInstrumentation = new PluginInstrumentation(originInstrumentation);
delegateActivityThread.setInstrumentation(pluginInstrumentation);
}
}
在構(gòu)造器中做的事情有點(diǎn)多脱惰,我們一步步來看下搏嗡。一開始得到插件 dex opt 輸出路徑 dexOutputPath
和私有目錄中存儲插件的路徑 dexInternalStoragePath
。這些路徑都是在 Global
類中事先定義好的:
/**
* 私有目錄中保存插件文件的文件夾名
*/
public static final String PRIVATE_PLUGIN_OUTPUT_DIR_NAME = "plugins-file";
/**
* 私有目錄中保存插件odex的文件夾名
*/
public static final String PRIVATE_PLUGIN_ODEX_OUTPUT_DIR_NAME = "plugins-opt";
但是根據(jù)常量定義的名稱來看拉一,總感覺作者在 context.getDir()
時把這兩個路徑搞反了 \(╯-╰)/采盒。
之后在構(gòu)造器中創(chuàng)建了 DelegateActivityThread
類的單例:
public final class DelegateActivityThread {
private static DelegateActivityThread SINGLETON = new DelegateActivityThread();
private Reflect activityThreadReflect;
public DelegateActivityThread() {
activityThreadReflect = Reflect.on(ActivityThread.currentActivityThread());
}
public static DelegateActivityThread getSingleton() {
return SINGLETON;
}
public Application getInitialApplication() {
return activityThreadReflect.get("mInitialApplication");
}
public Instrumentation getInstrumentation() {
return activityThreadReflect.get("mInstrumentation");
}
public void setInstrumentation(Instrumentation newInstrumentation) {
activityThreadReflect.set("mInstrumentation", newInstrumentation);
}
}
DelegateActivityThread 類的主要作用就是使用反射包裝了當(dāng)前的 ActivityThread ,并且一開始在 DelegateActivityThread 中使用 PluginInstrumentation 替換原始的 Instrumentation 蔚润。其實(shí) Activity 的生命周期調(diào)用都是通過 Instrumentation 來完成的磅氨。我們來看看 PluginInstrumentation 的構(gòu)造器相關(guān)代碼:
public class PluginInstrumentation extends DelegateInstrumentation
{
/**
* 當(dāng)前正在運(yùn)行的插件
*/
private PlugInfo currentPlugin;
/**
* @param mBase 真正的Instrumentation
*/
public PluginInstrumentation(Instrumentation mBase) {
super(mBase);
}
...
}
可以看到 PluginInstrumentation 是繼承自 DelegateInstrumentation 類的,而 DelegateInstrumentation 本質(zhì)上就是 Instrumentation 嫡纠。 DelegateInstrumentation 類中的方法都是直接調(diào)用 Instrumentation 類的:
public class DelegateInstrumentation extends Instrumentation {
private Instrumentation mBase;
/**
* @param mBase 真正的Instrumentation
*/
public DelegateInstrumentation(Instrumentation mBase) {
this.mBase = mBase;
}
@Override
public void onCreate(Bundle arguments) {
mBase.onCreate(arguments);
}
@Override
public void start() {
mBase.start();
}
@Override
public void onStart() {
mBase.onStart();
}
...
}
好了烦租,在 PluginManager.init()
方法中大概做的就是這些邏輯了。
0x02 PluginManager.loadPlugin
看完了上面的 PluginManager.init()
之后除盏,下一步就是調(diào)用 pluginManager.loadPlugin
去加載插件叉橱。一起來看看相關(guān)源碼:
/**
* 加載指定插件或指定目錄下的所有插件
* <p>
* 都使用文件名作為Id
*
* @param pluginSrcDirFile - apk或apk目錄
* @return 插件集合
* @throws Exception
*/
public Collection<PlugInfo> loadPlugin(final File pluginSrcDirFile)
throws Exception {
if (pluginSrcDirFile == null || !pluginSrcDirFile.exists()) {
Trace.store("invalidate plugin file or Directory :"
+ pluginSrcDirFile);
return null;
}
if (pluginSrcDirFile.isFile()) {
PlugInfo one = buildPlugInfo(pluginSrcDirFile, null, null);
if (one != null) {
savePluginToMap(one);
}
return Collections.singletonList(one);
}
// synchronized (this) {
// pluginPkgToInfoMap.clear();
// }
File[] pluginApkFiles = pluginSrcDirFile.listFiles(this);
if (pluginApkFiles == null || pluginApkFiles.length == 0) {
throw new FileNotFoundException("could not find plugins in:"
+ pluginSrcDirFile);
}
for (File pluginApk : pluginApkFiles) {
try {
PlugInfo plugInfo = buildPlugInfo(pluginApk, null, null);
if (plugInfo != null) {
savePluginToMap(plugInfo);
}
} catch (Throwable e) {
e.printStackTrace();
}
}
return pluginPkgToInfoMap.values();
}
在 loadPlugin
代碼的注釋中,我們可以知道加載的插件可以是一個也可以是一個文件夾下的多個者蠕。因?yàn)闀鶕?jù)傳入的 pluginSrcDirFile
參數(shù)去判斷是文件還是文件夾窃祝,其實(shí)道理都是一樣的,無非就是多了一個 for 循環(huán)而已蠢棱。在這里要注意一下锌杀,PluginManager 是實(shí)現(xiàn)了 FileFilter 接口的,因此在加載多個插件時泻仙,調(diào)用 listFiles(this)
會過濾當(dāng)前文件夾下非 apk 文件:
@Override
public boolean accept(File pathname) {
return !pathname.isDirectory() && pathname.getName().endsWith(".apk");
}
好了糕再,我們在 loadPlugin()
的代碼中會注意到,無論是加載單個插件還是多個插件都會調(diào)用 buildPlugInfo()
方法玉转。顧名思義突想,就是根據(jù)傳入的插件文件去加載:
private PlugInfo buildPlugInfo(File pluginApk, String pluginId,
String targetFileName) throws Exception {
PlugInfo info = new PlugInfo();
info.setId(pluginId == null ? pluginApk.getName() : pluginId);
File privateFile = new File(dexInternalStoragePath,
targetFileName == null ? pluginApk.getName() : targetFileName);
info.setFilePath(privateFile.getAbsolutePath());
//Copy Plugin to Private Dir
if (!pluginApk.getAbsolutePath().equals(privateFile.getAbsolutePath())) {
copyApkToPrivatePath(pluginApk, privateFile);
}
String dexPath = privateFile.getAbsolutePath();
//Load Plugin Manifest
PluginManifestUtil.setManifestInfo(context, dexPath, info);
//Load Plugin Res
try {
AssetManager am = AssetManager.class.newInstance();
am.getClass().getMethod("addAssetPath", String.class)
.invoke(am, dexPath);
info.setAssetManager(am);
Resources hotRes = context.getResources();
Resources res = new Resources(am, hotRes.getDisplayMetrics(),
hotRes.getConfiguration());
info.setResources(res);
} catch (Exception e) {
throw new RuntimeException("Unable to create Resources&Assets for "
+ info.getPackageName() + " : " + e.getMessage());
}
//Load classLoader for Plugin
PluginClassLoader pluginClassLoader = new PluginClassLoader(info, dexPath, dexOutputPath
, getPluginLibPath(info).getAbsolutePath(), pluginParentClassLoader);
info.setClassLoader(pluginClassLoader);
ApplicationInfo appInfo = info.getPackageInfo().applicationInfo;
Application app = makeApplication(info, appInfo);
attachBaseContext(info, app);
info.setApplication(app);
Trace.store("Build pluginInfo => " + info);
return info;
}
從上面的代碼中看到, buildPlugInfo()
方法中做的大致有四步:
- 復(fù)制插件 apk 到指定目錄;
- 加載插件 apk 的 AndroidManifest.xml 文件猾担;
- 加載插件 apk 中的資源文件袭灯;
- 為插件 apk 設(shè)置 ClassLoader。
復(fù)制插件 apk 到指定目錄
下面我們慢慢來分析绑嘹,第一步稽荧,會把傳入的插件 apk 復(fù)制到 dexInternalStoragePath
路徑下,也就是之前在 PluginManager 的構(gòu)造器中所指定的目錄工腋。這部分的代碼很簡單姨丈,就省略了。
加載插件 apk 的 AndroidManifest.xml 文件
第二步擅腰,根據(jù)代碼可知蟋恬,會使用 PluginManifestUtil.setManifestInfo()
去加載 AndroidManifest 里的信息,那就去看下相關(guān)的代碼實(shí)現(xiàn):
public static void setManifestInfo(Context context, String apkPath, PlugInfo info)
throws XmlPullParserException, IOException {
// 得到AndroidManifest文件
ZipFile zipFile = new ZipFile(new File(apkPath), ZipFile.OPEN_READ);
ZipEntry manifestXmlEntry = zipFile.getEntry(XmlManifestReader.DEFAULT_XML);
// 解析AndroidManifest文件
String manifestXML = XmlManifestReader.getManifestXMLFromAPK(zipFile,
manifestXmlEntry);
// 創(chuàng)建相應(yīng)的packageInfo
PackageInfo pkgInfo = context.getPackageManager()
.getPackageArchiveInfo(
apkPath,
PackageManager.GET_ACTIVITIES
| PackageManager.GET_RECEIVERS//
| PackageManager.GET_PROVIDERS//
| PackageManager.GET_META_DATA//
| PackageManager.GET_SHARED_LIBRARY_FILES//
| PackageManager.GET_SERVICES//
// | PackageManager.GET_SIGNATURES//
);
if (pkgInfo == null || pkgInfo.activities == null) {
throw new XmlPullParserException("No any activity in " + apkPath);
}
pkgInfo.applicationInfo.publicSourceDir = apkPath;
pkgInfo.applicationInfo.sourceDir = apkPath;
// 得到libDir趁冈,加載.so文件
File libDir = PluginManager.getSingleton().getPluginLibPath(info);
try {
if (extractLibFile(zipFile, libDir)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
pkgInfo.applicationInfo.nativeLibraryDir = libDir.getAbsolutePath();
}
}
} finally {
zipFile.close();
}
info.setPackageInfo(pkgInfo);
setAttrs(info, manifestXML);
}
在代碼中歼争,一開始會通過 apk 得到 AndroidManifest.xml 文件。然后使用 XmlManifestReader
去讀取 AndroidManifest 中的信息渗勘。在 XmlManifestReader
中會使用 XmlPullParser
去解析 xml 沐绒, XmlManifestReader
相關(guān)的源碼就不貼出來了,想要進(jìn)一步了解的童鞋可以自己去看旺坠,點(diǎn)擊這里查看 XmlManifestReader 源碼洒沦。接下來根據(jù) apkPath
得到相應(yīng)的 pkgInfo
,并且若有 libDir 會去加載相應(yīng)的 .so 文件价淌。最后會調(diào)用 setAttrs(info, manifestXML)
這個方法:
private static void setAttrs(PlugInfo info, String manifestXML)
throws XmlPullParserException, IOException {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
XmlPullParser parser = factory.newPullParser();
parser.setInput(new StringReader(manifestXML));
int eventType = parser.getEventType();
String namespaceAndroid = null;
do {
switch (eventType) {
case XmlPullParser.START_DOCUMENT: {
break;
}
case XmlPullParser.START_TAG: {
String tag = parser.getName();
if (tag.equals("manifest")) {
namespaceAndroid = parser.getNamespace("android");
} else if ("activity".equals(parser.getName())) {
addActivity(info, namespaceAndroid, parser);
} else if ("receiver".equals(parser.getName())) {
addReceiver(info, namespaceAndroid, parser);
} else if ("service".equals(parser.getName())) {
addService(info, namespaceAndroid, parser);
}else if("application".equals(parser.getName())){
parseApplicationInfo(info, namespaceAndroid, parser);
}
break;
}
case XmlPullParser.END_TAG: {
break;
}
}
eventType = parser.next();
} while (eventType != XmlPullParser.END_DOCUMENT);
}
在 setAttrs(PlugInfo info, String manifestXML)
方法中,使用了 pull 方式去解析 manifest 瞒津,并且根據(jù) activity 蝉衣、 recevicer 、 service 等調(diào)用不同的 addXxxx()
方法巷蚪。這些方法其實(shí)本質(zhì)上是一樣的病毡,我們就挑 addActivity()
方法來看一下:
private static void addActivity(PlugInfo info, String namespace,
XmlPullParser parser) throws XmlPullParserException, IOException {
int eventType = parser.getEventType();
String activityName = parser.getAttributeValue(namespace, "name");
String packageName = info.getPackageInfo().packageName;
activityName = getName(activityName, packageName);
ResolveInfo act = new ResolveInfo();
act.activityInfo = info.findActivityByClassNameFromPkg(activityName);
do {
switch (eventType) {
case XmlPullParser.START_TAG: {
String tag = parser.getName();
if ("intent-filter".equals(tag)) {
if (act.filter == null) {
act.filter = new IntentFilter();
}
} else if ("action".equals(tag)) {
String actionName = parser.getAttributeValue(namespace,
"name");
act.filter.addAction(actionName);
} else if ("category".equals(tag)) {
String category = parser.getAttributeValue(namespace,
"name");
act.filter.addCategory(category);
} else if ("data".equals(tag)) {
// TODO parse data
}
break;
}
}
eventType = parser.next();
} while (!"activity".equals(parser.getName()));
//
info.addActivity(act);
}
addActivity()
代碼中的邏輯比較簡單,就是創(chuàng)建一個 ResolveInfo
類的對象 act
屁柏,把 Activity 相關(guān)的信息全部裝進(jìn)去啦膜,比如有 ActivityInfo 、 intent-filter 等淌喻。最后把 act
添加到 info
中僧家。其他的 addReceiver
和 addService
也是同一個邏輯。而 parseApplicationInfo
也是把 Application 的相關(guān)信息封裝到 info
中裸删。感興趣的同學(xué)可以看一下相關(guān)的源碼八拱,點(diǎn)擊這里查看。到這里,就把加載插件中 AndroidManifest.xml 的代碼分析完了肌稻。
加載插件 apk 中的資源文件
再回到 buildPlugInfo()
的代碼中去清蚀,接下來就是第三步,加載插件中的資源文件了爹谭。
為了方便枷邪,我們把相關(guān)的代碼復(fù)制到這里來:
try {
AssetManager am = AssetManager.class.newInstance();
am.getClass().getMethod("addAssetPath", String.class)
.invoke(am, dexPath);
info.setAssetManager(am);
Resources hotRes = context.getResources();
Resources res = new Resources(am, hotRes.getDisplayMetrics(),
hotRes.getConfiguration());
info.setResources(res);
} catch (Exception e) {
throw new RuntimeException("Unable to create Resources&Assets for "
+ info.getPackageName() + " : " + e.getMessage());
}
首先通過反射得到 AssetManager
的對象 am
,然后通過反射其 addAssetPath
方法傳入 dexPath
參數(shù)來加載插件的資源文件诺凡,接下來就得到相應(yīng)插件的 Resource
對象 res
了东揣。這樣就實(shí)現(xiàn)了訪問插件中的資源文件了。那么到底 addAssetPath
這個方法有什么魔力呢绑洛?我們查看一下 Android 相關(guān)的源代碼(android/content/res/AssetManager.java):
/**
* Add an additional set of assets to the asset manager. This can be
* either a directory or ZIP file. Not for use by applications. Returns
* the cookie of the added asset, or 0 on failure.
* {@hide}
*/
public final int addAssetPath(String path) {
synchronized (this) {
int res = addAssetPathNative(path);
makeStringBlocks(mStringBlocks);
return res;
}
}
查看方法的注釋我們知道救斑,這個 addAssetPath()
方法就是用來添加額外的資源文件到 AssetManager 中去的,但是已經(jīng)被 hide 了真屯。所以我們只能通過反射的方式來執(zhí)行了脸候。這樣就解決了加載插件中的資源文件的問題了。
其實(shí)绑蔫,大多數(shù)插件化框架都是通過反射 addAssetPath()
的方式來解決加載插件資源問題运沦,基本上已經(jīng)成為了標(biāo)準(zhǔn)方案了。
為插件 apk 設(shè)置 ClassLoader
終于到了最后一個步驟了配深,如何為插件設(shè)置 ClassLoader 呢携添?其實(shí)解決的方案就是通過 DexClassLoader
。我們先來看 buildPlugInfo()
中的代碼:
PluginClassLoader pluginClassLoader = new PluginClassLoader(info, dexPath, dexOutputPath
, getPluginLibPath(info).getAbsolutePath(), pluginParentClassLoader);
info.setClassLoader(pluginClassLoader);
ApplicationInfo appInfo = info.getPackageInfo().applicationInfo;
Application app = makeApplication(info, appInfo);
attachBaseContext(info, app);
info.setApplication(app);
Trace.store("Build pluginInfo => " + info);
在代碼中創(chuàng)建了 pluginClassLoader
對象篓叶,而 PluginClassLoader
正是繼承自 DexClassLoader
的烈掠,將 dexPath
、 dexOutputPath
等參數(shù)傳入后缸托,就可以去加載插件中的類了左敌。 基本上所有的插件化框架都是通過 DexClassLoder
來作為插件 apk 的 ClassLoader 的。
之后在 makeApplication(info, appInfo)
就使用 PluginClassLoader
利用反射去創(chuàng)建插件的 Application 了:
/**
* 構(gòu)造插件的Application
*
* @param plugInfo 插件信息
* @param appInfo 插件ApplicationInfo
* @return 插件App
*/
private Application makeApplication(PlugInfo plugInfo, ApplicationInfo appInfo) {
String appClassName = appInfo.className;
if (appClassName == null) {
//Default Application
appClassName = Application.class.getName();
}
try {
return (Application) plugInfo.getClassLoader().loadClass(appClassName).newInstance();
} catch (Throwable e) {
throw new RuntimeException("Unable to create Application for "
+ plugInfo.getPackageName() + ": "
+ e.getMessage());
}
}
創(chuàng)建完插件的 Application 之后俐镐, 再調(diào)用 attachBaseContext(info, app)
方法把 Application 的 mBase 屬性替換成 PluginContext
對象矫限,PluginContext
類繼承自 LayoutInflaterProxyContext ,里面封裝了一些插件的信息佩抹,比如有插件資源叼风、插件 ClassLoader 等。值得一提的是棍苹,在插件中 PluginContext 可以得到“宿主”的 Context 无宿,也就是所謂的“破殼”。具體可查看 PluginContext 的源碼廊勃。
private void attachBaseContext(PlugInfo info, Application app) {
try {
Field mBase = ContextWrapper.class.getDeclaredField("mBase");
mBase.setAccessible(true);
mBase.set(app, new PluginContext(context.getApplicationContext(), info));
} catch (Throwable e) {
e.printStackTrace();
}
}
講到這里基本上把 buildPlugInfo()
中的邏輯講完了懈贺, pluginManager.loadPlugin
剩下的代碼都比較簡單经窖,相信大家一看就懂了。
0x03 PluginManager.startActivity
startActivity
在加載好插件 apk 之后梭灿,就可以使用插件了画侣。和平常無異,我們使用 PluginManager.startActivity
來啟動插件中的 Activity 堡妒。其實(shí) PluginManager
有很多 startActivity 的方法:
但是終于都會調(diào)用 startActivity(Context from, PlugInfo plugInfo, ActivityInfo activityInfo, Intent intent)
這個方法:
private DynamicActivitySelector activitySelector = DefaultActivitySelector.getDefault();
...
/**
* 啟動插件的指定Activity
*
* @param from fromContext
* @param plugInfo 插件信息
* @param activityInfo 要啟動的插件activity信息
* @param intent 通過此Intent可以向插件傳參, 可以為null
*/
public void startActivity(Context from, PlugInfo plugInfo, ActivityInfo activityInfo, Intent intent) {
if (activityInfo == null) {
throw new ActivityNotFoundException("Cannot find ActivityInfo from plugin, could you declare this Activity in plugin?");
}
if (intent == null) {
intent = new Intent();
}
CreateActivityData createActivityData = new CreateActivityData(activityInfo.name, plugInfo.getPackageName());
intent.setClass(from, activitySelector.selectDynamicActivity(activityInfo));
intent.putExtra(Globals.FLAG_ACTIVITY_FROM_PLUGIN, createActivityData);
from.startActivity(intent);
}
我們先來看代碼配乱, CreateActivityData
類是用來存儲一個將要創(chuàng)建的插件 Activity 的數(shù)據(jù),實(shí)現(xiàn)了 Serializable
接口皮迟,因此可以被序列化搬泥。總之伏尼, CreateActivityData
會存儲將要創(chuàng)建的插件 Activity 的類名和包名忿檩,再把它放入 intent
中。之后爆阶, intent
設(shè)置要創(chuàng)建的 Activity 為 activitySelector.selectDynamicActivity(activityInfo)
燥透,activitySelector
是 DefaultActivitySelector
類的對象,那么這 DefaultActivitySelector
到底是什么東西呢辨图?一起來看看 DefaultActivitySelector
的源碼:
public class DefaultActivitySelector implements DynamicActivitySelector {
private static DynamicActivitySelector DEFAULT = new DefaultActivitySelector();
@Override
public Class<? extends Activity> selectDynamicActivity(ActivityInfo pluginActivityInfo) {
return DynamicActivity.class;
}
public static DynamicActivitySelector getDefault() {
return DEFAULT;
}
}
其實(shí)很簡單班套,不管傳入的 pluginActivityInfo
參數(shù)是什么,返回的都是 DynamicActivity.class
故河。也就是我們在介紹 android-pluginmgr 簡單用法時吱韭,第二步在 AndroidManifest 中注冊的那個 DynamicActivity
。
看到這里的代碼鱼的,我們一定可以猜到什么理盆。因?yàn)檫@里的 intent
中設(shè)置即將啟動的 Activity 仍然為 DynamicActivity
,所以在后面的代碼中肯定會去動態(tài)地替換掉 DynamicActivity
凑阶。
動態(tài)Hook
之前在 PluginManager.init(this) 源碼這一小節(jié)中介紹了熏挎,當(dāng)前 ActivityThread
的 Instrumentation
已經(jīng)被替換成了 PluginInstrumentation
。所以在創(chuàng)建 Activity 的時候會去調(diào)用 PluginInstrumentation
里面的方法晌砾。這樣就可以在里面“做手腳”,實(shí)現(xiàn)了動態(tài)去替換 Activity 的思路烦磁。我們先來看一下 PluginInstrumentation
中部分方法的源碼:
private void replaceIntentTargetIfNeed(Context from, Intent intent)
{
if (!intent.hasExtra(Globals.FLAG_ACTIVITY_FROM_PLUGIN) && currentPlugin != null)
{
ComponentName componentName = intent.getComponent();
if (componentName != null)
{
String pkgName = componentName.getPackageName();
String activityName = componentName.getClassName();
if (pkgName != null)
{
CreateActivityData createActivityData = new CreateActivityData(activityName, currentPlugin.getPackageName());
ActivityInfo activityInfo = currentPlugin.findActivityByClassName(activityName);
if (activityInfo != null) {
intent.setClass(from, PluginManager.getSingleton().getActivitySelector().selectDynamicActivity(activityInfo));
intent.putExtra(Globals.FLAG_ACTIVITY_FROM_PLUGIN, createActivityData);
intent.setExtrasClassLoader(currentPlugin.getClassLoader());
}
}
}
}
}
@Override
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Fragment fragment, Intent intent, int requestCode)
{
replaceIntentTargetIfNeed(who, intent);
return super.execStartActivity(who, contextThread, token, fragment, intent, requestCode);
}
@Override
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Fragment fragment, Intent intent, int requestCode, Bundle options)
{
replaceIntentTargetIfNeed(who, intent);
return super.execStartActivity(who, contextThread, token, fragment, intent, requestCode, options);
}
@Override
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode)
{
replaceIntentTargetIfNeed(who, intent);
return super.execStartActivity(who, contextThread, token, target, intent, requestCode);
}
@Override
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options)
{
replaceIntentTargetIfNeed(who, intent);
return super.execStartActivity(who, contextThread, token, target, intent, requestCode, options);
}
我們發(fā)現(xiàn)养匈,在所有的 execStartActivity()
方法執(zhí)行前,都加上了 replaceIntentTargetIfNeed(Context from, Intent intent)
這個方法都伪,在方法里面 intent.setClass
中設(shè)置的還是 DynamicActivity.class
呕乎,把插件信息都檢查了一遍。
在這之后陨晶,會去執(zhí)行 PluginInstrumentation.newActivity
方法來創(chuàng)建即將要啟動的Activity 猬仁。也正是在這里帝璧,對之前的 DynamicActivity
進(jìn)行 Hook ,達(dá)到啟動插件 Activity 的目的湿刽。
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException
{
CreateActivityData activityData = (CreateActivityData) intent.getSerializableExtra(Globals.FLAG_ACTIVITY_FROM_PLUGIN);
//如果activityData存在,那么說明將要創(chuàng)建的是插件Activity
if (activityData != null && PluginManager.getSingleton().getPlugins().size() > 0) {
//這里找不到插件信息就會拋異常的,不用擔(dān)心空指針
PlugInfo plugInfo;
try
{
Log.d(getClass().getSimpleName(), "+++ Start Plugin Activity => " + activityData.pluginPkg + " / " + activityData.activityName);
// 得到插件信息類
plugInfo = PluginManager.getSingleton().tryGetPluginInfo(activityData.pluginPkg);
// 在該方法中會調(diào)用插件的Application.onCreate()
plugInfo.ensureApplicationCreated();
}
catch (PluginNotFoundException e)
{
PluginManager.getSingleton().dump();
throw new IllegalAccessException("Cannot get plugin Info : " + activityData.pluginPkg);
}
if (activityData.activityName != null)
{
// 在這里替換了className的烁,變成了插件Activity的className
className = activityData.activityName;
// 替換classloader
cl = plugInfo.getClassLoader();
}
}
return super.newActivity(cl, className, intent);
}
在 newActivity()
方法中,先拿到了插件信息 plugInfo
诈闺,然后會確保插件的 Application
已經(jīng)創(chuàng)建渴庆。然后在第25行會去替換掉 className
和 cl
。這樣雅镊,原本要創(chuàng)建的是 DynamicActivity
就變成了插件的 Activity
了襟雷,從而實(shí)現(xiàn)了創(chuàng)建插件 Activity 的目的,并且這個 Activity 是真實(shí)的 Activity 組件仁烹,具備生命周期的耸弄。
也許有童鞋會有疑問,如果直接在 startActivity
中設(shè)置要啟動的 Activity 為插件 Activity 卓缰,這樣不行嗎计呈?答案是肯定的,因?yàn)檫@樣就會拋出一個異常:ActivityNotFoundException:...have you declared this activity in your AndroidManifest.xml?
我相信這個異常大家很熟悉的吧僚饭,在剛開始學(xué)習(xí) Android 時震叮,大家都會犯的一個錯誤。所以鳍鸵,我想我們也明白了為什么要花這么大的一個功夫去動態(tài)地替換要創(chuàng)建的 Activity 苇瓣,就是為了繞過這個 ActivityNotFoundException
異常,達(dá)到去“欺騙” Android 系統(tǒng)的效果偿乖。
既然創(chuàng)建好了击罪,那么就來看看 PluginInstrumentation
里調(diào)用相關(guān)生命周期的方法:
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
lookupActivityInPlugin(activity);
if (currentPlugin != null) {
//初始化插件Activity
Context baseContext = activity.getBaseContext();
PluginContext pluginContext = new PluginContext(baseContext, currentPlugin);
try {
try {
//在許多設(shè)備上,Activity自身hold資源
Reflect.on(activity).set("mResources", pluginContext.getResources());
} catch (Throwable ignored) {
}
Field field = ContextWrapper.class.getDeclaredField("mBase");
field.setAccessible(true);
field.set(activity, pluginContext);
try {
Reflect.on(activity).set("mApplication", currentPlugin.getApplication());
} catch (ReflectException e) {
Trace.store("Application not inject success into : " + activity);
}
} catch (Throwable e) {
e.printStackTrace();
}
ActivityInfo activityInfo = currentPlugin.findActivityByClassName(activity.getClass().getName());
if (activityInfo != null) {
//根據(jù)AndroidManifest.xml中的參數(shù)設(shè)置Theme
int resTheme = activityInfo.getThemeResource();
if (resTheme != 0) {
boolean hasNotSetTheme = true;
try {
Field mTheme = ContextThemeWrapper.class
.getDeclaredField("mTheme");
mTheme.setAccessible(true);
hasNotSetTheme = mTheme.get(activity) == null;
} catch (Exception e) {
e.printStackTrace();
}
if (hasNotSetTheme) {
changeActivityInfo(activityInfo, activity);
activity.setTheme(resTheme);
}
}
}
// 如果是三星手機(jī)贪薪,則使用包裝的LayoutInflater替換原LayoutInflater
// 這款手機(jī)在解析內(nèi)置的布局文件時有各種錯誤
if (android.os.Build.MODEL.startsWith("GT")) {
Window window = activity.getWindow();
Reflect windowRef = Reflect.on(window);
try {
LayoutInflater originInflater = window.getLayoutInflater();
if (!(originInflater instanceof LayoutInflaterWrapper)) {
windowRef.set("mLayoutInflater", new LayoutInflaterWrapper(originInflater));
}
} catch (Throwable e) {
e.printStackTrace();
}
}
}
super.callActivityOnCreate(activity, icicle);
}
/**
* 檢查跳轉(zhuǎn)目標(biāo)是不是來自插件
*
* @param activity Activity
*/
private void lookupActivityInPlugin(Activity activity) {
ClassLoader classLoader = activity.getClass().getClassLoader();
if (classLoader instanceof PluginClassLoader) {
currentPlugin = ((PluginClassLoader) classLoader).getPlugInfo();
} else {
currentPlugin = null;
}
}
在 callActivityOnCreate()
中先去檢查了創(chuàng)建的 Activity 是否來自于插件媳禁。如果是,那么會給 Activity 設(shè)置 Context 画切、 設(shè)置主題等竣稽;如果不是,則直接執(zhí)行父類方法霍弹。在 super.callActivityOnCreate(activity, icicle)
中會去調(diào)用 Activity.onCreate()
方法毫别。其他的生命周期方法作者沒有特殊處理,這里就不講了典格。
分析到這岛宦,我們終于把 android-pluginmgr 插件化實(shí)現(xiàn)的方案完整地梳理了一遍。當(dāng)然耍缴,不同的插件化框架會有不同的實(shí)現(xiàn)方案砾肺,具體的仍然需要自己專心研究挽霉。另外我們發(fā)現(xiàn)該框架還沒有實(shí)現(xiàn)啟動插件 Service 的功能,如果想要了解变汪,可以參考下其他插件化框架侠坎。
0x04 總結(jié)
上面亂七八糟的流程講了一遍,可能還有一些童鞋不太懂疫衩,所以在這里給出一張 android-pluginmgr 的流程圖硅蹦。不懂的童鞋可以根據(jù)這張圖再好好看一下源碼,相信你會恍然大悟的闷煤。
最后童芹,如果對本文哪里有疑問的童鞋,歡迎留言鲤拿,一起交流假褪。