shadow插件框架調(diào)研與實踐

參考

【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簡介

GitHub

插件化原理: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)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內(nèi)容