
簡(jiǎn)介
在 Android 11 上,Android 運(yùn)行時(shí) (ART) 引入了一個(gè)名為 Structural Class Redefinition (類的結(jié)構(gòu)性重定義) 的 JVMTI API 擴(kuò)展。本文將介紹類的結(jié)構(gòu)性重定義的功能,并介紹在實(shí)現(xiàn)該功能所遇到的問題,包含我們對(duì)問題的思考、權(quán)衡及解決方案。類的結(jié)構(gòu)性重定義是一個(gè)運(yùn)行時(shí)功能,它擴(kuò)展了 Android 8 中引入的重定義類方法,Android Studio 里的 Apply Changes 可以通過它來改變類的自身結(jié)構(gòu),并可以在類中增加變量或者方法。
這可以被用在很多強(qiáng)大的功能中,例如擴(kuò)展 Apply Changes 來支持向應(yīng)用中增加新的資源。您可以 查看相關(guān)文檔 了解 Android Studio ‘Apply Changes’ 功能的工作機(jī)制,以及在后續(xù)博客中了解其如何使用類的結(jié)構(gòu)性重定義進(jìn)行擴(kuò)展。未來 Android Studio 會(huì)增加更加綜合和功能強(qiáng)大的工具來適配這些新的特性。
JVMTI 是一個(gè)標(biāo)準(zhǔn)的 API,開發(fā)工具可以通過它在底層與運(yùn)行時(shí)環(huán)境進(jìn)行交互和控制。利用該功能實(shí)現(xiàn)了很多我們熟知的開發(fā)工具,從 Android Studio 中的 Network 及 Memory 分析器,到調(diào)試器中的模擬框架,如 dexmaker-mockito-inline、MockK,再到 Layout 以及 Database 檢查器。您可以在 Android 文檔 中找到更多關(guān)于 Android JVMTI 的實(shí)現(xiàn)以及如何將其應(yīng)用于您自己的工具中。
結(jié)構(gòu)化重定義
類的結(jié)構(gòu)性重定義基于 Android Oreo (8.0) 中增加的重定義類進(jìn)行改進(jìn)。在 Oreo 中,僅有類中已有的方法才能被修改。類中定義的對(duì)象布局以及字段集、方法集不能以任何方式進(jìn)行修改。
類的結(jié)構(gòu)性重定義對(duì)類的修改提供了更高的自由度,使已有類中添加全字段和方法成為了可能,對(duì)可能新增的字段及方法的類型沒有任何限制。新增的字段初始值為 0 或 null,但是如果需要,JVMTI 代理可以使用 JVMTI 提供的其它方法為其初始化。和標(biāo)準(zhǔn)的類重定義一樣,當(dāng)前執(zhí)行的方法將延用之前的定義,接下來的調(diào)用才會(huì)使用新定義。為了保障結(jié)構(gòu)類重定義具有清晰一致的語義,如下修改將無法被執(zhí)行:
- 字段和方法被刪除或者修改其屬性
- 類名被修改
- 類的繼承關(guān)系 (父類及實(shí)現(xiàn)的接口) 被修改
結(jié)合 Android Studio 的支持以后,類的結(jié)構(gòu)性重定義可用于針對(duì)大多數(shù)編輯場(chǎng)景來實(shí)現(xiàn) Apply Changes 功能。本文剩余部分將介紹我們是如何實(shí)現(xiàn)該功能,以及實(shí)現(xiàn)該新的運(yùn)行時(shí)功能需要進(jìn)行的考慮和權(quán)衡。
重中之重,性能無害
實(shí)現(xiàn)結(jié)構(gòu)化重定義的主要挑戰(zhàn)是不能讓應(yīng)用在發(fā)布模式下受影響。對(duì)于每個(gè)開發(fā)者來說,當(dāng)他們的代碼在調(diào)試模式下運(yùn)行并且使用類似 Apply Changes 或者調(diào)試器這樣的工具時(shí),另一側(cè)可能有數(shù)百萬用戶在他們的手機(jī)上運(yùn)行這些應(yīng)用。因此,一個(gè)首要的原則就是任何 ART 中新增的針對(duì)開發(fā)者的新特性都不可以在應(yīng)用處于非調(diào)試模式的時(shí)候影響運(yùn)行時(shí)性能。這意味著我們不能對(duì)運(yùn)行時(shí)內(nèi)部核心功能進(jìn)行重大更改。例如我們不能修改對(duì)象的基本布局、內(nèi)存申請(qǐng)、垃圾回收機(jī)制,不能改動(dòng)類的加載和連接,以及 dex 字節(jié)碼的執(zhí)行。

