了解一下,Android 10中的ART虛擬機(2)

2021-02-23 神農和朋友們的雜文集

緣起

接著上期」了解一下,Android 10中的ART虛擬機(I)「,今天繼續介紹ART。今年春節十幾天假裡,我大概把profman和dex2oat整體看了一遍。出乎我意料的是,dex2oat居然再一次讓我看得萬念俱灰。

在我寫《深入了理解Android Java虛擬機ART》一書的時候,我最早也是先研究的dex2oat,稿子都寫了100多頁了,但數月過後就是無法拿下,所以只能放下它,轉而去研究ART Runtime。這是我寫深入理解Android 4本書來第一次碰到這種挫折。所以,書中dex2oat的字節碼到機器碼的編譯部分在第六章,但dex2oat的源碼分析卻在第九章。

我的困惑

本來以為有了Nougat ART的基礎,研究AOSP 10中的ART應該是水到渠成,但沒想到依然是困難重重。雖然我很快能把AOSP 10中dex2oat的執行順序梳理出來(這一點上和AOSP 7.0沒有太大區別),但感覺這一路的風景變化很大。這其中,最讓我感到恐慌的是這樣一個問題:我很強烈的感覺到,dex2oat很多特性、其中的很多代碼所對應的功能都和Java VM技術本身有著某種密切關係,但我卻不知道這些特性、功能到底是為了解決JVM的什麼問題——套用一個朋友的原話」就是每行代碼都能看懂,但就是不知道為什麼「——也就是知其然,不知其所以然

也就是說,我一直以來只是被動的去解釋ART的代碼,但卻沒有真正掌握JVM。並且,沒有掌握隨著Java語言本身的發展,隨著整體技術的發展,JVM本身所需要做的改進。我相信代碼的作者在改進ART虛擬機時腦子是有一條路線的,絕對不是一拍腦袋就想出來的。

我之前很單純的把上面這個問題歸結於我對語言本身不了解。這才有了前面幾期公眾號中我試圖以quickjs和javascript語言為研究課題在這方面有所突破。但這條路可能是失敗了。因為js引擎(其實就是javascript虛擬機)難度並不比jvm少。而且,quickjs引擎還涉及到從js代碼到quickjs字節碼的編譯,而這部分在jvm中是不涉及的,因為jvm處理的已經是字節碼了。

那麼,問題到底在哪?同時,我對研究ART的好處也產生了巨大的懷疑,研究這玩意到底學到了什麼?(這個質疑對我個人來說已經是非常嚴厲的了)。萬般無奈之下,我轉而去看一下別人關於JVM的書都講些什麼。於是,我在微信讀書PC版上完整的讀了周志明老師《深入理解Java虛擬機:JVM高級特性與最佳實踐》,最新版,兩遍。它給我帶來了意想不到的感受和認識。

我的一些認識

先簡單說下周老師這本書,我認為是名至實歸的深入理解。這種」深入理解「的表現方式並不是和我的書一樣,把代碼給你弄出來,一行一行去給你解釋。而更傾向於一種」深入淺出「:一個很複雜的東西,用最簡單,最讓人理解的方式表達出來,同時把它出現的背景,淵源,未來發展,甚至在實際案例中的價值都講出來。這就非常非常難了,光涉及」淵源「的地方就需要做大量長期的跟蹤工作。神農班之前安排過一個局,就是介紹Java泛型,有同學洋洋灑灑千字,但和書中對泛型在java中的介紹對比起來,這文字背後所體現的技術功底的差距就非常明顯了。

再說下我從周老師這本書裡看到的一些東西。先講講技術之外的認識。我必須客觀承認,周老師這本書我現在是能幾乎無障礙讀下來,而且有恍然大悟的體會。這個結果歸根結底還是這幾年在ART上花的功夫。這就是所謂的付出終有回報。尤其在周書中介紹編譯器這一塊,我沒感受到太大的難度,包括Java字節碼,Class的解析等。要知道在2015年前,我在Java上的經歷只不過用Java開發過一些Android UI程序,對JVM毫無所知。所以,通過這一點,我覺得前幾年對ART的投入,以及最終的結果——《深入理解Android Java虛擬機ART》這本書還是有效果。這個認識給了我一點信心。BTW,我感覺上述效果對其他讀者也是有的。大家可以試一下——也就是拿我的書結合周老師的書一起看看。

