聚美元件化實踐之路

Haoge發表於2018-01-04

從去年開始,就陸陸續續的越來越多的app開始進行了元件化重構。也有很多非常好的元件化方案部落格分享,所以這篇文章並不以介紹元件化方案作為主題,而是我們應該如何一步步的從一個古老的專案,慢慢一步步拆分,完成元件化重構的。

元件化的思想是好的,但是並不是所有的專案都適合使用元件化的方式進行開發,所以一般需要使用元件化的專案。基本都是具備專案迭代時期久遠、專案大而臃腫、專案組成員多溝通成本大、專案複雜維護成本很高等特點。這類的專案才會有元件化的用武之地。

而其他的一些人員少、功能簡單的小專案。就別去直接考慮元件化了。老老實實直接擼碼就行了。強行使用元件化只會增加維護成本與開發成本。得不償失~

元件化結構

元件化從來不是一個說重構就能重構的東西,在進行元件化重構之前。最好先對元件化的結構有一個基本的理解:

聚美元件化實踐之路

上圖為元件化最基本的結構。大致可以看出。元件化主要分為三層:

  1. app殼:

    此為元件化的執行容器,殼中定義app入口,依賴業務元件進行執行。

  2. 業務元件:

    此為元件化的中間層,在一個大型專案組中。都有細分下來的不同的業務組,比如管登入的、管購物的、管視訊的等等。這些不同業務組分別維護一個各自的業務元件,以達到各自業務組業務解耦的效果。

    原則上來說:各個業務元件之間不能有直接依賴!所有的業務元件均需要可以做到獨立執行的效果。對於測試的時候,需要依賴多個業務元件的功能進行整合測試的時候。可以使用app殼進行多元件依賴管理執行。

  3. 基礎元件:

    基礎元件也叫基礎功能元件。此部分元件為上層業務元件提供基本的功能支援。如基礎網路元件、基礎檢視元件、基礎資料存取元件等,以及元件化的核心通訊元件:路由元件。

以上即是元件化的最基本結構,當然在真正的專案之中,不可能會存在這麼簡單的結構,都是需要根據你的具體現狀進行擴充的。比如你可以在基礎元件與業務元件之間,新增一層特殊的功能元件。此層的功能元件只被一個或者多個元件進行依賴,只要不破壞這層由下到上單向依賴鏈即可

聚美元件化實踐之路

準備

元件化重構從來都不是說重構就能重構的,首先得有個強有力的領導去支援執行,然後你才可能去具體的進行重構。

其次,你得提前對你們的專案進行大方向的分層結構劃分,哪些東西需要放在什麼層。需要提前有個明確的劃分。

重構你的基礎元件:即你的各種基礎功能框架需要提前從專案中拆分出來。包括網路、圖片載入、資料儲存、埋點、路由等。

建立元件化專案結構

建立基礎元件合集

你需要建立一個基礎library module。用於依賴所有的基礎功能元件,如baselib。

作用:用於統一依賴基礎功能庫,並統籌、關聯好各功能框架的關係,做好各功能庫的初始化封裝操作。提供上層業務元件直接呼叫。

建立各自業務線的業務元件及app殼

與普通的元件化方案做法不同,普通的元件化方案是使用一個變數進行控制。使得業務元件可以在application與library之間進行靈活切換。使得元件也是application。application也是元件。

但是這種做法,因為總是在libraryModule與applicationModule之間進行切換。很容易導致各種混亂問題:比如Manifest衝突,R檔案衝突等。

所以我們採用的是多app殼分組載入的方式:

聚美元件化實踐之路

可以看到,每個業務線的業務元件。都分別有一個各自的app殼模組。而主app殼依賴所有的業務元件. 在進行業務開發時。各自業務組成員可以直接執行各自的app殼模組進行測試,主app殼進行全量打包。

在拆分初期,這個時候的建議以原本的專案application作為主app殼

預留一個核心業務元件出來。比如登入元件:此類元件為業務元件,但是又被所有其他元件所需要,所以將其單獨作為核心業務元件獨立出來。然後別的業務元件。通過各自的app殼工程。依賴進入即可:再次提醒業務元件之間不能直接進行依賴

聚美元件化實踐之路

