Android完全組件化demo發佈

(本文提出的組件化方案已經開源,參見Android完全組件化開源項目)java

今年6月份開始,我開始負責對「獲得app」的android代碼進行組件化拆分,在動手以前我查閱了不少組件化或者模塊化的文章,雖然有一些收穫,可是不多有文章可以給出一個總體且有效的方案,大部分文章都只停留在組件單獨調試的層面上,涉及組件之間的交互就不多了,更不用說組件生命週期、集成調試和代碼邊界這些最棘手的問題了。有感於此,我以爲頗有必要設計一套完整的組件化方案,通過幾周的思考,反覆的推倒重建,終於造成了一個完整的思路,整理在個人第一篇文章中Android完全組件化方案實踐。這兩個月以來,獲得的Android團隊按照這個方案開始了組件化的拆分,通過兩期的努力,目前已經拆分兩個大的業務組件以及數個底層lib庫,並對以前的方案進行了一些完善。從使用效果上來看,這套方案徹底能夠達到了咱們以前對組件化的預期,而且架構簡單,學習成本低,對於一個急需快速組件化拆分的項目是很適合的。如今將這套方案開源出來,歡迎你們共同完善。代碼地址:https://github.com/luojilab/DDComponentForAndroidandroid

雖然說開源的是一個總體的方案,代碼量其實不多,簡單起見demo中作了一些簡化,請你們在實際應用中注意一下幾點: (1)目前組件化的編譯腳本是經過一個gradle plugin提供的,如今這個插件發佈在本地的repo文件夾中,真正使用的使用請發佈到本身公司的maven庫 (2)組件開發完成後發佈aar到公共倉庫,在demo中這個倉庫用componentrelease的文件夾代替,這裏一樣須要換成本地的maven庫 (3)方案更側重的是單獨調試、集成編譯、生命週期和代碼邊界等方面,我認爲這幾部分是已發表的組件化方案所缺少的或者比較模糊的。組件之間的交互採用接口+實現的方式,UI之間的跳轉用的是一箇中央路由的方式,在這兩方面目前已有一些更完善的方案,例如經過註解來暴露服務以及自動生成UI跳轉代碼等,這也是該方案後面須要着力優化的地方。若是你已經有更好的方案,能夠替換,更歡迎推薦給我。git

1、AndroidComponent使用指南

首先咱們看一下demo的代碼結構,而後根據這個結構圖再次從單獨調試(發佈)、組件交互、UI跳轉、集成調試、代碼邊界和生命週期等六個方面深刻分析,之因此說「再次」,是由於上一篇文章咱們已經講了這六個方面的原理,這篇文章更側重其具體實現。github

AndroidComponent結構圖.png

代碼中的各個module基本和圖中對應,從上到下依次是:編程

  • app是主項目,負責集成衆多組件,控制組件的生命週期
  • reader和share是咱們拆分的兩個組件
  • componentservice中定義了全部的組件提供的服務
  • basicres定義了全局通用的theme和color等公共資源
  • basiclib中是公共的基礎庫,一些第三方的庫(okhttp等)也統一交給basiclib來引入

圖中沒有體現的module有兩個,一個是componentlib,這個是咱們組件化的基礎庫,像Router/UIRouter等都定義在這裏;另外一個是build-gradle,這個是咱們組件化編譯的gradle插件,也是整個組件化方案的核心。bash

咱們在demo中要實現的場景是:主項目app集成reader和share兩個組件,其中reader提供一個讀書的fragment給app調用(組件交互),share提供一個activity來給reader來調用(UI跳轉)。主項目app能夠動態的添加和卸載share組件(生命週期)。而集成調試和代碼邊界是經過build-gradle插件來實現的。 ###1 單獨調試和發佈 單獨調試的配置與上篇文章基本一致,經過在組件工程下的gradle.properties文件中設置一個isRunAlone的變量來區分不一樣的場景,惟一的不一樣點是在組件的build.gradle中不須要寫下面的樣板代碼:架構

if(isRunAlone.toBoolean()){    
apply plugin: 'com.android.application'
}else{  
 apply plugin: 'com.android.library'
}

