[譯] 格子拼貼 — 關於模塊化的故事

插圖來自 Virginia Poltrack前端

咱們爲何以及如何進行模塊化,模塊化後會發生什麼?

這篇文章深刻探討了 Restitching Plaid 模塊化部分。java

在這篇文章中,我將全面介紹如何將一個總體的、龐大的、普通的應用轉化爲一個模塊化應用束。如下是咱們已取得的成果:android

  • 總體體積減小超過 60%
  • 極大地加強代碼健壯性
  • 支持動態交付、按需打包代碼

咱們作的全部事情,都不會影響用戶體驗。ios

Plaid 初印象

導航 Plaidgit

Plaid 是一個具備使人感到愉悅的 UI 的應用。它的主屏幕顯示的新聞來自多個來源。 這些新聞被點擊後展現詳情,從而出現分屏效果。 該應用同時具備搜索功能和一個關於模塊。基於這些已經存在的特徵,咱們選擇一些進行模塊化。github

新聞來源(Designer News 和 Dribbble)成爲了它本身擁有的動態功能模塊。關於和搜索特徵一樣被模塊化爲動態功能。npm

動態功能容許在不直接於基礎應用包含代碼狀況下提供代碼。正由於此,經過連續步驟可實現按需下載功能。後端

接下來介紹 Plaid 結構

如許多安卓應用同樣,Plaid 最初是做爲普通應用構建的單一模塊。它的安裝體積僅 7MB 一下。然而許多數據並未在運行時用到。api

代碼結構

從代碼角度來看,Plaid 基於包從而有明確邊界定義。但隨大量代碼庫的出現,這些邊界會被跨越且依賴會潛入其中。模塊化要求咱們更加嚴格地限定這些邊界,從而提升和改善代碼分離。緩存

本地庫

最大未用到的數據塊來自 Bypass,一個咱們用來在 Plaid 呈現標記的庫。它包括用於多核 CPU 體系架構的本地庫,這些本地庫最終在普通應用佔大約 4MB 左右。應用束容許僅交付設備架構所需的庫,將所需體積減小1MB左右。

可提取資源

許多應用使用柵格化資產。它們與密度有關且一般佔應用文件體積很大一部分。應用可從配置應用中受益不淺,配置應用中每一個顯示密度都被放在一個獨立應用中,容許設備定製安裝,也大大減小下載和體積。

Plaid 顯示圖形資源時,很大程度依賴於 vector drawables。因這些與密度無關且已保存許多文件,故此處數據節省對咱們並不是太有影響。

拼貼起來

在模塊化中,咱們最初把 ./gradlew assemble 替換爲 ./gradlew bundle。Gradle 如今將生成一個 Android App Bundle(aab),替換生成應用。一個安卓應用束需用到動態功能 Gradle 插件,咱們稍後介紹。

安卓應用束

相對單個應用,安卓應用束生成許多小的配置應用。這些應用可根據用戶設備定製,從而在發送過程和磁盤上保存數據。應用束也是動態功能模塊先決條件。

在 Google Play 上傳應用束後,可生成配置應用。隨着應用束成爲開放規範,其它應用商店也可實現該交付機制。爲 Google Play 生成並簽署應用,應用必須註冊到由 Google Play 簽名的應用程序

優點

這種封裝改變給咱們帶來了什麼?

Plaid 如今設備減小 60% 以上體積,等同大約 4MB 數據。

這意味每一位用戶都能爲其它應用預留更多空間。 同時下載時間也因文件大小縮小而改善。

無需修改任何一行代碼便可實現這一大幅度改進。

實現模塊化

咱們爲實現模塊化所選的方法:

  1. 將全部代碼和資源塊移動到核心模塊中。
  2. 識別可模塊化功能。
  3. 將相關代碼和資源移動到功能模塊中。

綠色:動態功能 | 深灰色:應用模塊 | 淺灰色:庫

上面圖表向咱們展現了 Plaid 模塊化現狀:

  • 旁路模塊 和外部 分享依賴 包含在覈心模塊當中
  • 應用 依賴於 核心模塊
  • 動態功能模塊依賴於 應用

應用模塊

應用 模塊基本上是現存的應用,被用來建立應用束且向咱們展現 Plaid。許多用來運行 Plaid 的代碼不必必須包含在該模塊中,而是可移至其它任何地方。

Plaid 的 核心模塊

爲開始重構,咱們將全部代碼和資源都移動至一個 com.android.library 模塊。進一步重構後,咱們的核心模塊僅包含各個功能模塊間共享所須要代碼和資源。這將使得更加清晰地分離依賴項。

外部庫

經過旁路模塊將一個第三方依賴庫包含在覈心模塊中。此外經過 gradle api 依賴關鍵字,將全部其它 gradle 依賴從 應用 移動至 核心模塊

Gradle 依賴聲明:api vs implementation_

經過 api 代替 implementation 可在整個程序中共享依賴項。這將減小每個功能模塊體積大小,因本例 核心模塊 中依賴項僅需包含在單一模塊中。此外還使咱們的依賴關係更加易於維護,由於它們被聲明在一個單一文件而非在多個 build.gradle 文件間傳播。

