关于插件化有很多知识点可讲,市面上也有很多成熟的第三方库,这篇 Blog 不是讲解这些第三方库的使用,而是探索如何从零开始使用"反射"、"Hook"等知识点,在不安装插件 apk 的情况下,实现自己的宿主 App 加载放在 sd 卡的插件 apk,从而一窥 Android 插件化的原理。
背景
Android 开发中经常有这样的情况:模块化不清晰、方法数超限、想在不重新安装 App 的情况下增加新的模块,基于这样的需求,结合 Android DexClassLoader 可以加载 dex 文件以及包含 dex 的压缩文件(apk 和 jar)的特点,催生出了 Android 插件化技术。
原理
1.DexClassLoader 可以加载外部 dex 文件以及包含 dex 的压缩文件(apk 和 jar)。
2.熟知 Activity 的启动流程,利用 Hook 技术启动外部 dex 文件中的 Activity。
实现步骤
实现流程图
具体步骤
我们知道 Activity 必须在 AndroidManifest 中配置才能正常启动,否则会报 ActivityNotFound 异常,而外部插件 apk 中的 Activity 肯定是无法在宿主 App 中配置的,这样因为找不到相关配置 startActivity 就会 crash。为了方便理解,我们先去实现如何在 AndroidManifest 没有配置 TestActivity 的情况下,启动宿主 App 的 TestActivity(注意 TestActivity 是宿主 App 而不是外部 apk 或 dex 文件的)。
步骤1:绕过 AndroidManifest 检测
Activity 启动过程中是应用程序进程与 AMS 频繁交互的过程。AMS 处于系统 SystemServer 进程中,我们无法修改,所以只能 Hook 应用程序进程部分,以实现需求。
这里采用占位策略,原理是提前在 AndroidManifest 中配置一个占位页面<activity android:name=".SubActivity" />
,在应用程序进程将 targetIntent 传给 AMS 之前,替换 targetIntent 为该占位 intent,之后在 AMS 传回应用程序进程之后、应用程序进程调用 intent 启动 Activity 之前,将占位 intent 再次替换为 targetIntent 即可。下面具体实现:
首先实现工具类 FieldUtil
,方便后续反射操作:
<1>
public class FieldUtil {
/**
* 获取Field对应的值
*
* @param clazz
* @param target
* @param name
* @return
* @throws Exception
*/
public static Object getField(Class clazz, Object target, String name) throws Exception {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
return field.get(target);
}
/**
* 获取Field
*
* @param clazz
* @param name
* @return
* @throws Exception
*/
public static Field getField(Class clazz, String name) throws Exception {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
return field;
}
/**
* 给Field赋值
*
* @param clazz
* @param target
* @param name
* @param value
* @throws Exception
*/
public static void setField(Class clazz, Object target, String name, Object value) throws Exception {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
field.set(target, value);
}
}
那么应用程序进程是什么时候将 intent 传给 AMS 的呢?通过层层查找 startActivity 源码,最终定位在下面的源码上:
android.app.Instrumentation
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
IApplicationThread whoThread = (IApplicationThread) contextThread;
...
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(who);
int result = ActivityManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);//1.
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}
IActivityManager 是 AMS 在客户端的代理类,通过它与 AMS 跨进城通信,看下注释1处 ActivityManagerService 源码:
public static IActivityManager getService() {
return IActivityManagerSingleton.get();
}
private static final Singleton<IActivityManager> IActivityManagerSingleton =
new Singleton<IActivityManager>() {
@Override
protected IActivityManager create() {
final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
final IActivityManager am = IActivityManager.Stub.asInterface(b);
return am;
}
};
这里用到 Singleton 实现单例,接着研究 Singleton 的源码:
public abstract class Singleton<T> {
private T mInstance;
protected abstract T create();
public final T get() {
synchronized (this) {
if (mInstance == null) {
mInstance = create();
}
return mInstance;
}
}
}
于是 Hook 点找到了:利用反射,替换 Singleton<IActivityManager> 中的 IActivityManager 为自己的代理类,即可插入替换 targetIntent 的代码。下面是具体实现:
首先创建自己的代理类,用于替换 targetIntent 为占位 intent 以绕过检测,这里使用动态代理:
<2>
public class IActivityManagerProxy implements InvocationHandler {
private Object mActivityManager;
private static final String TAG = "IActivityManagerProxy";
public IActivityManagerProxy(Object activityManager) {
this.mActivityManager = activityManager;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//执行原方法之前,先执行代理方法
if ("startActivity".equals(method.getName())) {
Intent intent = null;
int index = 0;
//找到intent参数
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Intent) {
index = i;
break;
}
}
intent = (Intent) args[index];
Intent subIntent = new Intent();
String packageName = "com.app.dixon.studyplug";//1.替换 TargetIntent 的参数
subIntent.setClassName(packageName, packageName + ".SubActivity");//替换 TargetIntent 的参数
subIntent.putExtra(HookHelper.TARGET_INTENT, intent);//2.
args[index] = subIntent;
}
return method.invoke(mActivityManager, args);
}
}
注释1处,创建占位 intent 用于替换传给 AMS 的 targetIntent;
注释2处,将 targetIntent 存储起来,方便后续拿出启动。
创建完代理类,就可以 Hook 替换 IActivityManager 了,由于 Singleton<IActivityManager> 是静态的,所以替换整个进程生效。
创建 HookHelper 类,Hook Singleton<IActivityManager>.mInstance:
<3>
public class HookHelper {
public static final String TARGET_INTENT = "target_intent";
public static final String TARGET_INTENT_NAME = "target_intent_name";
public static final String TAG = "HOOK";
/**
* Hook IActivityManager 由于IActivityManagerSingleton是静态成员变量 所以是全局Hook
*
* @throws Exception
*/
public static void hookAMS() throws Exception {
Object defaultSingleton = null;
if (Build.VERSION.SDK_INT >= 26) {//版本号 > 8.0
Class<?> activityManagerClazz = Class.forName("android.app.ActivityManager");
//获取Singleton<IActivityManager>
defaultSingleton = FieldUtil.getField(activityManagerClazz, null, "IActivityManagerSingleton");
} else {
Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
//获取ActivityManagerNative中的gDefault字段
defaultSingleton = FieldUtil.getField(activityManagerNativeClazz, null, "gDefault");
}
//替换Singleton中的值
//1.获取class,找到其属性
Class<?> singletonClazz = Class.forName("android.util.Singleton");
Field mInstanceField = FieldUtil.getField(singletonClazz, "mInstance");
//2.获取IActivityManager
Object iActivityManager = mInstanceField.get(defaultSingleton);
//3.获取IActivityManager的Proxy代理类
Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager"); //IActivityManager的全路径
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class<?>[]{iActivityManagerClazz},
new IActivityManagerProxy(iActivityManager));
//4.替换
mInstanceField.set(defaultSingleton, proxy);
Log.e(TAG, "Hook Finish");
}
}
大致步骤是:获取 android.app.ActivityManager
中的静态成员变量 Singleton<IActivityManager>,获取其 mInstance 属性,创建动态代理类 IActivityManagerProxy,替换 mInstance。详情已经在上述注释中标明。
之后在 Application 中调用:
<4>
public class MyApplication extends Application {
public static Application application;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
application = this;
try {
HookHelper.hookAMS();
} catch (Exception e) {
Log.e(HookHelper.TAG, "The reason for the error is " + e.toString());
e.printStackTrace();
}
}
通过上述操作,AMS 遍历 AndroidManifest 时检测的是占位 intent 而不是 targetIntent,故不会抛出 ActivityNotFound 的异常。
步骤2:还原 targetIntent
为了绕过检测我们将 targetIntent 临时替换为了占位 intent,相应的,在 AMS 允许应用程序进程启动 Activity 时,我们应当将占位 intent 还原为 targetIntent。
那么什么时间点还原合适呢?
我们知道 ActivityThread 作为应用程序进程的主线程,在很多方面起了关键的作用,其中包括 Activity 的启动。其中 handleLaunchActivity
中有一行源码如下:
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
...
Activity a = performLaunchActivity(r, customIntent);
...
}
这里 r 为 ActivityClientRecord 类型,它有个 Intent 类型的成员变量名为 intent
,这个 intent
就是上面 AMS 传回给应用程序进程的 intent。
回到该方法的上一步,看下它的源码:
public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
case LAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
} break;
ActivityThread 通过特殊的 Handler :H 来分发 AMS 发来的各种事件,其中 Handler 的 dispatchMessage 源码如下:
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
结合上述源码,我们知道了 msg.obj 中包含了之前了占位 intent,所以我们只需要将 H 的 mCallback 赋值为自定义的 Callback,并在 Callback.handleMessage 中做替换 intent 的操作,之后再重新手动调用 H.handleMessage(msg);
即可。具体实现如下:
首先实现自定义的 HCallback 类,在其中做替换 intent 操作:
<5>
public class HCallback implements Handler.Callback {
public static final int LAUNCH_ACTIVITY = 100;
Handler mHandler;
public HCallback(Handler handler) {
mHandler = handler;
}
@Override
public boolean handleMessage(Message msg) {
//执行原handleMessage方法之前执行Hook的HandleMessage
if (msg.what == LAUNCH_ACTIVITY) {
Object r = msg.obj;
try {
//获取之前消息中的真实Intent
Intent intent = (Intent) FieldUtil.getField(r.getClass(), r, "intent");
Intent target = intent.getParcelableExtra(HookHelper.TARGET_INTENT);
if (target != null) {
//替换
FieldUtil.setField(r.getClass(), r, "intent", target);
}
} catch (Exception e) {
e.printStackTrace();
}
}
//手动重新调用handleMessage
mHandler.handleMessage(msg);
return true;
}
}
接下来需要把我们上述自定义的 HCallback 赋值给 ActivityThread.H.mCallback,在 HookHelper 类中添加如下方法:
<6>
/**
* 目标是对 ActivityThread 的 mH.callback 进行替换,而 ActivityThread 单进程只有一个,所以是全局替换
*
* @throws Exception
*/
public static void hookHandler() throws Exception {
//获取ActivityThread.mH
Class activityThreadClazz = Class.forName("android.app.ActivityThread");
Object currentActivityThread = FieldUtil.getField(activityThreadClazz, null, "sCurrentActivityThread");
Field mHField = FieldUtil.getField(activityThreadClazz, "mH");
Handler mH = (Handler) mHField.get(currentActivityThread);
//替换H.mCallback
FieldUtil.setField(Handler.class, mH, "mCallback", new HCallback(mH));
}
ActivityThread 单进程只有一个,可以通过它的静态成员变量 sCurrentActivityThread
获得,获取到之后将 HCallback 赋值给 mH.mCallback 即可。
最后记得在上述 Application 中调用:
<4>改
public class MyApplication extends Application {
public static Application application;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
application = this;
try {
HookHelper.hookAMS();
HookHelper.hookHandler();
} catch (Exception e) {
Log.e(HookHelper.TAG, "The reason for the error is " + e.toString());
e.printStackTrace();
}
}
}
通过上述俩大步骤,我们在项目中创建任意 Activity,然后删除掉它在 AndroidManifest 中的配置,然后通过下面代码也可以正常启动,如示例:
startActivity(new Intent(MainActivity.this, TestActivity.class));
那么有一个问题:这样启动的 Activity 可以正常遵从 AMS 对生命周期的管理吗?
答案是肯定,AMS 通知应用程序进程创建 Activity 之后是通过 Token 进行后续生命周期通信的,而 Token 依赖于真实创建的 TargetActivity,所以 TargetActivity 是有生命周期的。有兴趣的可以单独研究源码,这里不再深入探讨。
步骤3:加载插件 dex
步骤1、2实现了不配置 AndroidManifest 也能正常启动 Activity,但我们的终极目标是启动外部 apk,首先就需要把外部 apk 加载进来。
使用 Android 提供的 DexClassLoader
可以加载外部 dex 文件或加载包含 dex 的文件,如 apk、jar 等。这里我创建了 AppClassLoaderHelper
类,用于获取加载了外部 apk 的 ClassLoader
。源码如下:
<7>
public class AppClassLoaderHelper {
private static final Map<String, ClassLoader> classLoaderCache = new HashMap<>();
private static final Map<String, Resources> resourceCache = new HashMap<>();
private static final String TAG = "AppClassLoaderHelper";
/**
* @param appPath
* @return 得到对应插件的ClassLoader对象
*/
public static ClassLoader getDexClassLoader(Context context, String appPath) {
if (classLoaderCache.containsKey(appPath)) {
return classLoaderCache.get(appPath);
}
Log.e(TAG, "path is " + appPath);
String dexOutFilePath = context.getCacheDir().getAbsolutePath();
Log.e(TAG, "dexOutFilePath is " + dexOutFilePath);
DexClassLoader classLoader = new DexClassLoader(appPath, dexOutFilePath, null, context.getClassLoader());
classLoaderCache.put(appPath, classLoader);
return classLoader;
}
利用 DexClassLoader 将外部的 apk 加载了进来,他的构造函数如下:
DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)
四个参数分别是:
1.dexPath 的全路径。
2.optimizedDirectory:加载的 dex 存放的目录。
3.librarySearchPath:library 库路径。
4.parent:父类 ClassLoader,双亲委托不是本文重点,有兴趣可以 Google 了解。
这里我新建了一个项目,用于生成插件 apk。项目很简单,只有一个空页面:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
生成 apk 之后将该文件存放到手机 sd 卡根目录下:/storage/emulated/0/app-debug.apk
。
因为我放的位置特殊,所以需要在 AndroidManifest 中配置读取 sd 卡的权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
Android 6.0 及以上还需要动态获取,所以假如报 ClassNotFound 异常,检查你的 App 是否真的有 sd 卡读写权限。
之后就可以尝试在宿主 App 中启动我们的目标页面了:
<8>
public void startOtherApp(View view) {
try {
ClassLoader loader = AppClassLoaderHelper.getDexClassLoader(MyApplication.application, "/storage/emulated/0/app-debug.apk");
Class<?> targetClass = loader.loadClass("com.app.dixon.plugin.MainActivity");
Intent intent = new Intent(MainActivity.this, targetClass);
intent.putExtra(HookHelper.TARGET_INTENT_NAME, intent.getComponent().getClassName());
startActivity(intent);
} catch (ClassNotFoundException e) {
//classNotFound 注意有可能是权限问题
e.printStackTrace();
}
}
这里我通过加载了外部 apk 文件的 ClassLoader 去获取目标页面 com.app.dixon.plugin.MainActivity
的 Class,之后创建 intent,并赋值 HookHelper.TARGET_INTENT_NAME
用于标记这是一个插件 apk 的页面,便于后续识别。最后通过 startActivity(intent)
启动。
运行,果然 Crash 了,报错如下:
java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.app.dixon.studyplug/com.app.dixon.plugin.MainActivity}: java.lang.ClassNotFoundException: Didn't find class "com.app.dixon.plugin.MainActivity" on path: DexPathList[[zip file "/data/app/com.app.dixon.studyplug-RqW1yxJOWJ4JvMTrVZogWA==/base.apk"],nativeLibraryDirectories=[/data/app/com.app.dixon.studyplug-RqW1yxJOWJ4JvMTrVZogWA==/lib/arm64, /system/lib64, /vendor/lib64]]
这是为什么呢?仔细分析,可以看出:
我们使用 DexClassLoader 获取到的插件 Activity 的 Class 只是用于创建 Intent,在上述步骤2中、真正 new Activity 时,使用的 ClassLoader 仍然是宿主 App 的 ClassLoader,宿主 App 的 ClassLoader 从来没有加载过外部插件 apk,当然会报 ClassNotFoundException。
步骤4:替换插件 Activity ClassLoader
经过上述分析,我们需要在加载插件 apk 时,将宿主 App 的 ClassLoader 替换为自定义的 DexClassLoader。
替换时机一定和 new Activity
的时机有关,上面我们说到 Activity 示例是 performLaunchActivity
方法返回的,看下它的源码:
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
...
经常做单元测试的同学可能对 Instrumentation 这个类不会陌生,它是 ActivityThread 的成员变量之一,用于转交执行 Activity 的一些关键方法。这里可以看到 Activity 就是它创建的,它的 newActivity 源码如下:
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
return (Activity)cl.loadClass(className).newInstance();
}
第一个参数 cl 就是 Activity 对应的 ClassLoader,所以这儿的做法是,替换 ActivityThread.mInstrumentation,重写它的 newActivity 方法,使其在启动插件 apk 时,加载自定义的 DexClassLoader。下面是具体实现:
<9>
public class InstrumentationProxy extends Instrumentation {
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,
IllegalAccessException, ClassNotFoundException {
String intentName = intent.getStringExtra(HookHelper.TARGET_INTENT_NAME);
if (!TextUtils.isEmpty(intentName)) {//1.
//通过自定义的classLoader加载目标类
Activity activity = super.newActivity(AppClassLoaderHelper.getDexClassLoader(MyApplication.application, "/storage/emulated/0/app-debug.apk"), intentName, intent);
return activity;
}
return super.newActivity(cl, className, intent);
}
}
注释1处,前面我们给 intent 赋值了 HookHelper.TARGET_INTENT_NAME,这里我们利用该标识判断:如果是插件 apk,就使用自定义 DexClassLoader,否则还是走宿主 ClassLoader。
TARGET_INTENT_NAME
意味着插件 apk 也需要传此标识,这对插件的独立开发是不友好的。
实际上有更好的实现方式,不需要传递标识也能识别是否是插件 apk 中的 class,详情参考 Github 源码。
接下来就是想办法把 ActivityThread.mInstrumention
替换为上述 InstrumentationProxy
,在 HookHelper 中增加方法:
<10>
/**
* Hook newActivity 使其不加载系统、而加载自定义ClassLoader中的Activity
*
* @param context
* @throws Exception
*/
public static void hookInstrumentation(Context context) throws Exception {
Class activityThreadClazz = Class.forName("android.app.ActivityThread");
Object currentActivityThread = FieldUtil.getField(activityThreadClazz, null, "sCurrentActivityThread");
FieldUtil.setField(activityThreadClazz, currentActivityThread, "mInstrumentation", new InstrumentationProxy());
}
记得在 Application 中调用:
<4>再改
public class MyApplication extends Application {
public static Application application;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
application = this;
try {
HookHelper.hookAMS();
HookHelper.hookHandler();
HookHelper.hookInstrumentation();
} catch (Exception e) {
Log.e(HookHelper.TAG, "The reason for the error is " + e.toString());
e.printStackTrace();
}
}
}
然后运行,不再报 ClassNotFound 的异常了,说明插件 Activity 的 Class 正常找到,且该页面能正常创建了。
但是,仍然发生了 Crash,这次的异常是资源找不到。
仔细想想,我们在加载外部插件 apk 的时候,从头到尾都只加载了 Class,没有加载其资源,插件 Activity 使用的 mResources 是宿主 App 的,资源当然会找不到!
步骤5:替换插件 Activity mResources
在 AppClassLoaderHelper 中增加如下方法加载插件资源并获取其 Resources,关于资源加载的原理这里不再深入探讨,直接看下面源码:
<11>
/**
* @param appPath
* @return 得到对应插件的Resource对象
*/
public static Resources getPluginResources(Context context, String appPath) {
if (resourceCache.containsKey(appPath)) {
return resourceCache.get(appPath);
}
try {
AssetManager assetManager = AssetManager.class.newInstance();
//反射调用方法addAssetPath(String path)
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
//将未安装的Apk文件的添加进AssetManager中,第二个参数是apk的路径
addAssetPath.invoke(assetManager, appPath);
Resources superRes = context.getResources();
Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
resourceCache.put(appPath, mResources);
return mResources;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
mResources
是 ContextThemeWrapper
的成员变量之一,而 Activity 继承自 ContextThemeWrapper
,所以只需要将插件 Activity 的 mResources 重新赋值为上述 getPluginResources
返回的 resources 即可。还记得 InstrumentationProxy
吗?我们刚才在那里 new Activity
,所以只需要紧随其后更换 mResources 即可。
<9>改
public class InstrumentationProxy extends Instrumentation {
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,
IllegalAccessException, ClassNotFoundException {
String intentName = intent.getStringExtra(HookHelper.TARGET_INTENT_NAME);
if (!TextUtils.isEmpty(intentName)) {
//通过自定义的classLoader加载目标类
Activity activity = super.newActivity(AppClassLoaderHelper.getDexClassLoader(MyApplication.application, "/storage/emulated/0/app-debug.apk"), intentName, intent);
//新增:
//通过自定义的resources加载目标资源
//这样TargetActivity使用的就是其Apk本身的资源
try {
FieldUtil.setField(ContextThemeWrapper.class, activity, "mResources", AppClassLoaderHelper.getPluginResources(MyApplication.application, "/storage/emulated/0/app-debug.apk"));
} catch (Exception e) {
e.printStackTrace();
}
return activity;
}
return super.newActivity(cl, className, intent);
}
}
到这里似乎没什么问题了,然后点击启动插件 apk,boom~crash
ClassLoader 正常加载了,资源也映射正确了,为什么还是 crash 了呢?
查找原因,crash 说资源仍然找不到,资源号是 0x7xxxxx。通过查找该资源,发现是宿主 App 的 R.mipmap.ic_launcher,就是 Android app 默认的图标。插件 Activity 使用的资源是自己 apk 的,当它使用宿主 app 的 id 去查找资源当然会找不到了。
分析到这里,我明白当前的错误也许和插件 app 使用的 AppTheme 有关系,也许是 TopBar 不完全由 Activity 掌控,导致 TopBar 的资源 id 仍然使用宿主 App 的。为了演示方便(其实是懒),我这里暂时将插件 apk 的目标页面设置成了没有 TopBar 的主题:
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:windowNoTitle">true</item>
</style>
该问题后续已修复。
果然,再次启动插件 apk,终于成功了!
步骤6:后续实践
在插件 apk 页面里,我首先放了张图,资源能正确加载,如图:
随后我在插件 apk 里又新建了一个 OtherActivity,然后插件 MainActivity 通过下面代码启动 OtherActivity:
public void start(View view) {
Intent intent = new Intent(MainActivity.this,OtherActivity.class);
intent.putExtra("target_intent_name", intent.getComponent().getClassName());
startActivity(intent);
}
这里 target_intent_name
就是我们之前识别插件页面的标识,有了这个标识,才会给当前 Activity 加载正确的 ClassLoader 和 Resources。这里测试插件 A 页面启动插件 B 页面没有问题。
总结
插件化涉及到的内容很多,本文只是对插件化实现的一个从零开始的探索,原理基于此,相信今后扩展、完善、源码探索也就有据可循。
Github 源码地址,后续会不断完善。
本人能力有限,步骤1、2部分参考了 Android 进阶揭秘一书,错误之处还请指出。
[TOC]