天天看點

Android性能優化之Android 10+ dex2oat實踐背景探索實踐驗證總結

作者:位元組跳動終端技術——郭海洋

對于<code>android app</code>的性能優化來說,方式方法以及工具都有很多,而<code>dex2oat</code>作為其中的一員,卻可能不被大衆所熟知。它是<code>android</code>官方應用于運作時,針對<code>dex</code>進行<code>編譯優化</code>的程式,通過對<code>dex</code>進行一系列的指令優化、編譯機器碼等操作,提升<code>dex加載速度</code>和<code>代碼運作速度</code>,進而提升安裝速度、啟動速度、以及應用使用過程中的流暢度,最終提升使用者日常的使用體驗。

它的适用範圍也比較廣,可以用于<code>primary apk</code>和<code>secondary apk</code>的<code>正常場景</code>和<code>插件場景</code>。(<code>primary apk</code>是指的<code>正常場景</code>下的<code>主包</code>(<code>base.apk</code>)或者<code>插件場景</code>下的<code>宿主包</code>,<code>secondary apk</code>是指的<code>正常場景</code>下的<code>自行加載的包</code>(<code>.apk</code>)或者<code>插件場景</code>下的<code>插件包</code>(<code>.apk</code>))。

而随着<code>android</code>系統版本的更疊,發現原本可以在<code>應用程序</code>上觸發<code>dex2oat</code>編譯的方式,卻在<code>targetsdkversion&amp;gt;=29</code>且<code>android 10+</code>的系統上,不再允許使用。其原因是系統在<code>targetsdkversion=29</code>的時候,對此做了限制,不允許<code>應用程序</code>上觸發<code>dex2oat</code>編譯(android 運作時 (art) 不再從應用程序調用 dex2oat。這項變更意味着 art 将僅接受系統生成的 oat 檔案)(<code>oat</code>為<code>dex2oat</code>後的産物)。

那目前是否會受到這個限制的影響呢?

在<code>2020</code>年的時候<code>android 11</code>系統正式釋出,各大應用市場就開始限制<code>app</code>的<code>targetsdkversion&amp;gt;=29</code>,而<code>android 11</code>系統距今已經釋出一年之久,也就意味着,現如今<code>app</code>的<code>targetsdkversion&amp;gt;=29</code>是不可避免的。而且随着新<code>android</code>裝置的不斷疊代,越來越多的使用者,使用上了攜帶新系統的新機器,使得<code>android 10+</code>系統的占有量逐漸增加,目前為止<code>android 10+</code>系統的占有量約占整體的<code>30%~40%</code>左右,也就是說這部分機器将會受到這個限制的影響。

那這個限制有什麼影響呢?

這個限制的關鍵是,不允許<code>應用程序</code>上觸發<code>dex2oat</code>編譯,換句話說就是并不影響系統自身去觸發<code>dex2oat</code>編譯,那麼限制的影響也就是,影響那些需要通過<code>應用程序</code>去觸發<code>dex2oat</code>編譯的場景。

對于<code>primary apk</code>和<code>secondary apk</code>,它們在<code>正常場景</code>和<code>插件場景</code>下,系統都會收集其運作時的<code>熱點代碼</code>并用于<code>dex2oat</code>進行<code>編譯優化</code>。此處觸發<code>dex2oat</code>編譯是系統行為,并不受限于上述限制。但觸發此處<code>dex2oat</code>編譯的條件是比較苛刻的,它要求裝置必須處于空閑狀态且要連接配接電源,而且其校驗的間隔是一天。

在上述條件下,由系統觸發的<code>dex2oat</code>編譯,基本上很難觸發,進而導緻<code>dex加載速度</code>下降<code>80%</code>以上,<code>代碼運作速度</code>下降<code>11%</code>以上,使得應用的<code>anr</code>率提升、流暢度下降,最終影響使用者的日常使用體驗。

