YDL Android 組件化實(shí)踐與拓展 (1) - 基礎(chǔ)功能

組件化概述

問(wèn):什么是組件睹欲,什么是組件化供炼?

答:在軟件開(kāi)發(fā)領(lǐng)域,組件(Component)是對(duì)數(shù)據(jù)和方法的簡(jiǎn)單封裝窘疮,功能單一袋哼,高內(nèi)聚,并且是業(yè)務(wù)能劃分的最小粒度考余。舉個(gè)我們生活中常見(jiàn)的例子就是電腦主板上每個(gè)元件電容器件先嬉,每個(gè)元件負(fù)責(zé)的功能單一、容易組裝楚堤、即插即拔疫蔓,但作用有限含懊,需要一定的依賴條件才可使用。

問(wèn):組件化衅胀、模塊化容易混淆岔乔,兩者區(qū)別又是什么?

答:模塊化就是將一個(gè)程序按照其功能做拆分滚躯,分成相互獨(dú)立的模塊雏门,以便于每個(gè)模塊只包含與其功能相關(guān)的內(nèi)容,模塊我們相對(duì)熟悉,比如登錄功能可以是一個(gè)模塊,搜索功能可以是一個(gè)模塊等等掸掏。而組件化就是更關(guān)注可復(fù)用性茁影,更注重關(guān)注點(diǎn)分離,如果從集合角度來(lái)看的話丧凤,可以說(shuō)往往一個(gè)模塊包含了一個(gè)或多個(gè)組件募闲,或者說(shuō)模塊是一個(gè)容器,由組件組裝而成愿待。簡(jiǎn)單來(lái)說(shuō)浩螺,組件化相比模塊化粒度更小,兩者的本質(zhì)思想都是一致的仍侥,都是把大往小的方向拆分要出,都是為了復(fù)用和解耦,只不過(guò)模塊化更加側(cè)重于業(yè)務(wù)功能的劃分农渊,偏向于復(fù)用患蹂,組件化更加側(cè)重于單一功能的內(nèi)聚,偏向于解耦腿时。

問(wèn):組件化能帶來(lái)什么好處况脆?

答:簡(jiǎn)單來(lái)說(shuō)就是提高工作效率,解放生產(chǎn)力批糟,好處如下:

  • 代碼簡(jiǎn)潔,冗余量少看铆,維護(hù)方便徽鼎,易擴(kuò)展新功能。
  • 提高編譯速度弹惦,從而提高并行開(kāi)發(fā)效率否淤。
  • 避免模塊之間的交叉依賴,做到低耦合棠隐、高內(nèi)聚石抡。
  • 引用的第三方庫(kù)代碼統(tǒng)一管理,避免版本統(tǒng)一助泽,減少引入冗余庫(kù)啰扛。
  • 定制項(xiàng)目可按需加載嚎京,組件之間可以靈活組建,快速生成不同類型的定制產(chǎn)品隐解。
  • 制定相應(yīng)的組件開(kāi)發(fā)規(guī)范鞍帝,可促成代碼風(fēng)格規(guī)范,寫(xiě)法統(tǒng)一煞茫。
  • 系統(tǒng)級(jí)的控制力度細(xì)化到組件級(jí)的控制力度帕涌,復(fù)雜系統(tǒng)構(gòu)建變成組件構(gòu)建。
  • 每個(gè)組件有自己獨(dú)立的版本续徽,可以獨(dú)立編譯蚓曼、測(cè)試、打包和部署钦扭。

實(shí)踐難點(diǎn)

組件間通訊