複製代碼

而只須要引入一個插件com.dd.comgradle(源碼就在build-gradle),在這個插件中會自動判斷apply com.android.library仍是com.android.application。實際上這個插件還能作更「智能」的事情,這個在集成調試章節中會詳細闡述。app

單獨調試所必須的AndroidManifest.xml、application、入口activity等類定義在src/main/runalone下面,這個比較簡單就不贅述了。maven

若是組件開發並測試完成,須要發佈一個release版本的aar文件到中央倉庫,只須要把isRunAlone修改成false,而後運行assembleRelease命令就能夠了。這裏簡單起見沒有進行版本管理,你們若是須要本身加上就行了。值得注意的是,發佈組件是惟一須要修改isRunAlone=false的狀況,即便後面將組件集成到app中,也不須要修改isRunAlone的值,既保持isRunAlone=true便可。因此實際上在Androidstudio中,是能夠看到三個application工程的,隨便點擊一個都是能夠獨立運行的,而且能夠根據配置引入其餘須要依賴的組件。這背後的工做都由com.dd.comgradle插件來默默完成。ide

項目中有三個application工程.png
###2 組件交互 在這裏組件的交互專指組件之間的數據傳輸,在咱們的方案中使用的是接口+實現的方式,組件之間徹底面向接口編程。

在demo中咱們讓reader提供一個fragment給app使用來講明。首先reader組件在componentservice中定義本身的服務

public interface ReadBookService {
    Fragment getReadBookFragment();
}
複製代碼

而後在本身的組件工程中,提供具體的實現類ReadBookServiceImpl:

public class ReadBookServiceImpl implements ReadBookService {
    @Override
    public Fragment getReadBookFragment() {
        return new ReaderFragment();
    }
}
複製代碼

提供了具體的實現類以後,須要在組件加載的時候把實現類註冊到Router中,具體的代碼在ReaderAppLike中,ReaderAppLike至關於組件的application類,這裏定義了onCreate和onStop兩個生命週期方法,對應組件的加載和卸載。

public class ReaderAppLike implements IApplicationLike {
    Router router = Router.getInstance();
    @Override
    public void onCreate() {
        router.addService(ReadBookService.class.getSimpleName(), new ReadBookServiceImpl());
    }
    @Override
    public void onStop() {
        router.removeService(ReadBookService.class.getSimpleName());
    }
}
複製代碼

在app中如何使用如reader組件提供的ReaderFragment呢?注意此處app是看不到組件的任何實現類的,它只能看到componentservice中定義的ReadBookService,因此只能面向ReadBookService來編程。具體的實例代碼以下:

Router router = Router.getInstance();
if (router.getService(ReadBookService.class.getSimpleName()) != null) {
    ReadBookService service = (ReadBookService) router.getService(ReadBookService.class.getSimpleName());
    fragment = service.getReadBookFragment();
    ft = getSupportFragmentManager().beginTransaction();
    ft.add(R.id.tab_content, fragment).commitAllowingStateLoss();
}
複製代碼

這裏須要注意的是因爲組件是能夠動態加載和卸載的,所以在使用ReadBookService的須要進行判空處理。咱們看到數據的傳輸是經過一箇中央路由Router來實現的,這個Router的實現其實很簡單,其本質就是一個HashMap,具體代碼你們參見源碼。

經過上面幾個步驟就能夠輕鬆實現組件之間的交互,因爲是面向接口,因此組件之間是徹底解耦的。至於如何讓組件之間在編譯階段不不可見,是經過上文所說的com.dd.comgradle實現的,這個在第一篇文章中已經講到,後面會貼出具體的代碼。 ###3 UI跳轉 頁面(activity)的跳轉也是經過一箇中央路由UIRouter來實現,不一樣的是這裏增長了一個優先級的概念。具體的實現就不在這裏贅述了,代碼仍是很清晰的。

頁面的跳轉經過短鏈的方式,例如咱們要跳轉到share頁面,只須要調用

UIRouter.getInstance().openUri(getActivity(), "componentdemo://share", null);
複製代碼