這種分層結構的好處有:

  1. 業務元件不再在library與application之間進行切換。開發環境統一,不易出現環境切換衝突
  2. app殼單獨獨立出來。可以在殼工程中新增一些特有的獨立程式碼,由於各自的殼功能不會參與到主app殼中去進行編譯,所有這裡面你可以針對各自的業務。新增一些獨立的入口管理類。比如新增一個RootActivity,在此新增一個可以跳轉到任意頁面的列表,方便進行測試執行等。

gradle統一配置管理

元件化重構後,module變多了,所以就需要對所有module的一些gradle指令碼進行統一配置管理。避免混亂。

  • 新建dependencies.gradle指令碼。新增統一的依賴版本號管理:
ext {
    COMPILE_SDK_VERSION = 25
    BUILD_TOOLS_VERSION = '25.0.0'
    MIN_SDK_VERSION = 16
    TARGET_SDK_VERSION = 19
    
    // SUPPORT
    SUPPORT_VERSION = '23.2.0'
    SUPPORTDEPS = [
            supportV4   : "com.android.support:support-v4:${SUPPORT_VERSION}",
            supportV13  : "com.android.support:support-v13:${SUPPORT_VERSION}",
            appcompatV7 : "com.android.support:appcompat-v7:${SUPPORT_VERSION}",
            cardview    : "com.android.support:cardview-v7:${SUPPORT_VERSION}",
            design      : "com.android.support:design:${SUPPORT_VERSION}",
            annotations : "com.android.support:support-annotations:${SUPPORT_VERSION}",
            multidex    : 'com.android.support:multidex:1.0.1'
    ]
    ...
}
複製程式碼

此指令碼統一配置管理所有的版本號相關的資料。外部需要使用版本號及依賴時,需要統一從此檔案配置屬性中進行讀取。比如要依賴supportV4包:

compile "${SUPPORTDEPS.supportV4}"
複製程式碼
  • 定義baseConfig.gradle。統一配置元件基礎編譯指令碼
boolean isAppModule = project.plugins.hasPlugin('com.android.application')
android {
    compileSdkVersion Integer.parseInt("${COMPILE_SDK_VERSION}")
    buildToolsVersion "${BUILD_TOOLS_VERSION}"

    lintOptions {
        abortOnError false
    }
    defaultConfig {
        if (isAppModule) {
            applicationId "com.haoge.component.demo"
        }
        minSdkVersion Integer.parseInt("${MIN_SDK_VERSION}")
        targetSdkVersion Integer.parseInt("${TARGET_SDK_VERSION}")

        versionCode Integer.parseInt("${DEFAULE_CONFIG.versionCode}")
        versionName "${DEFAULE_CONFIG.versionName}"
    }
}
複製程式碼

這樣。就可以使用apply語法。讓所有元件module。都統一依賴此gradle指令碼。進行統一環境配置了。

細心點的可以發現。我在baseConfig中,新增了預設的applicationId的指定。這是因為對於大部分應用而言。都有用過各種的第三方sdk。特別是第三方登入,這種的sdk框架。很多都會需要進行包名驗證的,所以建議有此種情況的,在此新增上預設的applicationId指定較好。

如果有嫌麻煩的又動手能力強的。可以考慮自己封裝個gradle外掛來進行統一配置管理

為元件新增資源字首

我們需要對各自的元件,分別設定他自身的資源字首來作為命名約束,避免出現不同的元件對不同的資源起了同一個命名,導致編譯衝突等問題。

android {
    resourcePrefix 'lg_'
}
複製程式碼

這個資源字首的作用是:當你在該module下建立了一個資源命名時,若名字不能與此字首進行匹配,則將會進行即時提醒。避免衝突。

大檔案資源、圖片資源統一管理

元件化之後。資源管理也是個問題,圖片資源、assets資源、raw檔案資源等。都具有佔用資源大、基本很少修改等特點。所以這裡最好將其單獨拆分出來。統一提供給所有元件進行使用:

所以,可以考慮將此類大檔案資源,統一放入元件化的最底層。使得不同元件不用自己單獨維護一份此大檔案資源。避免資源浪費的現象。比如可以直接將此部分資源。直接放入baselib中,作為基礎功能提供庫進行使用。

做好各元件的application派發

可能有人會問:為什麼要做元件的application的生命週期派發?

