安卓插件化实例

原创 2017年08月11日 17:55:59

最近想研究研究安卓插件化的知识,看了看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();
    }

如果需要顺利运行demo的话,需要将chajiandemo生成的apk命名为chajiandemo.apk然后放到手机存储的根目录下,再次强调下demo是基于安卓5.1的
[ Demo下载 ]

版权声明:本文为博主原创文章,未经博主允许不得转载。

如何使用HierarchyViewer分析优化布局

为什么使用HierarchyViewer     不合理的布局会使我们的应用程序UI性能变慢,HierarchyViewer能够可视化的角度直观地获得UI布局设计结构和各种属性的信息,帮助我...

Android Launcher常用修改

文章出处:http://blog.csdn.net/wangjicong_215/article/details/52605255 Android Launcher 一些默认修改(一) ...

安卓学习笔记 6-13 插件化开发(换肤)

插件化开发比较流行且易实现的用法是切换主题 首先,我们需要对布局进行监听,不然用户可能在更改主题后,下次进入app会看见原始的主题变成之后的,影响体验 我们来写一个类,继承自LayoutInf...

安卓旧项目使用Small框架插件化改造踩坑记

我们团队把一个10万行安卓代码的旧项目(电商系统管理台App),使用Small框架做了插件化改造。把项目分成了10多个插件模块,解除了业务模块之间的代码耦合,为业务功能的快速迭代和多团队并行开发做好基...
  • offbye
  • offbye
  • 2016年07月23日 10:14
  • 4753

安卓插件化与热修复的选型

参考文章: 安卓插件化的过去现在和未来 张涛 http://kymjs.com/code/2016/05/04/01 安卓插件化从入门到放弃 包建强 http://www.infoq.com/cn/...

android -- 框架 安卓应用程序插件化开发框架 -AAP Framework【开源项目】

介绍 这个框架的初衷,是为了方便让程序模块化、插件化,将一个apk应用拆分为多个apk。 不明白这个插件化、模块化是怎么回事的话,可以看看腾讯微信的安卓客户端中的插件配置。 在这里我...

安卓应用程序插件化开发框架 -AAP Framework

介绍 这个框架的初衷,是为了方便让程序模块化、插件化,将一个apk应用拆分为多个apk。 不明白这个插件化、模块化是怎么回事的话,可以看看腾讯微信的安卓客户端中的插件配置。  在这里我会以腾讯微...

安卓插件化框架学习-前情提要

酝酿了很久,终于下定决心写一系列关于去年在“掌视亿通信息技术有限公司”学习到的新技术,权当记录那段大家为了理想而疯狂的峥嵘岁月吧 接触插件化框架是从16年8月或更早些时候开始的,因为我们招来了7年技...

安卓应用程序插件化开发框架 -AAP Framework【开源项目】

介绍 这个框架的初衷,是为了方便让程序模块化、插件化,将一个apk应用拆分为多个apk。 不明白这个插件化、模块化是怎么回事的话,可以看看腾讯微信的安卓客户端中的插件配置。 在这里我...

安卓之插件化开发使用DexClassLoader&AssetManager来更换皮肤

这篇文章主要使用DexClassLoader来实现插件化更换皮肤,即将皮肤独立出来做成一个皮肤插件apk,当用户想使用该皮肤时需下载(不需要安装)对应的皮肤插件apk 效果图【为方便测试,主要通过改变...
  • cxmscb
  • cxmscb
  • 2016年09月06日 11:08
  • 1249
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:安卓插件化实例
举报原因:
原因补充:

(最多只允许输入30个字)