Android打开插件Activity的方式有很多种,类名固定的可以使用预注册的方式。代理也是一种很好的方式,同时代理的方式也可以用于打开插件中的Service。
这两种方式都有一些弊端,这篇文章要分享的是如何更好地打开插件中的Activity,采用Instrumentation注入的方式。
当开发者实现一个Activity时,不能自己添加一个带参数的构造方法。如果添加了,也需要实现一个无参构造方法。原因是在调用Context.startActivity(…)后,系统会利用反射的方式根据activityClass实例化一个Activity对象:
Activity activity = (Activity)clazz.newInstance();
其中clazz就是传入的Activity的子类。这一行代码是在Instrumentaion.java中的newActivity(…)方法中的。也就是说,Activity的实例化是在Instrumentation这个类里面,通过反射的方式进行的。
每个Activity对象都持有了一个mInstrumentation的变量,该变量并不是Activity自己创建的,而是由ActivityThread传递给Activity的。而在ActivityThread中,持有了一个名为mInstrumentation的变量。因此,创建Activity的Instrumentation的对象,是被ActivityThread持有着。
如果一个Activity没有注册,想要打开它,会抛出异常:
Unable to find explicit activity class XXXXActivity have you declared this activity in your AndroidManifest.xml?
这个异常是Instrumentation的checkStartActivityResult方法中抛出的:
/** @hide */ public static void checkStartActivityResult(int res, Object intent) { if (res >= ActivityManager.START_SUCCESS) { return; } switch (res) { case ActivityManager.START_INTENT_NOT_RESOLVED: case ActivityManager.START_CLASS_NOT_FOUND: if (intent instanceof Intent && ((Intent)intent).getComponent() != null) throw new ActivityNotFoundException( "Unable to find explicit activity class " + ((Intent)intent).getComponent().toShortString() + "; have you declared this activity in your AndroidManifest.xml?"); throw new ActivityNotFoundException( "No Activity found to handle " + intent); ...(以下省略) } }
很明显的一件事,插件中的Activity是没有在AndroidManifest.xml中注册的,直接打开肯定抛异常崩溃。读者可能会想:我要是直接重写这个方法,不管什么情况全部不抛异常,校验不就通过了吗?但是很抱歉,这个方法是static类型的,子类无法重写。这个条件我们没有办法解决,只能绕过去。
Instrumentation有一个很重要的事:checkStartActivityResult(…)方法 是在 newActivity(…)方法之前执行的。请看这两个信息:
当开发者使用一个已经注册过的Activity去接受校验时,肯定能通过校验; 实例化出来的Activity一定是经过校验的那一个Activity吗?不一定。让我们看一下Instrumentation#newActivity(…)方法的具体实现(有两个重载实现):
第一个:
public Activity newActivity(Class clazz, Context context, IBinder token, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, Object lastNonConfigurationInstance) throws InstantiationException, IllegalAccessException { Activity activity = (Activity)clazz.newInstance(); ActivityThread aThread = null; activity.attach(context, aThread, this, token, 0, application, intent, info, title, parent, id, (Activity.NonConfigurationInstances)lastNonConfigurationInstance, new Configuration(), null); return activity; }
第二个:
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException { return (Activity)cl.loadClass(className).newInstance(); }
注意参数列表中有一个 Intent intent 参数,这个参数就是我们在startActivity(Intent intent);传入的那个intent。两个重载方法的参数列表中都包含了这个intent。
所以绕过注册验证的思路大致就出来了:
最后将pluginActivity就打开我们插件中的Activity了。修改后的代码如下:
其中,SonaInner.getRelayActivity() 就是事先约定好的中继Activity,也就是上面说得ActivityA。
另外,PluginManager.loadClassFromPlugin(…) 方法可以简单理解为是Class.forName()
package com.mzdxl.sona.hooks; import android.app.Activity; import android.app.Application; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.AssetManager; import android.content.res.Resources; import android.os.Bundle; import android.os.IBinder; import android.view.ContextThemeWrapper; import com.mzdxl.sona.Log; import com.mzdxl.sona.PluginManager; import com.mzdxl.sona.SonaInner; import java.lang.reflect.Field; import java.lang.reflect.Method; /** * @author 郑海鹏 * @since 2016/5/16 21:57 */ public class InstrumentationHook extends android.app.Instrumentation{ /* ------------------------------------------------------------- Fields ------------------------------------------------------------- */ public static final String PLUGIN_ACTIVITY_NAME = "plugin_activity"; public static final String PLUGIN_PATH = "plugin_path"; protected String pluginPath; /* ------------------------------------------------------------- System Override / Implements Methods ------------------------------------------------------------- */ @Override public Activity newActivity(Class clazz, Context context, IBinder token, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, Object lastNonConfigurationInstance) throws InstantiationException, IllegalAccessException { Log.i("CustomInstrumentation#newActivity 执行了!code 1"); Activity handleResult = createActivity(intent); if (handleResult != null){ return handleResult; } return super.newActivity(clazz, context, token, application, intent, info, title, parent, id, lastNonConfigurationInstance); } @Override public Activity newActivity(ClassLoader cl, String fromClassName, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException { Log.i("CustomInstrumentation#newActivity 执行了!code 2"); Activity handleResult = createActivity(intent); if (handleResult != null){ handleResult.attach(context, null, this, token, 0, application, intent, info, title, parent, id, (Activity.NonConfigurationInstances)lastNonConfigurationInstance, new Configuration(), null); return handleResult; }` return super.newActivity(cl, fromClassName, intent); } @Override public void callActivityOnCreate(Activity activity, Bundle icicle) { super.callActivityOnCreate(activity, icicle); injectResources(activity); } /* ------------------------------------------------------------- Methods ------------------------------------------------------------- */ /** * 根据Intent中的class判断是否存在中继Activity,如果目标Activity是中继Activity则打开插件 */ @SuppressWarnings("unchecked") protected Activity createActivity(Intent intent){ SonaInner.checkInit(); // 获得Intent中要打开的Activity String className = intent.getComponent().getClassName(); // 判断该Activity是否是中继Activity if (SonaInner.getRelayActivity().getName().equals(className)) { // 如果是中继Activity,取出真实想启动的插件的Activity的类名、插件的位置 String pluginActivityName = intent.getStringExtra(PLUGIN_ACTIVITY_NAME); pluginPath = intent.getStringExtra(PLUGIN_PATH); // 实例化Activity Class PluginActivity; try { if (pluginPath == null){ // 如果插件保存地址为null,不从插件中找 PluginActivity = (Class) Class.forName(pluginActivityName); }else{ PluginActivity = PluginManager.loadClassFromPlugin(getContext(), pluginPath, pluginActivityName); } return PluginActivity == null ? null : PluginActivity.newInstance(); } catch (Exception e) { e.printStackTrace(); Log.e("Intent中传入的插件的Class名无法实例化, 或者Class名不是一个Activity的类名,或者对应插件中不包含这个Activity。"); } } return null; } /** * 注入插件的资源 */ protected void injectResources(Activity activity){ if (pluginPath == null){ return; } // 获取Activity的Resource资源 Resources hostResource = activity.getApplication().getResources(); // 获取插件的Resource try { // 获得系统assetManager AssetManager assetManager = AssetManager.class.newInstance(); // 将插件地址添加到资源地址 Method method_addAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); method_addAssetPath.setAccessible(true); method_addAssetPath.invoke(assetManager, pluginPath); // 获得新的完整的资源 Resources resources = new Resources(assetManager, hostResource.getDisplayMetrics(), hostResource.getConfiguration()); Field field_mResources = ContextThemeWrapper.class.getDeclaredField("mResources"); field_mResources.setAccessible(true); field_mResources.set(activity, resources); } catch (Exception e) { e.printStackTrace(); Log.e("复制插件Resource时出现异常"); } } }
在1.2中已经提到了,Instrumentation的对象是保存在ActivityThread里的。很不幸,ActivityThread对象我们无法直接获得到,同时连ActivityThread这个类我们在代码中都不能直接调用。ActivityThread有一个静态方法:
public static ActivityThread currentActivityThread() { return sCurrentActivityThread; }
该方法返回了当前的ActivityThread。
我们可以通过反射的方式,先获得ActivityThread,再获得currentActivityThread方法,最后调用这个方法就可以获得ActivityThread的变量(设为activityThread)了。最后把我们自己的Instrumentation对象替换掉activityThread里的mInstrumentation就可以了。代码如下:
其中:
package com.mzdxl.sona; import android.content.res.AssetManager; import com.mzdxl.sona.hooks.InstrumentationHook; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * @author 郑海鹏 * @since 2016/5/17 10:25 */ public class HookManager { /* ------------------------------------------------------------- Fields ------------------------------------------------------------- */ static Class ActivityThread; static Method ActivityThread_method_currentActivityThread; static Object obj_activityThread; static Method AssetManager_method_addAssetPath; /* ------------------------------------------------------------- Static Methods ------------------------------------------------------------- */ /** * 初始化操作,获得一些基本的类、变量、方法等。 */ public static void init() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { // 获得ActivityThread类 ActivityThread = Class.forName("android.app.ActivityThread"); // 获得ActivityThread#currentActivityThread()方法 ActivityThread_method_currentActivityThread = ActivityThread.getDeclaredMethod("currentActivityThread"); // 根据currentActivityThread方法获得ActivityThread对象 obj_activityThread = ActivityThread_method_currentActivityThread.invoke(ActivityThread); // 获得AssetManager#addAssetPath()方法 AssetManager_method_addAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); AssetManager_method_addAssetPath.setAccessible(true); } /** * 注入Sona的Instrumentation */ public static void injectInstrumentation() throws NoSuchFieldException, IllegalAccessException { Log.i("开始注入Sona的Instrumentation。"); // 获得ActivityThread类中的Instrumentation字段 Field field_instrumentation = obj_activityThread.getClass().getDeclaredField("mInstrumentation"); field_instrumentation.setAccessible(true); // 创建出一个新的Instrumentation InstrumentationHook obj_custom_instrumentation = new InstrumentationHook(); // 用Instrumentation字段注入Sona的Instrumentation变量 field_instrumentation.set(obj_activityThread, obj_custom_instrumentation); } }
最后我们需要在自己Application中的onCreate()方法中调用HookManager.init() 和 HookManager.injectInstrumentation()就可以替换为我们自己的Instrumentation了。