Android元件化開發實踐和案例分享

楊充發表於2019-03-02

目錄介紹

  • 1.為什麼要元件化
    • 1.1 為什麼要元件化
    • 1.2 現階段遇到的問題
  • 2.元件化的概念
    • 2.1 什麼是元件化
    • 2.2 區分模組化與元件化
    • 2.3 元件化優勢好處
    • 2.4 區分元件化和外掛化
    • 2.5 application和library
    • 2.6 注意第三方sdk拆分問題
  • 3.建立元件化框架
    • 3.1 傳統APP架構圖
    • 3.2 元件化需要考慮問題
    • 3.3 架構設計圖
    • 3.4 元件通訊是通過路由轉發
    • 3.5 解決考慮問題
    • 3.6 業務元件的生命週期
    • 3.7 Fragment通訊難點
  • 4.實際開發案例
    • 4.1 元件化實踐的開源專案
    • 4.1 如何建立模組
    • 4.2 如何建立依賴
    • 4.3 如何統一配置檔案
    • 4.4 元件化的基礎庫
    • 4.5 元件模式和整合模式如何切換
    • 4.6 元件化解決重複依賴
    • 4.7 元件化注意要點
    • 4.8 元件化時資源名衝突
    • 4.9 元件化開發遇到問題
  • 5.元件間通訊
    • 5.1 選擇那個開源路由庫
    • 5.2 阿里Arouter基礎原理
    • 5.3 使用Arouter注意事項
  • 6.關於其他
    • 6.1 參考部落格連結
    • 6.2 我的部落格介紹
    • 6.3 開源專案地址

0.元件化開發案例開源地址

github.com/yangchong21…

1.為什麼要元件化

1.1 為什麼要元件化

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

1.2 現階段遇到的問題

  • 結合投資界,新芽客戶端分析
    • 程式碼量膨脹,不利於維護,不利於新功能的開發。專案工程構建速度慢,在一些電腦上寫兩句程式碼,重新編譯整個專案,測試的話編譯速度起碼 10-20 分鐘,有的甚至更長。
    • 不同模組之間程式碼耦合嚴重,有時候修改一處程式碼而牽動許多模組。每個模組之間都有引用第三方庫,但有些第三方庫版本不一致,導致打包APP時候程式碼冗餘,容易引起版本衝突。
    • 現有專案基於以前其他人專案基礎上開發,經手的人次過多,存在著不同的程式碼風格,專案中程式碼規範亂,類似的功能寫法卻不一樣,導致不統一。

2.元件化的概念

2.1 什麼是元件化

  • 什麼是元件化呢?
    • 元件(Component)是對資料和方法的簡單封裝,功能單一,高內聚,並且是業務能劃分的最小粒度。
    • 元件化是基於元件可重用的目的上,將一個大的軟體系統按照分離關注點的形式,拆分成多個獨立的元件,使得整個軟體系統也做到電路板一樣,是單個或多個元件元件組裝起來,哪個元件壞了,整個系統可繼續執行,而不出現崩潰或不正常現象,做到更少的耦合和更高的內聚。

2.2 區分模組化與元件化

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

2.3 元件化優勢好處

  • 簡單來說就是提高工作效率,解放生產力,好處如下:
    • 1.提高編譯速度,從而提高並行開發效率。
      • 問題:那麼如何提高編譯速度的呢?元件化框架可以使模組單獨編譯除錯,可以有效地減少編譯的時間。
    • 2.穩定的公共模組採用依賴庫方式
      • 提供給各個業務線使用,減少重複開發和維護工作量。程式碼簡潔,冗餘量少,維護方便,易擴充套件新功能。
    • 3.每個元件有自己獨立的版本,可以獨立編譯、測試、打包和部署。
      • 針對開發程式設計師多的公司,元件化很有必要,每個人負責自己的模組,可以較少提交程式碼衝突。
      • 為新業務隨時整合提供了基礎,所有業務可上可下,靈活多變。
      • 各業務線研發可以互不干擾、提升協作效率,並控制產品質量。
    • 4.避免模組之間的交叉依賴,做到低耦合、高內聚。
    • 5.引用的第三方庫程式碼統一管理,避免版本統一,減少引入冗餘庫。
      • 這個可以建立一個公共的gradle管理的檔案,比如一個專案有十幾個元件,想要改下某個庫或者版本號,總不至於一個個修改吧。這個時候提取公共十分有必要
    • 6.定製專案可按需載入,元件之間可以靈活組建,快速生成不同型別的定製產品。

