1. 程式人生 > >Android元件化框架設計與實踐

Android元件化框架設計與實踐

轉載自:Android元件化框架設計與實踐

在目前移動網際網路時代,每個 APP 就是流量入口,與過去 PC Web 瀏覽器時代不同的是,APP 的體驗與迭代速度影響著使用者的粘性,這同時也對從事移動開發人員提出更高要求,進而移動端框架也層出不窮。

服務端與移動端對比

上圖顯示的是傳統的服務端架構和客戶端 App 架構對比。傳統的服務端架構中最底下是一個 OS,一般是 Linux,最上面服務端的業務,而中間有非常多的層次可以在架構上,按照我們的意願搭建中間的各個層次的銜接環節,使得架構具有足夠的靈活性和擴充套件性。但是到了 App 就會面對一個完全不同的現狀,App 的OS(Android或iOS)本質上並不是一個很瘦的像 Linux 這樣的 OS,而是在 OS 上有一個很重的 App Framework,開發一個普通的客戶端應用所要用到的絕大多數介面都在 Framework 裡,而上面的業務也是一個非常複雜多樣化的業務,最後會發現“架構”是在中間的一個非常尷尬的夾心層,因為會遇到很多在服務端架構中不需要面臨的挑戰。比如以下兩點:

  • 體積的制約。體積對使用者來說是一個非常敏感的概念,如果我們要在架構上做很多事情的話,通常意味著架構佔據的程式碼量會比較大。在服務端架構中我們可以容忍我們在架構層面去做幾十兆的程式碼。但是對於客戶端架構,即使你的架構只有一兩兆,對於一個客戶端可能都佔據了 10%,20%的容量。
  • 效能的挑戰。從效能上來看,對於服務端架構我們通常關注的是吞吐率,我們不會去關注啟動速度。一個服務端的啟動哪怕是花了一兩分鐘,只要它運作起來吞吐率足夠高,支援的併發能力足夠好,響應速度足夠快,我們就認為這是一個良好的架構。但客戶端不同,客戶端的程序對使用者而言,往往是一個棧態的,手機裡面使用完一個應用,退出之後可能過不了多久就會被回收掉,當用戶下次再開啟的時候,它會再次啟動程序,需要重新完成一次初始化的流程。如果在這個上面做了很多事情的話,會導致程式啟動的速度會很慢,在很多使用者看來,這就是一種不可接受的使用者體驗。

客戶端 APP 與服務端在架構上是有著一定的區別,在選擇對客戶端架構需要謹慎對待,需要有許多權衡的條件,在此前提上,是否有一種歸一的方式呢,可以分而治之,並行開發,把業務分隔成一個個單獨的元件,整個架構圍繞元件開發,構建也是元件,一切皆元件。答案是有的,那就是打造客戶端元件框架。

起源,為何元件化

客戶端 APP 自身在飛速發展,APP 版本不斷迭代,新功能不斷增加,業務模組數量不斷增加,業務上的處理邏輯越變越複雜,同時每個模組程式碼也變得越來越多,這就引發一個問題,所維護的程式碼成本越來越高,稍微一改動可能就牽一髮而動全身,改個小的功能點就需要回歸整個 APP 測試,這就對開發和維護帶來很大的挑戰。同時原來APP 架構方式是單一工程模式,業務規模擴大,隨之帶來的是團隊規模擴大,那就涉及到多人協作問題,每個移動端軟體開發人員勢必要熟悉如此之多程式碼,如果不按照一定的模組元件機制去劃分,將很難進行多人協作開發,隨著單一專案變大,而且 Andorid 專案在編譯程式碼方面就會變得非常卡頓,在單一工程程式碼耦合嚴重,每修改一處程式碼後都需要重新編譯打包測試,導致非常耗時,最重要的是這樣的程式碼想要做單元測試根本無從下手,所以必須要有一個更靈活的架構去代替過去單一工程模式。

