Android 組件化代碼中心化問題之.api化方案

1、代碼中心化問題

將一個大型的項目拆分紅多個Module或者新開的組件化項目,想要的預期是這些module之間是平級的關係.這樣一來就可使得業務相對集中,每一個人均可以專一在一件事上。同時,代碼的耦合度也會隨之下降,達到高度解耦狀態,由於同級的module不存在依賴關係,在編譯上就是隔離的,這會讓組件間的依賴很是清楚,同時也具備更高的重用性,組件強調複用,模塊強調職責劃分。 他們沒有很是嚴格的劃分。java

達到可複用要求的模塊,那麼這個模塊就是組件。每一個組件的可替代性、熱插拔、獨立編譯都將可行,python

1.1 代碼中心化在Android組件化中的問題體現

貌似Android的組件化是很是簡單且可行的,AS提供的module建立方式加gradle.properies 自定義屬性可讀,或者ext全局可配置的project屬性亦或kotlin dsl 中kotlin的語法糖都爲咱們提供了application和library的切換。git

而後將代碼放在不一樣的倉庫位置最好是單獨git 倉庫級別的管理隔離,就能達到咱們想要解決的一系列問題。github

然而事情並非想象的那麼簡單...shell

一些列的問題接踵而至,於我而言影響最深的就是應用設計時使用映射型數據庫,致使集成模式和組件模式中複用出現問題;最終使用註解配合Java特性生成代碼,雖然不完美可是依然解決了此問題。正當我爲了勝利歡呼的時刻,一片《微信Android模塊化架構重構實踐》文章進入個人眼簾。數據庫

隨即閃現出了一個重要且緊急的問題,代碼中心化的問題api

這個問題是怎麼出現的呢?在微信Android模塊化架構重構實踐中是這樣描述的微信

"""markdown

然而隨着代碼繼續膨脹,一些問題開始突顯出來。首先出問題的是基礎工程libnetscene和libplugin。基礎工程一直處於不斷膨脹的狀態,同時主工程也在不斷變大。同時基礎工程存在中心化問題,許多業務Storage類被附着在一個核心類上面,長此以往這個類已經無法看了。此外當初爲了平滑切換到gradle避免結構變化太大以及太多module,咱們將全部工程都對接到一個module上。缺乏了編譯上的隔離,模塊間的代碼邊界出現一些劣化。雖然緊接着開發了工具來限制模塊間的錯誤依賴,但這段時間裏的影響已經產生。在上面各類問題之下,許多模塊已經稱不上「獨立」了。因此當咱們從新審視代碼架構時,之前良好模塊化的架構設計已經逐漸變了樣。網絡

"""

再看他們分析問題的緣由:

"""

翻開基礎工程的代碼,咱們看到除了符合設計初衷的存儲、網絡等支持組件外,還有至關多的業務相關代碼。這些代碼是膨脹的來源。但代碼怎麼來的,非要放這?一切不合理皆有背後的邏輯。在以前的架構中,咱們大量適用Event事件總線做爲模塊間通訊的方式,也基本是惟一的方式。使用Event做爲通訊的媒介,天然要有定義它的地方,好讓模塊之間都能知道Event結構是怎樣的。這時候基礎工程好像就成了存放Event的惟一選擇——Event定義被放在基礎工程中;接着,遇到某個模塊A想使用模塊B的數據結構類,怎麼辦?把類下沉到基礎工程;遇到模塊A想用模塊B的某個接口返回個數據,Event好像不太適合?那就把代碼下沉到基礎工程吧……

就這樣愈來愈多的代碼很「天然的」被下沉到基礎工程中。

咱們再看看主工程,它膨脹的緣由不同。分析一下基本能肯定的是,首先做爲主幹業務一直還有需求在開發,膨脹在所不免,缺乏適當的內部重構但暫時不是問題的核心。另外一部分緣由,則是由於模塊的生命週期設計好像已經不知足使用須要。以前的模塊生命週期是從「Account初始化」到「Account已註銷」,因此能夠看出在這時機以外確定還有邏輯。放在之前這不是個大問題,剛啓動還不等「Account初始化」就要執行的邏輯哪有那麼多。而如今不同,再簡單的邏輯堆積起來也會變複雜。此時,在模塊生命週期外的邏輯基本上只能放主工程。

此外的問題,模塊邊界破壞、基礎工程中心化,都是代碼持續劣化的幫兇...

"""

看完以後就陷入了沉思,這個問題不就是咱們面臨的問題嗎?不只是在組件化中,在不少造成依賴關係的場景中都有此類問題。

假設有user組建和分享組件,分享組件須要user組件提供數據。

具體是怎麼體現的呢,咱們來看一組圖:

1.1.1 圖1

解決方式爲分享組件依賴user組件,能解決問題,假設,有一個組件A,須要引用分享組件,就必須依賴分享組件和user組件,這就一舉打破了組件編譯隔離的遠景,組件化將失去香味兒。

1.1.2 圖2

