插件化與組件化
插件化就是將一個app分為一個宿主和多個模塊(插件),宿主是被真正安裝到設備的apk,負責加載插件,每個插件都是一個獨立的apk,最終打包發布時宿主和插件分開或者聯合打包。
組件化也是將一個app分為一個宿主和多個模塊(組件),每個組件可以是一個單獨的模塊,也可以相互依賴,最終打包發布時宿主和組件打包成一個apk。
關于插件化與組件化的解釋,這里參考了這篇文章。
為什么組件化
- 模塊解耦,業務模塊組件更加獨立。
- 重用公共庫模塊,減少重復開發和維護的工作量。
- 并行開發,模塊組件支持熱更新,加快版本迭代速度,解決用戶需要頻繁更新app問題。
- 有效減少編譯時間,可以單獨編譯和調試單個模塊,提高開發效率。
- 方便測試,可以針對單個模塊進行測試。
注意:組件化/插件化只是針對一些重運營和大型app的需要而誕生的,如果你的app沒有這方面的需求就沒必要了,不然反而變得麻煩。
選哪個框架
框架 | 作者 | 描述 |
---|---|---|
DroidPlugin | 360 | 插件化框架,免安裝運行apk |
VirtualApp | asLody | 插件化框架,與DroidPlugin類似 |
Small | 林光亮 | 一個輕量,跨平臺,高度透明的組件化框架 |
Atlas | 阿里巴巴 | 手機淘寶的容器化框架,目前了解不多,不評論 |
DynamicAPK | 攜程 | 組件化框架,目前已停止維護 |
Dynamic-load-apk | 百度 | 組件化框架,使用代理的方式實現Activity生命周期,代碼中需要用that代替this |
這里只用過DroidPlugin和Small,而最終選擇了Small,不用DroidPlugin的主要原因是它不支持插件間的代碼和資源相互調用,和項目需求不符合。選擇Small主要有以下原因:
- 已經過商業應用的驗證,目前本人已知使用Small的應用有酷狗和千米電商云
- 對項目代碼改動不大
- 支持組件模塊間的依賴
- 文檔比較完善
關于Small與各框架的詳細對比可以看這里。
Small
- Small Github:https://github.com/wequick/Small
- Small 文檔:http://code.wequick.net/Small/cn/home
- Small FAQ:https://github.com/wequick/Small/wiki/Android-FAQ
1. 集成Small
關于如何集成Small可以查看文檔這里
2. 項目結構說明
Small 將一個 APK 拆分為多個公共庫插件、業務模塊插件,它們都是 Android Studio 下的一個 Module。
- 業務模塊插件:
Phone & Tablet Module
,模塊名稱格式app.*
,包名格式packageName.app.*
。 - 公共庫插件:
Android Library
,模塊名稱格式lib.*
,packageName.lib.*
。
Small 通過特定的包名格式識別插件,所以包名需要符合規范。
這里以 Small 的 sample 項目為例子,對各模塊做一個簡單說明:
- app:宿主模塊,一般只加載和啟動插件,不包含業務邏輯
- app+stub:app模塊的子模塊,該模塊的代碼和資源為其他模塊所共享,打包時將自動并入app模塊,用于存放各模塊共享的資源和代碼。
- app.detail:業務模塊插件
- app.home:業務模塊插件
- app.main:業務模塊插件
- app.mine:業務模塊插件
- app.ok-if-stub:訪問
app.stub
模塊資源測試 - jni_plugin:jni庫依賴測試
- lib.analytics:數據統計庫
- lib.style:樣式庫
- lib.utils:工具類庫
- web.about:本地web網頁模塊
關于業務模塊插件的劃分,在項目中我是以業務模塊頁面跳轉為一個分界點,比如業務流程是: 登錄注冊 -> 主頁 -> 直播室
,那么就劃分為 app.user
, app.home
和 app.live
。
3. 依賴關系
- 宿主不能依賴任何插件
-
lib.*
之間不能相互依賴(代碼可以,資源不可以,建議還是不要依賴) -
app.*
可以依賴lib.*
4. 打包發布
關于插件的編譯打包流程可以查看Small的文檔,下面是我在編譯打包過程遇到的一些問題:
- 如果
lib.*
中的資源有增減,先把public.txt
刪除,build 時會自動重新生成資源id,否則有可能遇到Resources$NotFoundException
。 - 正式打包發布時建議完整執行一遍
cleanLib -> cleanBundle -> buildLib -> buildBundle
命令,并確保每個步驟順利編譯。 - 插件編譯打包完成后就在
app\smallLibs
目錄下,現在 app(宿主) 模塊就是一個完整的項目了,對 app 模塊打包簽名就可以了。
5. 實際應用中遇到的問題
統一管理不同 Module 的依賴庫版本
如果不同的插件中引用了同一個第三方庫的不同版本,可能出現 pre-verified 異常。
所以,注意抽取公共庫和 統一管理不同 Module 的依賴庫版本。
配置插件(so)生成目錄
Small 默認情況下是把插件生成到 armeabi 目錄下,如果想更改可以在 local.properties
添加如下配置:
bundle.arch=armeabi-v7a
或者在 project-level 下的 build.gradle 中添加如下配置(建議):
System.setProperty("bundle.arch", "armeabi-v7a")
或者以命令行參數方式設置
gradlew buildLib -Dbundle.arch=armeabi-v7a
small plugin 中通過讀取
bundle.arch
屬性設置插件輸出目錄,具體可以查看RootExtension.getBundleOutput
app.A和app.B都依賴同樣的第三方庫(jar,aar)會不會沖突?
會的,公共庫可以放在 app.stub
或者 lib.*
中,app.A
和 app.B
通過依賴 lib.*
共享該庫。
解決集成 Bmob 時 okhttp 庫沖突問題
這是原來的配置:
compile "cn.bmob.android:bmob-sdk:3.5.0"
錯誤日志如下:
Error:Execution failed for task ':demo:transformClassesWithJarMergingForDebug'.
> com.android.build.api.transform.TransformException: java.util.zip.ZipException: duplicate entry: okhttp3/Address.class
嘗試過下面的方案:
compile ("cn.bmob.android:bmob-sdk:3.5.0") {
exclude group: "com.squareup.okhttp3"
exclude group: "com.squareup.okio"
}
理論上這樣該結束了,但遇到的情況還要復雜一點,有 lib.a 和 lib.b,lib.b 依賴 lib.a(bmob-sdk在這里),app 依賴 lib.b,我在 lib.a 中添加如上配置發現并沒有效果,用 everything 搜了一下,發現 lib.b 的 build 目錄下也有一個 bmob-sdk
多個 Module 包含重復的庫可以在 app 目錄下的 build.gradle 添加如下配置過濾掉重復的庫
android {
configurations {
all*.exclude group: "com.squareup.okio", module: "okio"
all*.exclude group: "com.squareup.okhttp3"
all*.exclude group: 'com.google.code.gson'
}
}
解決方法出自這里
編譯是沒問題了,但是后來打包插件 so 時發現 bmob-sdk 中的 okhttp, okio, gson 還是會被打進去,會導致啟動失敗...
最終的解決方案是把 bmob-sdk-3.5.0.aar(具體位置可以用 everything 搜索一下) 中的 res, jni 和 libs 中的 BmobSDK_3.5.0_20160630.jar 直接拷貝的自己的 lib 工程對應目錄,然后去掉 gradle 中的依賴配置,這樣就不存在沖突了。
AndroidManifest.xml
注意把第三方庫或者SDK需要用到的權限和相關組件的配置添加到 app(宿主)或者 app+stub 模塊下的 AndroidManifest.xml。
In strict mode, we do not allow vendor aars
Execution failed for task ':app.test:processReleaseResources'.
> In strict mode, we do not allow vendor aars, please declare them in host build
- compile('com.android.support:recyclerview-v7:25.0.0')
- compile('com.android.support:design:25.0.0')
or turn off the strict mode in root build.gradle:
small {
strictSplitResources = false
}
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug
解決辦法:
- cleanLib,再buildLib試下
- 添加
strictSplitResources = false
配置
參考 issues 175 和 issues 201
怎樣判斷是否 Debug 模式
開始時想通過訪問 lib 中的 BuildConfig.DEBUG
在各插件中判斷是否 debug 模式,后來發現 lib 中的 BuildConfig.DEBUG
只會一直返回 false。最后是通過訪問 application 節點的 android:debuggable
解決了該問題。解決方案出自這里。
/**
* app 是否 debug 模式
*
* @param context
*/
public static boolean isDebug(Context context) {
if (isDebug == null) {
isDebug = context.getApplicationInfo() != null &&
(context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
}
return isDebug;
}
自定義 Application 放哪里
Small 支持每個插件有自己的 Application(支持 MultiDexApplication),在插件被加載時執行 Applicaiton 的生命周期方法,但一般情況下我們只需要一個 Application。
如果把自定義 Application 放 app
或 app+stub
,無法訪問 lib 模塊下的代碼,放在某個插件下其他插件又訪問不了,所以放在一個 lib 下最合適,所有插件通過引用該 lib 并在 AndroidManifest.xml
的 application 節點配置自定義 Application。
由于插件被加載時都會執行一次 Application 的生命周期,所以為了防止重復初始化,這里通過一個靜態的布爾值變量 isInited
記錄是否已經初始化。示例代碼如下:
public class MyApplication extends Application {
private static boolean isInited = false;
@Override public void onCreate() {
super.onCreate();
if (!isInited) {
isInited = true;
init();
}
}
private void init(){
}
}
這樣,無論是整包運行,還是調試單個插件都能正常完成 Application 的初始化。
更多問題建議查看 Small 的 issues,因為很多問題都已經有人遇到并解決了。