常見(jiàn)組件通訊方式

  • 本地廣播纫版,也就是LoacalBroadcastRecevier。更多是用在同一個(gè)應(yīng)用內(nèi)的不同系統(tǒng)規(guī)定的組件進(jìn)行通信土全,好處在于:發(fā)送的廣播只會(huì)在自己的APP內(nèi)傳播捎琐,不會(huì)泄漏給其他的APP,其他APP無(wú)法向自己的APP發(fā)送廣播裹匙,不用被其他APP干擾瑞凑。本地廣播好比對(duì)講通信,成本低概页,效率高籽御,但有個(gè)缺點(diǎn)就是兩者通信機(jī)制全部委托與系統(tǒng)負(fù)責(zé),我們無(wú)法干預(yù)傳輸途中的任何步驟惰匙,不可控制技掏,一般在組件化通信過(guò)程中采用比例不高。

  • 進(jìn)程間的AIDL项鬼。這個(gè)粒度在于進(jìn)程哑梳,而我們組件化通信過(guò)程往往是在線程中,況且AIDL通信也是屬于系統(tǒng)級(jí)通信绘盟,底層以Binder機(jī)制鸠真,雖說(shuō)Android提供模板供我們實(shí)現(xiàn),但往往使用者不好理解龄毡,交互比較復(fù)雜吠卷,往往也不適用應(yīng)用于組件化通信過(guò)程中。

  • 匿名的內(nèi)存共享沦零。比如用Sharedpreferences祭隔,在處于多線程場(chǎng)景下,往往會(huì)線程不安全路操,這種更多是存儲(chǔ)一一些變化很少的信息疾渴,比如說(shuō)組件里的配置信息等等千贯。

  • Intent Bundle傳遞。包括顯性和隱性傳遞程奠,顯性傳遞需要明確包名路徑丈牢,組件與組件往往是需要互相依賴,這背離組件化中SOP(關(guān)注點(diǎn)分離原則)瞄沙,如果走隱性的話己沛,不僅包名路徑不能重復(fù),需要定義一套規(guī)則距境,只有一個(gè)包名路徑出錯(cuò)申尼,排查起來(lái)也稍顯麻煩,這個(gè)方式往往在組件間內(nèi)部傳遞會(huì)比較合適垫桂,組件外與其他組件打交道則使用場(chǎng)景不多师幕。

主流方式

引入BaseModule放置所有對(duì)外接口,組件層的模塊都依賴于基礎(chǔ)層诬滩,從而產(chǎn)生第三者聯(lián)系霹粥,這種第三者聯(lián)系最終會(huì)編譯在APP Module中,那時(shí)將不會(huì)有這種隔閡疼鸟,那么其中的Base Module就是跨越組件化層級(jí)的關(guān)鍵后控,也是模塊間信息交流的基礎(chǔ)。

缺點(diǎn)

這種方式的問(wèn)題在于因?yàn)樗械臉I(yè)務(wù)組件對(duì)外接口都定義在BaseModule中空镜,所有業(yè)務(wù)組件都依賴BaseModule浩淘,那么無(wú)論是修改還是新增變動(dòng)都會(huì)涉及到整個(gè)項(xiàng)目層面。并且無(wú)論Module 2 組件是否使用到Module 1組件的對(duì)外功能都會(huì)引入Module 1 組件所有的對(duì)外接口吴攒,增加了業(yè)務(wù)組件代碼之間的關(guān)聯(lián)性张抄,模糊了各業(yè)務(wù)組件的職責(zé)邊界。

image

優(yōu)化方式

每個(gè)組件聲明自己提供的服務(wù) Service API接口洼怔,聲明完成后抽取API到獨(dú)立組件中署惯。組件業(yè)務(wù)層依賴API層,并實(shí)現(xiàn)接口功能并注冊(cè) Service 實(shí)現(xiàn)到一個(gè)統(tǒng)一的路由 Router 中去镣隶。如果要使用某個(gè)組件的功能泽台,只需要依賴該組件API層后向Router 請(qǐng)求這個(gè) Service 的實(shí)現(xiàn),具體的實(shí)現(xiàn)細(xì)節(jié)我們?nèi)徊魂P(guān)心矾缓,只要能返回我們需要的結(jié)果就可以了。

image

實(shí)踐原理

步驟

  • 讀取 組件配置信息
  • 創(chuàng)建 Service API 環(huán)境
  • 獨(dú)立 Service API 于業(yè)務(wù)組件目錄
  • 同步時(shí)打包 Service API 為單獨(dú)jar包
  • 開(kāi)發(fā)時(shí)使業(yè)務(wù)組件依賴 Service API jar包
  • 發(fā)布時(shí)業(yè)務(wù)組件aar包和Service API jar包單獨(dú)發(fā)布

