安卓插件化实例

最近想研究研究安卓插件化的知识,看了看Android插件化完美实现代码觉得很好很强大,所以就来造个轮子,学习记录下。
首先声明下,实现的例子是基于安卓5.1的,而且实现的功能仅仅是能启动插件的Activity,当然了原理弄懂了,别的也好说,那么下面正式开始。

实现插件化大概有三个难点

1:使我们插件中的代码可以被宿主程序调用
2:Activity等四大组件可以有正常的生命周期
3:插件可以正常使用资源文件,就是正常的调用R什么的

我们在自定义的Application中解决以上三个问题

使我们插件中的代码可以被宿主程序调用

apk中的代码都是通过ClassLoader来加载,而ClassLoader中的dexElements就是指的dex文件,我们通过反射将插件中的dex添加进dexElements中,这样宿主程序就能执行插件中的代码了

//将插件apk中的代码导入
String cachePath = getCacheDir().getAbsolutePath();
DexClassLoader mClassLoader = new DexClassLoader(MyApplication.PLUGIN_PATH, cachePath, null, getClassLoader());
DexHookHelper.inject(mClassLoader);

public class DexHookHelper {

    /**
 * 加载插件
 * @param loader
 */
    public static void inject(DexClassLoader loader){
        //拿到本应用的ClassLoader
        PathClassLoader pathLoader = (PathClassLoader) MyApplication.getContext().getClassLoader();
        try {
            //获取宿主pathList
            Object suZhuPathList = getPathList(pathLoader);
            Object chaJianPathList = getPathList(loader);
            Object dexElements = combineArray(
                    //获取本应用ClassLoader中的dex数组
                    getDexElements(suZhuPathList),
                    //获取插件CassLoader中的dex数组
                    getDexElements(chaJianPathList));
            //将合并的pathList设置到本应用的ClassLoader
            setField(suZhuPathList, suZhuPathList.getClass(), "dexElements", dexElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
 * 获取pathList字段
 * @param baseDexClassLoader 需要获取pathList字段的ClassLoader
 * @return 返回pathList字段
 */
    private static Object getPathList(Object baseDexClassLoader)
            throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        //通过这个ClassLoader获取pathList字段
        return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    /**
 * 反射需要获取的字段
 */
    private static Object getField(Object obj, Class<?> cl, String field)
            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        //反射需要获取的字段
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }

    /**
 * 获取DexElements
 */
    private static Object getDexElements(Object paramObject)
            throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
        return getField(paramObject, paramObject.getClass(), "dexElements");
    }

    /**
 * 反射需要设置字段的类并设置新字段
 */
    private static void setField(Object obj, Class<?> cl, String field,
                                 Object value) throws NoSuchFieldException,
            IllegalArgumentException, IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        localField.set(obj, value);
    }

    /**
 * 合成dex数组
 */
    private static Object combineArray(Object suzhu, Object chajian) {
        //获取原数组类型
        Class<?> localClass = suzhu.getClass().getComponentType();
        //获取原数组长度
        int i = Array.getLength(suzhu);
        //插件数组加上原数组的长度
        int j = i + Array.getLength(chajian);
        //创建一个新的数组用来存储
        Object result = Array.newInstance(localClass, j);
        //一个个的将dex文件设置到新数组中
        for (int k = 0; k < j; ++k) {
            if (k < i) {
                Array.set(result, k, Array.get(suzhu, k));
            } else {
                Array.set(result, k, Array.get(chajian, k - i));
            }
        }
        return result;
    }
}

需要说明的是热更新基本上也是基于上面的方法

Activity等四大组件可以有正常的生命周期

首先我们先了解下Activity的启动流程,当在应用中我们需要启动Activity后,最终会调用到AMS在本地的一个代理类上,然后通过IPC通信告知AMS,在AMS中如果检验正常后,通过IPC通知我们的ActivityThread里的一个binder类,然后利用Handler的方式转到主线程中去启动指定的Activity
但是在AMS中并不会持有我们的Activity对象,AMS在通知启动Activity的时候会传递过来一个Binder对象,而在Activity中会有一个Map对象,键就是传递过来的binder,而值可以认为是我们的Activity,这样的话如果AMS需要调用某个Activity的时候只需要传进来binder对象以及操作信息,我们的ActivityThread就可以知道要对哪个Activity回调哪个生命周期了。
也就是说如果告知AMS我们要启动AActivity,然后在AMS校验成功后,AMS通知我们的进程可以启动AActivity时,我们启动BActivity也是完全可以的,系统在回调生命周期的时候也是完全正常的,所以我们首先在manifest中定义一个Activity用于占位