舉個例子:都知道。網路庫、圖片載入庫等,都需要進行對應的初始化操作才能進行使用的,但是在元件化中,如果不進行各自application的派發。不能進行一個統一流程的初始化操作。那麼可能你元件A需要自己手動寫基礎庫的初始化操作。元件B、元件C也需要。最後你的主app殼也需要,這個時候。就容易亂了!

所以需要有個結構。來讓各自的元件。分別完成自身的元件的功能初始化。

比如基礎功能元件:初始化網路、圖片框架等,上層的業務元件A,初始化自身的其他功能操作。各自的元件分別只初始化自身這部分的操作。而不用管所依賴的其他元件需要進行什麼初始化。

這部分的生命週期派發可以參考demo中的baselib的delegate包下的類:

聚美元件化實踐之路

demo連結放在了文章末尾。

元件間通訊

路由通訊

元件間通訊的核心是路由框架,這部分框架需要放置在最底層的基礎功能元件中,提供上層進行使用,這裡我使用的是我自己的路由框架Router:一款單品、元件化、外掛化全支援的路由框架

此路由框架支援在單品、元件化、外掛化中均能使用。如果你想要為元件化之後,能在後期有需要的情況下,方便的從元件化切換到外掛化的環境中去,建議使用此Router

如果你們專案中已經有使用自己的路由框架,且也直接支援元件化環境使用。建議這塊就最好別考慮換了。實話說換一個路由框架任務還是挺重的。

因為基本所有的介紹元件化的blog,都對其中的路由框架,做了非常詳細的說明,所以這塊我就不準備展開進行詳細的贅述了,如有感興趣的,可以參考上方的連結進行了解使用。

事件通訊

與路由通訊不同的是:路由主要用於做介面跳轉通訊,對於普通的事件通訊作用不大。比如說我是元件A,需要調元件B中的某個介面,並獲取返回資料進行操作。這個時候,就需要別的方式來進行實現了。

很多人一說到事件通訊。可能就會想起使用EventBus了。的確EventBus是個很好的事件通訊框架,但是相信用過的人都知道。一旦EventBus被濫用。隨著時間的迭代,由於其獨特的解耦特性,會使得你的程式碼很難進行除錯、維護。

所以這個時候,我們摒棄了使用EventBus來作為元件間時間通訊的橋樑。而是簡單的使用控制反轉的手段。將元件間通訊協議定義在底層基礎元件中,上層的業務元件分別實現底層對應的各自的協議介面來進行通訊。

我們以登入元件為例:

首先,在基礎元件層新增一個協議介面。這個介面用於定義登入元件所對外提供的時間通訊入口,比如退出登入、清理cookie等:

public interface LoginPipe extends Pipe{
    void logout();
    void clearCookie();
}
複製程式碼

然後。在登入元件中。實現此協議介面。並註冊入對應的通訊管理器:

// 實現協議介面。
public final class LoginPipeImpl implements LoginPipe {
    @Override
    void logout() {
        // do something
    }
    
    void clearCookie() {
        // do something
    }
}
複製程式碼
// 註冊此實現進協議管理器中
// PipeManager也位於基礎元件中。
PipeManager.register(LoginPipe.class, new LoginPipeImpl());
複製程式碼

然後即可在別的元件中。通過此PipeManager協議管理器。根據協議類。獲取到對應的實現類進行直接呼叫了:

PipeManager.get(LoginPipe.class).logout();
複製程式碼

上面這種做法,雖然的確很簡單,但是具備以下幾點優點:

  • 提高各元件協議的內聚性。更適於各自元件對各自的協議介面進行統一管理維護。
  • 實現方案簡單易懂,易於除錯。
  • 在元件化拆分程式中,便於方便後期對主app殼無關程式碼進行刪除。

最後一條可能相對比較複雜一點。所以下面我們針對這條進行展開描述:

上面我們提到了。在對老舊專案進行元件化重構的時候。使用主module作為的主app殼,而app殼其實是需要沒有具體的業務程式碼的。所以這個地方存在衝突。但是我們元件化拆分也不是可以一蹴而就的,只能慢慢一步步、一個頁面一個頁面的進行拆分並測試。所以拆分過程其實是個漫長的痛苦的過程。

而在拆分過程中,很難避免的就是新舊程式碼均需要同時存在的尷尬場面。而這種尷尬的場面會一直持續到所有元件均拆分完畢之後。

