shadow插件框架调研与实践

参考

【Android 修炼手册】常用技术篇 -- Android 插件化解析 https://juejin.im/post/6844903885476233229#heading-22

腾讯插件框架Shadow解析之动态化和插件加载:https://juejin.im/post/6844903975381270536

Android Tencent Shadow 插件接入指南:https://www.jianshu.com/p/f00dc837227f

Shadow源码分析—如何启动插件Activity:https://www.codenong.com/js90f177bd29ce/

Tencent Shadow—零反射全动态Android插件框架正式开源: https://mp.weixin.qq.com/s/lBiJEdD81yOBVqsqXpPRww

作者亲笔系列文章:https://juejin.im/user/536217405890903/posts

插件方案:调研时间2021-03

VirtualAPK(滴滴):

stars:8.3K, issue:105, 最新版本:2018.9,最近更新:2018.12, 支持Android9.0(代码好久没更新)

Atlas(阿里):

stars:7.9K, issue:80, 最新版本:2019.4, 最近更新:2020.9, 不支持Android9.0(接入及改造成本高,组件化框架,非多进程,未来不支持动态更新)

RePlugin(360):

stars6.5K, issue:295, 最新版本:2020.8, 最近更新:2020.12, 支持Android9.0(优点:业界口碑较好,时间较长,较稳定,只Hook了Classloader。缺点:demo跑不起来,gradle用的还是2.3,issuse多无人回复,AndroidX未适配)

Shadow(腾讯):

stars5.2K, issue:92, 最新版本:2021.2, 最近更新:实时更新, 支持Android9.0(优点:原理与RePlugin相似,只Hook了Classloader.parent,无代码侵入性,支持AndroidX;代码更新频繁,作者在线,腾讯部分业务APP有使用,稳定性有一定保障;缺点:半开源,接入麻烦,扩展Activity方法时可能需要二次开发,api不友善;需要自己实现插件下载和版本检测,跨进程通信so加载)

what:shadow简介

GitHub

插件化原理:Proxy代理,Shadow通过AOP实现代理

接入消耗:16k,160个方法(19年数据)

1、代理(shadow方案)

通过在宿主的PathClassloader中插入自定义的父Classloader加载插件的Activity类(此处反射了PathClassloader的parent字段)

资源通过packageManager.getResourcesForApplication实现宿主和插件的Resource隔离

Activity生命周期通过代理Activity转调

编译期通过AOP替换插件的Activity父类为ShadowActivity

2、Hook(欺上瞒下方案,反射AMS或Instrumentation):Android9.0以后限制了对系统api的反射,所以Hook方案有很大的风险。

why:为什么需要插件化?

动态化:动态部署新功能,热修复老功能

发布次数的提升,发布频次的降低

包体积缩小:功能逻辑抽离到远程服务端,可以尽可能的缩小apk尺寸

快速灰度与验证体系

ABTest

一期todo

工作台支持简单动态化功能,下发新入口icon/scheme,下载/更新plugin,插件独立调用网络请求,并可以使用宿主提供的功能

模块抽取:将B端“工作台”的“老师管理”功能改造成插件,抽取到plugin_other

支持host向plugin同步token、userInfo等本地数据(shadow本身支持的aidl方式)

ShadowHelper.onUpdatePluginConfig(androidApp, Constants.TOKEN, EnvConfig.API_TOKEN);

ShadowHelper.onUpdatePluginConfig(androidApp, Constants.USER_INFO, GsonUtils.toJson(WeUserManager.getUser(DUserInfo.class)));

支持plugin向host发送Event消息(plugin通过HostEventProvider,向Host发送Event广播;Host中的ShadowEventReceiver接收后转发;Event类需要keep)

### plugin:HostEventProvider

### -> sendBroadcast ->

### host:ShadowEventReceiver -> EventBus.getDefault().post() -> onEvent(PluginOtherApiEvent)

HostEventProvider.getDefault().post(new PluginOtherApiEvent("hello host"));

支持host向plugin跳转(通过ShadowInterceptor,拦截Arouter跳转,并转发到plugin页面;需要在Constants.ROUTER_MAP中配置要跳转的plugin页面,可以通过appGlobalConfig动态下发schemes)

### host:HostEventProvider

### -> Arouter.navigation -> ShadowInterceptor.process -> ShadowHelper.loadPlugin()

### -> 解析path和param,从Constants.ROUTER_MAP中获取对应的activityClassName="com.maltbaby.plugin_other.teacher_manager.YASchoolTeacherActivity"

### -> 隐式跳转到plugin的

ARouter.getInstance()

.build(PluginOtherApiScheme.PLUGIN_OTHER_TEACHER_MANAGER)

.withString(SchemeKey.SCHOOL_ID, String.valueOf(mSchoolId))

.withBoolean(ConstantsKey.MODEL_BOOL_KEY, mSchool.isAdmin())