<activity android:name=".XWActivity" />

public class XWActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

}

欺骗AMS分为两部分,首先是在向AMS发送启动请求的时候,将请求信息修改下,将真正的意图隐藏,换成启动我们的占位Activity,主要是通过反射获取AMS的代码对象,之后我们再创建一个代理对象来拦截并修改信息

/**
 * Hook AMS
 * 主要完成的操作是 "把真正要启动的Activity临时替换为在AndroidManifest.xml中声明的替身Activity"
 * 进而骗过AMS
 */
    private static void hookActivityManagerNative() throws ClassNotFoundException,
            NoSuchMethodException, InvocationTargetException,
            IllegalAccessException, NoSuchFieldException {
        //获取ActivityManagerNative的类
        Class<?> activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
        //拿到gDefault字段
        Field gDefaultField = activityManagerNativeClass.getDeclaredField("gDefault");
        gDefaultField.setAccessible(true);
        //从gDefault字段中取出这个对象的值
        Object gDefault = gDefaultField.get(null);
        // gDefault是一个 android.util.Singleton对象; 我们取出这个单例里面的字段
        Class<?> singleton = Class.forName("android.util.Singleton");
        //这个gDefault是一个Singleton类型的,我们需要从Singleton中再取出这个单例的AMS代理
        Field mInstanceField = singleton.getDeclaredField("mInstance");
        mInstanceField.setAccessible(true);
        //ams的代理对象
        Object rawIActivityManager = mInstanceField.get(gDefault);
        // 创建一个这个对象的代理对象, 然后替换这个字段, 让我们的代理对象帮忙干活,这里我们使用动态代理
        Class<?> iActivityManagerInterface = Class.forName("android.app.IActivityManager");
        Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class<?>[]{iActivityManagerInterface}, new IActivityManagerHandler(rawIActivityManager));
        mInstanceField.set(gDefault, proxy);
    }

class IActivityManagerHandler implements InvocationHandler {

    private static final String TAG = "IActivityManagerHandler";
    Object mBase;

    public IActivityManagerHandler(Object base) {
        mBase = base;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        if ("startActivity".equals(method.getName())) {
            Log.e("Main","startActivity方法拦截了");
            // 找到参数里面的第一个Intent 对象
            Intent raw;
            int index = 0;
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof Intent) {
                    index = i;
                    break;
                }
            }
            raw = (Intent) args[index];
            //创建一个要被掉包的Intent
            Intent newIntent = new Intent();
            // 替身Activity的包名, 也就是我们自己的"包名"
            String stubPackage = MyApplication.getContext().getPackageName();
            // 这里我们把启动的Activity临时替换为 ZhanKengActivitiy
            ComponentName componentName = new ComponentName(stubPackage, XWActivity.class.getName());
            newIntent.setComponent(componentName);

            // 把我们原始要启动的TargetActivity先存起来
            newIntent.putExtra(ActivityHookHelper.EXTRA_TARGET_INTENT, raw);

            // 替换掉Intent, 达到欺骗AMS的目的
            args[index] = newIntent;
            Log.e("xw","startActivity方法 hook 成功");
            Log.e("xw","args[index] hook = " + args[index]);
            return method.invoke(mBase, args);
        }

        return method.invoke(mBase, args);
    }
}