將user組件中的公共數據部分下沉到base組件,分享組件依賴base組件便可實現數據提供,然而當很是多的組件須要互相提供數據時,將出現中心化問題,只須要分享組件的B組件不得不依賴base組件,引入其餘數據。也就形成了代碼中心化下沉失去組件化的意義。

2、 怎麼解決代碼中心化問題

微信面對這個痛心疾首的問題時發出了「君有疾在腠理,不治將恐深」 的感慨,但也出具了很是厲害的操做-.api 化

這個操做很是高級,作法很是騰訊,可是此文檔中只提到了精髓,沒有具體的操做步驟,對咱們來說依然存在挑戰,

2.1 什麼是代碼中心化問題的.api方案

先看一下具體的操做過程是什麼樣的,

上圖3中,咱們使用某種技術將user組件中須要共享數據的部分抽象成接口,利用AS對文件類型的配置將(kotlin)後拽修改成.api ,而後再建立一個同包名的module-api 組件用來讓其餘組件依賴,

分享組件和其餘組件以及自身組件在module模式下均依賴該組件,這樣就能完美的將須要共享的數據單獨出去使用了,

2.1.1 SPI 方式實現

這個有點相似SPI(Service Provider Interface)機制,具體可參考:www.jianshu.com/p/46b42f7f5…

(來源上面的文檔)

大概就是說咱們能夠將要共享的數據先抽象到接口中造成標準服務接口,而後在具體的實現中,而後在對應某塊中實現該接口,當服務提供者提供了接口的一種具體實現後,在jar包的META-INF/services目錄下建立一個以「接口全限定名」爲命名的文件,內容爲實現類的全限定名;

而後利用 ServiceLoader 來加載配置文件中指定的實現,此時咱們在不一樣組件之間經過ServiceLoader加載須要的文件了

2.1.2 利用ARouter

利用ARouter 在組件間傳遞數據的方式+ gralde 自動生成module-api 組件,造成中心化問題的.api 化

假設咱們知足上述的全部關係,而且構建正確,那咱們怎麼處理組件間的通訊,

Arouter 阿里通訊路由

@Route(path = "/test/activity")
public class YourActivity extend Activity {
    ...
}

跳轉:

ARouter.getInstance().build("/test/activity").withLong("key1", 666L).navigation()
複製代碼
// 聲明接口,其餘組件經過接口來調用服務

public interface HelloService extends IProvider {

   String sayHello(String name);

}

// 實現接口

@Route(path = "/yourservicegroupname/hello", name = "測試服務")

public class HelloServiceImpl implements HelloService {

    @Override

    public String sayHello(String name) {

        return "hello, " + name;

    }

    @Override

    public void init(Context context) {

    }

}

//測試

public class Test {

    @Autowired

    HelloService helloService;

    @Autowired(name = "/yourservicegroupname/hello")

    HelloService helloService2;

    HelloService helloService3;

    HelloService helloService4;

    public Test() {
    
        ARouter.getInstance().inject(this);

    }

    public void testService() {

    // 1. (推薦)使用依賴注入的方式發現服務,經過註解標註字段,便可使用,無需主動獲取

    // Autowired註解中標註name以後,將會使用byName的方式注入對應的字段,不設置name屬性,會默認使用byType的方式發現服務(當同一接口有多個實現的時候,必須使用byName的方式發現服務)

    helloService.sayHello("Vergil");

    helloService2.sayHello("Vergil");

    // 2. 使用依賴查找的方式發現服務,主動去發現服務並使用,下面兩種方式分別是byName和byType

    helloService3 = ARouter.getInstance().navigation(HelloService.class);

    helloService4 = (HelloService)ARouter.getInstance().build("/yourservicegroupname/hello").navigation();

    helloService3.sayHello("Vergil");

    helloService4.sayHello("Vergil");
    }

}
複製代碼

假如user組件的用戶信息須要給支付組件使用,那咱們怎麼處理?

ARouter 能夠經過上面的IProvider 注入服務的方式通訊,或者使用EventBus這種方式

data class UserInfo(val uid: Int, val name: String)
/** *@author kpa *@date 2021/7/21 2:15 下午 *@email billkp@yeah.net *@description 用戶登陸、獲取信息等 */

interface IAccountService : IProvider {
    //獲取帳號信息 提供信息*
    fun getUserEntity(): UserInfo?
}

//注入服務

@Route(path = "/user/user-service")
class UserServiceImpl : IAccountService {

    //...

}
複製代碼

在支付組件中

IAccountService accountService = ARouter.getInstance().navigation(IAccountService.class);

UserInfo bean = accountService. getUserEntity();
複製代碼

問題就暴露在了咱們眼前,支付組件中的IAccountService 和UserInfo 從哪裏來?

這也就是module-api 須要解決的問題,在原理方面:

  1. 將須要共享的數據和初始化數據的類文件設計爲.api文件

打開AS-> Prefernces -> File Types 找到kotlin (Java)選中 在File name patterns 裏面添加".api"(注意這個後綴隨意開心的話均可以設置成.kpa)

