Android打开插件中Activity的实现原理


摘要

Android打开插件Activity的方式有很多种,类名固定的可以使用预注册的方式。代理也是一种很好的方式,同时代理的方式也可以用于打开插件中的Service。这两种方式在之前的博客中都有分享:

  • 预注册的方式打开插件Activity:http://blog.csdn.net/h28496/article/details/49966503
  • 代理的方式打开插件中的Activity:http://blog.csdn.net/h28496/article/details/50414873

这两种方式都有一些弊端,这篇文章要分享的是如何更好地打开插件中的Activity,采用Instrumentation注入的方式。
其大致步骤为:

  1. 在创建Intent时,目标Activity为已注册的Activity(设为ActivityA);
  2. 在 intent 中 put 插件Activity的信息(设为PluginActivity);
  3. 继承Instrumentation,实现子类(设为InstrumentationHook),重写newActivity()方法,使得要实例化ActivityA时,实例化PluginActivity;
  4. 在Application的onCreate()方法中,利用反射,通过ActivityThread的currentActivityThread()方法获得ActivityThread的实例(该实例会传递给后续创建的所有Activity);
  5. 通过反射的方式,替换ActivityThread实例中的mInstrumentation变量(原类型是Instrumentation,替换为子类InstrumentationHook的变量)

1. Activity是如何被实例化的

1.1 在哪一个类、哪一个方法中实例化的

当开发者实现一个Activity时,不能自己添加一个带参数的构造方法。如果添加了,也需要实现一个无参构造方法。原因是在调用Context#startActivity(…)后,系统会利用反射的方式根据activityClass实例化一个Activity对象:

Activity activity = (Activity)clazz.newInstance();

其中clazz就是传入的Activity的子类。这一行代码在Instrumentaion.java中的newActivity(…)方法中。可以看出,Activity的实例化是在Instrumentation这个类里面,通过反射的方式进行的。

1.2 谁持有了Instrumentation的引用

每个Activity对象都持有了一个mInstrumentation的变量,该变量是由ActivityThread传递给Activity的。在ActivityThread中,有了一个名为mInstrumentation的变量。


2. 如何偷梁换柱打开插件的Activity

2.1 Activity是否在AndroidManifest.xml注册的校验

如果一个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中注册的,直接打开肯定抛异常崩溃。同时由于checkStartActivityResult方法是静态方法,所以不能通过重写这个方法绕过校验。

2.2 如何绕过Activity的注册校验

在Instrumentation中,checkStartActivityResult(…)方法 是在 newActivity(…)方法之前执行的。由于:
1. 当开发者使用一个已经注册过的Activity去接受校验时,肯定能通过校验;
2. 实例化出来的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。
所以绕过注册验证的思路大致就出来了:

  1. 第一步:
    Intent intent = new Intent(this, 某个已经注册的ActivityA);
    intent.putString(“插件中的的Activity的类名”);
  2. 系统在接受startActivity后,校验”已经注册Activity”是否注册,结果肯定是已经注册,顺利通过;
  3. 接着来到newActivity(…)方法准备实例化Activity
    我们通过反射的方式,事先替换掉newActivity(…)方法,改为我们自己的方法。在我们自己的newActivity方法中做以下工作:
Created with Raphaël 2.1.0 开始 从intent中获得通过校验的Activity的类,赋值给replyActivityClass A == ActivityA ? 从intent中获得插件中的Activity的类名,赋值给pluginActivityClassName 根据类名反射获得对应的类, pluginActivityClass 由C实例化出插件Activity对象pluginActivity 返回 说明并不是要打开插件中的Activity 结束 yes no

最后将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<? extends Activity> PluginActivity;            try {                if (pluginPath == null){                    // 如果插件保存地址为null,不从插件中找                    PluginActivity = (Class<? extends Activity>) 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时出现异常");        }    }}

2.3 如何用自己的newActivity方法替换系统方法

在1.2中已经提到了,Instrumentation的对象是保存在ActivityThread里的。很不幸,ActivityThread对象我们无法直接获得到,同时连ActivityThread这个类我们在代码中都不能直接调用。ActivityThread有一个静态方法:

public static ActivityThread currentActivityThread() {    return sCurrentActivityThread;}

该方法返回了当前的ActivityThread。
我们可以通过反射的方式,先获得ActivityThread,再获得currentActivityThread方法,最后调用这个方法就可以获得ActivityThread的变量(设为activityThread)了。最后把我们自己的Instrumentation对象替换掉activityThread里的mInstrumentation就可以了。代码如下:
其中:

  • ActivityThread_method_currentActivityThread 就是ActivityThread的currentActivityThread方法;
  • activityThread就是当前的ActivityThread。
  • InstrumentationHook 是我们自定义的Instrumentaion,修改了系统的newActivity。
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了。

如果读者不太清楚如何加载插件中的类到内存中等细节,可以参考前面两篇文章。

  • 预注册的方式打开插件Activity:http://blog.csdn.net/h28496/article/details/49966503
  • 代理的方式打开插件中的Activity:http://blog.csdn.net/h28496/article/details/50414873

附录

PluginManager的实现

package com.mzdxl.sona;import android.content.Context;import java.io.File;import java.util.LinkedHashMap;import java.util.Map;import dalvik.system.DexClassLoader;/** * @author 郑海鹏 * @since 2016/5/17 11:16 */public class PluginManager {    /* -------------------------------------------------------------                                              Fields    ------------------------------------------------------------- */    static Map classLoaderMap = new LinkedHashMap<>();    /* -------------------------------------------------------------                                           Methods    ------------------------------------------------------------- */    /**     * 加载一个插件     */    /*package*/ static DexClassLoader loadPlugin(Context context, String pluginPath){        boolean isPluginExist = new File(pluginPath).exists();        if (!isPluginExist){            Log.e("指定位置不存在插件文件。");            return null;        }        String dexSaveFolder = context.getDir("sona", 0).getAbsolutePath();        DexClassLoader dexClassLoader = new DexClassLoader(pluginPath, dexSaveFolder, null, context.getClassLoader());        classLoaderMap.put(pluginPath, dexClassLoader);        return dexClassLoader;    }    /**     * 从一个插件中获取到一个类     * @param pluginPath 插件路径     * @param className 要加载的类的类名     */    public static Class loadClassFromPlugin(Context context, String pluginPath, String className) throws ClassNotFoundException {        DexClassLoader dexClassLoader = classLoaderMap.get(pluginPath);        if (dexClassLoader == null){            dexClassLoader = loadPlugin(context, pluginPath);        }        if (dexClassLoader != null){            return dexClassLoader.loadClass(className);        }        return null;    }}

更多相关文章

  1. Handler Looper源码解析(Android消息传递机制)
  2. Android任务切换方法
  3. Android画板
  4. 错误ava.lang.RuntimeException: Unable to start activity Comp
  5. 深入理解Android(安卓)WebView
  6. Android中Service组件详解
  7. Android中系统状态栏的隐藏和显示
  8. 浅谈Java中Collections.sort对List排序的两种方法
  9. Python list sort方法的具体使用

随机推荐

  1. 【干货】让你薪资翻10倍的网站 大学生与
  2. 2020最全的Lucene7 入门教程
  3. SpringBoot持久层支持 - Springboot中如
  4. 初学Redis最清晰完整的教程
  5. Springboot使用Mybatis实现完整的 增删改
  6. 你应该知道的jvm知识-方法调用
  7. SSM框架如何编写分页查询
  8. 最全面solrr入门教程
  9. 配置 nginx ssl 认证,并同时支持 http 80
  10. 如何在Eclipse中使用 Git详细步骤