android技術介紹
A. Android無線開發的幾種常用技術(阿里巴巴資深
完整的開發一個android移動App需要經過從分解需求、架構設計到開發調試、測試、上線發布等多個階段,在發布後還會有產品功能上的迭代演進,此外還會面對性能、安全、無線網路質量等多方面的問題。
移動App的產品形態各不相同,有的是內容類,有的是工具類,有的是社交類,所以它們的業務邏輯所偏重的核心技術有些差別,但它們都會用到一些常用的技術方案。今天我們就先來簡單介紹一下這些常用技術,以後會專門分專題來詳細介紹這些技術的原理和使用場景。
1. Multidex
在Dalvik虛擬機所使用的dex文件格式中,用原生類型short來索引文件中的方法數,也就是最多隻能有4個位元組65536個method,在打包apk的過程中會把工程所需要的全部class文件都合並壓縮到一個dex文件中,也就是說自己開發的代碼加上外部引用的庫的方法總數不能超過65535。
隨著業務邏輯的不斷增長,很容易就會超過這個限制,在編譯期間就會遇到這樣一個錯誤:
還好google官方給出了一個解決方案Multidex,它會把dex文件拆成兩個或多個,第二個dex文件叫classes2.dex,在Application實例化後會從apk中解壓出classes2.dex並將其拷貝到應用的目錄下,通過反射將其注入到當前的ClassLoader中。但是這個方案非但不能解決一切問題也不能直接拿來用,而要加入自己的一些改造,來解決NoClassDefFoundError、INSTALL_FAILED_DEXOPT等問題,以保證自己的dex被順利的載入流暢的執行。
2. Plugin
Multidex雖然可以解決方法數的限制,但隨著業務邏輯越來越多,apk的大小也變得越來越多,而且有一些功能並非全部用戶都想用的,所以會把一些功能模塊獨立出來做成插件,讓用戶可以按需下載更新,這樣既減小了包大小,又改善了用戶體驗。
插件類似於windows的dll文件,放在某個特定目錄,應用程序主框架會用LoadLibrary載入各dll文件,按插件介面去訪問插件。Android的插件技術也是這樣,利用一個進程可以運行多個apk的機制,用ClassLoader將宿主apk之外的類載入進來,插件的context可以通過createPackageContext方法創建。因為插件中的activity,service等組件如果沒有在AndroidManifest.xml中聲明將不能運行,所以需要預先在AndroidManifest.xml中聲明一個代理類(ProxyActivity),將這個ProxyActivity傳給插件,讓插件的activity也有訪問資源的能力。
3. Hot Patch
有時一些嚴重的crash bug或漏洞需要緊急修復,但有些用戶不會或不願意立即升級,而且頻繁升級,沒有特別的功能更新只是修復bug的升級,對活躍用戶是一種傷害。熱補丁就可以解決這樣的窘境,它是一種可以線上修復的技術方案,有動態改變方法的能力,一般大型的移動應用都會使用熱補丁來處理緊急事件。
Hot Patch可以通過hook來修改java的method,注入自己的代碼,實現非侵入式的runtime修改,或者採用正向編程,通過工具生成patch文件,通過jni bridge指向補丁文件中的方法。還有就是利用ClassLoader,在dex中查找class時,如果找到類則返回,找不到就從下一個dex文件中繼續查找,由此可以想到,在把問題修復後,可以單獨生成一個dex,通過反射插入到dexElements數組的最前面,這樣就能讓dalvik載入補丁里的類了。
4. Push通道
Push是移動App常用的一種無線技術,基礎是基於TCP的心跳機制,和客戶端維持一個長連接。用處是向客戶端推送消息,或者代替客戶端定時去從伺服器pull的策略,改為客戶端接收到push消息後再去pull。
如果每個應用都自己實現push通道的話,cpu就會不定時地經常被喚醒,耗電量達到難以容忍的程度,而且自己搭建push平台的成本也很大,實時性和效率也存在問題,一般都直接使用一些服務商提供的push方案,這些push平台一般都經過了優化設計,在跨平台和網路穿透性、長連接心跳包、多客戶端App鏈路復用、服務和連接保活等技術上做了優化。比如Agoo最初是淘寶無線事業部開發的push服務,在逐漸完善和支撐淘系其他app後,通過服務端容量、通訊協議優化、業務和開放能力的拓展改進後,與友盟等合作,開始向第三方提供推送服務。
5. 應用加固
一款熱門的移動app或游戲發布後會受到很多的關注,經常會遇到二次打包的盜版行為,破解者要麼修改游戲的資源文件、道具、分值甚至直接把訪問的站點指向自己架設的伺服器,損害了開發者的利益;要麼偷偷植入自己的惡意代碼,表面上看起來跟正版的app完全一樣,在後台卻盜取用戶隱私,植入木馬;要麼通過反向工程學習原app的核心技術,打破技術上的競爭壁壘。
為了防止被破解只通過混淆是遠遠不夠的,即使是在native層混淆也還是會被人熟練的反編譯,所以需要一套對apk的保護方案來反調試、防逆向和防篡改。一般的加固方法都是對原apk先進行加密,然後和殼合並生成新的apk。殼是用來解密apk的dex文件。當應用啟動時,殼先解密原apk,准備好自己定義的ClassLoader,然後獲取源程序中的Application名稱,通過反射找到正確的Application對象,運行它的onCreate方法,這樣原apk才能被真正運行。其他一些反調試的方法有針對反編譯工具,在源程序中加入一些無效的指令或無效的指針,引發反編譯工具的崩潰,還有就是加花指令,利用一些跳轉,堆棧操作等指令,讓破解者無法清楚地理解反匯編後的內容。
6. 其他
除了上述幾點外,在服務端還會涉及灰度策略、鏈路流量優化、動態更新配置、防DNS劫持等技術,在客戶端會涉及用戶埋點上報、在線監控、進程保活、H5和native混合開發、注入框架等。
B. Android 多返回棧技術詳解
用戶通過系統返回按鈕導航回去的一組頁面,在開發中被稱為返回棧 (back stack)。多返回棧即一堆 "返回棧",對多返回棧的支持是在 Navigation 2.4.0-alpha01 和 Fragment 1.4.0-alpha01 中開始的。本文將為您展開多返回棧的技術詳解。
無論您在使用 Android 全新的 手勢導航 還是傳統的導航欄,用戶的 "返回" 操作是 Android 用戶體驗中關鍵的一環,把握好返回功能的設計可以使應用更加貼近整個生態系統。
在最簡單的應用場景中,系統返回按鈕僅僅 finish 您的 Activity。在過去您可能需要覆寫 Activity 的 onBackPressed() 方法來自定義返回操作,而在 2021 年您無需再這樣操作。我們已經在 OnBackPressedDispatcher 中提供了 針對自定義返回導航的 API。實際上這與 FragmentManager 和 NavController 中 已經 添加的 API 相同。
這意味著當您使用 Fragments 或 Navigation 時,它們會通過 OnBackPressedDispatcher 來確保您調用了它們返回棧的 API,系統的返回按鈕會將您推入返回棧的頁面逐層返回。
多返回棧不會改變這個基本邏輯。系統的返回按鈕仍然是一個單向指令 —— "返回"。這對多返回棧 API 的實現機制有深遠影響。
在 surface 層級,對於 多返回棧的支持 貌似很直接,但其實需要額外解釋一下 "Fragment 返回棧" 到底是什麼。FragmentManager 的返回棧其實包含的不是 Fragment,而是由 Fragment 事務組成的。更准確地說,是由那些調用了 addToBackStack(String name) API 的事務組成的。
這就意味著當您調用 commit() 提交了一個調用過 addToBackStack() 方法的 Fragment 事務時, FragmentManager 會執行所有您在事務中所指定的操作 (比如 替換操作 ),從而將每個 Fragment 轉換為預期的狀態。然後 FragmentManager 會將該事務作為它返回棧的一部分。
當您調用 popBackStack() 方法時 (無論是直接調用,還是通過系統返回鍵以 FragmentManager 內部機制調用),Fragment 返回棧的最上層事務會從棧中彈出 -- 比如新添加的 Fragment 會被移除,隱藏的 Fragment 會顯示。這會使得 FragmentManager 恢復到最初提交 Fragment 事務之前的狀態。
也就是說 popBackStack() 變成了銷毀操作: 任何已添加的 Fragment 在事務被彈出的時候都會丟失它的狀態。換言之,您會失去視圖的狀態,任何所保存的實例狀態 (Saved Instance State),並且任何綁定到該 Fragment 的 ViewModel 實例都會被清除。這也是該 API 和新的 saveBackStack() 方法之間的主要區別。 saveBackStack() 可以實現彈出事務所實現的返回效果,此外它還可以確保視圖狀態、已保存的實例狀態,以及 ViewModel 實例能夠在銷毀時被保存。這使得 restoreBackStack() API 後續可以通過已保存的狀態重建這些事務和它們的 Fragment,並且高效 "重現" 已保存的全部細節。太神奇了!
而實現這個目的必須要解決大量技術上的問題。
雖然 Fragment 總是會保存 Fragment 的視圖狀態,但是 Fragment 的 onSaveInstanceState() 方法只有在 Activity 的 onSaveInstanceState() 被調用時才會被調用。為了能夠保證調用 saveBackStack() 時 SavedInstanceState 會被保存,我們 還 需要在 Fragment 生命周期切換 的正確時機注入對 onSaveInstanceState() 的調用。我們不能調用得太早 (您的 Fragment 不應該在 STARTED 狀態下保存狀態),也不能調用得太晚 (您需要在 Fragment 被銷毀之前保存狀態)。
這樣的前提條件就開啟了需要 解決 FragmentManager 轉換到對應狀態的問題,以此來保障有一個地方能夠將 Fragment 轉換為所需狀態,並且處理可重入行為和 Fragment 內部的狀態轉換。
在 Fragment 的重構工作進行了 6 個月,進行了 35 次修改時,發現 Postponed Fragment 功能已經嚴重損壞,這一問題使得被推遲的事務處於一個中間狀態 —— 既沒有被提交也並不是未被提交。之後的 65 個修改和 5 個月的時間里,我們幾乎重寫了 FragmentManager 管理狀態、延遲狀態切換和動畫的內部代碼,具體請參見我們之前的文章《全新的 Fragment: 使用新的狀態管理器》。
隨著技術問題的逐步解決,包括更加可靠和更易理解的 FragmentManager ,我們新增加了兩個 API: saveBackStack() 和 restoreBackStack() 。
如果您不使用這些新增 API,則一切照舊: 單個 FragmentManager 返回棧和之前的功能相同。現有的 addToBackStack() 保持不變 —— 您可以將 name 賦值為 null 或者任意 name 。然而,當您使用多返回棧時, name 的作用就非常重要了: 在您調用 saveBackStack() 和之後的 restoreBackStack() 方法時,它將作為 Fragment 事務的唯一的 key。
舉個例子,會更容易理解。比如您已經添加了一個初始的 Fragment 到 Activity,然後提交了兩個事務,每個事務中包含一個單獨的 replace 操作:
也就是說我們的 FragmentManager 會變成這樣:
比如說我們希望將 profile 頁換出返回棧,然後切換到通知 Fragment。這就需要調用 saveBackStack() 並且緊跟一個新的事務:
現在我們添加 ProfileFragment 的事務和添加 EditProfileFragment 的事務都保存在 "profile" 關鍵字下。這些 Fragment 已經完全將狀態保存,並且 FragmentManager 會隨同事務狀態一起保持它們的狀態。很重要的一點: 這些 Fragment 的實例並不在內存中或者在 FragmentManager 中 —— 存在的僅僅只有狀態 (以及任何以 ViewModel 實例形式存在的非配置狀態)。
替換回來非常簡單: 我們可以在 "notifications" 事務中同樣調用 saveBackStack() 操作,然後調用 restoreBackStack() :
這兩個堆棧項高效地交換了位置:
維持一個單獨且活躍的返回棧並且將事務在其中交換,這保證了當返回按鈕被點擊時, FragmentManager 和系統的其他部分可以保持一致的響應。實際上,整個邏輯並未改變,同之前一樣,仍然彈出 Fragment 返回棧的最後一個事務。
這些 API 都特意按照最小化設計,盡管它們會產生潛在的影響。這使得開發者可以基於這些介面設計自己的結構,而無需通過任何非常規的方式保存 Fragment 的視圖狀態、已保存的實例狀態、非配置的狀態。
當然了,如果您不希望在這些 API 之上構建您的框架,那麼可以使用我們所提供的框架進行開發。
Navigation Component 最初 是作為通用運行時組件進行開發的,其中不涉及 View、Fragment、Composable 或者其他屏幕顯示相關類型及您可能會在 Activity 中實現的 "目的地界面"。然而,NavHost 介面 的實現中需要考慮這些內容,通過它添加一個或者多個 Navigator 實例時,這些實例 確實 清楚如何與特定類型的目的地進行交互。
這也就意味著與 Fragment 的交互邏輯全部封裝在了 navigation-fragment 開發庫和它其中的 FragmentNavigator 與 DialogFragmentNavigator 中。類似的,與 Composable 的交互邏輯被封裝在完全獨立的 navigation-compose 開發庫和它的 ComposeNavigator 中。這里的抽象設計意味著如果您希望僅僅通過 Composable 構建您的應用,那麼當您使用 Navigation Compose 時無需任何涉及到 Fragment 的依賴。
該級別的分離意味著 Navigation 中有兩個層次來實現多返回棧:
仍需特別注意那些 尚未 更新的 Navigator ,它們無法支持保存自身狀態。底層的 Navigator API 已經整體重寫來支持狀態保存 (您需要覆寫新增的 navigate() 和 popBackStack() API 的重載方法,而不是覆寫之前的版本),即使 Navigator 並未更新, NavController 仍會保存 NavBackStackEntry 的狀態 (在 Jetpack 世界中向後兼容是非常重要的)。
如果您僅僅在應用中使用 Navigation,那麼 Navigator 這個層面更多的是實現細節,而不是您需要直接與之交互的內容。可以這么說,我們已經完成了將 FragmentNavigator 和 ComposeNavigator 遷移到新的 Navigator API 的工作,使其能夠正確地保存和恢復它們的狀態,在這個層面上您無需再做任何額外工作。
如果您正在使用 NavigationUI,它是用於連接您的 NavController 到 Material 視圖組件的一系列專用助手,您會發現對於菜單項、 BottomNavigationView (現在叫 NavigationRailView ) 和 NavigationView ,多返回棧是 默認啟用 的。這就意味著結合 navigation-fragment 和 navigation-ui 使用就可以。
NavigationUI API 是基於 Navigation 的其他公共 API 構建的,確保您可以准確地為自定義組件構建您自己的版本。保證您可以構建所需的自定義組件。啟用保存和恢復返回棧的 API 也不例外,在 Navigation XML 中通過 NavOptions 上的新 API,也就是 navOptions Kotlin DSL,以及 popBackStack() 的重載方法可以幫助您指定 pop 操作保存狀態或者指定 navigate 操作來恢復之前已保存的狀態。
比如,在 Compose 中,任何全局的導航模式 (無論是底部導航欄、導航邊欄、抽屜式導航欄或者任何您能想到的形式) 都可以使用我們在與 底部導航欄集成 所介紹的相同的技術,並且結合 saveState 和 restoreState 屬性一起調用 navigate() :
對用戶來說,最令人沮喪的事情之一便是丟失之前的狀態。這也是為什麼 Fragment 用一整頁來講解 保存與 Fragment 相關的狀態,而且也是我非常樂於更新每個層級來支持多返回棧的原因之一:
如果您希望了解 更多使用該 API 的示例,請參考 NavigationAdvancedSample (它是最新更新的,且不包含任何用於支持多返回棧的 NavigationExtensions 代碼)。
對於 Navigation Compose 的示例,請參考 Tivi。
如果您遇到任何問題,請使用官方的問題追蹤頁面提交關於 Fragment 或者 Navigation 的 bug,我們會盡快處理。