2.4 區分元件化和外掛化

  • 元件化和外掛化的區別
    • 元件化不是外掛化,外掛化是在【執行時】,而元件化是在【編譯時】。換句話說,外掛化是基於多APK的,而元件化本質上還是隻有一個 APK。
    • 元件化和外掛化的最大區別(應該也是唯一區別)就是元件化在執行時不具備動態新增和修改元件的功能,但是外掛化是可以的。
  • 元件化的目標
    • 元件化的目標之一就是降低整體工程(app)與元件的依賴關係,缺少任何一個元件都是可以存在並正常執行的。app主工程具有和元件進行繫結和解綁的功能。

2.5 application和library

  • 在studio中,對兩種module進行區分,如下所示
    • 一種是基礎庫library,比如常見第三方庫都是lib,這些程式碼被其他元件直接引用。
    • 另一種是application,也稱之為Component,這種module是一個完整的功能模組。比如分享module就是一個Component。
    • 為了方便,統一把library稱之為依賴庫,而把Component稱之為元件,下面所講的元件化也主要是針對Component這種型別。
  • 在專案的build.gradle檔案中
    //控制元件模式和整合模式
    if (rootProject.ext.isDouBanApplication) {
        //是Component,可以獨立執行
        apply plugin: `com.android.application`
    } else {
        //是lib,被依賴
        apply plugin: `com.android.library`
    }
    複製程式碼

2.6 注意第三方sdk拆分問題

  • 看了很多部落格,幾乎沒有部落格說出在拆分業務元件時,遇到第三方sdk整合的問題。比如:當你的app可以使用微信登陸,在app主工程時,登陸是正常的,這個時候你是通過主工程app的包名去微信開放平臺申請id和key值。但是當你將登陸註冊拆分出獨立的業務元件時,則該元件的包名是跟app主工程包名不一樣的,那麼這個時候,如果切換成元件模式則第三方登陸就有可能出現問題。
  • 也就是說,你使用某些第三方sdk時,當初用app的包名去申請得到key值[這個值是根據包名生成的],然後當你拆分業務元件時,自然元件包名和app包名不一樣,那麼當切換成元件application可以獨立執行時,則可能會出現bug,由包名導致的問題。個人建議,涉及到第三方sdk拆分,可以封裝成lib被依賴即可,或者你刻意把包名弄成一樣的。

3.建立元件化框架

3.1 傳統APP架構圖

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

3.2 元件化需要考慮問題

  • 考慮的問題
  • 分而治之,並行開發,一切皆元件。要實現元件化,無論採用什麼樣的技術方式,需要考慮以下七個方面問題:
    • 程式碼解耦。
      • 如何將一個龐大的工程分成有機的整體?這個需要一步步來了!
      • 對已存在的專案進行模組拆分,模組分為兩種型別,一種是功能元件模組,封裝一些公共的方法服務等,作為依賴庫對外提供;另一種是業務元件模組,專門處理業務邏輯等功能,這些業務元件模組最終負責組裝APP。
    • 元件單獨執行。
      • 因為每個元件都是高度內聚的,是一個完整的整體,如何讓其單獨執行和除錯?
      • 通過 Gradle指令碼配置方式,進行不同環境切換,我自己操作是新增一個boolean值的開關。比如只需要把 Apply plugin: `com.android.library` 切換成Apply plugin: `com.android.application` 就可以獨立執行呢!
      • 需要注意:當切換到application獨立執行時,需要在AndroidManifest清單檔案上進行設定,因為一個單獨除錯需要有一個入口的Activity。
    • 元件間通訊。
      • 由於每個元件具體實現細節都互相不瞭解,但每個元件都需要給其他呼叫方提供服務,那麼主專案與元件、元件與元件之間如何通訊就變成關鍵?
      • 這個我是直接用阿里開源的路由框架,當然你可以根據需要選擇其他大廠的開源路由庫。引用阿里的ARouter框架,通過註解方式進行頁面跳轉。
    • 元件生命週期。
      • 這裡的生命週期指的是元件在應用中存在的時間,元件是否可以做到按需、動態使用、因此就會涉及到元件載入、解除安裝等管理問題。
    • 整合除錯。
      • 在開發階段如何做到按需編譯元件?一次除錯中可能有一兩個元件參與整合,這樣編譯時間就會大大降低,提高開發效率。
    • 程式碼隔離。
      • 元件之間的互動如果還是直接引用的話,那麼元件之間根本沒有做到解耦,如何從根本上避免元件之間的直接引用?目前做法是主專案和業務元件都會依賴公共基礎元件庫,業務元件通過路由服務依賴庫按需進行查詢,用於不同元件之間的通訊。
    • 告別結構臃腫,讓各個業務變得相對獨立,業務元件在元件模式下可以獨立開發,而在整合模式下又可以變為AAR包整合到“APP殼工程”中,組成一個完整功能的 APP。

3.3 架構設計圖

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