同樣這樣的問題在我們工作具體專案中處處碰到,就拿我們組內負責的某個移動端 APP 來說,就碰到如下幾個問題:

  1. 程式碼量膨脹,不利於維護,不利於新功能的開發。
  2. 專案工程構建速度慢,在一些電腦上寫兩句程式碼,重新編譯測試的話編譯速度起碼 10-20 分鐘,有的甚至更長。
  3. 不同模組之間程式碼耦合嚴重,比如訊息模組嚴重耦合視訊模組,如果修改視訊模組,相應的訊息模組也需要修改,不然會產生一連串問題。
  4. 每個模組之間都有引用第三方庫,但有些第三方庫版本不一致,導致打包 APP 時候程式碼冗餘,容易引起版本衝突。
  5. 有些定製專案如果只需要訊息模組,其他模組不需要的話,做不到按需載入打包,因為模組之間有互聯依賴。
  6. 現有專案基於以前其他人專案基礎上開發,經手的人次過多,存在著不同的程式碼風格,專案中程式碼規範亂,類似的功能寫法卻不一樣,導致不統一。

專案工程架構模式改變是大勢所趨,那又該如何做呢?那就是:打造元件化開發框架,用以解決目前所面臨問題,在講解如何打造之前,需要談談元件化概念,元件化框架是什麼。

概念,元件化是什麼

問:什麼是元件,什麼是元件化?

答:在軟體開發領域,元件(Component)是對資料和方法的簡單封裝,功能單一,高內聚,並且是業務能劃分的最小粒度。舉個我們生活中常見的例子就是電腦主機板上每個元件電容器件,每個元件負責的功能單一、容易組裝、即插即拔,但作用有限,需要一定的依賴條件才可使用。如下圖:

電容元件

那麼同樣,元件化 就是基於元件可重用的目的上,將一個大的軟體系統按照分離關注點的形式,拆分成多個獨立的元件,使得整個軟體系統也做到電路板一樣,是單個或多個元件元件組裝起來,哪個元件壞了,整個系統可繼續執行,而不出現崩潰或不正常現象,做到更少的耦合和更高的內聚。

問:元件化、模組化容易混淆,兩者區別又是什麼?

答:模組化就是將一個程式按照其功能做拆分,分成相互獨立的模組,以便於每個模組只包含與其功能相關的內容,模組我們相對熟悉,比如登入功能可以是一個模組,搜尋功能可以是一個模組等等。而元件化就是更關注可複用性,更注重關注點分離,如果從集合角度來看的話,可以說往往一個模組包含了一個或多個元件,或者說模組是一個容器,由元件組裝而成。簡單來說,元件化相比模組化粒度更小,兩者的本質思想都是一致的,都是把大往小的方向拆分,都是為了複用和解耦,只不過模組化更加側重於業務功能的劃分,偏向於複用,元件化更加側重於單一功能的內聚,偏向於解耦。

問:元件化能帶來什麼好處?

答:簡單來說就是提高工作效率,解放生產力,好處如下:

  • 程式碼簡潔,冗餘量少,維護方便,易擴充套件新功能。
  • 提高編譯速度,從而提高並行開發效率。
  • 避免模組之間的交叉依賴,做到低耦合、高內聚。
  • 引用的第三方庫程式碼統一管理,避免版本統一,減少引入冗餘庫。
  • 定製專案可按需載入,元件之間可以靈活組建,快速生成不同型別的定製產品。
  • 制定相應的元件開發規範,可促成程式碼風格規範,寫法統一。
  • 系統級的控制力度細化到元件級的控制力度,複雜系統構建變成元件構建。
  • 每個元件有自己獨立的版本,可以獨立編譯、測試、打包和部署。

設計,構建元件化框架

回到剛開始講的 APP 單一工程模式,看張常見 APP 單一工程模式架構圖:

APP單一工程模式架構

上圖是目前比較普遍使用的 Android APP 技術架構,往往是在一個介面中存在大量的業務邏輯,而業務邏輯中充斥著各種網路請求、資料操作等行為,整個專案中也沒有模組的概念,只有簡單的以業務邏輯劃分的資料夾,並且業務之間也是直接相互呼叫、高度耦合在一起的。單一工程模型下的業務關係,總的來說就是:你中有我,我中有你,相互依賴,無法分離。如下圖:

業務模組互相耦合

元件化的指導思想是:分而治之,並行開發,一切皆元件。要實現元件化,無論採用什麼樣的技術方式,需要考慮以下七個方面問題:

  1. 程式碼解耦。如何將一個龐大的工程分成有機的整體?

  2. 元件單獨執行。因為每個元件都是高度內聚的,是一個完整的整體,如何讓其單獨執行和除錯?

  3. 元件間通訊。由於每個元件具體實現細節都互相不瞭解,但每個元件都需要給其他呼叫方提供服務,那麼主專案與元件、元件與元件之間如何通訊就變成關鍵?

  4. UI 跳轉。UI 跳轉指的是特殊的資料傳遞,跟元件間通訊區別有什麼不同?

  5. 元件生命週期。這裡的生命週期指的是元件在應用中存在的時間,元件是否可以做到按需、動態使用、因此就會涉及到元件載入、解除安裝等管理問題。

  6. 整合除錯。在開發階段如何做到按需編譯元件?一次除錯中可能有一兩個元件參與整合,這樣編譯時間就會大大降低,提高開發效率。

  7. 程式碼隔離。元件之間的互動如果還是直接引用的話,那麼元件之間根本沒有做到解耦,如何從根本上避免元件之間的直接引用,也就是如何從根本上杜絕耦合的產生?

元件化架構目標:告別結構臃腫,讓各個業務變得相對獨立,業務元件在元件模式下可以獨立開發,而在整合模式下又可以變為 AAR 包整合到“ APP 殼工程”中,組成一個完整功能的 APP。

先給出框架設計圖,然後再對這七個問題進行一一解答。

A元件化框架架構

從圖中可以看到,業務元件之間是獨立的,互相沒有關聯,這些業務元件在整合模式下是一個個 Library,被 APP 殼工程所依賴,組成一個具有完整業務功能的 APP 應用,但是在元件開發模式下,業務元件又變成了一個個 Application,它們可以獨立開發和除錯,由於在元件開發模式下,業務元件們的程式碼量相比於完整的專案差了很遠,因此在執行時可以顯著減少編譯時間。

各個業務元件通訊是通過路由轉發,如圖:

路由轉發通訊

這是元件化工程模型下的業務關係,業務之間將不再直接引用和依賴,而是通過“路由”這樣一箇中轉站間接產生聯絡。

那麼針對以上提出的七個問題,具體解決如下:

1,程式碼解耦問題

對已存在的專案進行模組拆分,模組分為兩種型別,一種是功能元件模組,封裝一些公共的方法服務等,作為依賴庫對外提供,一種是業務元件模組,專門處理業務邏輯等功能,這些業務元件模組最終負責組裝APP。

2,元件單獨執行問題

通過 Gradle 指令碼配置方式,進行不同環境切換。比如只需要把 Apply plugin: 'com.android.library' 切換成Apply plugin: 'com.android.application' 就可以,同時還需要在 AndroidManifest 清單檔案上進行設定,因為一個單獨除錯需要有一個入口的 Activity。比如設定一個變數 isModule,標記當前是否需要單獨除錯,根據isModule 的取值,使用不同的 gradle 外掛和 AndroidManifest 清單檔案,甚至可以新增 Application 等 Java 檔案,以便可以做一下初始化的操作。

3,元件間通訊問題

通過介面+實現的結構進行元件間的通訊。每個元件宣告自己提供的服務 Service API,這些 Service 都是一些介面,元件負責將這些 Service 實現並註冊到一個統一的路由 Router 中去,如果要使用某個元件的功能,只需要向Router 請求這個 Service 的實現,具體的實現細節我們全然不關心,只要能返回我們需要的結果就可以了。在元件化架構設計圖中 Common 元件就包含了路由服務元件,裡面包括了每個元件的路由入口和跳轉。

4,UI 跳轉問題