.navigation();

支持plugin向host跳转(plugin通过PluginARouter,向Host发送Arouter广播;Host中的ShadowARouterReceiver接收后转发;)

### plugin:PluginARouter -> HostARouterProvider

### -> sendBroadcast ->

### host:ShadowARouterReceiver -> 解析path和param,ARouter.getInstance()

## 注意:"/maltbaby/webView/"必须配置在com.maltbaby.plugin_other.Constants.HOST_ROUTER_CLASS_MAP,用来区分ARouter跳host还是跳plugin

PluginARouter.getInstance()

.build("/maltbaby/webView/")

.withString(SchemeKey.WEB_URL, "http://www.baidu.com")

.navigation();

支持plugin向host进行startActivityForResult的跳转(同样通过PluginARouter;host需要通过HostEventProvider.setResult向plugin发广播;plugin接收到以后反射调用onActivityResult)

### plugin:TeacherInfoActivity.java,通过PluginARouter.getInstance()进行跳转,并正常传递requestCode

PluginARouter.getInstance()

.build(SchemeUrls.TEACHER_PERMISSION_MANAGER)

.with(extraSchool)

.navigation(this, RequestCodes.FOR_RESULT_PERMISSIONS);

public void onActivityResult(int requestCode, int resultCode, Intent result) {

if (requestCode == RequestCodes.FOR_RESULT_PERMISSIONS) {

    // do some thing

}

}

### host:YAPermissionsActivity.java,需要通过HostEventProvider.setResult()替代Activity.setResult()

HostEventProvider.getDefault().setResult(RequestCodes.FOR_RESULT_PERMISSIONS, RESULT_OK, intent);

setResult(RESULT_OK, intent);

finish();

支持外部scheme跳转到APP启动plugin(通过ShadowLoadingActivity转发外部shcheme跳转)

### 原先配置在YASchoolTeacherActivity的scheme="/school/schoolTeacher/manager"

### 现在统一配置在ShadowLoadingActivity

   

       

       

       

       

            android:host="${HOST}"

            android:path="/school/schoolTeacher/manager"

            android:scheme="${SCHEME}" />

       

            android:host="${HOST}"

            android:path="/plugin_other/main"

            android:scheme="${SCHEME}" />

   

### ShadowLoadingActivity中,收到scheme="/school/schoolTeacher/manager"的时候

### 通过ARouter转发到"/plugin_other_api/teacher_manager"

class ShadowLoadingActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_shadow_loading)

        intentToSchemeActivity()

    }

    private fun intentToSchemeActivity() {

        if (intent?.data?.path?.contains("/school/schoolTeacher/manager") == true) {

            val schoolId = intent?.data?.getQueryParameter("schoolId")

            val role = intent?.data?.getQueryParameter("schoolRole")

            ARouter.getInstance()

                    .build("/plugin_other_api/teacher_manager")

                    .withString("schoolId", schoolId)

                    .withString("schoolRole", role)

                    .navigation()

        } else if (intent?.data?.path?.contains("/plugin_other/main") == true) {

            ARouter.getInstance()

                    .build("/plugin_other_api/main")

                    .navigation()

        }

        finish()

    }

}

插件更新下载机制:支持通过AppGlobalConfig接口下发插件下载url,根据手机型号,os,或者UserId等过滤条件配置plugin是否生效(plugin下载目录:/sdcard/Android/data/com.enjoy.malt.teacher/cache/shadow)

支持APK内部的plugin组件和外部plugin同时存在,并支持降级(打包组件和插件的时,manifest需要不同配置,参考->插件打包流程)

plugin启动失败率和耗时埋点:阿里云log,日志库=maltbaby-umaten,日志topic=PLUGIN

一期问题

plugin和host的跳转其实是隐式跳转,需要分别在host和plugin的Constans中配置ActivityName

plugin要复用宿主的UI,如:分享,IM,需要通过EventBus发送消息,而不是在plugin中直接调用

liveData订阅无效,需要升级shadow

插件无法使用BaseEnjoyActivity,29以下,@OnLifecycleEvent注解会crash,目前使用BaseEnjoyActivity4Plugin

plugin向host发消息要用HostEventProvider,跳转到host要用PluginArouter

plugin首次加载时间较长,要5秒左右;大小12M+3M

多进程问题:IM、SP、SQLite等数据共享

内存、存储空间不足的判断

插件页面的数据埋点暂不可用(因为ActivityLifeCyclerCallback回调时,只能获取到ProxyActivity,无法获取到具体的插件Activity)

插件安装后无法降级,比如先加载plugin1,加载plugin2后,再加载plugin1,然而显示的功能还是plugin2的

5.0设备loadPlugin的时候会发生NativeCrash

插件打包流程

打整包:和正常打包流程一样

打插件包:需要修改2处AndroidManifest.xml的配置

plugin_other/AndroidManifest.xml