3.4 元件通訊是通過路由轉發

  • 傳統以前工程下模組
    • 記得剛開始進入Android開發工作時,只有一個app主工程,後期幾乎所有的需求都寫在這個app主工程裡面。只有簡單的以業務邏輯劃分的資料夾,並且業務之間也是直接相互呼叫、高度耦合在一起的。
    • 導致後期改專案為元件化的時候十分痛苦,不同模組之間的業務邏輯實在關聯太多,但還是沒辦法,於是目錄4步驟一步步實踐。終極目標是,告別結構臃腫,讓各個業務變得相對獨立,業務元件在元件模式下可以獨立開發。
  • 元件化模式下如何通訊
    • 這是元件化工程模型下的業務關係,業務之間將不再直接引用和依賴,而是通過“路由”這樣一箇中轉站間接產生聯絡。在這個開源專案中,我使用的阿里開源的路由框架。關於Arouter基礎使用和程式碼分析,可以看我這篇部落格:Arouter使用與程式碼解析
    • image

3.6 業務元件的生命週期

  • 按照理想狀態的來看待的話
    • 各個業務元件之間沒有任何依賴關係,這時我們可以把每個獨立的業務元件看成一個可執行的app,所以業務元件的生命週期和應與獨立的app保持一致。

3.7 Fragment通訊難點

  • 在網上看到很多部落格說,如何拆分元件,按模組拆分,或者按照功能拆分。但很少有提到fragment在拆分元件時的疑問,這個讓我很奇怪。
  • 先來說一個業務需求,比如一個購物商城app,有4個模組,做法一般是一個activity+4個fragment,這個大家都很熟悉,這四個模組分別是:首頁,發現,購物車,我的。然後這幾個頁面是用fragment寫的,共用一個宿主activity,那麼在做元件化的時候,我想把它按照業務拆分成首頁,發現,購物車和我的四個獨立的業務模組。
  • 遇到疑問:
    • 如果是拆分成四個獨立的業務模組,那麼對應的fragment肯定要放到對應的元件中,那麼這樣操作,當主工程與該業務元件解綁的情況下,如何拿到fragment和傳遞引數進行通訊。
    • Fragment 中 開啟Activity帶requestCode,開啟的Activity關閉後,不會回撥Fragment中的onActivityResult。只會呼叫Fragment 所在Activity的onActivityResult。
    • 多fragment單activity攔截器不管用,難道只能用於攔截activity的跳轉?那如果是要實現登入攔截的話,那不是隻能在PathReplaceService中進行了?
  • 網路解決辦法
    • 第一個疑問:由於我使用阿里路由,所以我看到zhi1ong大佬說:用Router跳轉到這個Activity,然後帶一個引數進去,比方說tab=2,然後自己在onCreate裡面自行切換。但後來嘗試,還是想問問廣大程式設計師有沒有更好的辦法。
    • 第二個疑問:還是zhi1ong大佬說,通過廣播,或者在Activity中轉發這個事件,比方說讓Fragment統一依賴一個介面,然後在Activity中轉發。

4.實際開發案例

4.1 元件化實踐的開源專案

  • 關於元件化開發一點感想
    • 關於網上有許多關於元件化的部落格,講解了什麼是元件化,為何要元件化,以及元件化的好處。大多數文章提供了元件化的思路,給我著手元件化開發提供了大量的便利。感謝前輩大神的分享!雖然有一些收穫,但是很少有文章能夠給出一個整體且有效的方案,或者一個具體的Demo。
    • 但是畢竟看部落格也是為了實踐做準備,當著手將之前的開源案例改版成元件化案例時,出現了大量的問題,也解決了一些問題。主要是學些了元件化開發流程。
    • 大多數公司慢慢著手元件化開發,在小公司,有的人由於之前沒有做過元件化開發,嘗試元件化也是挺好的;在大公司,有的人一去只是負責某個模組,可能剛開始元件化已經有人弄好了,那學習實踐元件化那更快一些。業餘實踐,改版之前開源專案,寫了這篇部落格,耗費我不少時間,要是對你有些幫助,那我就很開心呢。由於我也是個小人物,所以寫的不好,勿噴,歡迎提出建議!
  • 關於元件化開源專案
    • 專案整體架構模式採用:元件化+MVP+Rx+Retrofit+design+Dagger2+VLayout+X5
    • 包含的模組:wanAndroid【kotlin】+乾貨集中營+知乎日報+番茄Todo+精選新聞+豆瓣音樂電影小說+小說讀書+簡易記事本+搞笑視訊+經典遊戲+其他更多等等
    • 此專案屬於業餘時間練手的專案,介面資料來源均來自網路,如果存在侵權情況,請第一時間告知。本專案僅做學習交流使用,API資料內容所有權歸原作公司所有,請勿用於其他用途。
    • 關於開源元件化的專案地址:github.com/yangchong21…

