組件化概述
問(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é)邊界。
優(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é)果就可以了。
實(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)
核心代碼
//默認(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ù)列表
組件目錄
<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ú)私分享