日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > Android >内容正文

Android

Android N混合编译与对热补丁影响深度解析

發布時間:2025/3/15 Android 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Android N混合编译与对热补丁影响深度解析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

大約在六月底,Tinker在微信全量上線了一個補丁版本,隨即華為反饋在Android N上微信無法啟動。冷汗冒一地,Android N又搞了什么東東?為什么與instant run保持一致的補丁方式也跪了?talk is cheap,show me the code。趁著臺風妮妲肆虐廣東,終于有時間總結一把。在此非常感謝華為工程師謝小靈與胡海亮的幫助,事實上微信與各大廠商都保持著非常緊密的聯系。

無法啟動的原因

我們遵循從問題出發的思路,針對華為提供的日志,我們看到微信在Android N上啟動時會報IllegalAccessError。可以從/data/user/0/com.tencent.mm/tinker/patch-a002c56d/dex/classes2.dex看到,的確跟補丁是有關系的。

java.lang.IllegalAccessError: Illegal class access: 'com.tencent.mm.ui.conversation.ConversationOverscrollListView' attempting to access 'com.tencent.mm.ui.conversation.ConversationOverscrollListView$c' (declaration of 'com.tencent.mm.ui.conversation.ConversationOverscrollListView' appears in /data/user/0/com.tencent.mm/tinker/patch-a002c56d/dex/classes2.dex)

但是在我們手上Android N卻無法復現,同時跟華為的進一步溝通中,他們也明確只有一少部分N的用戶會出現問題。這就很難辦了,但是根據之前在art地址錯亂的經驗(似乎這里我還欠大家一篇分析文章),跟這里似乎有點相似。

但是Tinker已經做了全量替換,所以我懷疑由于Android N的某種機制這里只有部分用了補丁中的類,但是部分類導致使用了原來的dex中的。接下來就跟著我一起去研究Android N在編譯運行究竟做了什么改變吧?

Android N的混合編譯運行模式