4.1 如何建立模組

  • 根據3.3 架構設計圖可以知道
  • 主工程:
    • 除了一些全域性配置和主 Activity 之外,不包含任何業務程式碼。有的也叫做空殼app,主要是依賴業務元件進行執行。
  • 業務元件:
    • 最上層的業務,每個元件表示一條完整的業務線,彼此之間互相獨立。原則上來說:各個業務元件之間不能有直接依賴!所有的業務元件均需要可以做到獨立執行的效果。對於測試的時候,需要依賴多個業務元件的功能進行整合測試的時候。可以使用app殼進行多元件依賴管理執行。
    • 該案例中分為:幹活集中營,玩Android,知乎日報,微信新聞,頭條新聞,搞笑視訊,百度音樂,我的記事本,豆瓣音樂讀書電影,遊戲元件等等。
  • 功能元件:
    • 該案例中分為,分享元件,評論反饋元件,支付元件,畫廊元件等等。同時注意,可能會涉及多個業務元件對某個功能元件進行依賴!
  • 基礎元件:
    • 支撐上層業務元件執行的基礎業務服務。此部分元件為上層業務元件提供基本的功能支援。
    • 該案例中:在基礎元件庫中主要有,網路請求,圖片載入,通訊機制,工具類,分享功能,支付功能等等。當然,我把一些公共第三方庫放到了這個基礎元件中!

4.2 如何建立依賴

  • 關於工程中元件依賴結構圖如下所示
    • image
  • 業務模組下完整配置程式碼
    //控制元件模式和整合模式
    if (rootProject.ext.isGankApplication) {
        apply plugin: `com.android.application`
    } else {
        apply plugin: `com.android.library`
    }
    
    android {
        compileSdkVersion rootProject.ext.android["compileSdkVersion"]
        buildToolsVersion rootProject.ext.android["buildToolsVersion"]
    
    
        defaultConfig {
            minSdkVersion rootProject.ext.android["minSdkVersion"]
            targetSdkVersion rootProject.ext.android["targetSdkVersion"]
            versionCode rootProject.ext.android["versionCode"]
            versionName rootProject.ext.android["versionName"]
    
            if (rootProject.ext.isGankApplication){
                //元件模式下設定applicationId
                applicationId "com.ycbjie.gank"
            }
            javaCompileOptions {
                annotationProcessorOptions {
                    arguments = [AROUTER_MODULE_NAME: project.getName()]
                }
            }
        }
    
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile(`proguard-android.txt`), `proguard-rules.pro`
            }
        }
    
        //jdk1.8
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
    
        sourceSets {
            main {
                if (rootProject.ext.isGankApplication) {
                    manifest.srcFile `src/main/module/AndroidManifest.xml`
                } else {
                    manifest.srcFile `src/main/AndroidManifest.xml`
                }
                jniLibs.srcDirs = [`libs`]
            }
        }
    
    }
    
    dependencies {
        implementation fileTree(dir: `libs`, include: [`*.jar`])
        implementation project(`:library`)
        annotationProcessor rootProject.ext.dependencies["router-compiler"]
    }
    複製程式碼