可以說 UI 跳轉也是元件間通訊的一種,但是屬於比較特殊的資料傳遞。不過一般 UI 跳轉基本都會單獨處理,一般通過短鏈的方式來跳轉到具體的 Activity。每個元件可以註冊自己所能處理的短鏈的 Scheme 和 Host,並定義傳輸資料的格式,然後註冊到統一的 UIRouter 中,UIRouter 通過 Scheme 和 Host 的匹配關係負責分發路由。但目前比較主流的做法是通過在每個 Activity 上添加註解,然後通過 APT 形成具體的邏輯程式碼。目前方式是引用阿里的 ARouter 框架,通過註解方式進行頁面跳轉。

5,元件生命週期問題

在架構圖中的核心管理元件會定義一個元件生命週期介面,通過在每個元件設定一個配置檔案,這個配置檔案是通過使用註解方式在編譯時自動生成,配置檔案中指明具體實現元件生命週期介面的實現類,來完成元件一些需要初始化操作並且做到自動註冊,暫時沒有提供手動註冊的方式。

6,整合除錯問題

每個元件單獨除錯通過並不意味著整合在一起沒有問題,因此在開發後期我們需要把幾個元件機整合到一個 APP 裡面去驗證。由於經過前面幾個步驟保證了元件之間的隔離,所以可以任意選擇幾個元件參與整合,這種按需索取的載入機制可以保證在整合除錯中有很大的靈活性,並且可以加大的加快編譯速度。需要注意的一點是,每個元件開發完成之後,需要把 isModule 設定為 true並同步,這樣主專案就可以通過引數配置統一進行編譯。

7,程式碼隔離問題

如果還是 compile project(xxx:xxx.aar) 來引入元件,我們就完全可以直接使用到其中的實現類,那麼主專案和元件之間的耦合就沒有消除,那之前針對介面程式設計就變得毫無意義。我們希望只在 assembleDebug 或者 assembleRelease 的時候把 AAR 引入進來,而在開發階段,所有元件都是看不到的,這樣就從根本上杜絕了引用實現類的問題。

目前做法是主專案只依賴 Common 的依賴庫,業務元件通過路由服務依賴庫按需進行查詢,用反射方式進行元件載入,然後在主工程中呼叫元件服務,元件與元件之間呼叫則是通過介面+實現進行通訊,後續規劃通過自定義Gradle 外掛,通過位元組碼自動插入元件的依賴進行編譯打包,實現自動篩選 assembleDebug 或 assembleRelease 這兩個編譯命任務,只有屬於包含這兩個任務的命令才引入具體實現類,其他的則不引入。

程式碼,具體專案實踐

一,建立工程

1,APP空殼工程

通過AndroidStudio建立一個APP空殼工程,如圖:

APP空殼工程

然後在 APP 工程新增依賴具體業務元件 Module。比如:

依賴關係

2,具體業務元件Module

需要遵循一定元件命名規範,為何需要規範呢,因為需要通過元件命名規範來約束和保證元件的統一性和一致性,避免出現衝突。比如登陸元件,那麼名稱:b(型別)-ga(部門縮寫)-login(元件名稱),這就是我們基於共同的約定進行命名的,為後期維護和擴充套件都帶來辨識度。

Login業務元件

二,業務元件配置檔案

1,build.gradle配置文修改。如下:

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