包含 java.lang.Class 對(duì)象 (在 ART 中持有自身類型的靜態(tài)字段) 在內(nèi)所有對(duì)象,在加載之后就已經(jīng)確定了其大小和布局。這樣的特性使程序得以高效運(yùn)行,如上圖所示的 Parrot 類,我們可知任何一個(gè) Parrot 對(duì)象都擁有 piningFor 字段,并保存在偏移量為 0x8 的位置。這意味著 ART 可以生成高效的代碼,但與此同時(shí),我們也無法在對(duì)象被創(chuàng)建之后修改對(duì)象的布局,因?yàn)樵黾有伦侄挝覀儾粌H僅修改了當(dāng)前類的布局,同時(shí)影響了其所有子類。為了實(shí)現(xiàn)該功能,我們需要在無感且保證原子性的情況下,將原來的對(duì)象及實(shí)例替換成重定義的對(duì)應(yīng)類。
我們需要深入運(yùn)行時(shí)內(nèi)部,才能在不影響性能的前提下實(shí)現(xiàn)類的結(jié)構(gòu)性重定義。從根本上講,對(duì)一個(gè)類進(jìn)行結(jié)構(gòu)化重定義有 4 個(gè)關(guān)鍵步驟:
使用新的類定義為每一個(gè)被修改的類型創(chuàng)建 java.lang.Class 的對(duì)象;
使用新定義的類型重新創(chuàng)建所有原有類型對(duì)象;
將所有原有對(duì)象替換/更新成與之對(duì)應(yīng)的新對(duì)象;
確保所有編譯后的代碼及運(yùn)行時(shí)狀態(tài)相對(duì)于新類型布局而言都是正確的。
追求性能
和很多程序一樣,ART 自身也是多線程的,一是因?yàn)樗\(yùn)行的 DEX 字節(jié)碼本身帶有的多線程特性 (潛在原因),二是為了避免程序在運(yùn)行時(shí)出現(xiàn)暫停。在任何時(shí)刻,ART 都可能同步執(zhí)行許多操作,如: 執(zhí)行 Java 語言代碼,執(zhí)行垃圾回收,加載類、分配對(duì)象,執(zhí)行 finalizer 或其它事情。
這意味著單純地執(zhí)行重定義行為是存在明顯競(jìng)爭(zhēng)的。舉個(gè)例子: 如果在我們重新創(chuàng)建了所有舊對(duì)象后,一個(gè)新的實(shí)例被創(chuàng)建怎么辦?因此,我們必須非常謹(jǐn)慎地執(zhí)行每一個(gè)步驟,以確保不會(huì)遇到或者創(chuàng)建不一致的狀態(tài)。我們需要保證每一個(gè)線程都能夠了解到上圖所示的是原子性的轉(zhuǎn)換過程,并且所有操作是同步完成的。
對(duì)此,直接的解決方案為: 當(dāng)我們開始執(zhí)行重定義時(shí),停止一切操作。然后我們按上述方式執(zhí)行重新定義 (創(chuàng)建新的類和對(duì)象,然后替換舊的對(duì)象)。這樣帶來的好處是,我們無需付出任何實(shí)際投入就可以獲得所需的原子性。當(dāng)發(fā)現(xiàn)不一致時(shí),所有的代碼都會(huì)暫停,因此不一致的狀態(tài)不會(huì)顯露出來??上У氖?,這種方法有幾個(gè)問題。
其一,這會(huì)大大降低處理速度??赡苄枰匦聞?chuàng)建大量的對(duì)象,重新加載大量的類 (例如,如果需要編輯 java.util.ArrayList 類,可能有數(shù)千個(gè)實(shí)例與之相關(guān))。更嚴(yán)重的問題是,在所有線程都停止的情況下,分配對(duì)象是不可能的,這是為了防止死鎖,例如,我們?cè)诜峙鋬?nèi)存之前去等待一個(gè)已經(jīng)暫停的 GC 線程先完成回收工作。這種限制深入到 ART 及其 GC 的設(shè)計(jì)中。簡(jiǎn)單地刪除此限制來修改它是不可行的,尤其是為了一個(gè)僅在調(diào)試中使用的特性。又因?yàn)榻Y(jié)構(gòu)化重定義的主要操作是重新分配所有重定義的對(duì)象,所以去掉限制顯然是不可接受的。
那么我們現(xiàn)在該怎么辦呢?就 Java 代碼而言,我們?nèi)孕枰_保任何的改變需要立刻完成,但是我們無法讓所有的操作都停止。這里我們可以利用 Java 語言的特性,線程無法直接獲得堆以及關(guān)鍵的類加載狀態(tài),并且重要的 GC 管理線程永遠(yuǎn)不會(huì)分配或加載類。這意味著,我們暫停運(yùn)行時(shí)其它操作的唯一步驟是替換過程。我們可以在其余代碼仍在運(yùn)行的情況下分配所有的類及新對(duì)象,因?yàn)檫@些線程沒有任何新對(duì)象的引用,并且這些代碼仍是原始代碼,所以不會(huì)暴露不一致的狀態(tài)。
如果您對(duì)具體實(shí)現(xiàn)感興趣,可以訪問相關(guān)鏈接。Android 開源項(xiàng)目 (AOSP) 代碼搜索工具正式發(fā)布 這篇文章可以探索 Android 及 AOSP 是如何創(chuàng)建的。
由于我們?cè)试S應(yīng)用代碼繼續(xù)運(yùn)行,因此需要注意的是全部的狀態(tài)不會(huì)因?yàn)槲覀兊牟僮鞫淖儭榇?,我們必須按順序仔?xì)關(guān)閉運(yùn)行時(shí)的每個(gè)部分,以確保我們可以收集所需的所有信息,并且在運(yùn)行期間該信息不會(huì)失效。為了達(dá)到我們的目的,在重定義的時(shí)候,我們需要一個(gè)完整的列表包含所有重定義1的類及其子類的 java.lang.Class 對(duì)象,需要一個(gè)對(duì)應(yīng)的重定義的類的 Class 對(duì)象列表,需要一個(gè)包含該類全部實(shí)例的完整列表和一個(gè)包含全部重定義對(duì)象的完整列表。
由于加載新類的情況非常少 (并且我們需要新的 Class 對(duì)象以分配重定義的實(shí)例),我們可以先開始收集被重定義類的列表,并為重定義的類型創(chuàng)建新的 Class 對(duì)象。為確保這個(gè)列表完整且有效,我們需要在創(chuàng)建這個(gè)列表前 完全停止類加載2。為此,我們需要 從一開始就停止新類的加載,同時(shí)需等待正在進(jìn)行的類定義完成。一旦完成,我們就可以安全地 收集 和 重新創(chuàng)建 所有重定義類的 Class 對(duì)象。
至此,我們收集了所有所需的類,這些類會(huì)被用來重新創(chuàng)建那些需要進(jìn)行替換的實(shí)例。與處理類相似,我們需要暫停分配對(duì)象并等待所有線程 確認(rèn),以確保我們的對(duì)象列表是最新的3。在此與處理類相似,我們 收集所有舊的實(shí)例 并對(duì)每個(gè)實(shí)例 創(chuàng)建新版本。
至此我們擁有了所有的新對(duì)象,剩余要做的就是從舊對(duì)象復(fù)制字段值并且真正替換到新對(duì)象中。因?yàn)橐坏┪覀冮_始將新對(duì)象提供給線程或?qū)ο笠?,它們將不再處于不可見狀態(tài),并且線程在運(yùn)行時(shí)可以任意更改任何字段,我們需要在執(zhí)行這最后幾個(gè)步驟之前 停止所有線程。只要其它所有線程都已經(jīng)停止,我們便可以 將字段值從舊對(duì)象復(fù)制到新對(duì)象。
一旦完成上述操作,我們就可以 遍歷堆 并 使用重定義的新實(shí)例替換所有舊實(shí)例?,F(xiàn)在所剩余的就是做一些雜項(xiàng)工作,以確保相關(guān)事項(xiàng)能夠根據(jù)需要得到更新或清除,例如反射對(duì)象、各種運(yùn)行時(shí)解析緩存等。我們還確保能夠追蹤足夠的數(shù)據(jù),以允許所有運(yùn)行的代碼在重定義開始時(shí)能夠持續(xù)運(yùn)行。
總結(jié)
有了結(jié)構(gòu)化重定義的功能,許多全新的、更強(qiáng)大的調(diào)試和開發(fā)工具就應(yīng)運(yùn)而生。我們已經(jīng)探討過了 Apply Changes 的改進(jìn),并且 Android 領(lǐng)域里許多團(tuán)隊(duì)正在研究基于此功能開發(fā)其它強(qiáng)大的工具。這只是我們?cè)诿總€(gè) Android 版本發(fā)布時(shí)添加的許多改進(jìn)和新特性中的一部分。歡迎您閱讀我們最近的一篇 文章,關(guān)于我們?nèi)绾问褂?IO prefetching 來改進(jìn) Android 11 應(yīng)用程序的啟動(dòng)時(shí)間。
[1] 在此之前,我們會(huì)執(zhí)行一些檢查,以確保所有的類都符合重定義條件,并且新的定義都有效,不過這些驗(yàn)證很枯燥。
[2] 從技術(shù)上來看,繼續(xù)加載無關(guān)的類是安全的,但是由于加載類的工作方式,沒有辦法盡早區(qū)分這些情況以達(dá)到理想效果。
[3] 同樣,分配對(duì)象與 art 虛擬機(jī)跨線程同步機(jī)制的交互有很多細(xì)節(jié),這些細(xì)節(jié)使我們不能單純地暫停重定義類實(shí)例的分配。