Android进阶解密④—插件化原理

在学习插件化之前需要看前面几篇文章:

动态加载技术:

在程序运行时,动态加载一些程序中原本不存在的可执行文件并运行起来,,随着应用技术的发展,动态加载技术逐渐派生出两个分支,热修复和插件化;

  • 热修复:用于修复bug
  • 插件化:解决应用庞大,功能模块解耦,复用其他apk的代码
插件化思想:

将复用的apk作为插件,插入另一个apk中,比如淘宝中会有咸鱼的页面,用淘宝为咸鱼引流,使用插件化技术,可以直接使用咸鱼apk中的dex文件,这样省去再次开发一套咸鱼页面的成本,并且有效的降低了淘宝apk的耦合度;

Activity插件化原理:

插件化activity的目的是直接使用另一个apk的activity,而activity的启动和生命周期的管理需要经过AMS的处理,另一个apk的activity没有在本项目的manifest注册,肯定是无法通过的,所以我们需要hook startActivity的流程,绕过ams的验证,可以在本项目使用一个占坑activity,在发送给ams前将插件activity换成占坑activity去通过ams的验证,验证好以后在真实的启动时再将插件activity换回来;

步骤:
  • 事先在本项目准备好占坑activity
  • 使用占坑activity绕过ams验证
  • 还原插件activity

1. 准备占坑activity

直接在原项目准备一个空白的activity即可,记得必须在manifest注册,下文叫他SubActivity

2. 使用插件activity替换占坑activity

在交给ams进程验证之前,在用户进程会经过两个类的传递,Instrumentation, iActivityManager,者两个类都可以作为hook点,这里介绍hook iActivityManager的这种方法;

2.1 创建hook点的代理类,iActivityManagerProxy
public class IActivityManagerProxy implements InvocationHandler {

    private Object realActivityManager;

    public IActivityManagerProxy(Object realActivityManager) {
        this.realActivityManager = realActivityManager;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if ("startActivity".equals(method.getName())){
            //  首先找到,原本需要启动的插件activity的原始intent
            Intent originIntent = null;
            int index = 0;
            for (int i = 0;i<args.length;i++){
                if (args[i] instanceof Intent){
                    originIntent = (Intent) args[i];
                    index = i;
                    break;
                }
            }
            //  新建欺骗ams的占坑activity的intent
            Intent fakeIntent = new Intent();
            fakeIntent.setClass("xxx.xxx.xxx",SubActivity.class);
            //  将真实的intent保存在fakeIntent中用于第三步的还原操作
            fakeIntent.putExtra("real_intent",originIntent);
            //  将fakeIntent写回原来的arges数组中
            args[index] = fakeIntent;
        }
        return method.invoke(realActivityManager,args);
    }
}

这里使用的动态代理创建iActivityManager的代理,首先找到原本启动的插件Activity的Intent,然后新建一个启动SubActivity的intent替换它;

2.2替换原本的iActivityManager:
    public void hookAMS() throws Exception {
        // 获取ActivityManager getService 返回的单例
        Class ActivityManagerClazz = ActivityManager.class;
        Field IActivityManagerSingletonField = ActivityManagerClazz.getDeclaredField("IActivityManagerSingleton");
        Object IActivityManagerSingleton = IActivityManagerSingletonField.get(ActivityManagerClazz);

        //  通过单例.get()获取iActivityManager, 这两步需要参考源码的iActivityManager的获取
        Class singleClazz = IActivityManagerSingleton.getClass();
        Method getMethod = singleClazz.getDeclaredMethod("get");
        Object iActivityManager = getMethod.invoke(IActivityManagerSingleton,null);
        
        // 生成动态代理对象
        Object proxyInstance = Proxy.newProxyInstance(
                ActivityManagerClazz.getClassLoader(),
                ActivityManagerClazz.getInterfaces(),
                new IActivityManagerProxy(iActivityManager));

        // 将代理对象设置到单例上
        Field mInstanceField = singleClazz.getField("mInstance");
        mInstanceField.set(IActivityManagerSingleton,proxyInstance);
    }
  • 这个方法需要在startActivity前调用

3. 还原插件Activity

绕开ams验证后,我们还需要真实的启动TargetActivity,再学习了Handler机制后,我们知道message的处理顺序是首先会判断当前message.callback有没有逻辑,会首先执行callback;我们可以将Message作为Hook点