AMS顺利检验完成后,通知我们启动Activity的时候,我们再将信息修改回来,同样是通过反射来实现的

 /**
 * 由于之前我们用替身欺骗了AMS; 现在我们要换回我们真正需要启动的Activity
 * 不然就真的启动替身了, 狸猫换太子...
 * 到最终要启动Activity的时候,会交给ActivityThread 的一个内部类叫做 H 来完成
 * H 会完成这个消息转发; 最终调用它的callback
 */
    private static void hookActivityThreadHandler() throws Exception {

// 先获取到当前的ActivityThread对象
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        //他有一个方法返回了自己
        Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
        currentActivityThreadMethod.setAccessible(true);
        //执行方法得到ActivityThread对象
        Object currentActivityThread = currentActivityThreadMethod.invoke(null);

        // 由于ActivityThread一个进程只有一个,我们获取这个对象的mH
        Field mHField = activityThreadClass.getDeclaredField("mH");
        mHField.setAccessible(true);
        //得到H这个Handler
        Handler mH = (Handler) mHField.get(currentActivityThread);

        Field mCallBackField = Handler.class.getDeclaredField("mCallback");
        mCallBackField.setAccessible(true);
        //设置我们自己的CallBackField
        mCallBackField.set(mH, new ActivityThreadHandlerCallback(mH));

    }

public class ActivityThreadHandlerCallback implements Handler.Callback {

    Handler mBase;

    public ActivityThreadHandlerCallback(Handler base) {
        mBase = base;
    }

    @Override
    public boolean handleMessage(Message msg) {
        Log.e("Main", "handleMessage what = " + msg.what);
        switch (msg.what) {
            // ActivityThread里面 "LAUNCH_ACTIVITY" 这个字段的值是100
            case 100:
                handleLaunchActivity(msg);
                break;
        }

        mBase.handleMessage(msg);
        return true;
    }

    private void handleLaunchActivity(Message msg) {
        // 这里简单起见,直接取出TargetActivity;
        Log.e("Main", "handleLaunchActivity方法 拦截");
        Object obj = msg.obj;
        try {
            // 把替身恢复成真身
            Field intent = obj.getClass().getDeclaredField("intent");
            intent.setAccessible(true);
            Intent raw = (Intent) intent.get(obj);

            Intent target = raw.getParcelableExtra(ActivityHookHelper.EXTRA_TARGET_INTENT);
            if (target != null) {
                raw.setComponent(target.getComponent());
            }
            Log.e("xw", "target = " + target);

        } catch (Exception e) {
            throw new RuntimeException("hook launch activity failed", e);
        }
    }

}

插件可以正常使用资源文件

插件中的Activity创建,回调等都是在宿主程序中执行的,那么插件中想要获取资源的时候也会去宿主程序的资源管理器中获取,这显然是获取不到的,我们需要给插件创建他自己的资源管理器,并提供方法使其能够获取到

private AssetManager assetManager;
private Resources newResource;
private Resources.Theme mTheme;

private void creatPluginResources() {
        try {
            assetManager = AssetManager.class.newInstance();
            Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
            addAssetPathMethod.setAccessible(true);

            addAssetPathMethod.invoke(assetManager, PLUGIN_PATH);

            Resources supResource = getResources();
            newResource = new Resources(assetManager, supResource.getDisplayMetrics(), supResource.getConfiguration());

            mTheme = newResource.newTheme();
            mTheme.setTo(super.getTheme());
        } catch (Exception e) {
            Log.e("xw", "创建插件的配置资源失败" + e.getMessage());
            e.printStackTrace();
        }
    }

    @Override
    public AssetManager getAssets() {
        return assetManager == null ? super.getAssets() : assetManager;
    }

    @Override
    public Resources getResources() {
        return newResource == null ? super.getResources() : newResource;
    }

    @Override
    public Resources.Theme getTheme() {
        return mTheme == null ? super.getTheme() : mTheme;
    }

在宿主程序的MyApplication中我们为插件定义资源管理器,如果插件想使用资源文件的话,需要复写几个方法

@Override
    public Resources getResources() {
        if(getApplication() != null && getApplication().getResources() != null){
            return getApplication().getResources();
        }
        return super.getResources();
    }

    @Override
    public AssetManager getAssets() {
        if(getApplication() != null && getApplication().getAssets() != null){
            return getApplication().getAssets();
        }
        return super.getAssets();
    }

    @Override
    public Resources.Theme getTheme() {
        if(getApplication() != null && getApplication().getTheme() != null){
            return getApplication().getTheme();
        }
        return super.getTheme();
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xxpr_ybgg

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值