對于之前來說改進方案就是通過<code>應用程序</code>觸發<code>dex2oat</code>編譯來彌補系統觸發<code>dex2oat</code>編譯的不足,而如今因限制會導緻部分機器無法生效。

如何才能讓使用者體會到<code>dex2oat</code>帶來的體驗提升呢?問題又如何解決呢?

下面通過探索,一步步的逼近真相,解決問題~

探索之前,先明确下核心點,本次探索的目标就是為了讓使用者體會到<code>dex2oat</code>帶來的體驗提升,其最大的阻礙就是系統觸發<code>dex2oat</code>的編譯條件太苛刻,導緻難以觸發,之前的成功實踐就是基于<code>app次元</code>手動觸發<code>dex2oat</code>編譯來彌補系統觸發<code>dex2oat</code>的編譯的不足。

而現在仍需探索的原因就是,原本的成功實踐,目前在某些機器上已經受限,為了完成目标,解決掉現有的問題,自然而然的想法就是,限制究竟是什麼?限制是如何生效的?是否可以繞過?

目前對于限制的了解,應該僅限于背景中的描述,那<code>google官方</code>是怎麼說的呢?

android 運作時 (art) 不再從應用程序調用 <code>dex2oat</code>。這項變更意味着 art 将僅接受系統生成的 oat 檔案。(android 運作時隻接受系統生成的 oat 檔案)

通過<code>google官方</code>的描述大緻可以了解為,原本<code>art</code>會從應用程序調用<code>dex2oat</code>,現在不再從應用程序調用<code>dex2oat</code>了,進而使得應用程序沒有時機觸發<code>dex2oat</code>,進而達到限制<code>app次元</code>觸發<code>dex2oat</code>的目的。

但問題确實有這麼簡單嘛?

通過對比<code>android 9</code> 和 <code>android 10</code>的代碼時發現,<code>android 9</code>在建構<code>classloader</code>的時候會觸發<code>dex2oat</code>,但是 <code>android 10</code> 上相關代碼已經被移除,此處同<code>google官方</code>的說法一緻。

但如果限制僅僅如此的話,可以按照原本<code>art</code>從應用程序調用<code>dex2oat</code>的方式,然後手動從應用程序調用就可以了。

由于<code>android`` ``10</code>相關代碼已經移除,是以檢視下<code>android 9</code>的代碼,看下之前是如何從應用程序調用<code>dex2oat</code>的,相關代碼連結:https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r52/runtime/oat_file_assistant.cc#698,通過檢視代碼可以看出,是通過拼接`dex2oat`的指令來觸發執行的,按照如上代碼,拼接`dex2oat`指令的僞代碼如下:

将上述拼接的<code>dex2oat</code>指令在<code>android`` ``9</code>機器的<code>app</code>程序觸發執行,确實得到符合預期的<code>dex2oat</code>産物,并可以正常加載和使用,說明指令拼接的是<code>ok</code>的,然後将上述指令在<code>android 10</code> 且<code>targetsdkversion&amp;gt;=29</code>機器的<code>app</code>程序觸發執行,發現并沒有得到<code>dex2oat</code>産物,并且得到如下日志:

這個日志說明了什麼呢?

可以看到日志資訊裡有<code>avc: denied</code>關鍵詞,說明此操作受<code>selinux</code>規則管控,并被拒絕。

在進行日志分析之前,先補充一下<code>selinux</code>的相關知識,下面是<code>google官方</code>的說明:

android 使用安全增強型 linux (selinux) 對所有程序強制執行強制通路控制 (mac),甚至包括以 root/超級使用者權限運作的程序(linux 功能)

簡單說,<code>selinux</code>就是<code>android</code>系統以程序次元對其進行強制通路控制的管理體系。<code>selinux</code>是依靠配置的規則對程序進行限制通路權限。

下面回歸正題,分析下日志。

日志細節分析如下:

<code>type=1400</code> :表示<code>syscall</code>;

<code>denied { ``execute`` }</code>:表示<code>執行權限</code>被拒絕;