然後拆分過程中,你也會遇到另外一個問題:就是各業務組的拆分計劃其實是不同步的,也就是說很可能你當前拆的業務。需要呼叫到別的業務組的功能,而這個功能這個時候。很可能還根本沒有被提交到拆分計劃表上來。所以這個時候。你就必須要在你拆分的元件中,還是先直接呼叫老專案中的邏輯程式碼。

所以使用上面的事件通訊機制。你會需要在主app中建議一個臨時的協議介面。比如:

public interface MainPipe {
    void doSomething();
}
複製程式碼

然後主專案實現並註冊它。提供給你的元件進行使用。而其他元件遇到此種類似問題時,也於此類似。在此MainPipe種繼續新增對應的通訊協議方法並實現即可。

由於這樣的做法。將所有的主app的臨時協議介面。均放置於此MainPipe中。提升了協議的內聚性。當所有業務組均完成元件化重構之後。那麼就可以統一的直接對此MainPipe進行重構,將其中各自元件的協議遷移至各自元件的協議類中,然後就可以安全地進行主app中無關業務程式碼統一刪除了。使其成為真正的主app殼工程。

資料通訊

很多時候,其實元件間通訊。傳遞的資料都是普通的簡單資料,但是也有一些時候。會需要傳遞複雜資料。比如進行跨元件呼叫api介面並獲取返回資料時,或者說讀取使用者完整資料時。

以讀取使用者完整資料為例,資料通訊的協議定義仍舊以上方的事件通訊機制作為實現載體:

public class User {
    String uid;
    String nickname;
    String email;
    String phone;
    ...
}
複製程式碼

這個User類包含了所有的使用者資訊在裡面。然後現在需要將此user例項進行跨元件傳遞時。你就需要定義一個協議方法。提供獲取此User例項的入口:

public interface LoginPipe {
    User getUser();
}
複製程式碼

這是正常的做法,但是這樣做的話,你就需要將此User例項也一起拷貝到協議定製層,即基礎元件中來。

而在開發過程中,這種現象很常見。而且很多時候,隨著需求一更改,所需要傳遞的資料也不一樣。也不可能每次都去將對應的實體bean進行遷移,放入協議定製層。這樣就太麻煩了。

所以對於這種跨元件通訊的做法。建議的方式是通過json資料來進行資料通訊

json通訊的機制,即可完美的避免實體bean遷移的問題。也能讓接收方按需解析讀取資料:

比如我接收方的元件。當前只需要nickname與uid兩個資料。其他資料我不管。那麼我就可以只解析此兩個欄位的資料即可。做到按需解析。

說到這裡。推薦一波我的另一個框架Parceler, 此框架是封裝的Bundle的存取操作。也支援json的自動轉換功能。具體用法可以參考我另一篇部落格,有興趣的可以看看。

Parceler: 優雅的使用Bundle進行資料存取就靠它了!(文章最後有關於元件化、外掛化下應該如何使用此框架的說明)

優化加速

隨著元件化拆分重構的進行。你會發現專案下的元件被拆分得越來越多,雖然你已經對元件的拆分粒度。進行過把控了。但是元件化後module持續增加是不爭的事實,這個時候。隨著module的持續增加。你的專案編譯時間也會出現暴漲。

我們知道。專案編譯流程中,第一步會將所有的library module先進行打包編譯。生成對應的aar。提供給app進行使用,app等待所有module打包完畢後,再解壓aar。進行資源、程式碼合併,並打包成apk執行執行。

所以我們製作了一個gradle加速外掛。用於提前將module進行aar編譯好。跳過module打包aar的過程。實現編譯加速的效果。

具體原理可以參考這篇文章:Speedup:專為專案下Library project過多所設計的加速外掛

更多小貼士

因為在元件化開發環境下,你將會遇到的問題遠遠不止以上這麼點,當然上面這些都是很主要的。

所以這裡新增此小貼士環節,用於新增一些平時我們開發時。可能會遇到的問題。或者說,一些在特定環境下的編碼建議之類的。(這些要點很可能不能在demo中得到體現,所以請儘量認真看下描述)

巧用ActivityLifecycleCallbacks做初始化

因為元件化有個特點: 各自業務組可以任意選擇自己的開發模式,如mvp,mvvm,RN等。

Android元件化demo

相關文章