插件化框架android-pluginmgr全解析
0x00 前言:插件化的介紹
閱讀須知:閱讀本文的童鞋最好是有過插件化框架使用經歷或者對插件化框架有過了解的。前方高能,大牛繞道。
最近一直在關注 Android 插件化方面,所以今天的主題就確定是 Android 中比較熱門的“插件化”了。所謂的插件化就是下載 apk 到指定目錄,不需要安裝該 apk ,就能利用某個已安裝的 apk (即“宿主”)調用起該未安裝 apk 中的 Activity 、Service 等組件(即“插件”)。
Android 插件化的發展到目前為止也有一段時間了,從一開始任主席的 dynamic-load-apk 到今天要分析的 android-pluginmgr 再到360的 DroidPlugin ,也代表著插件化的思想從頂部的應用層向下到 Framework 層滲入。最早插件化的思想是 dynamic-load-apk 實現的, dynamic-load-apk 在“宿主” ProxyActivity 的生命周期中利用接口回調了“插件” PluginActivity 的“生命周期”,以此來間接實現 PluginActivity 的“生命周期”。也就是說,其實插件中的 “PluginActivity” 并不具有真正 Activity 的性質,實質就是一個普通類,只是利用接口回調了類中的生命周期方法而已。比接口回調更好的方案就是利用 ActivityThread 、Instrumentation 等去動態地 Hook 即將創建的 ProxyActivity ,也就是說表面上創建的是 ProxyActivity ,其實實際上是創建了 PluginActivity 。這種思想相比于 dynamic-load-apk 而言,插件中 Activity 已經是實質上的 Activity ,具備了生命周期方法。今天我們要解析的 android-pluginmgr 插件化框架就是基于這種思想的。最后就是像 DroidPlugin 這種插件化框架,改動了 ActivityManagerService 、 PackageManagerService 等 Android 源碼,以此來實現插件化。總之,并沒有哪種插件化框架是最好的,一切都是要根據自身實際情況而決定的。
熟悉插件化的童鞋都知道,插件化要解決的有三個基本難題:
-
插件中 ClassLoader 的問題;
-
插件中的資源文件訪問問題;
-
插件中 Activity 組件的生命周期問題。
基本上,解決了上面三個問題,就可以算是一個合格的插件化框架了。但是要注意的是,插件化遠遠不止這三個問題,比如還有插件中 .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);
基本的用法就像以上這五步,另外需要注意的是,“插件”中所需要的權限都要在“宿主”的 AndroidManifest.xml 中進行申明。
PluginManager.init(this)源碼
下面我們來分析下 PluginManager.init(this); 的源碼:
/**
- 初始化插件管理器,請不要傳入易變的Context,那將造成內存泄露!
*
- @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);
}</code></pre>
可以看到在 init(Context context) 中主要創建了一個 SINGLETON 單例,所以我們就要追蹤 PluginManager 構造器的源碼了:
/**
- 插件管理器私有構造器
*
- @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);
}
}</code></pre>
在構造器中做的事情有點多,我們一步步來看下。一開始得到插件 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";</code></pre>
但是根據常量定義的名稱來看,總感覺作者在 context.getDir() 時把這兩個路徑搞反了 \(╯-╰)/。
之后在構造器中創建了 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);
}
}</code></pre>
DelegateActivityThread 類的主要作用就是使用反射包裝了當前的 ActivityThread ,并且一開始在 DelegateActivityThread 中使用 PluginInstrumentation 替換原始的 Instrumentation 。其實 Activity 的生命周期調用都是通過 Instrumentation 來完成的。我們來看看 PluginInstrumentation 的構造器相關代碼:
public class PluginInstrumentation extends DelegateInstrumentation
{
/**
* 當前正在運行的插件
*/
private PlugInfo currentPlugin;
/**
* @param mBase 真正的Instrumentation
*/
public PluginInstrumentation(Instrumentation mBase) {
super(mBase);
}
...
}</code></pre>
可以看到 PluginInstrumentation 是繼承自 DelegateInstrumentation 類的,而 DelegateInstrumentation 本質上就是 Instrumentation 。 DelegateInstrumentation 類中的方法都是直接調用 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();
}
...
}</code></pre>
好了,在 PluginManager.init() 方法中大概做的就是這些邏輯了。
0x02 PluginManager.loadPlugin
看完了上面的 PluginManager.init() 之后,下一步就是調用 pluginManager.loadPlugin 去加載插件。一起來看看相關源碼:
/**
- 加載指定插件或指定目錄下的所有插件
- <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();
}</code></pre>
在 loadPlugin 代碼的注釋中,我們可以知道加載的插件可以是一個也可以是一個文件夾下的多個。因為會根據傳入的 pluginSrcDirFile 參數去判斷是文件還是文件夾,其實道理都是一樣的,無非就是多了一個 for 循環而已。在這里要注意一下,PluginManager 是實現了 FileFilter 接口的,因此在加載多個插件時,調用 listFiles(this) 會過濾當前文件夾下非 apk 文件:
@Override
public boolean accept(File pathname) {
return !pathname.isDirectory() && pathname.getName().endsWith(".apk");
}
好了,我們在 loadPlugin() 的代碼中會注意到,無論是加載單個插件還是多個插件都會調用 buildPlugInfo() 方法。顧名思義,就是根據傳入的插件文件去加載:
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;
}</code></pre>
從上面的代碼中看到, buildPlugInfo() 方法中做的大致有四步:
- 復制插件 apk 到指定目錄;
- 加載插件 apk 的 AndroidManifest.xml 文件;
- 加載插件 apk 中的資源文件;
- 為插件 apk 設置 ClassLoader。
復制插件 apk 到指定目錄
下面我們慢慢來分析,第一步,會把傳入的插件 apk 復制到 dexInternalStoragePath 路徑下,也就是之前在 PluginManager 的構造器中所指定的目錄。這部分的代碼很簡單,就省略了。
加載插件 apk 的 AndroidManifest.xml 文件
第二步,根據代碼可知,會使用 PluginManifestUtil.setManifestInfo() 去加載 AndroidManifest 里的信息,那就去看下相關的代碼實現:
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);
// 創建相應的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);
}</code></pre>
在代碼中,一開始會通過 apk 得到 AndroidManifest.xml 文件。然后使用 XmlManifestReader 去讀取 AndroidManifest 中的信息。在 XmlManifestReader 中會使用 XmlPullParser 去解析 xml , XmlManifestReader 相關的源碼就不貼出來了,想要進一步了解的童鞋可以自己去看, 點擊這里查看 XmlManifestReader 源碼 。接下來根據 apkPath 得到相應的 pkgInfo ,并且若有 libDir 會去加載相應的 .so 文件。最后會調用 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);
}</code></pre>
在 setAttrs(PlugInfo info, String manifestXML) 方法中,使用了 pull 方式去解析 manifest ,并且根據 activity 、 recevicer 、 service 等調用不同的 addXxxx() 方法。這些方法其實本質上是一樣的,我們就挑 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);
}</code></pre>
addActivity() 代碼中的邏輯比較簡單,就是創建一個 ResolveInfo 類的對象 act ,把 Activity 相關的信息全部裝進去,比如有 ActivityInfo 、 intent-filter 等。最后把 act 添加到 info 中。其他的 addReceiver 和 addService 也是同一個邏輯。而 parseApplicationInfo 也是把 Application 的相關信息封裝到 info 中。到這里,就把加載插件中 AndroidManifest.xml 的代碼分析完了。
加載插件 apk 中的資源文件
再回到 buildPlugInfo() 的代碼中去,接下來就是第三步,加載插件中的資源文件了。
為了方便,我們把相關的代碼復制到這里來:
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());
}</code></pre>
首先通過反射得到 AssetManager 的對象 am ,然后通過反射其 addAssetPath 方法傳入 dexPath 參數來加載插件的資源文件,接下來就得到相應插件的 Resource 對象 res 了。這樣就實現了訪問插件中的資源文件了。那么到底 addAssetPath 這個方法有什么魔力呢?
/**
- 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;
}
}</code></pre>
查看方法的注釋我們知道,這個 addAssetPath() 方法就是用來添加額外的資源文件到 AssetManager 中去的,但是已經被 hide 了。所以我們只能通過反射的方式來執行了。這樣就解決了加載插件中的資源文件的問題了。
其實,大多數插件化框架都是通過反射 addAssetPath() 的方式來解決加載插件資源問題,基本上已經成為了標準方案了。
為插件 apk 設置 ClassLoader
終于到了最后一個步驟了,如何為插件設置 ClassLoader 呢?其實解決的方案就是通過 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);</code></pre>
在代碼中創建了 pluginClassLoader 對象,而 PluginClassLoader 正是繼承自 DexClassLoader 的,將 dexPath 、 dexOutputPath 等參數傳入后,就可以去加載插件中的類了。 基本上所有的插件化框架都是通過 DexClassLoder 來作為插件 apk 的 ClassLoader 的。
之后在 makeApplication(info, appInfo) 就使用 PluginClassLoader 利用反射去創建插件的 Application 了:
/**
- 構造插件的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());
}
}</code></pre>
創建完插件的 Application 之后, 再調用 attachBaseContext(info, app) 方法把 Application 的 mBase 屬性替換成 PluginContext 對象, PluginContext 類繼承自 LayoutInflaterProxyContext ,里面封裝了一些插件的信息,比如有插件資源、插件 ClassLoader 等。值得一提的是,在插件中 PluginContext 可以得到“宿主”的 Context ,也就是所謂的“破殼”。
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();
}
}</code></pre>
講到這里基本上把 buildPlugInfo() 中的邏輯講完了, pluginManager.loadPlugin 剩下的代碼都比較簡單,相信大家一看就懂了。
0x03 PluginManager.startActivity
startActivity
在加載好插件 apk 之后,就可以使用插件了。和平常無異,我們使用 PluginManager.startActivity 來啟動插件中的 Activity 。其實 PluginManager 有很多 startActivity 的方法:

startActivity截圖
但是終于都會調用 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);
}</code></pre>
我們先來看代碼, CreateActivityData 類是用來存儲一個將要創建的插件 Activity 的數據,實現了 Serializable 接口,因此可以被序列化。總之, CreateActivityData 會存儲將要創建的插件 Activity 的類名和包名,再把它放入 intent 中。之后, intent 設置要創建的 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;
}
}</code></pre>
其實很簡單,不管傳入的 pluginActivityInfo 參數是什么,返回的都是 DynamicActivity.class 。也就是我們在介紹 android-pluginmgr 簡單用法 時,第二步在 AndroidManifest 中注冊的那個 DynamicActivity 。
看到這里的代碼,我們一定可以猜到什么。因為這里的 intent 中設置即將啟動的 Activity 仍然為 DynamicActivity ,所以在后面的代碼中肯定會去動態地替換掉 DynamicActivity 。
動態Hook
之前在 PluginManager.init(this) 源碼 這一小節中介紹了,當前 ActivityThread 的 Instrumentation 已經被替換成了 PluginInstrumentation 。所以在創建 Activity 的時候會去調用 PluginInstrumentation 里面的方法。這樣就可以在里面“做手腳”,實現了動態去替換 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);
}</code></pre>
我們發現,在所有的 execStartActivity() 方法執行前,都加上了 replaceIntentTargetIfNeed(Context from, Intent intent) 這個方法,在方法里面 intent.setClass 中設置的還是 DynamicActivity.class ,把插件信息都檢查了一遍。
在這之后,會去執行 PluginInstrumentation.newActivity 方法來創建即將要啟動的Activity 。也正是在這里,對之前的 DynamicActivity 進行 Hook ,達到啟動插件 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存在,那么說明將要創建的是插件Activity
if (activityData != null && PluginManager.getSingleton().getPlugins().size() > 0) {
//這里找不到插件信息就會拋異常的,不用擔心空指針
PlugInfo plugInfo;
try
{
Log.d(getClass().getSimpleName(), "+++ Start Plugin Activity => " + activityData.pluginPkg + " / " + activityData.activityName);
// 得到插件信息類
plugInfo = PluginManager.getSingleton().tryGetPluginInfo(activityData.pluginPkg);
// 在該方法中會調用插件的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 已經創建。然后在第25行會去替換掉 className 和 cl 。這樣,原本要創建的是 DynamicActivity 就變成了插件的 Activity 了,從而實現了創建插件 Activity 的目的,并且這個 Activity 是真實的 Activity 組件,具備生命周期的。
也許有童鞋會有疑問,如果直接在 startActivity 中設置要啟動的 Activity 為插件 Activity ,這樣不行嗎?答案是肯定的,因為這樣就會拋出一個異常: ActivityNotFoundException:...have you declared this activity in your AndroidManifest.xml? 我相信這個異常大家很熟悉的吧,在剛開始學習 Android 時,大家都會犯的一個錯誤。所以,我想我們也明白了為什么要花這么大的一個功夫去動態地替換要創建的 Activity ,就是為了繞過這個 ActivityNotFoundException 異常,達到去“欺騙” Android 系統的效果。
既然創建好了,那么就來看看 PluginInstrumentation 里調用相關生命周期的方法:
@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 {
//在許多設備上,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) {
//根據AndroidManifest.xml中的參數設置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);
}
}
}
// 如果是三星手機,則使用包裝的LayoutInflater替換原LayoutInflater
// 這款手機在解析內置的布局文件時有各種錯誤
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);
}
/**
- 檢查跳轉目標是不是來自插件
*
- @param activity Activity
*/
private void lookupActivityInPlugin(Activity activity) {
ClassLoader classLoader = activity.getClass().getClassLoader();
if (classLoader instanceof PluginClassLoader) {
currentPlugin = ((PluginClassLoader) classLoader).getPlugInfo();
} else {
currentPlugin = null;
}
}</code></pre> 在 callActivityOnCreate() 中先去檢查了創建的 Activity 是否來自于插件。如果是,那么會給 Activity 設置 Context 、 設置主題等;如果不是,則直接執行父類方法。在 super.callActivityOnCreate(activity, icicle) 中會去調用 Activity.onCreate() 方法。其他的生命周期方法作者沒有特殊處理,這里就不講了。
分析到這,我們終于把 android-pluginmgr 插件化實現的方案完整地梳理了一遍。當然,不同的插件化框架會有不同的實現方案,具體的仍然需要自己專心研究。另外我們發現該框架還沒有實現啟動插件 Service 的功能,如果想要了解,可以參考下其他插件化框架。
0x04 總結
上面亂七八糟的流程講了一遍,可能還有一些童鞋不太懂,所以在這里給出一張 android-pluginmgr 的流程圖。不懂的童鞋可以根據這張圖再好好看一下源碼,相信你會恍然大悟的。

android-pluginmgr流程圖
來自:http://www.jianshu.com/p/b8ef0a92c060