舉例:

UserInfo.api

data class UserInfo(val userName: String, val uid: Int)
複製代碼

UserService.api

interface UserService {

fun getUserInfo(): UserInfo

}
複製代碼
  1. 生成包含共享的數據和初始化數據的類文件的module-api 組件

這步操做有如下實現方式,

  • 本身手動建立一個module-api 組件 顯然這是不可取可是可行的
  • 使用腳本語言shell 、python 等掃描指定路徑生成對應module-api
  • 利用Android 編譯環境及語言groovy,編寫gradle腳本,優點在於不用考慮什麼時候編譯,不打破編譯環境,書寫也簡單

3、module-api 腳本

找到這些問題出現的原理及怎麼去實現以後,從github上找到了優秀的人提供的腳本,徹底符合咱們的使用預期

def includeWithApi(String moduleName) {

def packageName = "com/xxx/xxx"

    //先正常加載這個模塊

    include(moduleName)

    //找到這個模塊的路徑
    String originDir = project(moduleName).projectDir
    //這個是新的路徑
    String targetDir = "${originDir}-api"
    //原模塊的名字
    String originName = project(moduleName).name
    //新模塊的名字
    def sdkName = "${originName}-api"
    //這個是公共模塊的位置,我預先放了一個 新建的api.gradle 文件進去
    String apiGradle = project(":apilibrary").projectDir
    // 每次編譯刪除以前的文件
    deleteDir(targetDir)

    //複製.api文件到新的路徑
    copy() {
        from originDir
        into targetDir
        exclude '**/build/'
        exclude '**/res/'
        include '**/*.api'
    }
    //直接複製公共模塊的AndroidManifest文件到新的路徑,做爲該模塊的文件
    copy() {
        from "${apiGradle}/src/main/AndroidManifest.xml"
        into "${targetDir}/src/main/"
    }
    //複製 gradle文件到新的路徑,做爲該模塊的gradle
    copy() {
        from "${apiGradle}/api.gradle"
        into "${targetDir}/"
    }

    //刪除空文件夾
    deleteEmptyDir(*new* File(targetDir))
    //todo 替換成本身的包名
    //爲AndroidManifest新建路徑,路徑就是在原來的包下面新建一個api包,做爲AndroidManifest裏面的包名
    String packagePath = "${targetDir}/src/main/java/" + packageName + "${originName}/api"
    //todo 替換成本身的包名,這裏是apilibrary模塊拷貝的AndroidManifest,替換裏面的包名
    //修改AndroidManifest文件包路徑
    fileReader("${targetDir}/src/main/AndroidManifest.xml", "commonlibrary", "${originName}.api")

    new File(packagePath).mkdirs()
    //重命名一下gradle
    def build = new* File(targetDir + "/api.gradle")

    if(build.exists()) {
        build.renameTo(new File(targetDir + "/build.gradle"))
    }
    // 重命名.api文件,生成正常的.java文件
    renameApiFiles(targetDir, '.api', '.java')
    //正常加載新的模塊
    include ":$sdkName"
  }

private void deleteEmptyDir(File dir) {

    if(dir.isDirectory()) {
        File[] fs = dir.listFiles()
        if(fs != null && fs.length > 0) {
            for (int i = 0; i < fs.length; i++) {
                File tmpFile = fs[i]
                if (tmpFile.isDirectory() {
                    deleteEmptyDir(tmpFile)
                }
                if (tmpFile.isDirectory() && tmpFile.listFiles().length <= 0){
                    tmpFile.delete()
                }
          }
       }
   if (dir.isDirectory() && dir.listFiles().length == 0) {
        dir.delete()
   }
 }

private void deleteDir(String targetDir) {

    FileTree targetFiles = fileTree(targetDir)

    targetFiles.exclude "*.iml"

    targetFiles.each { File file ->

        file.delete()

    }

}

/** * rename api files(java, kotlin...) **/

private def renameApiFiles(root_dir, String suffix, String replace) {

    FileTree* files = fileTree(root_dir).include("**/*$suffix")

    files.each {

        File file ->

        file.renameTo(*new* File(file.absolutePath.replace(suffix, replace)))

    }

}

//替換AndroidManifest裏面的字段*

def fileReader(path, name, sdkName) {

    def readerString = ""

    def hasReplace = false

    file(path).withReader('UTF-8') { reader ->

        reader.eachLine {

            if (it.find(name)) {
                it = it.replace(name, sdkName)
                hasReplace = true
            }
            readerString <<= it
            readerString << '\n'

        }
        if (hasReplace) {

            file(path).withWriter('UTF-8') {
                within ->
                within.append(readerString)
            }

        }

    return readerString

    }

}
複製代碼

使用:

includeWithApi ":user"
複製代碼

Democomponent-api

參考文獻:

《微信Android重構模塊化架構重構實踐》

《gradle實現API方案》

《高級開發必須理解的Java中SPI機制》

相關文章
相關標籤/搜索