具體是哪一個組件響應componentdemo://share這個短鏈呢?這就要看是哪一個組件處理了這個schme和host,在demo中share組件在本身實現的ShareUIRouter中聲明瞭本身處理這個短鏈,具體代碼以下:

private static final String SCHME = "componentdemo";
private static final String SHAREHOST = "share";
public boolean openUri(Context context, Uri uri, Bundle bundle) {
    if (uri == null || context == null) {
        return true;
    }
    String host = uri.getHost();
    if (SHAREHOST.equals(host)) {
        Intent intent = new Intent(context, ShareActivity.class);
        intent.putExtras(bundle == null ? new Bundle() : bundle);
        context.startActivity(intent);
        return true;
    }
    return false;
}
複製代碼

在這裏若是已經組件已經響應了這個短鏈,就返回true,這樣更低優先級的組件就不會接收到這個短鏈。

目前根據schme和host跳轉的邏輯是開發人員本身編寫的,這塊後面要修改爲根據註解生成。這部分已經有一些優秀的開源項目能夠參考,如ARouter等。 ###4 集成調試 集成調試能夠認爲由app或者其餘組件充當host的角色,引入其餘相關的組件一塊兒參與編譯,從而測試整個交互流程。在demo中app和reader均可以充當host的角色。在這裏咱們以app爲例。

首先咱們須要在根項目的gradle.properties中增長一個變量mainmodulename,其值就是工程中的主項目,這裏是app。設置爲mainmodulename的module,其isRunAlone永遠是true。

而後在app項目的gradle.properties文件中增長兩個變量:

debugComponent=readercomponent,com.mrzhang.share:sharecomponent
compileComponent=readercomponent,sharecomponent
複製代碼

其中debugComponent是運行debug的時候引入的組件,compileComponent是release模式下引入的組件。咱們能夠看到debugComponent引入的兩個組件寫法是不一樣的,這是由於組件引入支持兩種語法,module或者modulePackage:module,前者直接引用module工程,後者使用componentrelease中已經發布的aar。

注意在集成調試中,要引入的reader和share組件是不須要把本身的isRunAlone修改成false的。咱們知道一個application工程是不能直接引用(compile)另外一個application工程的,因此若是app和組件都是isRunAlone=true的話在正常狀況下是編譯不過的。祕密就在於com.dd.comgradle會自動識別當前要調試的具體是哪一個組件,而後把其餘組件默默的修改成library工程,這個修改只在當次編譯生效。

如何判斷當前要運行的是app仍是哪一個組件呢?這個是經過task來判斷的,判斷的規則以下:

  • assembleRelease → app
  • app:assembleRelease或者 :app:assembleRelease → app
  • sharecomponent:assembleRelease 或者:sharecomponent:assembleRelease→ sharecomponent

上面的內容要實現的目的就是每一個組件能夠直接在Androidstudio中run,也可使用命令進行打包,這期間不須要修改任何配置,卻能夠自動引入依賴的組件。這在開發中能夠極大加快工做效率。 ###5 代碼邊界 至於依賴的組件是如何集成到host中的,其本質仍是直接使用compile project(...)或者compile modulePackage:module@aar。那麼爲啥不直接在build.gradle中直接引入呢,而要通過com.dd.comgradle這個插件來進行諸多複雜的操做?緣由在第一篇文章中也講到了,那就是組件之間的徹底隔離,也能夠稱之爲代碼邊界。若是咱們直接compile組件,那麼組件的全部實現類就徹底暴露出來了,使用方就能夠直接引入實現類來編程,從而繞過了面向接口編程的約束。這樣就徹底失去了解耦的效果了,可謂前功盡棄。

那麼如何解決這個問題呢?咱們的解決方式仍是從分析task入手,只有在assemble任務的時候才進行compile引入。這樣在代碼的開發期間,組件是徹底不可見的,所以就杜絕了犯錯誤的機會。具體的代碼以下:

/**
 * 自動添加依賴,只在運行assemble任務的纔會添加依賴,所以在開發期間組件之間是徹底感知不到的,這是作到徹底隔離的關鍵
 * 支持兩種語法:module或者modulePackage:module,前者之間引用module工程,後者使用componentrelease中已經發布的aar
 * @param assembleTask
 * @param project
 */