網上關于Android N混合編譯的文章并不多,infoq上有一篇翻譯文章:Android N混合使用AOT編譯,解釋和JIT三種運行時。混合編譯運行主要指AOT編譯,解釋執行與JIT編譯,它主要解決的問題有以下幾個:

  • 應用安裝時間過長;在N之前,應用在安裝時需要對所有ClassN.dex做AOT機器碼編譯,類似微信這種比較大型的APP可能會耗時數分鐘。但是往往我們只會使用一個應用20%的功能,剩下的80%我們付出了時間成本,卻沒帶來太大的收益。
  • 降低占ROM空間;同樣全量編譯AOT機器碼,12M的dex編譯結果往往可以達到50M之多。只編譯用戶用到或常用的20%功能,這對于存儲空間不足的設備尤其重要。
  • 提升系統與應用性能;減少了全量編譯,降低了系統的耗電。在boot.art的基礎上,每個應用增加了base.art(這塊后面會詳細解析), 通過預加載與緩存提升應用性能。
  • 快速的系統升級;以往廠商ota時,需要對安裝的所有應用做全量的AOT編譯,這耗時非常久。事實上,同樣只有20%的應用是我們經常使用的,給不常用的應用,不常用的功能付出的這些成本是不值得的。
  • Android N為了解決這些問題,通過管理解釋,AOT與JIT三種模式,以達到一種運行效率、內存與耗電的折中。簡單來說,在應用運行時分析運行過的代碼以及“熱代碼”,并將配置存儲下來。在設備空閑與充電時,ART僅僅編譯這份配置中的“熱代碼”。我們先來看看Android N上有哪些編譯方法:

    Android N的編譯模式

    在compiler_filter.h,我們可以看到dex2oat一共有12種編譯模式:

    enum Filter { VerifyNone, // Skip verification but mark all classes as verified anyway.kVerifyAtRuntime, // Delay verication to runtime, do not compile anything.kVerifyProfile, // Verify only the classes in the profile, compile only JNI stubs.kInterpretOnly, // Verify everything, compile only JNI stubs.kTime, // Compile methods, but minimize compilation time.kSpaceProfile, // Maximize space savings based on profile.kSpace, // Maximize space savings.kBalanced, // Good performance return on compilation investment.kSpeedProfile, // Maximize runtime performance based on profile.kSpeed, // Maximize runtime performance.kEverythingProfile, // Compile everything capable of being compiled based on profile.kEverything, // Compile everything capable of being compiled. };

    以上12種編譯模式按照排列次序逐漸增強,那系統默認采用了哪些編譯模式呢?我們可以在在手機上執行getprop | grep pm查看:

    pm.dexopt.ab-ota: [speed-profile] pm.dexopt.bg-dexopt: [speed-profile] pm.dexopt.boot: [verify-profile] pm.dexopt.core-app: [speed] pm.dexopt.first-boot: [interpret-only] pm.dexopt.forced-dexopt: [speed] pm.dexopt.install: [interpret-only] pm.dexopt.nsys-library: [speed] pm.dexopt.shared-apk: [speed]

    其中有幾個我們是特別關心的,

  • install(應用安裝)與first-boot(應用首次啟動)使用的是[interpret-only],即只verify,代碼解釋執行即不編譯任何的機器碼,它的性能與Dalvik時完全一致,先讓用戶愉快的玩耍起來。
  • ab-ota(系統升級)與bg-dexopt(后臺編譯)使用的是[speed-profile],即只根據“熱代碼”的profile配置來編譯。這也是N中混合編譯的核心模式。
  • 對于動態加載的代碼,即forced-dexopt,它采用的是[speed]模式,即最大限度的編譯機器碼,它的表現與之前的AOT編譯一致。
  • 總的來說,程序使用loaddex動態加載的代碼是無法享受混合編譯帶來的好處,我們應當盡量采用ClassN.dex方式來符合Google的規范。這不僅在ota還是混合編譯上,都會帶來很大的提升。

    Android N的Profile文件

    在講[speed-profile]是怎樣編譯之前,這里先簡單描述一下profile文件。profile相關的核心代碼都在art/runtime/jit中。簡單來說,profile_saver.cc會開啟線程去專門收集已經resolved的類與函數,達到一定條件即會持久化存儲在/data/misc/profiles文件夾中。具體的條件可以在profile\_saver\_options.h中查看,在收集過程會出現類似以下的日志:

    tinker.sample.android I/art: Collecting resolved classes tinker.sample.android I/art: Collecting class profile for dex file /data/app/tinker.sample.android-1/base.apk types=2406 class_defs=1719 tinker.sample.android I/art: Dex location /data/app/tinker.sample.android-1/base.apk has 232 / 1719 resolved classes

    profile的存儲格式在offline\_profiling\_info.h中定義,我們也可以通過profman命令查看profile文件中的數據,命令如下:

    profman --profile-file=/data/misc/profiles/cur/0/tinker.sample.android/primary.prof --dump-only

    具體輸出如下:

    === profile === ProfileInfo: base.apk methods: 297,302,303,424,427,665,668,669,700,756,757,759,760,761,765,766,768,772,774, classes: 52,124,456,

    其中base.apk代表dex的位置,這里代表的是ClassN中的第一個dex。其他dex會使用類似base.apk:classes2.dex方式命名。后面的methods與classes代表的是它們在dex格式中的index,只有這些類與方法是我們需要在[speed-profile]模式中需要編譯。

    Android N的dex2oat編譯

    在這里我們比較關心系統究竟是什么時候會去對應用做類似增量的編譯,還有具體的編譯流程是怎么樣的?

    dex2oat編譯的時機

    首先我們來看系統在什么時候會對各個應用做這種漸進式編譯呢?手機在充電+空閑+四個小時間隔等多個條件下,通過BackgroundDexOptService.java中的JobSchedule下觸發編譯優化。

    new JobInfo.Builder(BACKGROUND_DEXOPT_JOB, sDexoptServiceName).setRequiresDeviceIdle(true).setRequiresCharging(true).setMinimumLatency(minLatency).build();

    dex2oat編譯的流程

    對于[speed-profile]模式,dex2oat編譯命令的核心參數如下:

    dex2oat --dex-file=./base.apk --oat-file=./base.odex --compiler-filter=speed-profile --app-image-file=./base.art--profile-file=./primary.prof ...

    入口文件位于dex2oat.cc中,在這里并不想貼具體的調用函數,簡單的描述一下流程:若dex2oat參數中有輸入profile文件,會讀取profile中的數據。與以往不同的是,這里不僅會根據profile文件來生成base.odex文件,同時還會生成稱為app_image的base.art文件。與boot.art類似,base.art文件主要為了加快應用的對“熱代碼”的加載與緩存。

    我們可以通過oatdump命令來看到art文件的內容,具體命令如下:

    oatdump --app-image=base.art --app-oat=base.odex --image=/system/framework/boot.art --instruction-set=arm64

    我們可以dump到art文件中的所有信息,這里我只將它的頭部信息輸出如下:

    IMAGE LOCATION: base.art IMAGE BEGIN: 0x77ea1000 IMAGE SIZE: 1597200 IMAGE SECTION SectionObjects: size=2040 range=0-2040 IMAGE SECTION SectionArtFields: size=0 range=2040-2040 IMAGE SECTION SectionArtMethods: size=0 range=2040-2040 IMAGE SECTION SectionRuntimeMethods: size=0 range=2040-2040 IMAGE SECTION SectionIMTConflictTables: size=0 range=2040-2040 IMAGE SECTION SectionDexCacheArrays: size=1591080 range=2040-1593120 IMAGE SECTION SectionInternedStrings: size=4040 range=1593120-1597160 IMAGE SECTION SectionClassTable: size=40 range=1597160-1597200 IMAGE SECTION SectionImageBitmap: size=4096 range=1597440-1601536

    base.art文件主要記錄已經編譯好的類的具體信息以及函數在oat文件的位置,一個class的輸出格式如下:

    0x78c8f768: java.lang.Class "com.tencent.mm.ui.d.a" (StatusInitialized)shadow$_klass_: 0x6fc76488 Class: java.lang.Classshadow$_monitor_: 0 (0x0)accessFlags: 524305 (0x80011)annotationType: null sun.reflect.annotation.AnnotationTypeclassFlags: 0 (0x0)classLoader: 0x787b5140 java.lang.ClassLoaderclassSize: 460 (0x1cc)clinitThreadId: 0 (0x0)componentType: null java.lang.ClasscopiedMethodsOffset: 3 (0x3)dexCache: 0x782290c8 java.lang.DexCachedexCacheStrings: 2036372056 (0x79609258)dexClassDefIndex: 12138 (0x2f6a)dexTypeIndex: 11797 (0x2e15)iFields: 2031076964 (0x790fc664)ifTable: 0x78836500 java.lang.Object[]methods: 2032787876 (0x7929e1a4)name: null java.lang.StringnumReferenceInstanceFields: 4 (0x4)numReferenceStaticFields: 0 (0x0)objectSize: 36 (0x24)primitiveType: 131072 (0x20000)referenceInstanceOffsets: 63 (0x3f)sFields: 0 (0x0)status: 10 (0xa)superClass: 0x78bcc968 Class: com.tencent.mm.pluginsdk.ui.b.bverifyError: null java.lang.ObjectvirtualMethodsOffset: 1 (0x1)vtable: null java.lang.Object

    method的輸出格式如下:

    0x792b639c ArtMethod: void com.tencent.mm.e.a.je.<init>() OAT CODE: 0x471dae14-0x471daece SIZE: Dex Instructions=10 StackMaps=0 AccessFlags=0x90001 0x792b63c0 ArtMethod: void com.tencent.mm.e.a.je.<init>(byte) OAT CODE: 0x471daee4-0x471daf52 SIZE: Dex Instructions=48 StackMaps=0 AccessFlags=0x90002 0x792b63e8 ArtMethod: void com.tencent.mm.e.a.jo.<init>() OAT CODE: 0x463d5f44-0x463d5f50 SIZE: Dex Instructions=10 StackMaps=0 AccessFlags=0x90001

    那么我們就剩下最后一個問題,app image文件是什么時候被加載,并且為什么它會影響熱補丁的機制?

    App image文件的加載

    在apk啟動時我們需要加載應用的oat文件以及可能存在的app image文件,它的大致流程如下:

  • 通過OpenDexFilesFromOat加載oat時,若app image存在,則通過調用OpenImageSpace函數加載;
  • 在加載app image文件時,通過UpdateAppImageClassLoadersAndDexCaches函數,將art文件中的dex_cache中dex的所有class插入到ClassTable,同時將method更新到dex_cache;
  • 在類加載時,使用時ClassLinker::LookupClass會先從ClassTable中去查找,找不到時才會走到DefineClass中。
  • 非常簡單的說,app image的作用是記錄已經編譯好的“熱代碼”,并且在啟動時一次性把它們加載到緩存。預先加載代替用時查找以提升應用的性能,到這里我們終于明白為什么base.art會影響熱補丁的機制。

    無論是使用插入pathlist還是parent classloader的方式,若補丁修改的class已經存在與app image,它們都是無法通過熱補丁更新的。它們在啟動app時已經加入到PathClassLoader的ClassTable中,系統在查找類時會直接使用base.apk中的class。

    instant run為什么沒有影響

    對于instant run來說,它的目標是快速debug。從上面的編譯條件看來,它是不太可能可以觸發[speed-profile]編譯的。事實上,它在dex2oat上面傳入了--debugable參數, 不過dex2oat并沒有單獨處理這個參數。感興趣的同學,可以再詳細研究這一塊。

    最后我們再來總結一下Android N混合編譯運行的整個流程,它就像一個小型生態系統那樣和諧。

    Android N上熱補丁的出路

    假設base.art文件在補丁前已經存在,這里存在三種情況:

  • 補丁修改的類都不app image中;這種情況是最理想的,此時補丁機制依然有效;
  • 補丁修改的類部分在app image中;這種情況我們只能更新一部分的類,此時是最危險的。一部分類是新的,一部分類是舊的,app可能會出現地址錯亂而出現crash。
  • 補丁修改的類全部在app image中;這種情況只是造成補丁不生效,app并不會因此造成crash。
  • 如何解決這個問題呢?下面根據當時我的一些思路分別說明:

    插樁?

    當時第一反應想到是通過插樁是否能阻止類被編譯到app image中,從而規避了這個問題。事實上,在生成profile時,使用的是ClassLinker::GetResolvedClasses函數,插樁并沒有任何作用。

    我這邊也專門單獨看了插樁后編譯的機器碼,僅僅是通過Trampoline模式跳回虛擬機查找而已。

    DEX CODE:...0x0018: 0e00 | return-void0x45f0dda2: f8d9e29c ldr.w lr, [r9, #668] ; pInvokeStaticTrampolineWithAccessCheck...

    miniloader方案

    假設我們實現一個最小化的loader,這部分代碼我們補丁時是不會去改變。然后其他代碼都通過動態方式加載,這套方案的確是可行的,但是并不會被采用,因為它會帶來以下幾個代價:

  • 對Android N之前,由于不使用ClassN方式,帶來首次加載過慢甚至黑屏的問題;
  • 對于Android N,不僅存在第一點問題,同時將混合編譯的好處完全廢掉了(因為動態加載的代碼是相當于完全編譯的);
  • 在微信中,補丁方案的原則應該是不能影響運行時的性能,所以這套方案也是不可取的。

    運行時替換PathClassLoader方案

    事實上,App image中的class是插入到PathClassloader中的ClassTable中。假設我們完全廢棄掉PathClassloader,而采用一個新建Classloader來加載后續的所有類,即可達到將cache無用化的效果。

    需要注意的問題是我們的Application類是一定會通過PathClassloader加載的,所以我們需要將Application類與我們的邏輯解耦,這里方式有兩種:

  • 采用類似instant run的實現;在代理application中,反射替換真正的application。這種方式的優點在于接入容易,但是這種方式無法保證兼容性,特別在反射失敗的情況,是無法回退的。
  • 采用代理Application實現的方法;即Application的所有實現都會被代理到其他類,Application類不會再被使用到。這種方式沒有兼容性的問題,但是會帶來一定的接入成本。
  • 我想說明的是許多號稱毫無兼容性問題的反射框架,在微信Android 數億用戶面前往往都是經不起考驗的。這也是為什么我們盡管采用增加接入成本方式也不愿意再多的使用反射的原因。總的來說,這種方式不會影響沒有補丁時的性能,但在加載補丁后,由于廢棄了App image帶來一定的性能損耗。具體數據如下:

    事實上,在Android N上我們不會出現完整編譯一個應用的base.odex與base.art的情況。base.art的作用是加快類與方法的第一次查找速度,所以在啟動時這個數據是影響最大的。在這種情況,廢棄base.art大約帶來15%左右的性能損耗。在其他情況下,這個數字應該是遠遠小于這個數字。

    Tinker的后續計劃

    在Android N上,Tinker全量合成方案帶來了一個較為嚴重的問題。即將Android N的混合編譯退化了,因為動態編譯的代碼采用的是[speed]方式完整編譯,它會占用比較多Rom空間。所以未來我們計劃根據平臺區分合成的方式,在Dalvik平臺我們合成一個完整的dex,但在Art平臺只合成需要的類,它的規則如下:

  • 修改跟新增的class;
  • 若class有field,method或interface數量變化,它們所有的子類;
  • 若class有field,method或interface數量變化d,它們以及它們所有子類的調用類。如果采用ClassN方式,即需要多個dex一起處理。
  • 規則看起來很復雜,同一個diff文件,根據不同平臺合成不同文件看起來也很復雜。更困難的是,dex格式是存在大量的互相引用,除了index區域,還有使用絕對地址引用的區域,大量的變長結構,4字節對齊......

    所以Tinker最終期望的結構圖應該如下,在art上面僅僅合成mini.dex即可:?

    結語

    建議大家通過"閱讀全文"查看,以獲得更好的閱讀體驗。盡管當前Tinker還沒有開啟內測,我們會盡力在開源前做的更好。讓Tinker無論在Dalvik還是Art上,都有著最好的表現,同時也懇請大家繼續耐心等候我們。


    原文地址: https://github.com/WeMobileDev/article/blob/master/Android_N%E6%B7%B7%E5%90%88%E7%BC%96%E8%AF%91%E4%B8%8E%E5%AF%B9%E7%83%AD%E8%A1%A5%E4%B8%81%E5%BD%B1%E5%93%8D%E8%A7%A3%E6%9E%90.md

    總結

    以上是生活随笔為你收集整理的Android N混合编译与对热补丁影响深度解析的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。