這篇網誌文章是與 Meta 團隊合作撰寫
Instagram Direct 是 Instagram 的核心介面之一,每天處理數十億則使用者訊息。經過多年的反覆運算,團隊已盡可能從舊版 Android View 系統中擠出每一項微調。不過,維護及擴充經過大幅最佳化的舊版介面會造成龐大的技術債和工程負擔,尤其是在團隊越來越常採用宣告式 UI 和 AI 程式碼輔助工具的情況下。
Instagram Direct 採用 Jetpack Compose 不只是單純的 UI 現代化,該團隊建構了 AI 原生 UI 程式碼集,比原始實作小了 50% ,同時將 AI 代理執行時間縮短 35% 、工程師與代理之間的互動次數減少 32% ,以及權杖成本降低 33% 。在與 Google 密切合作下,該團隊採用 Jetpack Compose,同時維持高標準的效能。透過效能最佳化,Meta 和 Google 不僅改善了 Instagram 的 Compose,也為更廣泛的 Android 開發人員生態系統帶來助益。
大規模翻新程式碼集
AI 迅速成為業界工程師的日常好幫手,應用於 Instagram 這類的大規模程式碼集時,已帶來實質的生產力提升。Instagram Direct 團隊設定了更遠大的目標,團隊並未只是讓 AI 工具處理現有程式碼,而是重新設計程式碼集和架構,讓 AI 成為設計核心,大幅提升 AI 的影響力,遠遠超出單純改造所能達成的效果。
Instagram Direct 團隊選擇 Jetpack Compose 做為建構 AI 原生 UI 架構的關鍵元件。由於具有宣告式特性,程式碼簡潔易懂,結構也更易於 AI 模型推論,且副作用較少、隱含狀態較少,元件界線也更清楚。
遷移至 Jetpack Compose 需要審慎規劃。每天有數億人透過 Instagram 傳送訊息,因此團隊必須逐步且順利地完成遷移作業,同時重新架構底層基礎,確保使用者體驗不受影響。為說明這項挑戰的規模,我們舉例來說:個別 UI 元件可呈現超過 160 種不同的狀態排列組合,而單一對話畫面就處理超過 200 種不同的訊息類型。
將這麼大的程式碼集遷移至 Compose 時,您可能會想走捷徑,在現有的檢視區塊階層中嵌入 Compose UI 元件。如果這是逐步遷移作業的其中一個步驟,則完全沒問題。不過,從長遠來看,在以 View 為基礎的程式碼集中整合 Compose 是一項挑戰。AI 工具通常會選擇阻力最小的路徑。如果混用宣告式和命令式 UI 程式碼,AI 可能會錯誤地混合兩者,導致細微錯誤、技術債和效能回歸。
建構 AI 原生 UI 架構
以 Instagram 的規模來說,架構抽象化是無可避免的,也是確保應用程式在成長過程中可維護的關鍵。以常見模式為例,每個 RecyclerView 項目類型都會以自訂 RecyclerViewItem 基礎類別的子項形式建立模型,該類別會公開常見的生命週期掛鉤,例如 onBind。
示例 1
class ChatItem( val features: FeatureFlagProvider ) : RecyclerViewItem<ComposeViewHolder, ChatUiState> { // Imperative context: // AI could often take the path of least resistance and generate a mutable // state here, dispatched outside the ChatUiState. This class survives // re-bindings and is shared across multiple items, ultimately leading to // unexpected, hard-to-reproduce bugs. var isPinned: Boolean = false override fun onBind(holder: ComposeViewHolder, uiState: ChatUiState) { // Imperative context val isPinnedChatsEnabled = features.isEnabled("pinned_chats_feature") // Declarative context holder.composeView.setContent { // Blending imperative and declarative contexts if (isPinnedChatsEnabled) { Button(onClick = { isPinned = !isPinned }) { Text(if (isPinned) "Unpin" else "Pin") } } ... } } }
在上方的程式碼片段中,有兩個問題悄悄出現。首先,系統會在命令式程式碼中讀取 isPinnedChatsEnabled 標記,然後在 Compose lambda 中擷取該標記,這是跨範例的細微耦合。其次,isPinned 會以可變動欄位形式存在於項目本身,而非 ChatUiState 中,因此會存留於 RecyclerView 重新繫結和跨列回收作業中,導致外洩並產生難以重現的錯誤。
即使為項目提供專屬的 @Composable 函式來清除程式碼,問題仍會存在。
示例 2
class ChatItem( val features: FeatureFlagProvider ) : ComposeRecyclerViewItem<ChatUiState> { // Imperative context val isPinnedChatsEnabled = features.isEnabled("pinned_chats_feature") var isPinned: Boolean = false // Declarative context @Composable override fun Content(uiState: ChatUiState) { // Blending imperative and declarative contexts if (isPinnedChatsEnabled) { Button(onClick = { isPinned = !isPinned }) { Text(if (isPinned) "Unpin" else "Pin") } } ... } }
這個範例刻意簡化,但說明瞭更廣泛的問題:AI 獲得的界線越少,隨著時間推移,產生的程式碼品質就越低。防護措施和技能有助於解決問題,但光靠這些還不夠,因為 AI 遇到阻礙時,通常會繞過這些措施來解除封鎖。
如要讓程式碼集適合 AI 使用,必須遵守兩項實用規則:
- 盡量減少對自訂環境的依附元件。AI 代理程式需要越多的程式碼集專屬知識才能做出正確變更,輸出內容的品質就越低。程式碼集越符合已知最佳做法,AI 生成的結果就越理想。
- AI 優先的程式碼集必須強制執行自身的界線。使用 AI 技能修補設計缺口無法擴充,因為載入環境定義的每項技能都會耗用權杖,並可能降低代理程式的效能。而是應由架構本身承擔這項重責大任。AI 代理程式自然會選擇阻力最小的路徑,因此設計應確保該路徑會產生正確的高品質程式碼,同時讓不良的設計決策難以表達,且成本高昂。
清單項目仍可由自己的抽象化表示,但在此情況下,所有 Compose 程式碼都位於建構函式中,因此無法存取類別成員或狀態,且唯一的引數來源是建構函式。這會讓它等同於一般的 @Composable 函式,同時符合現有架構。
示例 3
class ChatItem( val features: FeatureFlagProvider, val onPin: (Boolean) -> Unit, ) : ComposeItem<ChatUiState>( // Compose UI content = { uiState: ChatUiState -> val isPinnedChatsEnabled = features.isEnabled("pinned_chats_feature") if (isPinnedChatsEnabled) { Button(onClick = { onPin(!uiState.isPinned) }) { Text(if (uiState.isPinned) "Unpin" else "Pin") } } ... }, )
遷移如此龐大的程式碼集是一項艱鉅的任務。長期以來,構成大部分 Direct UI 的數百個 UI 元件,都必須與舊版對應項目共存,且兩者都必須平行維護。AI 工作流程可加快編寫大量程式碼的程序,因此有助於進行平行遷移。正因為採取這種做法,Direct 團隊才能在創紀錄的時間內完成遷移,而且完全沒有干擾到其他團隊成員,他們仍持續發布功能,每天改善數百萬人的使用體驗。
多位工程師針對遷移期間建構的可重複使用技能和慣例共用知識庫,執行自己的 AI 代理程式。這樣一來,團隊就能同步處理工作流程和最佳做法,不必讓每位工程師重新探索。在每個介面中,團隊都分階段執行遷移作業:
- 使用 AI 編寫所有 Compose 程式碼。
- 我們不斷改善,處理極端情況並縮小效能差距,直到 UI 在公開測試中推出給實際使用者為止。
將每個畫面的工作分成兩個階段,可讓一位工程師快速完成整個介面,並預先解決架構和棘手的極端情況。有了這些基礎,其他人就能專注於讓 UI 準備好投入生產,不必停下來自行做出這些技術決策,確保整體遷移作業快速進行。
遷移結果驗證了這種做法。對於遷移的 Instagram Direct 介面,Jetpack Compose 讓團隊減少 50%的 UI 程式碼總量。AI 生成的程式碼越少,輸出內容的品質就越高,每項工作的權杖費用也越低。
我們對 Instagram Direct 的 Android 程式碼集進行內部資料分析,比較 AI 代理工作階段使用 Compose UI 與使用 Android Views 執行相同工作的情況。從以下兩個層面來看,效率提升的成效相當明顯:
- 每行程式碼:工程師與專員的溝通次數減少 32% ,專員執行時間減少 35% (專員開始處理工程師要求到回覆的時間)。
- 每個代理程式工作階段:與 Views 相比,使用 Compose 後,整體權杖費用減少了 33% 。
我們同時回報輸出效率和典型工作階段數,因為這兩者都是有用的獨立結果。工程師與代理程式的交流次數和執行時間數據,可比較每個登陸輸出單位的資源用量,而權杖數據則可比較一般代理程式工作階段的總費用。
資料也顯示,這兩個架構處理複雜或脆弱程式碼的方式存在一致的差異。Meta 會追蹤程式碼變更的風險分數,評估整體程式碼品質,以及變更導致實際工作環境事件的可能性。這項分析是根據權杖消耗量、代理程式執行時間和工程師與代理程式的互動,綜合評估代理程式的資源效率。隨著檔案的風險分數提高,AI 代理工作階段的資源效率自然會降低。
當檔案的累積風險分數加倍時,以 Android Views 實作的 UI 會使代理程式資源效率降低 30% (以每個登陸字元計算)。在相同情況下, Jetpack Compose UI 的縮減幅度僅為 9%
在 Google 和 Meta 的合作下,Instagram Direct 團隊為採用 Compose 帶來了新觀點,他們從程式碼庫的 AI 適用性著手,而不僅是重寫 UI。這項工作揭示了 Compose 的優勢,可做為建構 AI 優先程式碼集和架構的基礎,尤其是在 Instagram 這類規模的應用程式中。
效能最佳化
Instagram Direct 是應用程式最不可或缺的介面之一,使用者希望隨時都能快速回應。採用 Jetpack Compose 實際上代表要大幅改寫 UI,而首要目標是維持高品質體驗,且不會發生任何回歸。
經過多年的反覆運算,Instagram 舊版以 View 為基礎的實作方式已達到極高的效能標準,而團隊在改用全新 UI 架構時,也必須達到相同標準。
Instagram 會評估數百甚至數千個成效指標。採用 Compose 時,以下三項最為重要:
- 互動準備時間:從開啟畫面到可以使用畫面之間的時間長度。
- 完整載入所需時間 :從開啟畫面到所有內容 (例如圖片) 完整載入所需的時間。
- 捲動效能:螢幕捲動的流暢度,不得有掉格情形。
這些指標會在正式版中於執行階段追蹤,因此您可以執行 A/B 測試,比較遷移的 Compose UI 與舊版 UI,並評估這項作業對效能的影響。
處理這類遷移作業的常見做法是從小處著手,先遷移少數 UI 元件,然後收集資料並研究這些元件的行為。雖然這些早期結果很有幫助,但只能呈現部分情況,而且會針對 Compose 採用情況提供偽陰性結果,原因如下:
- 不具代表性:遷移的 UI 元件可提供特定畫面整體成效的實用資料。不過,不同元件的行為各不相同,原因也無法概括而論,因此您不一定能從中推斷。
- 互通性成本:在大型 View 程式碼集內使用一小段 Compose 程式碼時,兩個系統之間會產生無法預測的橋接成本。這項額外負擔會扭曲評估結果,因此早期的小規模結果無法反映完整遷移作業的實際情況。
因此,雖然小型遷移作業很有用,但有時無法反映 Compose 的完整影響。 如果遷移的介面越多,且沒有中斷橋接,圖片的效能就會越好。
Instagram Direct 的核心畫面是以各種項目類型的長清單為基礎建構而成,最初是使用 RecyclerView 實作。架構會依賴自訂抽象化功能來擴充,但仍會受到以 View 為基礎的系統生命週期限制。
團隊的主要工作,是將數百個個別清單項目逐步遷移至現有 RecyclerView 架構中的 Compose,並在 A/B 測試下以小型獨立群組的形式推出正式版,同時確保使用者的訊息體驗不會出現明顯變化。
即使每個清單項目都已完全遷移至 Compose,這類設定的最大缺點仍是透過核心 RecyclerView 架構對舊版 View 系統有重大依附元件。因此,團隊決定投資以 Compose 原生替代方案 LazyColumn,取代以 RecyclerView 為基礎的核心架構。
這表示 Compose UI 元件應從封裝的架構中抽象化,同時仍與 RecyclerView 和 LazyColumn 相容。同樣重要的是,您必須能夠在執行階段透過功能標記在這兩者之間切換,以啟用 A/B 測試。
雖然新的 Compose 項目與 LazyColumn 原生相容,且可插入不間斷的組合樹狀結構,但我們也建立了互通性 API,可將這些項目插入 RecyclerView。因此,我們得以在 A/B 測試中,與 RecyclerView 並行推出 LazyColumn 設定,重複使用相同的 Compose 項目並提升效能,同時不影響團隊建構及改善功能。
Instagram 的規模、複雜度和敏感度,即使是最小的迴歸,對 Jetpack Compose 來說都是獨特的挑戰。為解決這些問題,我們需要反覆進行實作合作。Google 和 Meta 工程師密切合作,分析指標以找出並設計新的 Compose 功能,達到或超越以 View 為基礎的基準。透過這項合作關係,Jetpack Compose 新增了以下功能:可使用 LazyLayoutCacheWindows 暫停組合,以及追蹤顯示狀態。
使用 LazyLayoutCacheWindows 暫停組合
可暫停的組合 (在 Compose 1.10 中預設為啟用) 可讓耗用大量資源的 Lazy 清單項目跨影格遞增組合,避免發生延遲。與 LazyLayoutCacheWindow (在 Compose 1.9 中新增) 配對使用時,可大幅提升捲動流暢度。在 Meta 近期的內部測試中,與原始的 Compose 相比,結合可暫停的組合和單一檢視區塊 LazyLayoutCacheWindow,每分鐘的大型影格掉格數 (LFD/m) 約減少 13%。單獨使用 Cache Window 時,相較於相同基準,延遲時間減少了約 8%。LFD/m 是 Meta 內部指標,用於追蹤捲動時明顯的頓挫感。
在應用程式中使用 LazyLayoutCacheWindow,即可準備並保留檢視區塊周圍像素帶內的螢幕外項目,以便快速滑動。如要在應用程式中運用 LazyLayoutCacheWindows,請使用最新的 Compose 1.13.0-alpha03,並按照下方範例設定:
val cacheWindow = LazyLayoutCacheWindow(ahead = 150.dp, behind = 100.dp) // OR val cacheWindow = LazyLayoutCacheWindow(aheadFraction = 0.5f, behindFraction = 0.3f) LazyColumn(state = state, cacheWindow = cacheWindow) { ... }
設定快取視窗的方法有兩種。兩者描述的內容相同,都是指要保留多少螢幕外內容,但單位不同。
- Dp:固定絕對長度。無論裝置為何,
ahead = 150.dp都會保留超出可見邊緣的 150 dp 內容。 - 浮點數:可視區域的分數。
aheadFraction = 0.5f會預先組合半個畫面,因此絕對數量會隨著螢幕高度調整,支援各種板型規格:平板電腦或展開的折疊式裝置會顯示更多內容,小型手機則會顯示較少內容。
Instagram 團隊針對 Direct 的內容結構和項目大小,微調了快取視窗的浮點數分數。由於理想值會因特定 UI 參數而異,因此需要進行一些實驗,才能找出適當的平衡點。
使用 onVisibilityChanged 記錄曝光次數
onVisibilityChanged(Compose 1.9.0 新增) API 是 Google 和 Meta 技術合作的另一項重要成果。這項功能可讓大規模的 Jetpack Compose 介面,以一致的方式瞭解可組合函式何時實際顯示在畫面上,取代過去使用的自訂手動實作項目。光是在 Instagram Direct 中,這些瀏覽權限信號就用於數百個檔案,以支援取決於 UI 元素是否實際向使用者顯示的產品品質指標。
啟動效能
Instagram Direct 採用 Jetpack Compose 後,應用程式其他介面的效能也意外提升。Jetpack Compose 執行階段的預熱成本只會收取一次,而訊息功能是流量很高的介面,使用者通常會在工作階段初期造訪,因此 Instagram 中採用 Compose 的其他介面效能也明顯提升。
Instagram Direct 內部 Compose UI 的啟動效能經過最佳化,採用了基準設定檔,可在安裝時預先編譯熱門程式碼路徑,因此 Compose 能在首次啟動時快速算繪。
Instagram Direct 遷移至 Jetpack Compose 的經驗
- Jetpack Compose 可立即帶來投資回報:您不必使用進階 AI 工作流程,即可享有 Compose 的優勢。程式碼減少約 50%,代表維護的程式碼較少,錯誤的表面積也減少。
- 設計 AI 原生架構帶來顯著效益,包括 AI 代理執行時間減少 35%、工程師與代理之間的互動次數減少 32%,以及權杖成本降低 33%。
- 雖然有許多互通 API,也支援將 Views 和 Compose 結合在一起,但請盡量遷移較大的介面,而非個別的小型元件。這樣一來,UI 就能維持在單一不間斷的組合階層中,並啟用所有最佳的 Compose 原生效能最佳化功能。
- 將可暫停的組合與 LazyLayoutCacheWindow 配對:將這兩者配對,可獲得比單獨使用快取視窗更好的結果。如果只有快取視窗,大型項目仍可能會嘗試在單次傳遞中組合,進而可能超出影格預算。
- 為 Compose 本身做出貢獻!Meta 與 Jetpack Compose 團隊合作,在 Compose 中實現他們的意見回饋和想法。我們採用開放原始碼工具包,因此集中修正錯誤和提升效能時,大家都能受益。因此,請盡情提供意見回饋!
採用 Jetpack Compose 後,Instagram 在 AI 輔助開發方面獲得顯著進展,同時簡化了日常 UI 工程作業。宣告式方法可減少樣板、簡化狀態的推論,並提升整體開發人員生產力。Instagram 工程團隊期待在應用程式的更多介面中導入 Compose,並與 Google 持續合作,為 Instagram 和 Jetpack Compose 使用者帶來更多改善。
如果您還沒試過 Compose,現在有了 AI 輔助,遷移至 Jetpack Compose 比以往更加輕鬆。
確認聲明。感謝 Meta 的 Michal Zielinski 和 Matthew Du,以及 Google 的 Andrei Shikov 和 George Mount,他們透過 Meta 和 Google 的合作,為 Compose 帶來效能提升!感謝 Meta 的 Gary Ye 協助將 Compose 帶進 Instagram Direct,以及 Meta 的 Gopal Juneja 透過資料科學支援這項工作!
-
個案研究WhatsApp 是全球最大的即時通訊平台,為全球數十億使用者提供服務。這項工具是不同地區使用者預設的通訊方式,可透過私密、可靠且安全的訊息功能與他人交流。
Niharika Arora, Tracy Agyemang, Mayank Jain • 閱讀時間:8 分鐘 -
個案研究Tinder 的使命是為新一代單身人士提供輕鬆有趣的交友體驗,激發真實連結。
Ajesh Pai, Ulises Uriel Verduzco Díaz , Tracy Agyemang • 閱讀時間:4 分鐘 -
個案研究由於大多數 Android 應用程式都採用 Kotlin 做為主要語言,kotlinx.coroutines 已成為非同步程式設計的事實標準。這個程式庫提供設計完善的結構化方式,可管理 Kotlin 原生的並行流程。
Jonathan Starup, Andrei Shikov • 閱讀時間:7 分鐘
每週透過電子郵件接收最新的 Android 開發洞察資訊。