private void compileComponents(AssembleTask assembleTask, Project project) {
    String components;
    if (assembleTask.isDebug) {
        components = (String) project.properties.get("debugComponent")
    } else {
        components = (String) project.properties.get("compileComponent")
    }
    if (components == null || components.length() == 0) {
        return;
    }
    String[] compileComponents = components.split(",")
    if (compileComponents == null || compileComponents.length == 0) {
        return;
    }
    for (String str : compileComponents) {
        if (str.contains(":")) {
            File file = project.file("../componentrelease/" + str.split(":")[1] + "-release.aar")
            if (file.exists()) {
                project.dependencies.add("compile", str + "-release@aar")
            } else {
                throw new RuntimeException(str + " not found ! maybe you should generate a new one ")
            }
        } else {
            project.dependencies.add("compile", project.project(':' + str))
        }
    }
}
複製代碼

###6 生命週期 在上一篇文章中咱們就講過,組件化和插件化的惟一區別是組件化不能動態的添加和修改組件,可是對於已經參與編譯的組件是能夠動態的加載和卸載的,甚至是降維的。

首先咱們看組件的加載,使用章節5中的集成調試,能夠在打包的時候把依賴的組件參與編譯,此時你反編譯apk的代碼會看到各個組件的代碼和資源都已經包含在包裏面。可是因爲每一個組件的惟一入口ApplicationLike尚未執行oncreate()方法,因此組件並無把本身的服務註冊到中央路由,所以組件其實是不可達的。

在什麼時機加載組件以及如何加載組件?目前com.dd.comgradle提供了兩種方式,字節碼插入和反射調用。

  • 字節碼插入模式是在dex生成以前,掃描全部的ApplicationLike類(其有一個共同的父類),而後經過javassisit在主項目的Application.onCreate()中插入調用ApplicationLike.onCreate()的代碼。這樣就至關於每一個組件在application啓動的時候就加載起來了。
  • 反射調用的方式是手動在Application.onCreate()中或者在其餘合適的時機手動經過反射的方式來調用ApplicationLike.onCreate()。之因此提供這種方式緣由有兩個:對代碼進行掃描和插入會增長編譯的時間,特別在debug的時候會影響效率,而且這種模式對Instant Run支持很差;另外一個緣由是能夠更靈活的控制加載或者卸載時機。

這兩種模式的配置是經過配置com.dd.comgradle的Extension來實現的,下面是字節碼插入的模式下的配置格式,添加applicatonName的目的是加快定位Application的速度。

combuild {
    applicatonName = 'com.mrzhang.component.application.AppApplication'
    isRegisterCompoAuto = true
}
複製代碼

demo中也給出了經過反射來加載和卸載組件的實例,在APP的首頁有兩個按鈕,一個是加載分享組件,另外一個是卸載分享組件,在運行時能夠任意的點擊按鈕從而加載或卸載組件,具體效果你們能夠運行demo查看。

加載和卸載示例.png

2、組件化拆分的感悟

在最近兩個月的組件化拆分中,終於體會到了作到剝絲抽繭是多麼艱難的事情。肯定一個方案當然重要,更重要的是克服重重困難堅決的實施下去。在拆分中,組件化方案也不斷的微調,到如今終於能夠欣慰的說,這個方案是經歷過考驗的,第一它學習成本比較低,組內同事能夠快速的入手,第二它效果明顯,獲得原本run一次須要8到10分鐘時間(不事後面換了頂配mac,速度提高了不少),如今單個組件能夠作到1分鐘左右。最主要的是代碼結構清晰了不少,這位後期的並行開發和插件化奠基了堅實的基礎。

總之,若是你面前也是一個龐大的工程,建議你使用該方案,以最小的代價儘快開始實施組件化。若是你如今負責的是一個開發初期的項目,代碼量還不大,那麼也建議儘快進行組件化的規劃,不要給將來的本身增長徒勞的工做量。

相關文章
相關標籤/搜索