Android組件化開發,Small應用實踐

插件化與組件化

插件化就是將一個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

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.homeapp.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.Aapp.B 通過依賴 lib.* 共享該庫。

issues 318

解決集成 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

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 175issues 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 放 appapp+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,因為很多問題都已經有人遇到并解決了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容