<code>scontext=u:r:``untrusted_app``:s0:c12,c257,c512,c768</code>:表示主體的安全上下文,其中<code>untrusted_app</code>是<code>source type</code>;

<code>tcontext=u:object_r:``dex2oat_exec``:s0</code> :表示目标資源的安全上下文,其中<code>dex2oat_exec</code>是<code>target type</code>;

<code>tclass=file</code>:表示目标資源的<code>class</code>類型

<code>permissive=0</code>:目前的<code>sellinux</code>模式,<code>1</code>表示<code>permissive</code>(寬松的),<code>0</code>表示<code>enforcing</code>(嚴格的)

簡單的說就是,當在<code>android 10</code> 且<code>targetsdkversion&amp;gt;=29</code>的機器上的<code>app</code>程序上執行拼接的<code>dex2oat</code>指令的時候,是由<code>untrusted_app</code> **觸發<code>dex2oat_exec</code> ,** 而由于<code>untrusted_app</code>的規則限制,導緻其觸發<code>dex2oat_exec</code>的<code>execute</code>權限被拒絕。

下面簡單總結一下:

限制1:<code>android 10+</code>系統删除了在建構<code>classloader</code>時觸發<code>dex2oat</code>的相關代碼,來限制從<code>應用程序</code>觸發<code>dex2oat</code>的入口。

限制2:<code>android 10+</code>系統的相關<code>selinux</code>規則變更,限制<code>targetsdkversion&amp;gt;=29</code>的時候從<code>應用程序</code>觸發<code>dex2oat</code>。

現在通過查閱相關代碼和<code>selinux</code>規則以及使用代碼驗證,真正的見識到了限制到底是什麼樣子的,又是如何生效的,以及真真切切的感受到它的威力......

那既然知道限制是什麼以及限制如何生效的了,那是否可以繞過呢?

通過上面對限制的了解,可以先大膽的假設:

<code>targetsdkversion</code>設定小于<code>29</code>

僞裝應用程序為系統程序

關閉<code>android</code>系統的<code>selinux</code>檢測

修改規則移除限制

下面開始小心求證,上述假設是否可行?

對于<code>假設1</code>來說,如果全局設定<code>targetsdkversion</code>小于<code>29</code>的話,則會影響<code>app</code>後續在應用商店的上架,如果局部設定<code>targetsdkversion</code>小于<code>29</code>的話,不僅難以修改且時機難以把握,<code>dex2oat</code>是單獨的程序進行編譯操作的,不同的程序對其進行觸發編譯的時候,會将程序的<code>targetsdkversion</code>資訊作為參數傳給它,用于它内部邏輯的判斷,而程序資訊是存在于系統程序的。

對于<code>假設2</code>來說,目前還沒相關的已知操作可以做到類似效果...

對于<code>假設3</code>來說,<code>android</code>系統确實也提供了關閉<code>selinux</code>檢測的方法,但是需要<code>root</code>權限。

對于<code>假設4</code>來說,如果全局修改規則,需要重新編譯系統,才可以生效,如果局部修改規則(記憶體中修改),此處所需的權限也比較高,也無權操作。

是以,從目前來看,繞過基本不可行了...

那怎麼辦?限制繞不過去,目标無法達成了...

或許謎底就在謎面上,既然<code>android</code>系統限制隻能使用系統生成的,那我們就用系統生成的?

隻需要讓系統可以感覺到我們的操作,可以根據我們提供的操作去生成,可以由我們去控制生成的時機以及效果,這樣不如同在<code>應用程序</code>觸發<code>dex2oat</code>有一樣的效果了嘛?

那如何操作呢?

系統是否提供了可以供<code>應用程序</code>觸發系統行為,然後由系統觸發<code>dex2oat</code>的方式?

通過查閱android的官方文檔以及相關代碼發現可以通過如下方式進行操作(強制編譯):

基于配置檔案編譯:<code>adb shell cmd package compile -m speed-profile -f my-package</code>

全面編譯:<code>adb shell cmd package compile -m speed -f my-package</code>

