Android徹底組件化方案實踐

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

本文提出的組件化方案demo已經開源,參見文章 "Android徹底組件化方案開源" 。 文末有羅輯思維“得到app”的招聘廣告,歡迎各路牛人加入!! 一、模塊化、組件化與插件化 項目發展到一定程度,隨著人員的增多,代碼越來越臃腫,這時候就必須進行模塊化的拆分。在我看來,模塊化是一種指導理念,其核心 ...


本文提出的組件化方案demo已經開源,參見文章Android徹底組件化方案開源
文末有羅輯思維“得到app”的招聘廣告,歡迎各路牛人加入!!

一、模塊化、組件化與插件化

  項目發展到一定程度,隨著人員的增多,代碼越來越臃腫,這時候就必須進行模塊化的拆分。在我看來,模塊化是一種指導理念,其核心思想就是分而治之、降低耦合。而在Android工程中如何實施,目前有兩種途徑,也是兩大流派,一個是組件化,一個是插件化。
  提起組件化和插件化的區別,有一個很形象的圖:

組件化和插件化對比.png
  上面的圖看上去比較清晰,其實容易導致一些誤解,有下麵幾個小問題,圖中可能說的不太清楚:

  • 組件化是一個整體嗎?去了頭和胳膊還能存在嗎?左圖中,似乎組件化是一個有機的整體,需要所有器官都健在才可以存在。而實際上組件化的目標之一就是降低整體(app)與器官(組件)的依賴關係,缺少任何一個器官app都是可以存在並正常運行的。
  • 頭和胳膊可以單獨存在嗎?左圖也沒有說明白,其實答案應該是肯定的。每個器官(組件)可以在補足一些基本功能之後都是可以獨立存活的。這個是組件化的第二個目標:組件可以單獨運行。
  • 組件化和插件化可以都用右圖來表示嗎?如果上面兩個問題的答案都是YES的話,這個問題的答案自然也是YES。每個組件都可以看成一個單獨的整體,可以按需的和其他組件(包括主項目)整合在一起,從而完成的形成一個app
  • 右圖中的小機器人可以動態的添加和修改嗎?如果組件化和插件化都用右圖來表示,那麼這個問題的答案就不一樣了。對於組件化來講,這個問題的答案是部分可以,也就是在編譯期可以動態的添加和修改,但是在運行時就沒法這麼做了。而對於插件化,這個問題的答案很乾脆,那就是完全可以,不論實在編譯期還是運行時!
      本文主要集中講的是組件化的實現思路,對於插件化的技術細節不做討論,我們只是從上面的問答中總結出一個結論:組件化和插件化的最大區別(應該也是唯一區別)就是組件化在運行時不具備動態添加和修改組件的功能,但是插件化是可以的。
      暫且拋棄對插件化“道德”上的批判,我認為對於一個Android開發者來講,插件化的確是一個福音,這將使我們具備極大的靈活性。但是苦於目前還沒有一個完全合適、完美相容的插件化方案(RePlugin的饑餓營銷做的很好,但還沒看到療效),特別是對於已經有幾十萬代碼量的一個成熟產品來講,套用任何一個插件化方案都是很危險的工作。所以我們決定先從組件化做起,本著做一個最徹底的組件化方案的思路去進行代碼的重構,下麵是最近的思考結果,歡迎大家提出建議和意見。

    二、如何實現組件化

      要實現組件化,不論採用什麼樣的技術路徑,需要考慮的問題主要包括下麵幾個:
  • 代碼解耦。如何將一個龐大的工程拆分成有機的整體?
  • 組件單獨運行。上面也講到了,每個組件都是一個完整的整體,如何讓其單獨運行和調試呢?
  • 數據傳遞。因為每個組件都會給其他組件提供的服務,那麼主項目(Host)與組件、組件與組件之間如何傳遞數據?
  • UI跳轉。UI跳轉可以認為是一種特殊的數據傳遞,在實現思路上有啥不同?
  • 組件的生命周期。我們的目標是可以做到對組件可以按需、動態的使用,因此就會涉及到組件載入、卸載和降維的生命周期。
  • 集成調試。在開發階段如何做到按需的編譯組件?一次調試中可能只有一兩個組件參與集成,這樣編譯的時間就會大大降低,提高開發效率。
  • 代碼隔離。組件之間的交互如果還是直接引用的話,那麼組件之間根本沒有做到解耦,如何從根本上避免組件之間的直接引用呢?也就是如何從根本上杜絕耦合的產生呢?只有做到這一點才是徹底的組件化。

    2-1 代碼解耦

      把龐大的代碼進行拆分,Androidstudio能夠提供很好的支持,使用IDE中的multiple module這個功能,我們很容易把代碼進行初步的拆分。在這裡我們對兩種module進行區分,
  • 一種是基礎庫library,這些代碼被其他組件直接引用。比如網路庫module可以認為是一個library。
  • 另一種我們稱之為Component,這種module是一個完整的功能模塊。比如讀書或者分享module就是一個Component。
      為了方便,我們統一把library稱之為依賴庫,而把Component稱之為組件,我們所講的組件化也主要是針對Component這種類型。而負責拼裝這些組件以形成一個完成app的module,一般我們稱之為主項目、主module或者Host,方便起見我們也統一稱為主項目。
      經過簡單的思考,我們可能就可以把代碼拆分成下麵的結構:
    組件化簡單拆分
      這種拆分都是比較容易做到的,從圖上看,讀書、分享等都已經拆分組件,並共同依賴於公共的依賴庫(簡單起見只畫了一個),然後這些組件都被主項目所引用。讀書、分享等組件之間沒有直接的聯繫,我們可以認為已經做到了組件之間的解耦。但是這個圖有幾個問題需要指出:
    ● 從上面的圖中,我們似乎可以認為組件只有集成到主項目才可以使用,而實際上我們的希望是每個組件是個整體,可以獨立運行和調試,那麼如何做到單獨的調試呢?
    ● 主項目可以直接引用組件嗎?也就是說我們可以直接使用compile project(:reader)這種方式來引用組件嗎?如果是這樣的話,那麼主項目和組件之間的耦合就沒有消除啊。我們上面講,組件是可以動態管理的,如果我們刪掉reader(讀書)這個組件,那麼主項目就不能編譯了啊,談何動態管理呢?所以主項目對組件的直接引用是不可以的,但是我們的讀書組件最終是要打到apk裡面,不僅代碼要和併到claases.dex裡面,資源也要經過meage操作合併到apk的資源裡面,怎麼避免這個矛盾呢?
    ● 組件與組件之間真的沒有相互引用或者交互嗎?讀書組件也會調用分享模塊啊,而這在圖中根本沒有體現出來啊,那麼組件與組件之間怎麼交互呢?
      這些問題我們後面一個個來解決,首先我們先看代碼解耦要做到什麼效果,像上面的直接引用並使用其中的類肯定是不行的了。所以我們認為代碼解耦的首要目標就是組件之間的完全隔離,我們不僅不能直接使用其他組件中的類,最好能根本不瞭解其中的實現細節。只有這種程度的解耦才是我們需要的。

    2-2 組件的單獨調試

      其實單獨調試比較簡單,只需要把apply plugin: 'com.android.library'切換成apply plugin: 'com.android.application'就可以,但是我們還需要修改一下AndroidManifest文件,因為一個單獨調試需要有一個入口的actiivity。
      我們可以設置一個變數isRunAlone,標記當前是否需要單獨調試,根據isRunAlone的取值,使用不同的gradle插件和AndroidManifest文件,甚至可以添加Application等Java文件,以便可以做一下初始化的操作。
      為了避免不同組件之間資源名重覆,在每個組件的build.gradle中增加resourcePrefix "xxx_",從而固定每個組件的資源首碼。下麵是讀書組件的build.gradle的示例:

    if(isRunAlone.toBoolean()){    
    apply plugin: 'com.android.application'
    }else{  
     apply plugin: 'com.android.library'
    }
    .....
    resourcePrefix "readerbook_"
    sourceSets {
        main {
            if (isRunAlone.toBoolean()) {
                manifest.srcFile 'src/main/runalone/AndroidManifest.xml'
                java.srcDirs = ['src/main/java','src/main/runalone/java']
                res.srcDirs = ['src/main/res','src/main/runalone/res']
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }

      通過這些額外的代碼,我們給組件搭建了一個測試Host,從而讓組件的代碼運行在其中,所以我們可以再優化一下我們上面的框架圖。
    支持單獨調試的組件化

    2-3 組件的數據傳輸

      上面我們講到,主項目和組件、組件與組件之間不能直接使用類的相互引用來進行數據交互。那麼如何做到這個隔離呢?在這裡我們採用介面+實現的結構。每個組件聲明自己提供的服務Service,這些Service都是一些抽象類或者介面,組件負責將這些Service實現並註冊到一個統一的路由Router中去。如果要使用某個組件的功能,只需要向Router請求這個Service的實現,具體的實現細節我們全然不關心,只要能返回我們需要的結果就可以了。這與Binder的C/S架構很相像。
      因為我們組件之間的數據傳遞都是基於介面編程的,介面和實現是完全分離的,所以組件之間就可以做到解耦,我們可以對組件進行替換、刪除等動態管理。這裡面有幾個小問題需要明確:
    ● 組件怎麼暴露自己提供的服務呢?在項目中我們簡單起見,專門建立了一個componentservice的依賴庫,裡面定義了每個組件向外提供的service和一些公共model。將所有組件的service整合在一起,是為了在拆分初期操作更為簡單,後面需要改為自動化的方式來生成。這個依賴庫需要嚴格遵循開閉原則,以避免出現版本相容等問題。
    ● service的具體實現是由所屬組件註冊到Router中的,那麼是在什麼時間註冊的呢?這個就涉及到組件的載入等生命周期,我們在後面專門介紹。
    ● 一個很容易犯的小錯誤就是通過持久化的方式來傳遞數據,例如file、sharedpreference等方式,這個是需要避免的。
      下麵就是加上數據傳輸功能之後的架構圖:
    組件之間的數據傳輸

    2-4 組件之間的UI跳轉

      可以說UI的跳轉也是組件提供的一種特殊的服務,可以歸屬到上面的數據傳遞中去。不過一般UI的跳轉我們會單獨處理,一般通過短鏈的方式來跳轉到具體的Activity。每個組件可以註冊自己所能處理的短鏈的schme和host,並定義傳輸數據的格式。然後註冊到統一的UIRouter中,UIRouter通過schme和host的匹配關係負責分發路由。
      UI跳轉部分的具體實現是通過在每個Activity上添加註解,然後通過apt形成具體的邏輯代碼。這個也是目前Android中UI路由的主流實現方式。

    2-5 組件的生命周期

      由於我們要動態的管理組件,所以給每個組件添加幾個生命周期狀態:載入、卸載和降維。為此我們給每個組件增加一個ApplicationLike類,裡面定義了onCreate和onStop兩個生命周期函數。
  1. 載入:上面講了,每個組件負責將自己的服務實現註冊到Router中,其具體的實現代碼就寫在onCreate方法中。那麼主項目調用這個onCreate方法就稱之為組件的載入,因為一旦onCreate方法執行完,組件就把自己的服務註冊到Router裡面去了,其他組件就可以直接使用這個服務了。
  2. 卸載:卸載與載入基本一致,所不同的就是調用ApplicationLike的onStop方法,在這個方法中每個組件將自己的服務實現從Router中取消註冊。不過這種使用場景可能比較少,一般適用於一些只用一次的組件。
  3. 降維:降維使用的場景更為少見,比如一個組件出現了問題,我們想把這個組件從本地實現改為一個wap頁。降維一般需要後臺配置才生效,可以在onCreate對線上配置進行檢查,如果需要降維,則把所有的UI跳轉到配置的wap頁上面去。
      一個小的細節是,主項目負責載入組件,由於主項目和組件之間是隔離的,那麼主項目如何調用組件ApplicationLike的生命周期方法呢,目前我們採用的是基於編譯期位元組碼插入的方式,掃描所有的ApplicationLike類(其有一個共同的父類),然後通過javassisit在主項目的onCreate中插入調用ApplicationLike.onCreate的代碼。
      我們再優化一下組件化的架構圖:
    組件的生命周期.png

    2-6 集成調試

      每個組件單獨調試通過並不意味著集成在一起沒有問題,因此在開發後期我們需要把幾個組件機集成到一個app裡面去驗證。由於我們上面的機制保證了組件之間的隔離,所以我們可以任意選擇幾個組件參與集成。這種按需索取的載入機制可以保證在集成調試中有很大的靈活性,並且可以加大的加快編譯速度。
      我們的做法是這樣的,每個組件開發完成之後,發佈一個relaese的aar到一個公共倉庫,一般是本地的maven庫。然後主項目通過參數配置要集成的組件就可以了。所以我們再稍微改動一下組件與主項目之間的連接線,形成的最終組件化架構圖如下:
    最終結構圖.png

    2-7 代碼隔離

      此時在回顧我們在剛開始拆分組件化是提出的三個問題,應該說都找到瞭解決方式,但是還有一個隱患沒有解決,那就是我們可以使用compile project(xxx:reader.aar)來引入組件嗎?雖然我們在數據傳輸章節使用了介面+實現的架構,組件之間必須針對介面編程,但是一旦我們引入了reader.aar,那我們就完全可以直接使用到其中的實現類啊,這樣我們針對介面編程的規範就成了一紙空文。千里之堤毀於蟻穴,只要有代碼(不論是有意還是無意)是這麼做了,我們前面的工作就白費了。
      我們希望只在assembleDebug或者assembleRelease的時候把aar引入進來,而在開發階段,所有組件都是看不到的,這樣就從根本上杜絕了引用實現類的問題。我們把這個問題交給gradle來解決,我們創建一個gradle插件,然後每個組件都apply這個插件,插件的配置代碼也比較簡單:

    //根據配置添加各種組件依賴,並且自動化生成組件載入代碼
     if (project.android instanceof AppExtension) {
            AssembleTask assembleTask = getTaskInfo(project.gradle.startParameter.taskNames)
            if (assembleTask.isAssemble
                    && (assembleTask.modules.contains("all") || assembleTask.modules.contains(module))) {
              //添加組件依賴
               project.dependencies.add("compile","xxx:reader-release@aar")
              //位元組碼插入的部分也在這裡實現
            }
    }
    
    private AssembleTask getTaskInfo(List<String> taskNames) {
        AssembleTask assembleTask = new AssembleTask();
        for (String task : taskNames) {
            if (task.toUpperCase().contains("ASSEMBLE")) {
                assembleTask.isAssemble = true;
                String[] strs = task.split(":")
                assembleTask.modules.add(strs.length > 1 ? strs[strs.length - 2] : "all");
            }
        }
        return assembleTask
    }

三、組件化的拆分步驟和動態需求

3-1 拆分原則

  組件化的拆分是個龐大的工程,特別是從幾十萬行代碼的大工程拆分出去,所要考慮的事情千頭萬緒。為此我覺得可以分成三步:

  • 從產品需求到開發階段再到運營階段都有清晰邊界的功能開始拆分,比如讀書模塊、直播模塊等,這些開始分批先拆分出去
  • 在拆分中,造成組件依賴主項目的依賴的模塊繼續拆出去,比如賬戶體系等
  • 最終主項目就是一個Host,包含很小的功能模塊(比如啟動圖)以及組件之間的拼接邏輯

3-2 組件化的動態需求

  最開始我們講到,理想的代碼組織形式是插件化的方式,屆時就具備了完備的運行時動態化。在向插件化遷徙的過程中,我們可以通過下麵的集中方式來實現編譯速度的提升和動態更新。

  • 在快速編譯上,採用組件級別的增量編譯。在抽離組件之前可以使用代碼級別的增量編譯工具如freeline(但databinding支持較差)、fastdex等
  • 動態更新方面,暫時不支持新增組件等大的功能改進。可以臨時採用方法級別的熱修複或者功能級別的Tinker等工具,Tinker的接入成本較高。

四、總結

  本文是筆者在設計“得到app”的組件化中總結一些想法,在設計之初參考了目前已有的組件化和插件化方案,站在巨人的肩膀上又加了一點自己的想法,主要是組件化生命周期以及完全的代碼隔離方面。特別是最後的代碼隔離,不僅要有規範上的約束(針對介面編程),更要有機制保證開發者不犯錯,我覺得只有做到這一點才能認為是一個徹底的組件化方案。

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


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

-Advertisement-
Play Games
更多相關文章
  • 編程練習 製作一個跳轉提示頁面: 要求: 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非同步操作的朋友 ...
  • 今年6月份開始,我開始負責對“得到app”的android代碼進行組件化拆分,在動手之前我查閱了很多組件化或者模塊化的文章,雖然有一些收穫,但是很少有文章能夠給出一個整體且有效的方案,大部分文章都只停留在組件單獨調試的層面上,涉及組件之間的交互就很少了,更不用說組件生命周期、集成調試和代碼邊界這些最 ...
一周排行
    -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 ...