4.3 如何統一配置檔案

  • 由於元件化實踐中模組比較多,因此配置gradle,新增依賴庫時,需要考慮簡化工作。那麼究竟如何做呢?
  • 第一步,首先在專案根目錄下建立一個yc.gradle檔案。實際開發中只需要更改該檔案中版本資訊即可。
    • 我在網上看到的絕大多數案例,都是通過一個開關控制元件元件模式和整合模式的切換,但是這裡我配置了多個元件的開關,分別控制對應的元件切換狀態。
    ext {
    
        isApplication = false  //false:作為Lib元件存在, true:作為application存在
        isAndroidApplication = false  //玩Android模組開關,false:作為Lib元件存在, true:作為application存在
        isLoveApplication = false  //愛意表達模組開關,false:作為Lib元件存在, true:作為application存在
        isVideoApplication = false  //視訊模組開關,false:作為Lib元件存在, true:作為application存在
        isNoteApplication = false  //記事本模組開關,false:作為Lib元件存在, true:作為application存在
        isBookApplication = false  //book模組開關,false:作為Lib元件存在, true:作為application存在
        isDouBanApplication = false  //豆瓣模組開關,false:作為Lib元件存在, true:作為application存在
        isGankApplication = false  //乾貨模組開關,false:作為Lib元件存在, true:作為application存在
        isMusicApplication = false  //音樂模組開關,false:作為Lib元件存在, true:作為application存在
        isNewsApplication = false  //新聞模組開關,false:作為Lib元件存在, true:作為application存在
        isToDoApplication = false  //todo模組開關,false:作為Lib元件存在, true:作為application存在
        isZhiHuApplication = false  //知乎模組開關,false:作為Lib元件存在, true:作為application存在
        isOtherApplication = false  //其他模組開關,false:作為Lib元件存在, true:作為application存在
        
        android = [
                   compileSdkVersion       : 28,
                   buildToolsVersion       : "28.0.3",
                   minSdkVersion           : 17,
                   targetSdkVersion        : 28,
                   versionCode             : 22,
                   versionName             : "1.8.2"    //必須是int或者float,否則影響線上升級
        ]
    
        version = [
                   androidSupportSdkVersion: "28.0.0",
                   retrofitSdkVersion      : "2.4.0",
                   glideSdkVersion         : "4.8.0",
                   canarySdkVersion        : "1.5.4",
                   constraintVersion       : "1.0.2"
        ]
    
        dependencies = [
                    //support
                    "appcompat-v7"             : "com.android.support:appcompat-v7:${version["androidSupportSdkVersion"]}",
                    "multidex"                 : "com.android.support:multidex:1.0.1",
                    //network
                    "retrofit"                 : "com.squareup.retrofit2:retrofit:${version["retrofitSdkVersion"]}",
                    "retrofit-converter-gson"  : "com.squareup.retrofit2:converter-gson:${version["retrofitSdkVersion"]}",
                    "retrofit-adapter-rxjava"  : "com.squareup.retrofit2:adapter-rxjava2:${version["retrofitSdkVersion"]}",
                    //這裡省略一部分程式碼
            ]
    }
    複製程式碼
  • 第二步,然後在專案中的lib【注意這裡是放到基礎元件庫的build.gradle】中新增程式碼,如下所示
    apply plugin: `com.android.library`
    
    android {
        compileSdkVersion rootProject.ext.android["compileSdkVersion"]
        buildToolsVersion rootProject.ext.android["buildToolsVersion"]
    
    
        defaultConfig {
            minSdkVersion rootProject.ext.android["minSdkVersion"]
            targetSdkVersion rootProject.ext.android["targetSdkVersion"]
            versionCode rootProject.ext.android["versionCode"]
            versionName rootProject.ext.android["versionName"]
        }
    }
    
    
    dependencies {
        implementation fileTree(dir: `libs`, include: [`*.jar`])
        api rootProject.ext.dependencies["appcompat-v7"]
        api rootProject.ext.dependencies["design"]
        api rootProject.ext.dependencies["palette"]
        api rootProject.ext.dependencies["glide"]
        api (rootProject.ext.dependencies["glide-transformations"]){
            exclude module: `glide`
        }
        annotationProcessor rootProject.ext.dependencies["glide-compiler"]
        api files(`libs/tbs_sdk_thirdapp_v3.2.0.jar`)
        api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
        
        //省略部分程式碼
    }
    複製程式碼
  • 第三步,在其他model中新增依賴
    • implementation project(`:library`)即可。

4.4 元件化的基礎庫

  • 基礎庫元件封裝
    • 基礎庫元件封裝庫中主要包括開發常用的一些框架。可以直接看我的專案更加直觀!
    • 1、網路請求(採用Retrofit+RxJava框架),攔截器
    • 2、圖片載入(策略模式,Glide與Picasso之間可以切換)
    • 3、通訊機制(RxBus),路由ARouter簡單封裝工具類(不同model之間通訊)
    • 4、mvp框架,常用的base類,比如BaseActivity,BaseFragment等等
    • 5、通用的工具類,比如切割圓角,動畫工具類等等
    • 6、自定義view(包括對話方塊,ToolBar佈局,圓形圖片等view的自定義)
    • 7、共有的shape,drawable,layout,color等資原始檔
    • 8、全域性初始化非同步執行緒池封裝庫,各個元件均可以用到
  • 元件初始化
    • 比如,你將該案例中的新聞元件切換成獨立執行的app,那麼由於新聞跳轉詳情頁需要使用到x5的WebView,因此需要對它進行初始化。最剛開始做法是,為每一個可以切換成app的元件配置一個獨立的application,然後初始化一些該元件需要初始化的任務。但是這麼做,有一點不好,不是很方便管理。後來看了知乎元件化實踐方案後,該方案提出,開發了一套多執行緒初始化框架,每個元件只要新建若干個啟動 Task 類,並在 Task 中宣告依賴關係。但是具體怎麼用到程式碼中後期有待實現!
  • 如何簡化不熟悉元件化的人快速適應元件獨立執行
    • 設定多個元件開關,需要切換那個元件就改那個。如果設定一個開關,要麼把所有元件切成整合模式,要麼把所有元件切成元件模式,有點容易出問題。更多可以往下看!
  • 嚴格限制公共基礎元件的增長
    • 隨著開發不斷進行,要注意不要往基礎公共元件加入太多內容。而是應該減小體積!倘若是基礎元件過於龐大,那麼執行元件也是比較緩慢的!