上述指令不僅支援選擇編譯模式(<code>speed-profile</code> or <code>speed</code>),而且還可以選擇特定的<code>app</code>進行操作(<code>my-package</code>)。

通過運作上述指令發現确實可以在<code>targetsdkversion&amp;gt;=29</code>且<code>android 10+</code>的系統上編譯出對應的<code>dex2oat</code>産物,且可以正常加載使用!!!

但是上述指令僅支援<code>primary apk</code>并不支援<code>secondary apk</code>,感覺它的功能還不止于此,還可以繼續挖掘一下這個指令的潛力,下面看下這個指令的實作。

分析之前需要先确定指令對應的代碼實作,這裡使用了個小技巧,通過故意輸錯指令,發現最終崩潰的位置在<code>packagemanagershellcommand</code>,然後通過<code>debug</code>源碼,梳理了一下完整的代碼調用流程,細節如下。

為了友善了解,下面将代碼的調用流程使用時序圖描述出來。

下圖為<code>primary apk</code>的編譯流程:

無法複制加載中的内容

在梳理<code>primary apk</code>的編譯流程的時候,發現代碼中也有處理<code>secondary apk</code>的方法,下面梳理流程如下:

然後根據其代碼,梳理其編譯指令為:<code>adb shell cmd package compile -m speed -f --secondary-dex my-package</code>

至此,我們已經得到了一種可以借助指令使系統觸發<code>dex2oat</code>編譯的方式,且可以支援<code>primary apk</code>和<code>secondary apk</code>。

還有一些細節需要注意,<code>primary apk</code>的指令傳入的是app的包名,<code>secondary apk</code>的指令傳入的也是包名,那哪些<code>secondary apk</code>會參與編譯呢?

這就涉及到<code>secondary apk</code>的注冊了,隻有注冊了的<code>secondary apk</code>才會參與編譯。

下面是<code>secondary apk</code>注冊的流程:

對于<code>secondary apk</code>來說隻注冊不反注冊也不行,因為對于<code>secondary apk</code>來說,每次編譯僅想編譯新增的或者未被編譯過的,對于已經編譯過的,是不想其仍參與編譯,是以這些已經編譯過的,就需要進行反注冊。

下面是<code>secondary apk</code>反注冊的流程:

而且通過檢視源碼發現,觸發此處的方式其實有兩種:

方式一:使用<code>adb shell cmd package + 指令</code>。例如<code>adb shell cmd package compile -m quicken com.bytedance.demo</code> ,其含義就是觸發<code>runcompile</code>方法,然後指定編譯模式為<code>quicken</code>,指定編譯的包名為<code>com.bytedance.demo</code>,由于沒有指定是<code>secondary</code>,是以按照<code>primary</code>編譯。然後其底層通過<code>socket+binder</code>完成通信,最終交由<code>packagemanager</code>的<code>binder</code>處理。

方式二:使用<code>packagemanager</code>的<code>binder</code>,并設定<code>code=shell_command_transaction</code>,然後将指令以數組的形式封裝到<code>data</code>内即可。

對于方式一來說,依賴<code>adb</code>的實作,底層通信需要依賴<code>socket + binder</code>,而對于方式二來說,底層通信直接使用<code>binder</code>,相比來說更高效,是以最終選擇第二種方式。

下面簡單的總結一下。

在得知限制無法被繞過後,就想到是否可以使得<code>應用程序</code>可以觸發系統行為,然後由系統觸發<code>dex2oat</code>,然後通過查閱官方文檔找到對應的<code>adb指令</code>可以滿足訴求,不過此時僅看到<code>primary apk</code>的相關實作,然後繼續通過檢視代碼驗證其流程,找到<code>secondary apk</code>的相關實作,然後根據實際場景的需要,又繼續檢視代碼,找到注冊<code>secondary apk</code>和反注冊<code>secondary apk</code>的方法,然後通過對比<code>adb指令</code>的實作和<code>binder</code>的實作差異,最終選用<code>binder</code>的實作方式,來完成上述操作。