實(shí)踐過(guò)程

1. 定義組件配置信息

完整版 modular.gradle 文件

modular {
    packageName "com.ydl.other"
    // 模塊發(fā)布需要的參數(shù)
    publish {
        modules {
          //發(fā)布信息
          groupId = "com.ydl"
          artifactId = "m-other-module-xxxx"
          // 上報(bào)的業(yè)務(wù)模塊 aar 包的版本號(hào)
          version = "0.0.1"
        }

        api {
            //發(fā)布信息
            groupId = "com.ydl"
            artifactId = "m-other-api"
            // 上報(bào)的 API 層 aar 包的版本號(hào)
            version = "0.0.1"

            // API 層打包時(shí)需要引入的依賴
            apiDependencies {
                implementation "com.google.code.gson:gson:2.8.2"
            }
        }
    }
}

2. 創(chuàng)建 Gradle Plugin 讀取配置信息

項(xiàng)目結(jié)構(gòu)

image

核心代碼

                          //默認(rèn)引入項(xiàng)目目錄下的modular配置文件
                project.allprojects.each {
                    if (it == project) return
                    Project childProject = it
                    def modularScript = new File(childProject.projectDir, 'modular.gradle')
                    if (modularScript.exists()) {
                        modularExtension.childProject = childProject
                        project.apply from: modularScript
                    }
                }

3. 生成組件發(fā)布任務(wù)

  • 接入maven-publish 插件稻爬,發(fā)布jar/aar到私庫(kù)

    project.plugins.apply('maven-publish')
    
  • 生成任務(wù)信息實(shí)例

    核心代碼

       String displayName = project.getDisplayName()
            //去除單引號(hào)
            publication.project = displayName.substring(displayName.indexOf("'") + 1, displayName.lastIndexOf("'"))
            def buildMis = new File(project.projectDir, 'build/modular')
    
            publication.sourceSetName = publication.name
            publication.buildDir = new File(buildMis, publication.name)
    
            SourceSet misSourceSet = new SourceSet()
            def modularDir
            if (publication.sourceSetName.contains('/')) {
                modularDir = new File(project.projectDir, publication.sourceSetName + '/modular_api/')
            } else {
                modularDir = new File(project.projectDir, 'src/' + publication.sourceSetName + '/modular_api/')
            }
            misSourceSet.path = modularDir.absolutePath
            misSourceSet.lastModifiedSourceFile = new HashMap<>()
            project.fileTree(modularDir).each {
                if (it.name.endsWith('.java') || it.name.endsWith('.kt')) {
                    SourceFile sourceFile = new SourceFile()
                    sourceFile.path = it.path
                    sourceFile.lastModified = it.lastModified()
                    misSourceSet.lastModifiedSourceFile.put(sourceFile.path, sourceFile)
                }
            }
    
            publication.modularSourceSet = misSourceSet
            publication.invalid = misSourceSet.lastModifiedSourceFile.isEmpty()
    
  • 生成發(fā)布Task

    核心代碼

     def flavorName = ""
                if (publication.name != "module") {
                    flavorName = ModularUtil.captureName(publication.name)
                }
                AbstractArchiveTask bundleReleaseAar = project.getTasks().findByName("bundle${flavorName}ReleaseAar")
                if (bundleReleaseAar != null) {
                    mavenPublication.artifact(bundleReleaseAar)
                }
    
                //添加業(yè)務(wù)組件依賴關(guān)系至POM文件
                mavenPublication.pom.withXml {
                    // for dependencies and exclusions
                    def dependenciesNode = asNode().appendNode('dependencies')
                    project.configurations.implementation.allDependencies.withType(ModuleDependency) { ModuleDependency dp ->
                        if (dp.name!=null&&dp.version!=null){
                            def dependencyNode = dependenciesNode.appendNode('dependency')
                            dependencyNode.appendNode('groupId', dp.group)
                            dependencyNode.appendNode('artifactId', dp.name)
                            dependencyNode.appendNode('version', dp.version)
    
                            // for exclusions
                            if (dp.excludeRules.size() > 0) {
                                def exclusions = dependencyNode.appendNode('exclusions')
                                dp.excludeRules.each { ExcludeRule ex ->
                                    def exclusion = exclusions.appendNode('exclusion')
                                    exclusion.appendNode('groupId', ex.group)
                                    exclusion.appendNode('artifactId', ex.module)
                                }
                            }
                        }
                    }
                }
    