接下來介紹一下技術方面的認識。

首先,JVM從一開始就不純粹是為了Java語言而生的。我看了周老師書之後才注意到這一點。其實在Jvm規範中,人家開篇就說了」The Java Virtual Machine knows nothing of the Java programming language, only of a particular binary format, the class file format. A class file contains Java Virtual Machine instructions (or bytecodes) and a symbol table, as well as other ancillary information.「。

上面英文的意思是JVM和Java程式語言毛關係都沒有。它只認class文件。這句話完全拓展了我對JVM的認識。我一直對OS很痴迷。只不過Linux Kernel被其它人上上下下左左右右前前後後都玩透了,所以我才選擇設備端的JVM為目標。但我之前並未把jvm當做一個類似kernel,類似qemu這樣的虛擬機。我還計劃過今年什麼時候花點時間好好研究下真正的虛擬機QEMU(源碼都下好了)。但這句話一出,原來踏破鐵鞋無覓處,JVM就是VM(雖然實際上它還是和Java語言有各種各樣的關係,但我現在看待它的角度不太一樣了),不要把它和Java語言綁定死。實際上,幾十年來,java語言從1發展到今天的13,但字節碼卻只新增了一個invoke-dynamic。從這個角度看,kotlin這個語言其實也可以當做一門獨立於Java之外的語言了。甚至,只要有合適的工具,我們可以把C/C++/Javascript的代碼編譯成class文件,轉而由jvm執行了。再想多一點,某公司的某編譯器,貌似其願景和早已有幾十年歷史的jvm殊途同歸咯?

其次,我之前一直不明白jvm規範裡為什麼還要有linking(連結)的概念。我早期一直從事C++的開發。在C++開發中,連結往往是編譯階段的事情。而且,這個事情是通過一個比較明確的階段來做的。即先編譯,然後連結,然後生成最終的二進位。但寫java程序的時候,本身是感受不到連結的。為何JVM裡會有呢?其實,我們只要從一個高層次的角度來看待連結就能明白。連結,我猜測(沒考證,僅僅是根據書中的內容推斷的)應該在各種語言裡都存在。因為它是為了解決這樣的一個基本問題:即你寫的代碼如何調用別人寫的代碼?在C++開發中,這個工作是由程式設計師來主動完成的。但對jvm來說,連結是由jvm來完成的。其目標都是一樣,把你代碼裡調用的符號和真實的地方對應起來。

再有,對協程的重新認識。java裡的協程我最早在2010年左右就曾經接觸過,但作為一個C++開發者,我對線程的認識更熟悉,認為OS裡的線程調度是經過千錘百鍊的,而協程最終還是要轉換成對線程的使用,自己搞個調度器實在是🤮。但最近這幾年隨著go語言的使用,協程又重新展示了它的巨大價值。周老師書中展示了一個測試比較,用協程後,系統的吞吐量大增。所以,協程肯定是有價值的。貌似java語言後續要原生支持協程,只不過自己搞個調度器的難度確實不小。大家拭目以待。

最後,周老師書中對JIT和AOT優劣勢有非常詳細的介紹。這個讓我對某司某編譯器的搞法有了不同角度的評價。仁者見仁智者見智,這裡就不多說了。我總體感覺是,JVM的發展和Java語言的發展關係密切,如果僅從android這個小小的領域裡想引領jvm的發展,顯然是有極大和致命的不足。

以上是我基於周老師這本書的一些認識。結合我最近看《生而不凡》一書裡看到的一段話。人的成長大概有兩種方式,一個是來源於我們對現實認知的改變,另外一種則是來自於我們自身行為方式的升級。顯然,我現在對JVM的認知有了一些新的變化,也有了更多的信心。我收集了好幾本書,大概率會進一步,好好的從高層次角度把JVM摸清楚。

接下來,我簡單介紹下我對profman和dex2oat的一些研究成果。

dex2oat

aosp 10中dex2oat更加清爽了,有些類的安排也更合理。