既然探索已經完成,那麼下面就根據探索的結果,完成落地實踐,并驗證其效果。

示例代碼如下:

下面是針對本方案相容性驗證的結果:

目标版本

系統版本

手機品牌

register dex module

dex opt

unregister dex module

手機型号

target29

android 10

vivo

- yes

vivo iqoo

oppo

oppo r15

mi

mi 8

華為

華為 nova 7

android 11

vivo v20

oppo pdpm00(oppo android 11 對rom進行了修改,目前暫不支援)

mi m2011k2c

無此機器

android 12

piexl

本地真機

target30

vivo s1

oppo find x

華為 p20

vivo v2046a

目前來看,對于手機品牌來說,該方案均可以相容,僅<code>oppo且android 11</code>的機器上,由于對<code>rom</code>進行了修改限制,導緻此款機器不相容。

相容效果還算良好。

下面針對高中低端的機器上,驗證下優化前後<code>dex</code>加載速度的差異:

機器性能

機器型号

包大小

優化前平均耗時

優化後平均耗時

減少耗時占總耗時百分比

低端機

piexl 2

1.9m

269.5ms

12ms

95.5%

中端機

159ms

8.8ms

94%

高端機

48.3ms

6.5ms

86%

對于<code>dex加載</code>耗時的統計,是采用統計首次<code>new classloader</code>時<code>dex</code>加載的耗時。

<code>dex加載</code>耗時同<code>包大小</code>屬于<code>正相關</code>,包越大,加載耗時越多;同<code>機器性能</code>屬于<code>負相關</code>,機器性能越好,加載耗時越少。

通過上述資料可以看出,優化前後耗時差距還是非常明顯的,機器性能越差優化越明顯。

<code>dex加載</code>速度優化明顯。

下面針對高中低端的機器上,驗證下優化前後場景運作速度的差異:

45ms

36ms

20%

36.75ms

31.23ms

13.6%

13ms

11.5ms

11.5%

對于場景運作耗時的統計,是采用對場景啟動前後打點,然後計算時間差。

由于非全量編譯對運作速度影響較小,上述資料為未優化同全量編譯優化的對比資料。

<code>場景耗時</code>同<code>場景複雜度</code>屬于<code>正相關</code>,場景複雜度越高,場景耗時越多;同<code>機器性能</code>屬于<code>負相關</code>,機器性能越好,場景耗時越少。

通過上述資料可以看出,優化後對運作速度還是有質的提升的,且會随場景複雜度的提升,帶來更大的提升。

最終,通過假借系統之手來觸發<code>dex2oat</code>的方式,繞過<code>targetsdkversion&amp;gt;=29</code>且<code>android10+</code>上的限制,效果較為明顯,<code>dex</code>加載速度提升<code>80%</code>以上,場景運作速度提升<code>11%</code>以上。

關于位元組終端技術團隊

位元組跳動終端技術團隊(client infrastructure)是大前端基礎技術的全球化研發團隊(分别在北京、上海、杭州、深圳、廣州、新加坡和美國山景城設有研發團隊),負責整個位元組跳動的大前端基礎設施建設,提升公司全産品線的性能、穩定性和工程效率;支援的産品包括但不限于抖音、今日頭條、西瓜視訊、飛書、懂車帝等,在移動端、web、desktop等各終端都有深入研究。

就是現在!用戶端/前端/服務端/端智能算法/測試開發 面向全球範圍招聘!一起來用技術改變世界,感興趣請聯系[email protected],郵件主題履歷-姓名-求職意向-期望城市-電話。

位元組跳動應用開發套件mars是位元組跳動終端技術團隊過去九年在抖音、今日頭條、西瓜視訊、飛書、懂車帝等 app 的研發實踐成果,面向移動研發、前端開發、qa、 運維、産品經理、項目經理以及營運角色,提供一站式整體研發解決方案,助力企業研發模式更新,降低企業研發綜合成本。