4.5 元件模式和整合模式如何切換

  • 在玩Android元件下的build.gradle檔案,其他元件類似。
    • 通過一個開關來控制這個狀態的切換,module如果是一個庫,會使用com.android.library外掛;如果是一個應用,則使用com.android.application外掛
    //控制元件模式和整合模式
    if (rootProject.ext.isAndroidApplication) {
        apply plugin: `com.android.application`
    } else {
        apply plugin: `com.android.library`
    }
    複製程式碼
  • 整合模式如下所示
    • 首先需要在yc.gradle檔案中設定 isApplication=false。Sync下後,發現該元件是library
    ext {
        isAndroidApplication = false  //false:作為Lib元件存在, true:作為application存在
    複製程式碼
  • 元件模式如下所示
    • 首先需要在yc.gradle檔案中設定 isApplication=true。Sync下後,發現該元件是application,即可針對模組進行執行
    ext {
        isAndroidApplication = true  //false:作為Lib元件存在, true:作為application存在
    複製程式碼
  • 需要注意的地方,這個很重要
    • 首先看看網上絕大多數的作法,非常感謝這些大神的無私奉獻!但是我覺得多個元件用一個開關控制也可以,但是sourceSets裡面切換成元件app時,可以直接不用下面這麼麻煩,可以複用java和res檔案。
      • image
    • 接下來看看我的做法:
    • 下面這個配置十分重要。也就是說當該玩Android元件從library切換到application時,由於可以作為獨立app執行,所以序意設定applicationId,並且配置清單檔案,如下所示!
    • 在 library 和 application 之間切換,manifest檔案也需要提供兩套
    android {
        defaultConfig {
            if (rootProject.ext.isAndroidApplication){
                //元件模式下設定applicationId
                applicationId "com.ycbjie.android"
            }
        }
        sourceSets {
            main {
                if (rootProject.ext.isAndroidApplication) {
                    manifest.srcFile `src/main/module/AndroidManifest.xml`
                } else {
                    manifest.srcFile `src/main/AndroidManifest.xml`
                }
                jniLibs.srcDirs = [`libs`]
            }
        }
    }
    複製程式碼
    • 具體在專案中如下所示
    • image

4.6 元件化解決重複依賴

  • 重複依賴問題說明
    • 重複依賴問題其實在開發中經常會遇到,比如專案 implementation 了一個A,然後在這個庫裡面又 implementation 了一個B,然後你的工程中又 implementation 了一個同樣的B,就依賴了兩次。
    • 預設情況下,如果是 aar 依賴,gradle 會自動幫我們找出新版本的庫而拋棄舊版本的重複依賴。但是如果使用的是project依賴,gradle並不會去去重,最後打包就會出現程式碼中有重複的類了。
  • 解決辦法,舉個例子
    api(rootProject.ext.dependencies["logger"]) { 
        exclude module: `support-v4`//根據元件名排除 
        exclude group: `android.support.v4`//根據包名排除 
    }
    複製程式碼

4.7 元件化注意要點

  • 業務元件之間聯動導致耦合嚴重
    • 比如,實際開發中,購物車和首頁商品分別是兩個元件。但是遇到產品需求,比如過節做個活動,發個購物券之類的需求,由於購物車和商品詳情頁都有活動,因此會造成元件經常會發生聯動。倘若前期準備不足,隨著時間的推移,各個業務線的程式碼邊界會像元件化之前的主工程一樣逐漸劣化,耦合會越來越嚴重。
    • 第一種解決方式:使用 sourceSets 的方式將不同的業務程式碼放到不同的資料夾,但是 sourceSets 的問題在於,它並不能限制各個 sourceSet 之間互相引用,所以這種方式並不太友好!
    • 第二種解決方式:抽取需求為工具類,通過不同元件傳值而達到呼叫關係,這樣只需要改工具類即可改需求。但是這種只是符合需求一樣,但是用在不同模組的場景。
  • 元件化開發之資料庫分離
    • 比如,我現在開發的視訊模組想要給別人用,由於快取之類需要用到資料庫,難道還要把這個lib還得依賴一個體積較大的第三方資料庫?但是使用系統原生sql資料庫又不太方便,怎麼辦?暫時我也沒找到辦法……

4.8 元件化時資源名衝突

  • 資源名衝突有哪些?
    • 比如,color,shape,drawable,圖片資源,佈局資源,或者anim資源等等,都有可能造成資源名稱衝突。這是為何了,有時候大家負責不同的模組,如果不是按照統一規範命名,則會偶發出現該問題。
    • 尤其是如果string, color,dimens這些資源分佈在了程式碼的各個角落,一個個去拆,非常繁瑣。其實大可不必這麼做。因為android在build時,會進行資源的merge和shrink。res/values下的各個檔案(styles.xml需注意)最後都只會把用到的放到intermediate/res/merged/../valus.xml,無用的都會自動刪除。並且最後我們可以使用lint來自動刪除。所以這個地方不要耗費太多的時間。
  • 解決辦法
    • 這個問題也不是新問題了,第三方SDK基本都會遇到,可以通過設定 resourcePrefix 來避免。設定了這個值後,你所有的資源名必須以指定的字串做字首,否則會報錯。但是 resourcePrefix 這個值只能限定 xml 裡面的資源,並不能限定圖片資源,所有圖片資源仍然需要你手動去修改資源名。
  • 個人建議
    • 將color,shape等放到基礎庫元件中,因為所有的業務元件都會依賴基礎元件庫。在styles.xml需注意,寫屬性名字的時候,一定要加上字首限定詞。假如說不加的話,有可能會在打包成aar後給其他模組使用的時候,會出現屬性名名字重複的衝突,為什麼呢?因為BezelImageView這個名字根本不會出現在intermediate/res/merged/../valus.xml裡, 所以不要以為這是屬性的限定詞!

4.9 元件化開發遇到問題

  • 如何做到各個元件化模組能獲取到全域性上下文
    • 情景再現
      • 比如,剛開始線上專案是在app主工程裡建立的單利,那麼在lib中或者後期劃分的元件化,是無法拿到主工程的application類中的上下文。這個時候可以
    • 解決辦法
      • 很容易,在lib裡寫一個Utils工具類,然後在主工程application中初始化Utils.init(this),這樣就可以在lib和所有業務元件[已經依賴公共基礎元件庫]中拿到全域性上下文呢!
  • butterKnife使用問題
    • 儘管網上有不少部落格說可以解決butterKnife在不同元件之間的引用。但是我在實際開發時,遇到元件模式和整合模式切換狀態時,導致出現編譯錯誤問題。要是那位在元件化中解決butterKnife引用問題,可以告訴我,非常感謝!
  • 當元件化是lib時
    • 不能使用switch(R.id.xx),需要使用if..else來代替。
  • 不要亂髮bus訊息
    • 如果專案中大量的使用eventbus,那麼會看到一個類中有大量的onEventMainThread()方法,寫起來很爽,閱讀起來很痛苦。
    • 雖然說,前期使用EventBus或者RxBus傳送訊息來實現元件間通訊十分方便和簡單,但是隨著業務增大,和後期不斷更新,有的還經過多個程式設計師前前後後修改,會使程式碼閱讀量降低。專案中傳送這個Event的地方非常多,接收這個Event的地方也很多。在後期想要改進為元件化開發,而進行程式碼拆分時,都不敢輕舉妄動,生怕哪些事件沒有被接收。
  • 頁面跳轉存在問題
    • 如果一個頁面需要登陸狀態才可以檢視,那麼會寫if(isLogin()){//跳轉頁面}else{//跳轉到登入頁面},每次操作都要寫這些個相同的邏輯。
    • 原生startActivity跳轉,無法監聽到跳轉的狀態,比如跳轉錯誤,成功,異常等問題。
    • 後時候,後臺會控制從點選按鈕【不同場景下】跳轉到不同的頁面,假如後臺配置資訊錯誤,或者少了引數,那麼跳轉可能不成功或者導致崩潰,這個也沒有一個好的處理機制。
    • 阿里推出的開源框架Arouter,便可以解決頁面跳轉問題,可以新增攔截,或者即使後臺配置引數錯誤,當監聽到跳轉異常或者跳轉錯誤時的狀態,可以直接預設跳轉到首頁。我在該開源案例就是這麼做的!
  • 關於跳轉引數問題
    • 先來看一下這種程式碼寫法,這種寫法本沒有問題,只是在多人開發時,如果別人想要跳轉到你開發模組的某個頁面,那麼就容易傳錯值。建議將key這個值,寫成靜態常量,放到一個專門的類中。方便自己,也方便他人。
      //跳轉
      intent.setClass(this,CommentActivity.class);
      intent.putExtra("id",id);
      intent.putExtra("allNum",allNum);
      intent.putExtra("shortNum",shortNum);
      intent.putExtra("longNum",longNum);
      startActivity(intent);
      
      
      //接收
      Intent intent = getIntent();
      int allNum = intent.getExtras().getInt("allNum");
      int shortNum = intent.getExtras().getInt("shortNum");
      int longNum = intent.getExtras().getInt("longNum");
      int id = intent.getExtras().getInt("id");
      複製程式碼

5.元件間通訊

5.1 選擇那個開源路由庫

  • 比較有代表性的元件化開源框架有得到得到DDComponentForAndroid、阿里Arouter、聚美Router 等等。
    • 得到DDComponentForAndroid:一套完整有效的android元件化方案,支援元件的元件完全隔離、單獨除錯、整合除錯、元件互動、UI跳轉、動態載入解除安裝等功能。
    • 阿里Arouter:對頁面、服務提供路由功能的中介軟體,簡單且夠用好用,網上的使用介紹部落格也很多,在該元件化案例中,我就是使用這個。
    • Router:一款單品、元件化、外掛化全支援的路由框架

5.2 阿里Arouter基礎原理

  • 這裡只是說一下基礎的思路
    • 在程式碼里加入的@Route註解,會在編譯時期通過apt生成一些儲存path和activityClass對映關係的類檔案,然後app程式啟動的時候會拿到這些類檔案,把儲存這些對映關係的資料讀到記憶體裡(儲存在map裡),然後在進行路由跳轉的時候,通過build()方法傳入要到達頁面的路由地址。
      • 新增@Route註解然後編譯一下,就可以生成這個類,然後看一下這個類。如下所示:
      /**
       * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
      public class ARouter$$Group$$video implements IRouteGroup {
        @Override
        public void loadInto(Map<String, RouteMeta> atlas) {
          atlas.put("/video/VideoActivity", RouteMeta.build(RouteType.ACTIVITY, VideoActivity.class, "/video/videoactivity", "video", null, -1, -2147483648));
        }
      }
      複製程式碼
    • ARouter會通過它自己儲存的路由表找到路由地址對應的Activity.class(activity.class = map.get(path)),然後new Intent(),當呼叫ARouter的withString()方法它的內部會呼叫intent.putExtra(String name, String value),呼叫navigation()方法,它的內部會呼叫startActivity(intent)進行跳轉,這樣便可以實現兩個相互沒有依賴的module順利的啟動對方的Activity了。
      • 看_ARouter類中的 _navigation方法程式碼,在345行。
      private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
          final Context currentContext = null == context ? mContext : context;
      
          switch (postcard.getType()) {
              case ACTIVITY:
                  // Build intent
                  final Intent intent = new Intent(currentContext, postcard.getDestination());
                  intent.putExtras(postcard.getExtras());
      
                  // Set flags.
                  int flags = postcard.getFlags();
                  if (-1 != flags) {
                      intent.setFlags(flags);
                  } else if (!(currentContext instanceof Activity)) {    // Non activity, need less one flag.
                      intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                  }
      
                  // Set Actions
                  String action = postcard.getAction();
                  if (!TextUtils.isEmpty(action)) {
                      intent.setAction(action);
                  }
      
                  // Navigation in main looper.
                  runInMainThread(new Runnable() {
                      @Override
                      public void run() {
                          startActivity(requestCode, currentContext, intent, postcard, callback);
                      }
                  });
      
                  break;
              case PROVIDER:
                  //這裡省略程式碼
              case BOARDCAST:
              case CONTENT_PROVIDER:
              case FRAGMENT:
                  //這裡省略程式碼
              case METHOD:
              case SERVICE:
              default:
                  return null;
          }
          return null;
      }
      複製程式碼

5.3 使用Arouter注意事項

  • 使用阿里路由抽取工具類,方便後期維護!
    • 首先看一下網路上有一種寫法。
      //首先通過註解新增下面程式碼
      @Route(path = "/test/TestActivity")
      public class TestActivity extends BaseActivity {
      
      }
      
      //跳轉
      ARouter.getInstance().inject("/test/TestActivity");
      複製程式碼
    • 優化後的寫法
      • 下面這種做法,是方便後期維護。
      //存放所有的路由路徑常量
      public class ARouterConstant {
          //跳轉到視訊頁面
          public static final String ACTIVITY_VIDEO_VIDEO = "/video/VideoActivity";
          //省略部分diamagnetic
      }
      
      //存放所有的路由跳轉,工具類
      public class ARouterUtils {
          /**
           * 簡單的跳轉頁面
           * @param string                string目標介面對應的路徑
           */
          public static void navigation(String string){
              if (string==null){
                  return;
              }
              ARouter.getInstance().build(string).navigation();
          }
      }
      
      //呼叫
      @Route(path = ARouterConstant.ACTIVITY_VIDEO_VIDEO)
      public class VideoActivity extends BaseActivity {
      
      }
      ARouterUtils.navigation(ARouterConstant.ACTIVITY_VIDEO_VIDEO);
      複製程式碼

06.關於其他內容介紹

6.1 關於部落格彙總連結

6.1 參考部落格連結

6.2 關於我的部落格

6.3 開源專案地址

元件化實踐專案開源地址:github.com/yangchong21…

相關文章