Android徹底組件化demo發佈

来源:http://www.cnblogs.com/zhmqq/archive/2017/09/09/7499624.html
-Advertisement-
Play Games

今年6月份開始,我開始負責對“得到app”的android代碼進行組件化拆分,在動手之前我查閱了很多組件化或者模塊化的文章,雖然有一些收穫,但是很少有文章能夠給出一個整體且有效的方案,大部分文章都只停留在組件單獨調試的層面上,涉及組件之間的交互就很少了,更不用說組件生命周期、集成調試和代碼邊界這些最 ...


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

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

一、AndroidComponent使用指南

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

AndroidComponent結構圖.png

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

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

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

我們在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。實際上這個插件還能做更“智能”的事情,這個在集成調試章節中會詳細闡述。

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

如果組件開發並測試完成,需要發佈一個release版本的aar文件到中央倉庫,只需要把isRunAlone修改為false,然後運行assembleRelease命令就可以了。這裡簡單起見沒有進行版本管理,大家如果需要自己加上就好了。值得註意的是,發佈組件是唯一需要修改isRunAlone=false的情況,即使後面將組件集成到app中,也不需要修改isRunAlone的值,既保持isRunAlone=true即可。所以實際上在Androidstudio中,是可以看到三個application工程的,隨便點擊一個都是可以獨立運行的,並且可以根據配置引入其他需要依賴的組件。這背後的工作都由com.dd.comgradle插件來默默完成。

項目中有三個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

二、組件化拆分的感悟

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

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

【最後發個招聘廣告:】
羅輯思維“得到app”發展迅猛,我們急需各路Android和iOS大牛加入,只要你滿足下麵幾點,就是我們所需要的:
1、熱愛移動端開發(android/ios/rn),最好有2到3年的app開發經驗
2、對技術有極致追求,並能踏實的推動技術驅動,致力於實現自己更實現他人
3、如果你也喜歡知識付費類的產品,有一些自己的產品想法,那更是我們需要的人
我們的辦公地點就在長安街沿線的一座裝修精美的獨棟裡面,環境十分優美。至於待遇,絕對是業內領先的,從我們的內推獎是豪華的蘋果三件套就可見一斑。如果你感興趣,請發送簡歷到[email protected]


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • querySelector 和 querySelectorAll 在傳統的 JavaScript 開發中,查找 DOM 往往是開發人員遇到的第一個頭疼的問題,原生的 JavaScript 所提供的 DOM 選擇方法並不多,僅僅局限於通過 tag, name, id 等方式來查找,這顯然是遠遠不夠的, ...
  • 編程練習 製作一個跳轉提示頁面: 要求: 1. 如果打開該頁面後,如果不做任何操作則5秒後自動跳轉到一個新的地址,如慕課網主頁。 2. 如果點擊“返回”按鈕則返回前一個頁面。 效果: 註意: 在視窗中運行該程式時,該視窗一定要有歷史瀏覽記錄,否則"返回"無效果。 我的解答 <!DOCTYPE htm ...
  • 有時候會需要用到字元的ASCII碼,一時之間調試時可能會忘記字元與ASCII碼對應的數字。 最近喜歡用瀏覽器控制台直接跑JS代碼,將這個代碼直接貼到瀏覽器控制台,即可調試(谷歌瀏覽器快捷鍵 ctrl+shift+j) function GetAsciiCode(){ var str = prompt ...
  • 1 1 /* 2 CSS重置 3 * */ 4 5 body, 6 ul, 7 ol { 8 margin: 0px; 9 padding: 0px; 10 } 11 12 #flash { 13 width: 600px; 14 height: 300px; 15 margin: 100px;..... ...
  • 接著上文,重新在webpack文件夾下麵新建一個項目文件夾demo2,然後用npm init --yes初始化項目的package.json配置文件,然後安裝webpack( npm install [email protected] --save-dev ),然後創建基本的項目文件夾結構,好了,我們的又一 ...
  • webpack,我想大家應該都知道或者聽過,Webpack是前端一個工具,可以讓各個模塊進行載入,預處理,再進行打包。現代的前端開發很多環境都依賴webpack構建,比如vue官方就推薦使用webpack.廢話不多說,我們趕緊開始吧. 第一步、安裝webpack 新建文件夾webpack->再在we ...
  • 背景 之間在一篇介紹過 Table 組件《 React 實現一個漂亮的 Table 》 的文章中講到過,在企業級後臺產品中,用的最多且複雜的組件主要包括 Table、Form、Chart,在處理 Table 的時候我們遇到了很多問題。今天我們這篇文章主要是分享一下 Form 組件,在業務開發中, 相 ...
  • async await 解決非同步問題,這兩個關鍵字是es7提出的,所以測試,node和瀏覽器版本提高一些 async await 操作基於promise實現的 async await這兩個關鍵字是一起使用,分開使用會報錯 await 後面只能跟promise對象 不熟悉的promise非同步操作的朋友 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...