使用

任務(wù)列表

image

組件目錄

<img src="https://ww1.sinaimg.cn/large/b89b3521ly1ge89m66t2nj20k20qcdg0.jpg" alt="image-20200427132340054" style="zoom:50%;" />

接口定義

/**
 * Created by haorui on 2019-10-10.
 * Des:
 */
public interface UserService extends IProvider {
    UserInfo getUser();
}

接口實(shí)現(xiàn)

/**
 * Created by haorui on 2019-10-10.
 * Des:
 */
@Route(path = "/user/UserService")
public class UserServiceImpl implements UserService {
    public UserServiceImpl() {
    }
    @Override
    public UserInfo getUser() {
        return new UserInfo("from user");
    }
    @Override
    public void init(Context context) {

    }
}

依賴

compileOnly 'com.ydl:m-user-api:0.0.3'

調(diào)用

UserService mUserService = ARouter.getInstance().navigation(UserService.class);
mUserService.getUser();

總結(jié)

本文中說(shuō)明的方案在實(shí)踐過(guò)程中調(diào)研了現(xiàn)有市面上流行的組件化架構(gòu)嗜闻,并在其基礎(chǔ)上使用Gradle Plugin進(jìn)行優(yōu)化。降低項(xiàng)目業(yè)務(wù)組件之間的不必要的耦合依賴桅锄,節(jié)約接口層升級(jí)所帶來(lái)的維護(hù)成本琉雳,并封裝統(tǒng)一上傳Task样眠,避免了多次重復(fù)工作。

感謝以下極客的無(wú)私分享

斜杠Allen -- Android組件化框架設(shè)計(jì)與實(shí)踐

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末翠肘,一起剝皮案震驚了整個(gè)濱河市檐束,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌束倍,老刑警劉巖被丧,帶你破解...
    沈念sama閱讀 221,548評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異绪妹,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)多糠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)伏伯,“玉大人,你說(shuō)我怎么就攤上這事婶肩“煜荩” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,990評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵律歼,是天一觀的道長(zhǎng)民镜。 經(jīng)常有香客問(wèn)我,道長(zhǎng)苗膝,這世上最難降的妖魔是什么殃恒? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,618評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮辱揭,結(jié)果婚禮上离唐,老公的妹妹穿的比我還像新娘。我一直安慰自己问窃,他們只是感情好亥鬓,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著域庇,像睡著了一般嵌戈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上听皿,一...
    開(kāi)封第一講書(shū)人閱讀 52,246評(píng)論 1 308
  • 那天熟呛,我揣著相機(jī)與錄音,去河邊找鬼尉姨。 笑死庵朝,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播九府,決...
    沈念sama閱讀 40,819評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼椎瘟,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了侄旬?” 一聲冷哼從身側(cè)響起肺蔚,我...
    開(kāi)封第一講書(shū)人閱讀 39,725評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎儡羔,沒(méi)想到半個(gè)月后宣羊,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,268評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡笔链,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評(píng)論 3 340
  • 正文 我和宋清朗相戀三年段只,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鉴扫。...
    茶點(diǎn)故事閱讀 40,488評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡赞枕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出坪创,到底是詐尸還是另有隱情炕婶,我是刑警寧澤,帶...
    沈念sama閱讀 36,181評(píng)論 5 350
  • 正文 年R本政府宣布莱预,位于F島的核電站柠掂,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏依沮。R本人自食惡果不足惜涯贞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望危喉。 院中可真熱鬧宋渔,春花似錦、人聲如沸辜限。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,331評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)薄嫡。三九已至氧急,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間毫深,已是汗流浹背吩坝。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,445評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留哑蔫,地道東北人钾恢。 一個(gè)月前我還...
    沈念sama閱讀 48,897評(píng)論 3 376
  • 正文 我出身青樓手素,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親瘩蚪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容