相比nougat,新增了compact dex、vdex等內容。oat文件格式也發生了一些變化。

先說下如何使用dex2oat,我好不容易從編譯日誌文件裡扒拉出主機上生成boot.oat相關的執行命令(注意,aosp 10中,如果幹掉makefile裡的PREOPT的話,模擬器居然不會主動生成oat文件,而是直接以解釋方式執行。我之前在nougat上是沒有此問題的。報錯原因貌似是沒有權限操作/data/dalvikvm,.)

文字版的命令和輸入參數如下

out/soong/host/linux-x86/bin/dex2oatd

--avoid-storing-invocation

--write-invocation-to=out/soong/generic_x86_64/dex_apexjars/system/framework/x86_64/apex.invocation

--runtime-arg

-Xms64m

--runtime-arg

-Xmx64m

--compiler-filter=speed-profile

--profile-file=out/soong/generic_x86_64/dex_bootjars/boot.prof

--dirty-image-objects=frameworks/base/config/dirty-image-objects

--dex-file=out/soong/generic_x86_64/dex_apexjars_input/core-oj.jar

--dex-file=out/soong/generic_x86_64/dex_apexjars_input/core-libart.jar

--dex-file=out/soong/generic_x86_64/dex_apexjars_input/okhttp.jar

--dex-file=out/soong/generic_x86_64/dex_apexjars_input/bouncycastle.jar

--dex-file=out/soong/generic_x86_64/dex_apexjars_input/apache-xml.jar

--dex-file=out/soong/generic_x86_64/dex_apexjars_input/framework.jar

--dex-file=out/soong/generic_x86_64/dex_apexjars_input/ext.jar

--dex-file=out/soong/generic_x86_64/dex_apexjars_input/telephony-common.jar

--dex-file=out/soong/generic_x86_64/dex_apexjars_input/voip-common.jar

--dex-file=out/soong/generic_x86_64/dex_apexjars_input/ims-common.jar

--dex-file=out/soong/generic_x86_64/dex_apexjars_input/android.test.base.jar

--dex-location=/apex/com.android.runtime/javalib/core-oj.jar

--dex-location=/apex/com.android.runtime/javalib/core-libart.jar

--dex-location=/apex/com.android.runtime/javalib/okhttp.jar

--dex-location=/apex/com.android.runtime/javalib/bouncycastle.jar

--dex-location=/apex/com.android.runtime/javalib/apache-xml.jar

--dex-location=/system/framework/framework.jar

--dex-location=/system/framework/ext.jar

--dex-location=/system/framework/telephony-common.jar

--dex-location=/system/framework/voip-common.jar

--dex-location=/system/framework/ims-common.jar

--dex-location=/system/framework/android.test.base.jar

--generate-debug-info

--generate-build-id

--oat-symbols=out/soong/generic_x86_64/dex_apexjars_unstripped/system/framework/x86_64/apex.oat

--strip

--oat-file=out/soong/generic_x86_64/dex_apexjars/system/framework/x86_64/apex.oat

--oat-location=out/soong/generic_x86_64/dex_apexjars/system/framework/apex.oat

--image=out/soong/generic_x86_64/dex_apexjars/system/framework/x86_64/apex.art

--base=0x70000000

--instruction-set=x86_64

--instruction-set-variant=x86_64

--instruction-set-features=default

--android-root=out/empty

--no-inline-from=core-oj.jar

--abort-on-hard-verifier-error

--generate-mini-debug-info

有了上述命令,各位可以魔改dex2oat,例如加日誌,加上對其他CPU體系結構支持之類的,在host上運行就可以學習dex2oat的執行流程了,簡直不能太棒....

dex2oat裡涉及到vdex、oat的文件格式。由於時間原因,我先簡單介紹下內容。以下是oat文件格式。對比nougat的oat,貌似把dex文件內容去掉了。

(以前的oat文件是完整包含dex信息的),其他的初看起來沒有太大區別。

那dex文件放哪去了?答案在vdex裡。以下是vdex文件格式。這是從源碼注釋裡摳出來的。上面說,vdex包含了從apk/jar裡抽取出來的dex文件。