動態功能模塊

上面我提到了咱們識別的可被重構爲 com.android.dynamic-feature 的模塊。它們是:

:about
:designernews
:dribbble
:search
複製代碼

動態功能介紹

一個動態功能模塊本質上是一個 gradle 模塊,可從基礎應用模塊被獨立下載。它包含代碼、資源、依賴,就如同其它 gradle 模塊同樣。雖然咱們還沒在 Plaid 中使用動態交付,但咱們但願未來可減小最初下載體積。

偉大的功能改革

將全部東西都移動至核心模塊後,咱們將「關於」頁面標記爲具備最少依賴項的功能,故咱們將其重構爲一個新的 關於 模塊。這包括 Activties、Views、代碼僅用於該功能的內容。一樣,咱們把全部資源例如 drawables、strings 和動畫移動至一個新模塊。

咱們對每一個功能模塊進行重複操做,有時須要分解依賴項。

最後,核心模塊包含大部分共享代碼和主要功能。因爲主要功能僅顯示於應用模塊中,咱們把相關代碼和資源移回 應用

功能結構剖析

編譯後代碼可在包中進行結構優化。強烈建議在將代碼分解成不一樣編譯單元前,將代碼移動至與功能對應包中。幸運的是咱們不用必須重構,由於 Plaid 已很好地對應了功能。

功能和核心模塊以及各自體系結構層級

正如我提到的,Plaid 許多功能都經過新聞源提供。它們由遠程和本地 data 資源、domainUI 這些層級組成。

數據源不但顯示在主要功能提示中,也顯示在與對應功能模塊自己相關詳情頁中。域名層級在一個單一包中惟一。它必須分爲兩部分:一部分在應用中共享,另外一部分僅用在一個功能模塊中。

可複用部分被保存在覈心模塊,其它全部內容都在各自功能模塊。數據層和大部分域名層至少與其它一個模塊共享,而且同時也保存在覈心模塊。

包變化

咱們還對包名進行了優化,從而反映新的模塊化結構體系。 僅與 :dribbble 相關代碼從 io.plaidapp 移動至 io.plaidapp.dribbble。經過各自新的模塊名稱,這一樣運用於每個功能。

這意味着許多導包必須改變。

對資源進行模塊化會產生一些問題,由於咱們必須使用限定名稱消除生成的 R 類歧義。例如,導入本地佈局視圖會致使調用 R.id.library_image,而在覈心模塊相同文件中使用一個 drawable 會致使

io.plaidapp.core.R.drawable.avatar_placeholder
複製代碼

咱們使用 Kotlin 導入別名特性減輕了這一點,它容許咱們以下導入核心 R 文件:

import io.plaidapp.core.R as coreR
複製代碼

容許將呼叫站點縮短爲

coreR.drawable.avatar_placeholder
複製代碼

相較於每次都必須查看完整包名,這使得閱讀代碼變得簡潔和靈活得多。

資源移動準備

資源不一樣於代碼,沒有一個包結構。這使得經過功能劃分它們變得異常困難。可是經過在你的代碼中遵循一些約定,也何嘗不可能。

經過 Plaid,文件在被用到的地方做爲前綴。例如,資源僅用於以 dribbble_ 爲前綴的 :dribbble

未來,一些包含多個模塊資源的文件,例如 styles.xml 將在模塊基礎上進行結構化分組,而且每個屬性同時也做爲前綴。

舉個例子:在單塊應用中,strings.xml 包含了總體所用大部分字符串。 在一個模塊化應用內中,每個功能模塊僅包含對應模塊自己字符串資源。 字符串在模塊化前進行分組將更容易拆分文件。

像這樣遵循約定,能夠更快地、更容易地將資源轉移至正確地方。這一樣也有助於避免編譯錯誤和運行時序錯誤。

過程挑戰

同團隊良好溝通,對使得一個重要的重構任務像這樣易於管理而言,十分重要。傳遞計劃變動並逐步實現這些變動將幫助咱們合併衝突,而且將阻塞降到最低。

善意提醒

本文前面依賴關係圖表顯示,動態功能模塊瞭解應用模塊。另外一方面,應用模塊不能輕易地從動態功能模塊訪問代碼。但他們包含必須在某一時間執行的代碼。

應用對功能模塊沒足夠了解時訪問代碼,這將沒辦法在 Intent(ACTION_VIEW, ActivityName::class.java) 方法中經過它們的類名啓動活動。 有多種方式啓動活動。咱們決定顯示地指定組件名。

爲實現它,咱們在覈心模塊開發了 AddressableActivity 接口。

/**
 * An [android.app.Activity] that can be addressed by an intent.
 */
interface AddressableActivity {
    /**
     * The activity class name.
     */
    val className: String
}
複製代碼

使用這種方式,咱們建立了一個函數來統一活動啓動意圖建立:

/**
 * Create an Intent with [Intent.ACTION_VIEW] to an [AddressableActivity].
 */
fun intentTo(addressableActivity: AddressableActivity): Intent {
    return Intent(Intent.ACTION_VIEW).setClassName(
            PACKAGE_NAME,
            addressableActivity.className)
}
複製代碼