3.1 创建自定义CallBack,在handleMessage处理前,将fakeIntent换成真实的intent
  class MCallBack implements android.os.Handler.Callback {
        @Override
        public boolean handleMessage(Message msg) {
            try {
                Object activityClientRecord = msg.obj;
                // 获取fakeIntent
                Class acrClazz = activityClientRecord.getClass();
                Field intentField = acrClazz.getDeclaredField("intent");
                Intent intent = (Intent) intentField.get(activityClientRecord);
                // 取出targetActivity的Intent
                Intent realIntent = intent.getParcelableExtra("real_intent");
                // 将realIntent的内容设置到fakeIntent
                intent.setComponent(realIntent.getComponent());
                
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            msg.getTarget().handleMessage(msg);
            return true;
        }
    }
3.2 hook ActivityThread, 修改主线程的H(Handler)的CallBack属性,原理参考dispatchMessage方法
    private void hookActivityThread() throws Exception {
        Class activityThreadClass = Class.forName("android.app.ActivityThread");
        Field singleInstanceField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
        Object activityThreadInstance = singleInstanceField.get(null);
        
        Field mHField = activityThreadClass.getDeclaredField("mH");
        Handler handler = (Handler) mHField.get(activityThreadInstance);
        
        // 修改handler 的callback
        Class handlerClazz = handler.getClass();
        Field callbackField = handlerClazz.getDeclaredField("mCallback");
        callbackField.set(handler,new MCallBack());
    }

在Handler机制中有两个callback,一个是Handler.mCallback,一个是Message.callback

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

在loop中轮询处理message的时候会调用dispatchMessage;如果Message.callback不会null就处理Runnable的回调然后结束,如果msg.callback为null,则先执行Handler的mCallback,并根据Handler的mCallback.handleMessage的返回值判断是否执行Handler.handleMessage;
根据上面的流程,我们可以在ActivityThread的H处理startActivity这个Message的handleMessage前,在H的Callback中插入修改intent的代码,做到真实的开启TargetActivity

3.3 插件Activity的生命周期管理:

上面的操作只做到了开启activity,插件activity的生命周期是如何管理的,AMS通过token来对activity进行识别管理,而插件activity token的绑定是不受影响的,所以插件activity是具有生命周期的;

Service插件化原理

代理分发实现:

当启动插件Service时,就会先启动代理Service,当代理Service运行后,在其onStartCommand中启动插件Service;

步骤:
  • 项目中准备好代理Service
  • hook iActivityManager 启动代理Service
  • 代理分发:
  1. ProxyService需要长时间对插件Service进行分发,所以需要return START_STICKY ProxyService重新创建
  2. 创建插件Service,attach,onCreate;

1. 在项目中创建一个ProxyService,在manifest中注册;

2. hook iActivityManager,将要启动的TargetService换成ProxyService

2.1 创建自定义iActivityManagerProxy
public class IActivityManagerProxy implements InvocationHandler {

    private Object realActivityManager;

    public IActivityManagerProxy(Object realActivityManager) {
        this.realActivityManager = realActivityManager;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if ("startService".equals(method.getName())){
            Intent targetIntent = null;
            int index = 0;
            for (int i = 0;i<args.length;i++){
                if (args[i] instanceof Intent){
                    targetIntent = (Intent) args[i];
                    index = i;
                    break;
                }
            }
            Intent proxyIntent = new Intent();
            proxyIntent.setClassName("com.xx.xx","com.xx.xx.ProxyService");
            proxyIntent.putExtra("target_intent",targetIntent);
            args[index] = proxyIntent;
        }
        return method.invoke(realActivityManager,args);
    }
}
2.2 hook AMS替换原来的IActivityManager 同上
 public void hookAMS() throws Exception {
        // 获取ActivityManager getService 返回的单例
        Class ActivityManagerClazz = ActivityManager.class;
        Field IActivityManagerSingletonField = ActivityManagerClazz.getDeclaredField("IActivityManagerSingleton");
        Object IActivityManagerSingleton = IActivityManagerSingletonField.get(ActivityManagerClazz);

        //  通过单例.get()获取iActivityManager, 这两步需要参考源码的iActivityManager的获取
        Class singleClazz = IActivityManagerSingleton.getClass();
        Method getMethod = singleClazz.getDeclaredMethod("get");
        Object iActivityManager = getMethod.invoke(IActivityManagerSingleton,null);
        
        // 生成动态代理对象
        Object proxyInstance = Proxy.newProxyInstance(
                ActivityManagerClazz.getClassLoader(),
                ActivityManagerClazz.getInterfaces(),
                new IActivityManagerProxy(iActivityManager));

        // 将代理对象设置到单例上
        Field mInstanceField = singleClazz.getField("mInstance");
        mInstanceField.set(IActivityManagerSingleton,proxyInstance);
    }

只要在startService前调用这段代码,就会启动proxyService,下面我们在proxyService中对targetService进行分发;

2.3 在proxyService中启动TargetService:
  • 调用attach绑定Context
  • 调用onCreate
public class ProxyService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        try {
            // 准备attach方法的参数
            Class activityThreadClazz = null;
            activityThreadClazz = Class.forName("android.app.ActivityThread");
            Method getApplicationMethod = activityThreadClazz.getDeclaredMethod("getApplicationMethod");
            Field sCurrentActivityThreadField = activityThreadClazz.getDeclaredField("sCurrentActivityThread");
            sCurrentActivityThreadField.setAccessible(true);
            // activityThread
            Object activityThread = sCurrentActivityThreadField.get(null);
            // applicationThread
            Object applicationThread = getApplicationMethod.invoke(activityThread, null);
            Class iInterFaceClazz = Class.forName("android.os.IInterface");
            Method asBinderMethod = iInterFaceClazz.getDeclaredMethod("asBinder");
            asBinderMethod.setAccessible(true);
            // token
            Object token = asBinderMethod.invoke(applicationThread);
            // iActivityManager
            Class ActivityManagerClazz = ActivityManager.class;
            Field IActivityManagerSingletonField = ActivityManagerClazz.getDeclaredField("IActivityManagerSingleton");
            Object IActivityManagerSingleton = IActivityManagerSingletonField.get(ActivityManagerClazz);
            Class singleClazz = IActivityManagerSingleton.getClass();
            Method getMethod = singleClazz.getDeclaredMethod("get");
            Object iActivityManager = getMethod.invoke(IActivityManagerSingleton, null);

            // targetService
            Class serviceClazz = Class.forName("android.app.Service");
            Service targetService = (Service) serviceClazz.newInstance();

            // attach
            Method attachMethod = serviceClazz.getDeclaredMethod("attach");
            attachMethod.invoke(targetService, this,
                    activityThread, intent.getComponent().getClassName(),
                    token, getApplication(), iActivityManager);
            targetService.onCreate();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return START_STICKY;
    }
}

  • 首先准备attach需要的参数,通过反射获取
  • 调用targetService的attach方法
  • 调用targetService的onCreate方法