vdex文件裡有VerifierDeps,貌似和虛擬機對class文件的校驗有關。屬於JVM裡一個比較重要的功能,我希望以後有機會單獨拎出來說。這個地方也是讓我之前痛苦之處。即我完全不知道Verifier到底要幹啥...現在好了,我大概知道學習的方向了。

代碼中,vdex文件的生成在如下代碼處

另外,aosp10中,dex文件有標準dex文件和compact dex文件兩種。具體我還沒來得及細看。使用compact dex的話不確定能壓縮多少。這一塊應該屬於ART本身的一種優化。

最後,dex2oat在生成機器碼時,使用的寄存器分配算法支持圖著色法和線性分配。下面是對應的代碼截圖

關於dex2oat,先說這麼多。最關鍵的是最開始執行dex2oat的命令。通過這個命令,大家可以在host上跟蹤dex2oat的執行流程。

profman

profman和生成profile文件有關,它是PGO(profile guided optimzation)的基礎。簡單來說,profile裡統計了某個程序裡的熱點函數,然後機器碼生成的時候可以針對這些熱點函數做不同的優化。周老師一書中提到的PGO可以包含很多信息,但我從ART裡看到的profile信息好像並不多。

profile文件的生成有兩種方式:

從人類可讀的文本文件,通過profman轉換成.profile格式的文件。這個對art boot鏡像以及一些系統核心服務jar包有價值。因為這樣可以在做系統的時候就提前把字節碼轉成機器碼,所謂的Ahead of Time編譯(注意,周老師書中對JIT和AOT優劣勢有非常詳細的介紹,建議閱讀完我書第六章後再看一下,要不看不懂)。

APP運行時,由JIT模塊生成.profile文件。等一定時候後,dex2oat用這個profile文件來實施PGO。

先看第一個例子,framework/base/services/art-profile是一個典型案例

這個案例中,顯示的是人類可讀的profile信息(文本文件)。其包含的核心內容是哪個類(->之前)對應的哪個方法是熱點函數。注意,每一行前面還有HSP之類的字符。HSP是Hot、Start、Poststartup的意思,代表三種含義。

Hot是熱點函數,Startup和Poststartup對應虛擬機啟動前/後的場景。這三個理論上可指導不同的優化方法。但我研究還不深,暫且先不對細節進行介紹。

對了,系統類對應的profile文件位於frameworks/base/config/boot-image-profile.txt裡。它最終通過build/soong/java/dexpreopt_bootjars.go來指導系統類的PGO。

profman根據人類可讀的文件後,生成.profile文件。.profile文件更方便程序讀取。其文件格式如下:

此處我也不再多說。以後有機會再詳細介紹。

最後,我們介紹下程序運行時,哪裡會生成profile文件。答案在ART JIT模塊裡。代碼如下:

jit會啟動一個ProfileSaver線程,具體工作方式姑且不提,最終的profile文件由這個線程去處理。

最後,我們講一下應用開發裡經常碰到的對APP所做的性能監控。APP的性能監控和上述的Profile不是一回事。APP性能監控集中關注於某個函數調用花的時間。其採樣方式如下圖代碼所示:

上述代碼的else分支裡,APP性能監控是針對方法的,在方法Entered/Exited/Unwind之處加一些時間戳,這樣就能獲取每個函數調用花的時間。

if分支裡,還可能啟動一個sampling_thread_函數來定期掃描各個線程的調用棧。跟蹤棧頂的變化,即可知道函數調用的情況。總之,其目的是檢測函數調用所花費的時間。

後續的安排

我想重點樹立起和JVM密切有關的知識體系。有了ART源碼打底子,我相信這條路走得通。對JVM的掌握是非常有必要的,我感覺國家層面在底層基礎核心技術上會加大投入,JVM是一個非常合適的突破口。

最後的最後

我期望的結果不是朋友們從我的書、文章、博客後學會了什麼知識,幹成了什麼,而應該是說,神農,我可是踩在你的肩膀上的喔。

關於學習方面的問題,我已經討論完了。後面這個公眾號將對一些基礎的技術,新技術做一些學習和分享。也歡迎你的投稿。不過,正如我在公眾號「聯繫方式」裡說的那樣——鄭淵潔在童話大王《智齒》裡有一句話令我印象深刻,大意是「我有權保持沉默,但你說的每一句話都可能成為我靈感的源泉」。所以,影響不是單向的,很可能我從你那學到的東西更多。

