1. 程式人生 > >[譯] 格子拼貼 — 關於模組化的故事

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

插圖來自 Virginia Poltrack

我們為什麼以及如何進行模組化,模組化後會發生什麼?

這篇文章深入探討了 Restitching Plaid 模組化部分。

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

  • 整體體積減少超過 60%
  • 極大地增強程式碼健壯性
  • 支援動態交付、按需打包程式碼

我們做的所有事情,都不會影響使用者體驗。

Plaid 初印象

導航 Plaid

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

新聞來源(Designer News 和 Dribbble)成為了它自己擁有的動態功能模組。關於和搜尋特徵同樣被模組化為動態功能。

動態功能允許在不直接於基礎應用包含程式碼情況下提供程式碼。正因為此,通過連續步驟可實現按需下載功能。

接下來介紹 Plaid 結構

如許多安卓應用一樣,Plaid 最初是作為普通應用構建的單一模組。它的安裝體積僅 7MB 一下。然而許多資料並未在執行時用到。

程式碼結構

從程式碼角度來看,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前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