最簡單實現 AddressableActivity 方式爲僅需一個顯示類名做爲一個字符串。經過 Plaid,每個 活動 都經過該機制啓動。對一些包含意圖附加部分,必須經過應用各個組件傳遞到活動中。

以下文件查看咱們的實現過程:

Styleing 問題

相對於整個應用單一清單文件而言,如今對每個動態功能模塊,對清單文件進行了分離。 這些清單文件主要包含與它們組件實例化相關的一些信息,以及經過 dist: 標籤反應的一些與它們交付類型相關的一些信息。 這意味着活動和服務都必須聲明在包含有與組件對應的相關代碼的功能模塊中。

咱們遇到了一個將樣式模塊化的問題;咱們僅將一個功能使用的樣式提取到與該功能相關的模塊中,可是它們常常是經過隱式構建在覈心模塊之上。

PLaid 樣式結構部分

這些樣式經過模塊清單文件以主題形式被提供給組件活動使用。

一旦咱們將它們移動完畢,咱們會遇到像這樣編譯時問題:

* What went wrong:

Execution failed for task ‘:app:processDebugResources’.
> Android resource linking failed
~/plaid/app/build/intermediates/merged_manifests/debug/AndroidManifest.xml:177: AAPT:
error: resource style/Plaid.Translucent.About (aka io.plaidapp:style/Plaid.Translucent.About) not found.
error: failed processing manifest.
複製代碼

清單文件合併視圖將全部功能模塊中清單文件合併到應用模塊。合併失敗將致使功能模塊樣式文件在指定時間對應用模塊不可用。

爲此,咱們在覈心模塊樣式文件中爲每同樣式以下建立一份空聲明:

<! — Placeholders. Implementations in feature modules. →

<style name=」Plaid.Translucent.About」 />
<style name=」Plaid.Translucent.DesignerNewsStory」 />
<style name=」Plaid.Translucent.DesignerNewsLogin」 />
<style name=」Plaid.Translucent.PostDesignerNewsStory」 />
<style name=」Plaid.Translucent.Dribbble」 />
<style name=」Plaid.Translucent.Dribbble.Shot」 />
<style name=」Plaid.Translucent.Search」 />
複製代碼

如今清單文件合併在合併過程當中抓取樣式,儘管樣式的實際實現是經過功能模塊樣式引入。

另外一種避免如上問題作法是保持樣式文件聲明在覈心模塊。但這僅做用於全部資源引用同時也在覈心模塊中狀況。這就是咱們爲什麼決定經過上述方式的緣由。

動態功儀器測試

經過模塊化,咱們發現測試工具目前不能駐留在動態功能模塊中,而是必須包含在應用模塊中。對此咱們將在即將發佈的有關測試工做博客文章中進行詳細介紹。

接下來還會發生什麼?

動態代碼加載

咱們經過應用束使用動態交付,但初次安裝後不要經過 Play Core Library 下載這些文件。例如這將容許咱們將默認未啓用的新聞源(產品搜索)標記爲僅在用戶容許該新聞源後安裝。

進一步增長新聞源

經過模塊化過程,咱們保持考慮進一步增長新聞源可能性。分離清潔模塊工做以及實現按需交付可能性使得這一點更加劇要。

模塊精細化

咱們在模塊化 Plaid 方面取得很大進展。但仍有工做要作。產品搜索是一個新的新聞源,如今咱們並未放到動態功能模塊當中。同時一些已提取的功能模塊中的功能可從核心模塊中移除,而後直接集成到各自功能中。

爲什麼我決定模塊化 Plaid?

經過該過程,Plaid 如今是一個高度模塊化應用。全部這些都不會改變用戶體驗。咱們在平常開發中確實從這些努力中得到了一些益處。

安裝體積

PLaid 如今用戶設備平均減小 60% 體積。 這使得安裝更快,而且節省寶貴網絡開銷。

編譯時間

一個沒有緩存的調試構建如今需 32 秒而不是 48 秒。 同時任務從 50 項增加到 250 項。

這樣的時間節省,主要是因爲增長並行構建以及因爲模塊化而避免編譯。

未來,單個模塊變化不需對全部單個模塊進行編譯,而且使得連續編譯速度更快。

  • 做爲引用,這些是我構建 beforeafter timing 的一些提交。

可維護性

咱們在過程當中分離可各類依賴項,這使得代碼更加簡潔。同時,反作用愈來愈小。咱們的每一個功能模塊均可在愈來愈少交互下獨立工做。但主要益處是咱們必須解決的衝突合併愈來愈少。

結語

咱們使得應用體積減小超過 60%,完善了代碼結構而且將 PLaid 模塊化成動態功能模塊以及增長了按需交付潛力。

整個過程,咱們老是將應用保持在一個可隨時發送給用戶狀態。您今天可直接切換你的應用發出一個應用束以節省安裝體積。模塊化須要一些時間,但鑑於上文所見好處,這是值得付出努力的,特別是考慮到動態交付。

去查看 Plaid’s source code 瞭解咱們全部的變化和快樂模塊化過程!

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索