资源的插件化

参考这边换肤文章:安卓换肤实现原理

某东的插件化实践

1. 插件化做的事情:

插件化的目的就是在主工程使用插件工程的代码/类资源

1.1插件中四大组件的处理:

当使用插件的四大组件类时,比如插件activity的使用必须做特殊的处理,一般的处理方式有:

  • hook AMS,绕过AMS对四大组件的验证,手动管理生命周期
  • 直接在主工程的manifest注册插件的activity

方式1的特点是实现难度高,灵活性高,但是随着谷歌对于系统非sdk调用的限制,这个方式可能会在未来失效;
方式2的特点是实现简单,但是不够灵活,必须在manifest中写死

某东采用的方式2,直接在manifest中写死插件的四大组件注册

1.2插件中类的加载和使用:

每一个插件都设置一个ClassLoader,目的是整个插件的类都是由一个加载器加载,所有的插件的ClassLoader都在双亲委派中继承了另一个ClassLoader,这个目的是便于主工程的统一管理;


1.3DelegateClassLoader的替换:

替换LoadedAPK的ClassLoader为DelegateClassLoader即可;

1.4插件资源的引用

添加一个path给AssetManager就行,详见上文

2. 插件如何打包进主工程,即主工程如何集成插件包:

  • 将插件apk放入asset目录,通过assetManager去加载
  • 将插件apk文件修改后缀为.so 放入lib/armeabi目录,主工程apk在安装的时候会自动将这个目录的文件加载到data/data/< package_name >/lib/目录下,可以直接获取;

某东使用的第二种以so的形式放入lib目录自动加载,因为在运行时去使用AssetManager加载asset资源会影响程序的运行时速度

3. 插件和主工程如何通信

主要借鉴的airbnb的DeepLinkDispatch

DeepLinkDispatch类似Android原生的scheme协议,用于跳转到另一个APP的页面,比如在美团中打开高德地图,或者在微信中打开京东,

DeepLinkDispatch的实现思路:首先在插件工程中需要被打开的Activity添加注解,然后在主工程调用DeepLinkDispatch.startActivityDirect()传入注解中设置好的参数,最后会通过系统的apiContext.startActivity()开启页面

4. 插件化的未来

随着谷歌对于系统API的限制越来越严格,并且现在已经分成黑名单,深灰名单,浅灰名单留时间开发者调整,插件化应该是没有未来的,我们想想插件化到底是为了什么:

  • 独立编译,提高开发效率
  • 模块解耦,复用代码
东东的解决方案:

组件化:
传统的组件话是新建一个Android Library,在开发调试和实际引用的时候在Application和Library之间切换,东东的组件化是将每一个组件单独做成一个项目,然后在项目结构中保留Application和Android Library,library用于实现组件的功能,app用于开发调试,在主工程使用时直接通过依赖从云端的maven sync;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350