參考
【Android 修煉手冊】常用技術(shù)篇 -- Android 插件化解析?https://juejin.im/post/6844903885476233229#heading-22
騰訊插件框架Shadow解析之動態(tài)化和插件加載:https://juejin.im/post/6844903975381270536
Android Tencent Shadow 插件接入指南:http://www.lxweimin.com/p/f00dc837227f
Shadow源碼分析—如何啟動插件Activity:https://www.codenong.com/js90f177bd29ce/
Tencent Shadow—零反射全動態(tài)Android插件框架正式開源:?https://mp.weixin.qq.com/s/lBiJEdD81yOBVqsqXpPRww
作者親筆系列文章:https://juejin.im/user/536217405890903/posts
插件方案:調(diào)研時間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(接入及改造成本高,組件化框架,非多進(jìn)程,未來不支持動態(tài)更新)
RePlugin(360):
stars6.5K, issue:295, 最新版本:2020.8, 最近更新:2020.12, 支持Android9.0(優(yōu)點:業(yè)界口碑較好,時間較長,較穩(wěn)定,只Hook了Classloader。缺點:demo跑不起來,gradle用的還是2.3,issuse多無人回復(fù),AndroidX未適配)
Shadow(騰訊):
stars5.2K, issue:92, 最新版本:2021.2, 最近更新:實時更新, 支持Android9.0(優(yōu)點:原理與RePlugin相似,只Hook了Classloader.parent,無代碼侵入性,支持AndroidX;代碼更新頻繁,作者在線,騰訊部分業(yè)務(wù)APP有使用,穩(wěn)定性有一定保障;缺點:半開源,接入麻煩,擴(kuò)展Activity方法時可能需要二次開發(fā),api不友善;需要自己實現(xiàn)插件下載和版本檢測,跨進(jìn)程通信so加載)
what:shadow簡介
插件化原理:Proxy代理,Shadow通過AOP實現(xiàn)代理
接入消耗:16k,160個方法(19年數(shù)據(jù))
1、代理(shadow方案)
通過在宿主的PathClassloader中插入自定義的父Classloader加載插件的Activity類(此處反射了PathClassloader的parent字段)
資源通過packageManager.getResourcesForApplication實現(xiàn)宿主和插件的Resource隔離
Activity生命周期通過代理Activity轉(zhuǎn)調(diào)
編譯期通過AOP替換插件的Activity父類為ShadowActivity
2、Hook(欺上瞞下方案,反射AMS或Instrumentation):Android9.0以后限制了對系統(tǒng)api的反射,所以Hook方案有很大的風(fēng)險。
why:為什么需要插件化?
動態(tài)化:動態(tài)部署新功能,熱修復(fù)老功能
發(fā)布次數(shù)的提升,發(fā)布頻次的降低
包體積縮小:功能邏輯抽離到遠(yuǎn)程服務(wù)端,可以盡可能的縮小apk尺寸
快速灰度與驗證體系
ABTest
一期todo
工作臺支持簡單動態(tài)化功能,下發(fā)新入口icon/scheme,下載/更新plugin,插件獨立調(diào)用網(wǎng)絡(luò)請求,并可以使用宿主提供的功能
模塊抽取:將B端“工作臺”的“老師管理”功能改造成插件,抽取到plugin_other
支持host向plugin同步token、userInfo等本地數(shù)據(jù)(shadow本身支持的aidl方式)
ShadowHelper.onUpdatePluginConfig(androidApp, Constants.TOKEN, EnvConfig.API_TOKEN);
ShadowHelper.onUpdatePluginConfig(androidApp, Constants.USER_INFO, GsonUtils.toJson(WeUserManager.getUser(DUserInfo.class)));
支持plugin向host發(fā)送Event消息(plugin通過HostEventProvider,向Host發(fā)送Event廣播;Host中的ShadowEventReceiver接收后轉(zhuǎn)發(fā);Event類需要keep)
### plugin:HostEventProvider
### -> sendBroadcast ->
### host:ShadowEventReceiver -> EventBus.getDefault().post() -> onEvent(PluginOtherApiEvent)
HostEventProvider.getDefault().post(new PluginOtherApiEvent("hello host"));
支持host向plugin跳轉(zhuǎn)(通過ShadowInterceptor,攔截Arouter跳轉(zhuǎn),并轉(zhuǎn)發(fā)到plugin頁面;需要在Constants.ROUTER_MAP中配置要跳轉(zhuǎn)的plugin頁面,可以通過appGlobalConfig動態(tài)下發(fā)schemes)
### host:HostEventProvider
### -> Arouter.navigation -> ShadowInterceptor.process -> ShadowHelper.loadPlugin()
### -> 解析path和param,從Constants.ROUTER_MAP中獲取對應(yīng)的activityClassName="com.maltbaby.plugin_other.teacher_manager.YASchoolTeacherActivity"
### -> 隱式跳轉(zhuǎn)到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跳轉(zhuǎn)(plugin通過PluginARouter,向Host發(fā)送Arouter廣播;Host中的ShadowARouterReceiver接收后轉(zhuǎn)發(fā);)
### plugin:PluginARouter -> HostARouterProvider
### -> sendBroadcast ->
### host:ShadowARouterReceiver -> 解析path和param,ARouter.getInstance()
## 注意:"/maltbaby/webView/"必須配置在com.maltbaby.plugin_other.Constants.HOST_ROUTER_CLASS_MAP,用來區(qū)分ARouter跳host還是跳plugin
PluginARouter.getInstance()
.build("/maltbaby/webView/")
.withString(SchemeKey.WEB_URL, "http://www.baidu.com")
.navigation();
支持plugin向host進(jìn)行startActivityForResult的跳轉(zhuǎn)(同樣通過PluginARouter;host需要通過HostEventProvider.setResult向plugin發(fā)廣播;plugin接收到以后反射調(diào)用onActivityResult)
### plugin:TeacherInfoActivity.java,通過PluginARouter.getInstance()進(jìn)行跳轉(zhuǎn),并正常傳遞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跳轉(zhuǎn)到APP啟動plugin(通過ShadowLoadingActivity轉(zhuǎn)發(fā)外部shcheme跳轉(zhuǎn))
### 原先配置在YASchoolTeacherActivity的scheme="/school/schoolTeacher/manager"
### 現(xiàn)在統(tǒng)一配置在ShadowLoadingActivity
<activity android:name="com.listen.shadow.ShadowLoadingActivity" android:screenOrientation="portrait">
? ? <intent-filter>
? ? ? ? <action android:name="android.intent.action.VIEW" />
? ? ? ? <category android:name="android.intent.category.DEFAULT" />
? ? ? ? <category android:name="android.intent.category.BROWSABLE" />
? ? ? ? <data
? ? ? ? ? ? android:host="${HOST}"
? ? ? ? ? ? android:path="/school/schoolTeacher/manager"
? ? ? ? ? ? android:scheme="${SCHEME}" />
? ? ? ? <data
? ? ? ? ? ? android:host="${HOST}"
? ? ? ? ? ? android:path="/plugin_other/main"
? ? ? ? ? ? android:scheme="${SCHEME}" />
? ? </intent-filter>
</activity>
### ShadowLoadingActivity中,收到scheme="/school/schoolTeacher/manager"的時候
### 通過ARouter轉(zhuǎn)發(fā)到"/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()
? ? }
}
插件更新下載機(jī)制:支持通過AppGlobalConfig接口下發(fā)插件下載url,根據(jù)手機(jī)型號,os,或者UserId等過濾條件配置plugin是否生效(plugin下載目錄:/sdcard/Android/data/com.enjoy.malt.teacher/cache/shadow)
支持APK內(nèi)部的plugin組件和外部plugin同時存在,并支持降級(打包組件和插件的時,manifest需要不同配置,參考->插件打包流程)
plugin啟動失敗率和耗時埋點:阿里云log,日志庫=maltbaby-umaten,日志topic=PLUGIN
一期問題
plugin和host的跳轉(zhuǎn)其實是隱式跳轉(zhuǎn),需要分別在host和plugin的Constans中配置ActivityName
plugin要復(fù)用宿主的UI,如:分享,IM,需要通過EventBus發(fā)送消息,而不是在plugin中直接調(diào)用
liveData訂閱無效,需要升級shadow
插件無法使用BaseEnjoyActivity,29以下,@OnLifecycleEvent注解會crash,目前使用BaseEnjoyActivity4Plugin
plugin向host發(fā)消息要用HostEventProvider,跳轉(zhuǎn)到host要用PluginArouter
plugin首次加載時間較長,要5秒左右;大小12M+3M
多進(jìn)程問題:IM、SP、SQLite等數(shù)據(jù)共享
內(nèi)存、存儲空間不足的判斷
插件頁面的數(shù)據(jù)埋點暫不可用(因為ActivityLifeCyclerCallback回調(diào)時,只能獲取到ProxyActivity,無法獲取到具體的插件Activity)
插件安裝后無法降級,比如先加載plugin1,加載plugin2后,再加載plugin1,然而顯示的功能還是plugin2的
5.0設(shè)備loadPlugin的時候會發(fā)生NativeCrash
插件打包流程
打整包:和正常打包流程一樣
打插件包:需要修改2處AndroidManifest.xml的配置
plugin_other/AndroidManifest.xml
shadow_library/AndroidManifest.xml
執(zhí)行打包腳本:
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;例如當(dāng)前基線版本=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:是否開啟插件在啟動階段預(yù)加載
plugin_clean_cache:是否清除之前下載的插件files
filter:插件配置生效的過濾條件(取所有條件的交集,代碼邏輯在ABTestConfigModel.support())
1、os:Android系統(tǒng)版本,例如:28、29
2、brand:Android手機(jī)型號(模糊匹配),例如:huawei、vivo
3、version:app當(dāng)前的版本,例如:2.2.601
4、bizClient:baby還是Teacher端,例如,B、C
5、userId:配置userId的用戶生效
6、notUserId:配置userId的用戶不生效
json:插件詳細(xì)配置信息
1、partKey:插件名稱,默認(rèn)plugin_other
2、pluginUrl:plugin_other-release.zip的下載地址
3、managerUrl:plugin_other_manager-release.apk的下載地址
4、version:當(dāng)前插件版本2.2.601,則version配置成202601,會在data/com.enjoy.baby.teacher/shadow目錄下新建202601,存放plugin和manager文件
5、schemes:插件允許宿主跳轉(zhuǎn)的所有頁面,都需要在schemes中配置(若未配置,則無法跳轉(zhuǎn)到插件頁面)
工作臺的接口eapplication/v6/newApplicationList,需要支持插件的跳轉(zhuǎn)字段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參數(shù)需要和jumpUrl一致,launcher參數(shù)需要和插件中要跳轉(zhuǎn)的頁面ARouter的path一致。
工作臺代碼的onItemClick事件,會判斷是否傳遞了pluginModel字段,如果有,則通過ARouter跳轉(zhuǎn),而ARouter跳轉(zhuǎn)時,如果要跳轉(zhuǎn)的是一個插件的path,則會啟動插件,并跳轉(zhuǎn)。