android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion

    defaultConfig {
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion

        if (isModule.toBoolean()) {
            applicationId "com.hik.ga.business.login"
            versionCode 1
            versionName "1.0"
        } else {
            //ARouter
            javaCompileOptions {
                annotationProcessorOptions {
                    arguments = [ moduleName : project.getName() ]
                }
            }
        }
    }

    sourceSets {
        main {
            if (isModule.toBoolean()) {
                manifest.srcFile 'src/main/module/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                //整合開發模式下排除debug資料夾中的所有Java檔案
                java {
                    exclude 'debug/**'
                }
            }
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation project(':b-ga-common-function-comlib')

    if (!isModule.toBoolean()) {
        annotationProcessor "com.alibaba:arouter-compiler:${rootProject.annotationProcessor}"
    }
}

這裡需要有幾點說明一下:

1,通過 isModule.toBoolean() 方法來進行元件間整合模式和元件模式的切換,包括模組是屬於Application 還是 Library,由於集成了 ARouter,所以需要對 ARouter 配置檔案進行處理。

2,如果元件模式下, 則需要重新設定 AndroidManifest.xml 檔案,裡面配置新的Application路徑。比如Login元件單獨執行 AndroidManifest 清單檔案

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="hik.ga.business.applogin" >

    <application
        android:name="debug.LoginApplication"
        android:allowBackup="false"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/login_btn_str"
        android:supportsRtl="true"
        android:theme="@style/Theme.AppCompat.Light.NoActionBar">

        <activity
            android:name="hik.ga.business.applogin.login.views.LoginActivity"
            android:label="@string/login_btn_str"
            android:launchMode="singleTop"
            android:screenOrientation="portrait"
            android:theme="@style/AppTheme.NoActionBarFullScreen">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>
</manifest>

3,實現元件全域性應用配置類,這個類的目的是在元件載入時初始化一些元件自身的資源,如下:

public class LoginApplicationDelegate implements IApplicationDelegate {

    private static final String TAG = "LoginApplicationDelegate";

    @Override
    public void onCreate() {
        EFLog.d(TAG, "*------------------onCreate()---------------->");
    }

    @Override
    public void enterBackground() {
        EFLog.d(TAG, "*------------------enterBackground()---------------->");
    }

    @Override
    public void enterForeground() {
        EFLog.d(TAG, "*------------------enterForeground()---------------->");
    }

    @Override
    public void receiveRemoteNotification(Map<String, String> message) {
        EFLog.d(TAG, "receiveRemoteNotification msg = " + message);
    }

    @Override
    public void onTerminate() {
        EFLog.d(TAG, "*------------------onTerminate()---------------->");
    }

    @Override
    public void onConfigurationChanged(Configuration configuration) {
        EFLog.d(TAG, "*------------------onConfigurationChanged()---------------->");
    }

    @Override
    public void onLowMemory() {
        EFLog.d(TAG, "*------------------onLowMemory()---------------->");
    }

    @Override
    public void onTrimMemory(int var1) {
        EFLog.d(TAG, "*------------------onTrimMemory()---------------->");
    }
}

三,路由服務

1,定義公共元件路由API和入口,通過路由服務元件查詢,如圖:

公共路由服務

2,元件路由實現

每個元件對外提供什麼能力,首先需要在路由服務元件建立一個介面檔案,如下是登陸元件介面宣告和實現。

Login 介面:

定義Login介面

具體實現:

Login介面具體實現

路由使用:比如我們想從設定頁面跳轉到登陸頁面,使用 Login 接口裡的方法,使用如下:

ILoginProvider loginService = (ILoginProvider) ARouter.getInstance().build(RouterPath.ROUTER_PATH_TO_LOGIN_SERVICE).navigation();
if(loginService != null){
    loginService.accountToLogin(AccountActivity.this);
}

小結

總的來說,通過應用元件化框架,使得我們工作中的具體專案變得更輕、好組裝、編譯構建更快,不僅提高工作效率,同時自我對移動應用開發認知有進一步的提升。因為元件化框架具有通用性,特別適用於業務模組迭代多,量大的大中型專案,是一個很好的解決方案。至於元件化框架之後演化的道路,則是打造元件倉庫,完善元件開發規範,豐富元件功能庫,有一些粒度大的業務元件可以進一步的細化,對元件功能進行更單一的內聚,同時基於現有元件化框架,便於過度在未來打造外掛化框架,進一步升級 APP 動態能力,比如熱載入、熱修復等,那又是另一種使用場景和設計架構了,其實元件化和外掛化框架最大的區別就是在是否具備動態更新能力。

把專案簡化下,github地址:DemoComponent,感興趣的可以下過去看看。

參考文章:

1,Android元件化方案

2,Android徹底元件化方案實踐