shadow_library/AndroidManifest.xml

执行打包脚本:

debug: ./shadowAssemble.sh

release: ./shadowAssembleRelease.sh

会在"project/build"目录生成"plugin_other-debug.zip";

会在"project/shadow_manager/build/outputs/apk/debug"目录生成"shadow_manager-debug.apk" 

重命名:plugin_other-release.zip和plugin_other_manager-release.apk,并上传到阿里云oss;例如当前基线版本=2.2.601,则插件版本=2.2.6011或2.2.6012,在阿里云oss目录上新建2026011和2026012目录

在apollo平台配置appGlobalConfig如下,主要修改"plugin_config"配置:

[

    {

        "key": "feed_upload",

        "config": {

            "00_40": {

                "name": "cacheClose",

                "config": {

                    "upload_cache_image": "0",

                    "upload_cache_video": "0"

                }

            },

            "40_100": {

                "name": "cacheOpen",

                "config": {

                    "upload_cache_image": "1",

                    "upload_cache_video": "1"

                }

            }

        }

    },

    {

        "key": "plugin_config",

        "config": {

            "00_100": {

                "name": "plugin_other",

                "config": {

                    "json": "{\"partKey\":\"plugin_other\",\"pluginUrl\":\"https://test-common-static-resources.oss-cn-hangzhou.aliyuncs.com/app/operate/apk/2026011/plugin_other-debug.zip\",\"managerUrl\":\"https://test-common-static-resources.oss-cn-hangzhou.aliyuncs.com/app/operate/apk/2026011/plugin_other_manager-debug.apk\",\"launcher\":\"/plugin_other_api/teacher_manager\",\"version\":\"2026011\",\"launcherParams\":[{\"type\":\"String\",\"key\":\"schoolId\",\"value\":\"200173\"},{\"type\":\"String\",\"key\":\"schoolRole\",\"value\":\"ADMIN\"}],\"schemes\":[{\"scheme\":\"/plugin_other_api/teacher_manager\",\"activityName\":\"com.maltbaby.plugin_other.teacher_manager.YASchoolTeacherActivity\"},{\"scheme\":\"/plugin_other_api/teacher_info\",\"activityName\":\"com.maltbaby.plugin_other.teacher_manager.TeacherInfoActivity\"},{\"scheme\":\"/plugin_other_api/main\",\"activityName\":\"com.maltbaby.plugin_other.PluginOtherActivity\"}]}",

                    "plugin_enable": "1",

                    "filter": "{\"userId\":[],\"notUserId\":[],\"os\":[],\"brand\":[\"Redmi\",\"vivo\"],\"version\":[\"2.2.601\"],\"bizClient\":[\"B\"]}",

                    "plugin_preload_enable": "1",

          "plugin_clean_cache": "0"

                }

            }

        }

    }

]

plugin_enable:是否开启插件(1:开启,0:不开启)

plugin_preload_enable:是否开启插件在启动阶段预加载

plugin_clean_cache:是否清除之前下载的插件files

filter:插件配置生效的过滤条件(取所有条件的交集,代码逻辑在ABTestConfigModel.support())

1、os:Android系统版本,例如:28、29

2、brand:Android手机型号(模糊匹配),例如:huawei、vivo

3、version:app当前的版本,例如:2.2.601

4、bizClient:baby还是Teacher端,例如,B、C

5、userId:配置userId的用户生效

6、notUserId:配置userId的用户不生效

json:插件详细配置信息

1、partKey:插件名称,默认plugin_other

2、pluginUrl:plugin_other-release.zip的下载地址

3、managerUrl:plugin_other_manager-release.apk的下载地址

4、version:当前插件版本2.2.601,则version配置成202601,会在data/com.enjoy.baby.teacher/shadow目录下新建202601,存放plugin和manager文件

5、schemes:插件允许宿主跳转的所有页面,都需要在schemes中配置(若未配置,则无法跳转到插件页面)

工作台的接口eapplication/v6/newApplicationList,需要支持插件的跳转字段pluginModel,如下:

{       

  "jumpUrl": "maltbabyb://maltbaby_b/school/schoolTeacher/manager?schoolId=200173&schoolRole=ADMIN",

  "pluginModel": {

    "launcherParams": [

      {

        "type": "String",

        "value": "200173",

        "key": "schoolId"

      },

      {

        "type": "String",

        "value": "ADMIN",

        "key": "schoolRole"

      }

    ],

    "launcher": "/plugin_other_api/teacher_manager"

  }

}

pluginModel的launcherParams参数需要和jumpUrl一致,launcher参数需要和插件中要跳转的页面ARouter的path一致。

工作台代码的onItemClick事件,会判断是否传递了pluginModel字段,如果有,则通过ARouter跳转,而ARouter跳转时,如果要跳转的是一个插件的path,则会启动插件,并跳转。

你可能感兴趣的:(shadow插件框架调研与实践)