插圖來自 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 資料。
這意味每一位使用者都能為其它應用預留更多空間。 同時下載下傳時間也因檔案大小縮小而改善。
無需修改任何一行代碼即可實作這一大幅度改進。
實作子產品化
我們為實作子產品化所選的方法:
- 将所有代碼和資源塊移動到核心子產品中。
- 識别可子產品化功能。
- 将相關代碼和資源移動到功能子產品中。
綠色:動态功能 | 深灰色:應用子產品 | 淺灰色:庫
上面圖表向我們展示了 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 資源、domain、UI 這些層級組成。
資料源不但顯示在主要功能提示中,也顯示在與對應功能子產品本身相關詳情頁中。域名層級在一個單一包中唯一。它必須分為兩部分:一部分在應用中共享,另一部分僅用在一個功能子產品中。
可複用部分被儲存在核心子產品,其它所有内容都在各自功能子產品。資料層和大部分域名層至少與其它一個子產品共享,并且同時也儲存在核心子產品。
包變化
我們還對包名進行了優化,進而反映新的子產品化結構體系。 僅與
: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,每一個
活動
都通過該機制啟動。對一些包含意圖附加部分,必須通過應用各個元件傳遞到活動中。
如下檔案檢視我們的實作過程:
- AddressableActivity.kt: Helpers to start activities in a modularized world._github.com
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 項。
這樣的時間節省,主要是由于增加并行建構以及由于子產品化而避免編譯。
将來,單個子產品變化不需對所有單個子產品進行編譯,并且使得連續編譯速度更快。
- 作為引用,這些是我建構 before 和 after timing 的一些送出。
可維護性
我們在過程中分離可各種依賴項,這使得代碼更加簡潔。同時,副作用越來越小。我們的每個功能子產品都可在越來越少互動下獨立工作。但主要益處是我們必須解決的沖突合并越來越少。
結語
我們使得應用體積減少超過 60%,完善了代碼結構并且将 PLaid 子產品化成動态功能子產品以及增加了按需傳遞潛力。
整個過程,我們總是将應用保持在一個可随時發送給使用者狀态。您今天可直接切換你的應用發出一個應用束以節省安裝體積。子產品化需要一些時間,但鑒于上文所見好處,這是值得付出努力的,特别是考慮到動态傳遞。
去檢視 Plaid’s source code 了解我們所有的變化和快樂子產品化過程!
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改并 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。内容覆寫 Android、iOS、前端、後端、區塊鍊、産品、設計、人工智能等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微網誌、知乎專欄。