大致目錄:
* 前言
* 外置插件
* 安裝插件
* 升級插件
* 卸載插件
* 內置插件
* 添加內置插件
* 刪除內置插件
* 使用內置插件的時機
* 內置插件的升級
* 預加載插件
* 插件的運行
* 安全與簽名校驗
* 插件管理進程
* 插件的目錄結構
一、前言
無論是插件還是主程序,都可以對自己和其它插件做相應的插件管理工作。但需要理解的是:不是所有的 APK 都能作為 RePlugin
的插件并安裝進來的。必須要嚴格按照《插件接入指南》中所述完成接入,其編譯出的 APK 才能成為插件,且這個 APK 同時也可以被安裝到設備中。
二、外置插件
外置插件是指可通過“下載”、“放入SD卡”等方式來安裝并運行的插件。以下是外置插件的管理方案:
2.1 安裝插件
要安裝一個插件,只需使用 RePlugin.install()
方法,傳遞一個 APK 路徑即可。
RePlugin.install("/sdcard/exam.apk");
注意
-
無論安裝還是升級,都會將源文件移動(而非復制)到插件的安裝路徑(如 app_p_a)上,這樣可大幅度節省安裝和升級時間,但顯然的,源文件也就會消失。
若想改變這個行為,你可以參考
RePluginConfig
中的setMoveFileWhenInstalling()
方法升級插件和此等同,故不再贅述
2.1.1 最佳實踐
以下為 360 手機衛士或其它合作 App 采用的設計,可供你參考:
除非是基礎和核心功能插件,否則請盡量減少“靜默安裝”(指的是用戶無感知的情況下,偷偷在后臺安裝)插件的情況,以減少內部存儲空間的消耗,降低對用戶的影響。
若插件需要下載,則請覆寫
RePluginCallbacks.onPluginNotExistsForActivity()
方法,并在此打開你的下載頁面并控制其邏輯下載插件前建議告知用戶其插件大小,尤其針對運營商網絡的情況
有關“插件下載”的處理,以及針對插件安裝失敗原因做進一步的操作,請閱讀《自定義您的 RePlugin》 中“在插件不存在時,提示下載”一節。
2.1.2 安裝或升級失敗?
安裝或升級失敗(返回值為 Null
)的原因有如下幾種:
是否開啟了“簽名校驗”功能且簽名不在“白名單”之中?—— 通常在 Logcat 中會出現
“verifySignature: invalid cert: ”
。如是,則請參考安全與簽名校驗一節,了解如何將簽名加白,或關閉簽名校驗功能(默認為關閉)是否將 replugin-host-lib 升級到 2.1.4 及以上?—— 在 2.1.3 及之前版本,若沒有填寫
“meta-data”
,則可能導致安裝失敗,返回值為null
。官方在 2.1.4 版本中已經修復了此問題(衛士和其它 App 的所有插件都填寫了meta-data
,所以問題沒出現)APK 安裝包是否有問題?—— 請將插件 APK 直接安裝到設備上(而非作為插件)試試。如果在設備中安裝失敗,則插件安裝也一定是失敗的。
是否沒有 SD 卡的讀寫權限?—— 如果你的插件 APK 放到了 SD 卡上,則請務必確保主程序中擁有 SD 卡權限(主程序
Manifest
要聲明,且 ROM 允許),否則會出現權限問題,當然,放入應用的files
目錄則不受影響。設備內部存儲空間是否不足?—— 通常出現此問題時其 Logcat 會出現
“copyOrMoveApk: Copy/Move Failed”
的警告。如是,則需要告知用戶去清理手機。
2.2 升級插件
為了簡化操作,升級插件的做法和安裝是一樣的,仍可以直接調用 RePlugin.install()
方法。
RePlugin.install("/sdcard/exam_new.apk");
注意
如果插件正在運行,則不會立即升級,而是“緩存”起來。直到所有“正在使用插件”的進程結束并重啟后才會生效
升級可能會占用內部存儲空間(因為要釋放新的 APK)
不支持“插件降級”,但可以“同版本覆蓋”(在 RePlugin 2.1.5 版本中開始支持)
出于穩定性和實際需求考慮,
RePlugin
暫時沒有計劃支持“熱修復”方案。
2.2.1 最佳實踐
以下為 360 手機衛士或其它合作 App 采用的設計,可供你參考:
大部分情況下,應盡可能“靜默升級”,以減少對用戶的打擾
針對升級而言,可在后臺線程做一次“預加載”,提前釋放 Dex。具體做法:
PluginInfo pi = RePlugin.install("/sdcard/exam_new.apk");
if (pi != null) {
RePlugin.preload(pi);
}
-
若插件正在運行,則會有兩種場景,需分別對待:
若是遇到嚴重問題,需要“強制升級”,則應立即提示用戶,待同意后則重啟進程
通常情況下,建議在“鎖定屏幕”后重啟進程,讓其在后臺生效
若插件沒有運行,則可直接升級
2.3 卸載插件
要卸載插件,則需要使用 RePlugin.uninstall()
方法。只需傳遞一個“插件名”即可。
RePlugin.uninstall("exam");
注意
如果插件正在運行,則不會立即卸載插件,而是將卸載訴求記錄下來。直到所有“正在使用插件”的進程結束并重啟后才會生效
由于內置插件是捆在主程序包內的,故無法卸載“內置插件”(此處有待官方優化)。
出于穩定性和實際需求考慮,
RePlugin
暫時沒有計劃支持“熱卸載”方案。
2.3.1 最佳實踐
以下為 360 手機衛士或其它合作 App 采用的設計,可供你參考:
在卸載時彈出對話框,提示用戶“是否同意卸載”
-
若插件在運行時需要被卸載,則有兩種做法:
提示用戶“需要重新啟動應用才能生效”
在“鎖定屏幕”后重新啟動進程,讓其在后臺生效
若插件沒有運行,則可以直接卸載,無需提示用戶
三、內置插件
內置插件是指可以“隨著主程序發版”而下發的插件,通常這個插件會放到主程序的 Assets
目錄下。
針對內置插件而言,開發者無需調用安裝方法,由 RePlugin
來“按需安裝”。
“內置插件”是可以被“升級”的。升級后的插件等同于“外置插件”。
3.1 添加內置插件
添加一個內置插件是非常簡單的,甚至可以無需任何 Java 代碼。只需兩步即可:
將 APK 改名為:
[插件名].jar
([ ]
在實際上不需要添加)放入主程序的
assets/plugins
目錄
這樣,當編譯主程序時,RePlugin
的“動態編譯方案”會自動在 assets
目錄下生成一個名叫 “plugins-builtin.json”
文件,記錄了其內置插件的主要信息,方便運行時直接獲取。
必須改成 “[插件名].jar”
后,才能被 RePlugin-Host-Gradle
識別,進而成為“內置插件”。
[插件名] 可以是“包名”,也可以是“插件別名”。有關這方面的說明,請閱讀《插件的信息》中“插件命名”一節。
3.2 刪除內置插件
刪除內置插件非常簡單,直接移除相應的 Jar 文件,其余均交給 RePlugin
來自動化完成。
注意:若用戶已使用了內置插件,則即便用戶升級主程序,其包內已不帶這個內置插件,但用戶仍可繼續使用它
這樣可防止出現“用戶升級主程序后,發現內置插件突然用不了”的情況。
3.3 使用內置插件的時機
不同于“外置插件”需要先調用 RePlugin.install
方法后才能使用,內置插件可無需調用此方法。而一旦插件被使用,則 RePlugin
會在觸發相應邏輯前,為你做下列操作:
將內置插件釋放到數據目錄下(近似于調用
install()
方法)若需要加載 Dex,則還會釋放優化后的 Dex 到數據目錄下,這可能會需要一些時間
這樣做的好處是,不會占用太多的“內部存儲空間”,畢竟不是所有內置插件,都一定會被用到。
3.4 內置插件的升級
內置插件的升級分為兩種情況:主程序隨包升級、通過 install()
方法升級
主程序隨包升級:當用戶升級了帶“新版本內置插件”的主程序時,則
RePlugin
會在使用插件前先做升級通過
install()
方法升級:若通過RePlugin.install()
方法做的升級(大多為用戶從服務器上下載并更新),則RePlugin
在調用install()
方法時開始做升級。當然,其規則仍遵循安裝插件的規則,例如“插件運行時先不覆蓋”等。
值得注意的是,無論采用何種方式,均“不支持降級”,但支持“同版本覆蓋”升級,也即:
內置插件:只要 APK 的時間戳和大小發生變化就升級,若兩者均無變化,則不會升級。(在 RePlugin 2.1.5 版本中開始支持)
外置插件:只要調用
RePlugin.install()
方法即可將“內置插件”轉化為“外置插件”。同樣的,需遵循安裝插件規則。
3.5 最佳實踐
以下為 360 手機衛士或其它合作 App 采用的設計,可供你參考:
需控制“內置插件”的數量,因為會占用主程序 APK 的大小
-
比較適合成為“內置插件”的有:
核心業務插件:沒有它就等于“核心功能缺失”。比如 360 手機衛士的“首頁體檢”、“清理”插件等
基礎插件:各插件都需要用到,且為必須的。比如“安全 WebView”、“下載”插件等
啟動時必備插件:明確要在啟動時要用到的功能。比如 360 手機衛士的 “Push”、“常駐服務管理”等
可將一些啟動時必須要加載的,以及經常要用到的內置插件做一次“預加載”。具體做法:
RePlugin.preload("exam");
四、預加載插件
什么是預加載?一言以蔽之,就是將插件的 Dex “提前做釋放”,并將 Dex 緩存到內存中,這樣在下次啟動插件時,可無需走 dex2oat 過程,速度會快很多。
預加載不會做下列事情:
不會“啟動插件”
不會加載其
Application
對象不會打開
Activity
和其它組件等。
換言之,預加載的目的非常單純,就是提前釋放 Dex,僅此而已。
4.1 預加載的用法
如之前所述,預加載有兩種做法:
- 預加載當前安裝的插件
此為絕大多數用到的場景。直接預加載當前安裝的插件即可,如果當前正在運行這個插件,則調用此方法則是無效的,畢竟當前插件已經早就被使用過了。
可使用 RePlugin.preload(pluginName)
,例如:
RePlugin.preload("exam");
- 預加載新安裝的插件
此場景主要用于“后臺升級某個插件”。如果此插件“正在被使用”,則必須借助RePlugin.install()
方法的返回值(新插件的信息)來做預加載。
可使用 RePlugin.preload(PluginInfo)
,例如:
PluginInfo pi = RePlugin.install("/sdcard/exam_new.apk");
if (pi != null) {
RePlugin.preload(pi);
}
4.2 最佳實踐
以下為 360 手機衛士或其它合作 App 采用的設計,可供你參考:
建議將
RePlugin.preload()
方法的調用放到“工作線程”中進行。由于此方法是“同步”的,所以直接在 UI 線程中調用時,可能會卡住,甚至導致 ANR 問題。如果正在
preload
某插件,則無論在哪個進程和線程,在過程中加載這個插件時,可能會出現卡頓,這和為了安全起見,做了進程鎖有關。建議在preload
做完后再打開此插件。
五、插件的運行
插件運行的場景有很多,包括:
打開插件的四大組件
獲取插件的
PackageInfo/Context/ClassLoader
等預加載
(preload)
使用插件
Binder
如果想判斷插件是否在運行,可使用 RePlugin.isPluginRunning()
方法。
六、安全與簽名校驗
作為一家安全公司旗下的開源項目,其“安全性”是作為其重點之一來考慮的。曾經有幾個 App 在使用動態加載 Dex 方案(非 RePlugin
)時,被爆出有可能攜帶“病毒”,經追查發現是由于沒有對外來的 Dex 和 Apk 做“校驗”導致。所以說,一旦不做校驗,則不排除惡意人會劫持 DNS 或網絡,并通過網絡來下發惡意插件,對你的應用造成很不好的影響。
若開啟此開關,則一旦簽名校驗失敗,則會在 Logcat 中提示
“verifySignature: invalid cert”
,且install()
方法返回null
。此外,出于性能考慮,內置插件無需做“簽名校驗”,僅“外置插件”會做。
要打開簽名校驗也是非常簡單的。只需兩步:
第一步:打開開關
例如,若你繼承 RePluginApplication
,則請在創建 RePluginConfig
時調用其 setVerifySign(true)
即可。
當然,更推薦的做法是傳遞 !BuildConfig.DEBUG
參數。這表示:若為 Debug
環境下則無需校驗簽名,只有 Release
才會校驗。以下是具體用法:
@Override
protected RePluginConfig createConfig() {
RePluginConfig c = new RePluginConfig();
c.setVerifySign(!BuildConfig.DEBUG);
...
return c;
}
如果你是“非繼承式”,則需要在調用 RePlugin.App.attachBaseContext()
的地方,傳遞RePluginConfig
,并設置 setVerifySign
即可。以下是具體用法:
RePluginConfig c = new RePluginConfig();
c.setVerifySign(!BuildConfig.DEBUG);
...
RePlugin.App.attachBaseContext(context, c);
自 RePlugin 2.1.4 版本開始,默認將“關閉”簽名校驗,之前默認為“開啟”。
第二步:加入合法簽名
光是打開其開關還是不夠的,還應該將“合法的簽名”加入到 RePlugin
的“白名單”中,可調用 RePlugin.addCertSignature()
來完成。例如:
// Add signature to "White List"
RePlugin.addCertSignature("379C790B7B726B51AC58E8FCBCFEB586");
其中,其參數傳遞的是簽名證書的 MD5,且去掉“:”’。
請務必去掉“:”,且不要傳遞 SHA1 或其它非簽名 MD5 內容
獲取簽名的做法有很多,比較推薦的是使用 keytool
工具,可參見此文檔的介紹。
出于性能考慮,RePlugin 不會自動將“主程序簽名”加入進來。如有需要,建議你自行加入。
6.1 最佳實踐
以下為 360 手機衛士或其它合作 App 采用的設計,可供你參考:
強烈建議開啟安全和簽名校驗
若在調用
install()
方法前就已對 APK 做了校驗(例如,手機衛士是云控加密 MD5 + V5 簽名校驗),則可關閉,以避免重復校驗請盡量不要使用和“主程序”一樣的簽名,而是單獨創建一個
七、插件管理進程
由于 RePlugin
支持獨特的“跨進程安全通訊”(見 IPC 類)以及復雜的插件管理機制,為保證插件能統一由“一個中心”來管理,提高每個進程的啟動、運行速度,官方團隊在設計 RePlugin
之初,就設計了一個“插件管理進程”,所有插件、進程等信息均在此進程中被記錄,各進程均從此中獲取、修改等,而無需像其它那樣,要求“每個進程各自初始化信息”。RePlugin 的這種做法有點像 AMS
。
7.1 目前我們有兩種進程可以作為“插件管理進程”:
7.1.1 以“常駐進程”作為“插件管理進程”(默認)
在 RePlugin 2.1.7 及以前版本,這是唯一的方式。RePlugin
默認的“常駐進程”名為“:GuardService”
,通常在后臺運行,存活時間相對較久。這樣的最大好處是:應用“冷啟動”的概率被明顯的降低,大部分都變成了“熱啟動”,速度更快。
適合作為常駐進程的場景包括:
以后臺服務為主要業務的應用,例如:手機安全類、健身和健康監控類、OS 內應用等
需要有常駐通知欄的應用,例如:音樂類、清理類等
需保持常連接(例如
Push
等)的應用,如:即時通訊類、泛社交類等
目前市面上多數應用都集成了推送功能(例如友盟、極光推送),常駐進程可以掛載在那里。
優點,這是結合“常駐進程”長期存活的特點而展開的:
各進程啟動時,插件信息的獲取速度會更快(因直接通過
Binder
從常駐進程獲取)只要常駐進程不死,其它進程殺掉重啟后,仍能快速啟動(熱啟動,而非“冷啟動”)
如果做得好的話,甚至可以做到 “0 秒啟動”,如 360 手機衛士。
缺點:
若應用為“冷啟動”(無任何進程時啟動),則需要同時拉起“常駐進程”,時間可能有所延長
若應用對“進程”數量比較敏感,則此模式會無形中“多一個進程”
7.1.2 以“主進程”作為“插件管理進程”
和“常駐進程”不同的是,自 RePlugin 2.2.0 開始,主進程也可以作為“插件管理進程”。這樣做的最大好處是:應用啟動時,可以做到“只有一個進程”(注意,這不代表你不能開啟其它插件進程,這里只是說沒有“常駐進程”了而已)。當然,代價是享受不到“常駐進程”時的一些好處。
從適用場景上來看,只要是不符合上述“常駐進程”中所涉及到的場景的,本模式都適合。
優點:
無需額外啟動任何進程,例如你的應用只有一個進程的話,那采用此模型后,也只有一個進程
應用冷啟動(無任何進程時啟動)的時間會短一些,因為無需再拉起額外進程
缺點:
“冷啟動”的頻率會更高,更容易被系統回收,再次啟動的速度略慢于“熱啟動”
7.2 如何使用?
若不設置,則默認是以“常駐進程”作為“插件管理進程”。
如需切換到以“主進程”作為“插件管理進程”(也即不產生額外進程),則需要在宿主的 app/build.gradle
中添加下列內容,用以設置 persistentEnable
字段為 False
:
apply plugin: 'replugin-host-gradle'
repluginHostConfig {
// ... 其它RePlugin參數
// 設置為“不需要常駐進程”
persistentEnable = false
}
八、插件的目錄結構
無論是內置插件,還是外置插件,為了保證穩定性,RePlugin
會把經過驗證的插件放到一個特殊的目錄下,以防止“源文件”被刪除后的一些問題。
由于歷史原因,內置插件和外置插件的存放路徑略有不同。以下將分別予以說明。以下為簡化起見,將 “/data/data/[你的主程序包名]”
統一簡化成“主程序路徑”:
外置插件(未來將只有這一種目錄):
APK 存放路徑:主程序路徑/app_p_a
Dex 存放路徑:主程序路徑/app_p_od
Native 存放路徑:主程序路徑/app_p_n
插件數據存放路徑:主程序路徑/app_plugin_v3_data
內置插件 & 舊 P-N 插件(未來將等同于外置插件):
APK 存放路徑:主程序路徑/app_plugin_v3
Dex 存放路徑:主程序路徑/app_plugin_v3_odex
Native 存放路徑:主程序路徑/app_plugin_v3_libs
插件數據存放路徑:主程序路徑/app_plugin_v3_data
8.1 文件的組織形式
外置插件:為了方便使用,插件會有一個 JSON 文件,用來記錄所有已安裝插件的信息。目前位于
“主程序路徑/app_p_a/p.l”
中。有興趣的朋友可以自行打開此文件來閱覽其中內容。內置插件:不同于外置插件,內置插件 的 JSON 文件只存放于主程序
“assets/plugins-builtin.json”
文件下。每次會從那里獲取信息。
官方計劃將“內置插件”的管控做到和“外置插件”的一致。屆時兩者的管理將變得統一起來。
- P-N 插件(即將廢棄):由于歷史原因,P-N 插件不采用記錄 Json 的形式,而是在
“主程序路徑/files”
下,檢索所有“p-n-”
開頭,且末尾為“.jar”
的文件,并讀取其內容頭,進而找到插件的信息,并記錄到內存中。由于該方案即將廢棄(雖然截止到 2017 年 7 月,衛士多數插件仍然在用,同樣穩定),故這里不再贅述。