神農和朋友們的雜文集

長按識別二維碼關注我們

相關焦點

  • Android虛擬機之 ART
    而且 JVM 是基於棧的虛擬機,而 Android 是基於寄存器的虛擬機上面提到棧和寄存器虛擬機,那麼它倆有什麼區別呢?  基於棧虛擬機讀取指令是在內存上工作的,而寄存器讀取指令則是在高速緩存區(寄存器)上工作,CPU 直接操作,執行更快。棧虛擬機更不依賴於硬體,而寄存器虛擬機會根據硬體來進行優化,所以不適合多平臺移植。並且如果做同一個操作,棧的指令比寄存器的指令更多。
  • Android APP安全測試入門
    另外一款就是SDK模擬器(Software Development Kit)了,這款是特別高大上的,類似虛擬機vm一樣,可以建立多個虛擬機,安裝不同的android系統。Software Development Kit下載地址:1http://developer.android.com/sdk/index.html下載之後打開包中的eclipse,然後進行模擬器Android鏡像的下載,包中自帶的鏡像是Android 5.0的鏡像,建議下載老版本的,方便測試App,新版的鏡像部分App在安裝的過程中可能會有不兼容的情況
  • Windows10 中跑 Android(基於 Hyper-V)
    在Windows 10 中運行虛擬機,同樣能安裝 Android-x86 版。目前Oracle VM VirtualBox 6.X 版本,VMware Workstation 16(當前還是2020H1預覽版)都已經支持和 Hyper-V 共存了,如果只從性能的來考慮,VMware 是最強的。
  • 【Sobug漏洞時間】Android APP安全測試入門
    SDK小工具SDK中自帶了幾款很不錯的小工具,我比較常用的有adb和emulator。ADB是一個客戶端-伺服器端程序,其中客戶端是你用來操作的電腦,伺服器端是android設備。SDK包中默認就有這倆款小工具AdbAdb命令如下:adb devices 查看啟動的虛擬機設備,如圖:
  • 在Ubuntu18.04中Android Studio開發環境搭建
    作為移動端的作業系統Android佔據了半壁以上的江山,那麼今天就談談在Ubuntu18.04中,Android App應用的開發環境構建。第一步,從Android Studio官網,下載應用包。官網: http://www.android-studio.org下載目錄: /home/username/Downloads/android-studio-ide-173.4720617-linux.zip解壓文件: 右鍵解壓zip文件,然後拷貝android-studio
  • 某Android模擬器的廣告植入分析
    Patch後,發現請求一個也沒有,而且測試並不影響後面的APP網絡使用(應該是走的虛擬機網絡)。猜測這個廣告應該是一個窗口疊加產生的,它並不在啟動的虛擬機內,但是如果點擊就會調用命令進行對應的請求和安裝???但是從模擬器內部看感覺不像,應該是對Android的功能設定的。
  • VMware Workstation 10.0.2 最新虛擬機下載
    今天,熱門虛擬化軟體公司VMware發布VMware Workstation 10.0.2虛擬機最新維護版,版本號也升級至10.0.2.1744117,繼續支持簡體中文,無需第三方的漢化包了。本次VMware Workstation 10.0.2繼續修復已知問題,提升性能。
  • Android Systrace 基礎知識(10) - Binder 和鎖競爭解讀
    的實現有一個比較深入的了解的話,推薦你閱讀下面三篇文章理解 Android Binder 機制 1/3:驅動篇[13]理解 Android Binder 機制 2/3:C++層[14]理解 Android Binder 機制 3/3:Java 層[15]「之所以要單獨講 Systrace 中的 Binder 和鎖,是因為很多卡頓問題和響應速度的問題
  • 開發總結:Android反編譯方法的總結
    不過比較區別的是Android上的二進位代碼被編譯成為Dex的字節碼,所有的Java文件最終會編譯進該文件中去,作為託管代碼既然虛擬機可以識別,那麼我們就可以很輕鬆的反編譯。所有的類調用、涉及到的方法都在裡面體現到,至於邏輯的執行可以通過實時調試的方法來查看,當然這需要藉助一些我們自己編寫的跟蹤程序。
  • 在Linux上安裝Android 4.4 KitKat
    大多數的Linux發行版中,官方源都有VirtualBox,例如在Ubuntu中安裝$ sudo apt-get install virtualbox第二步: 下載並在VirtualBox中安裝Android 4.4 kitkat2.
  • Android開發5年,技術增長乏力,你差點什麼?
    其實,多線程使用最多的場景就是網絡請求中,而網絡往往就被我們以retrofit或者okhttp替代了,但是它底層卻大量在運用多線程;JVM就更不用說了,雖然我們普通的開發涉及不到JVM,然而App性能與它有著千絲萬縷的聯繫,每個App都會有自己的art虛擬機,甚至每一個進程都是有自己獨立的虛擬機,內存的回收是由虛擬機來管理的,GC回收算法怎樣,adj內存管理,這一切都基於虛擬機,那麼
  • 可能是目前最全的《Android面試題及解析》(379頁)
    文章中所列主要為大綱部分,詳細內容可以在文末自行獲取哈!如果你熟練掌握本文中列出的知識點,相信將會大大增加你通過前兩輪技術面試的機率!這些內容都供大家參考,互相學習。1.Fragment 跟 Activity 之間是如何傳值的3.Fragment 的 replace 和 add 方法的區別2、DVM以及ART是如何對JVM進行優化的?你覺還有優化空間嗎?1、 MeasureSpec的原理和計算規則是怎樣的?2、 你寫過的最複雜的view 是怎樣的?都遇到了哪些複雜地方?3、 Android是如何通過Activity進行交互的?
  • 研究了一下Android JNI,有幾個知識點不太懂.
    下圖是JNIEnv的指針結構:JNIEnv其實是一個指向本地線程數據的接口指針,指針裡面包含指向函數接口的指針,每一個接口函數在這表中都有一個預定義的偏移位置,類似C++虛函數表。代碼如下:typedef const struct JNINativeInterface *JNIEnv; struct JNINativeInterface { void* reserved0; void* reserved1; void* reserved2; void
  • 如何反編譯Android 5.0 framework
    以前分散在framework文件夾根目錄裡的那些odex文件全部集中在了framework文件夾中的arm(或arm64)子文件夾中,而且通過正常的反編譯發現這些odex並不是像5.0以前一樣是我們所需要的東西。圖:arm文件夾裡的各種odex文件2.
  • [實踐] Android5.1.1源碼 - 讓某個APP以解釋執行模式運行
    本文只簡單的講了一下原理。在「實踐」一節講了具體做法。zygote是什麼,或者好奇zygote如何啟動,可以去看老羅的文章: Android系統進程Zygote啟動過程的原始碼分析▲老羅的文章內容請點擊「閱讀原文」老羅的文章分析的是Android2.3的源碼,所以下面提到的與zygote有關的函數在老羅的文章裡面可能沒有,如果想要對下面提到的與zygote有關的函數有一個簡單的了解可以看我的文章
  • JNI解析以及在Android中的實際應用
    從Java1.1開始,JNI標準成為java平臺的一部分,它允許Java代碼和其他語言寫的代碼進行動態交互,JNI標準保證本地代碼能工作在任何Java 虛擬機環境,目前的很多熱修復補的開源項目。,可以2個都勾上。
  • Android開發必備的「80」個開源庫
    http://bxbxbai.github.io/2014/10/07/android-develop-resource/Segmentfault 上回答較好的一些問題https://segmentfault.com/a/1190000004063006Android 界面設計視覺規範http://www.woshipm.com/
  • 什麼是虛擬機?虛擬機是幹什麼用的?怎麼樣使用虛擬機?
    1.虛擬機的概念比較寬泛,通常人們接觸到的虛擬機概念有VMware那樣的硬體模擬軟體,也有JVM這樣的介於硬體和編譯程序之間的軟體。 2.虛擬機是一個抽象的計算機,和實際的計算機一樣,具有一個指令集並使用不同的存儲區域。它負責執行指令,還要管理數據、內存和寄存器。