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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 运维知识 > Android >内容正文

Android

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

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

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

無法啟動(dòng)的原因

我們遵循從問題出發(fā)的思路,針對(duì)華為提供的日志,我們看到微信在Android N上啟動(dòng)時(shí)會(huì)報(bào)IllegalAccessError。可以從/data/user/0/com.tencent.mm/tinker/patch-a002c56d/dex/classes2.dex看到,的確跟補(bǔ)丁是有關(guān)系的。

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卻無法復(fù)現(xiàn),同時(shí)跟華為的進(jìn)一步溝通中,他們也明確只有一少部分N的用戶會(huì)出現(xiàn)問題。這就很難辦了,但是根據(jù)之前在art地址錯(cuò)亂的經(jīng)驗(yàn)(似乎這里我還欠大家一篇分析文章),跟這里似乎有點(diǎn)相似。

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

Android N的混合編譯運(yùn)行模式

網(wǎng)上關(guān)于Android N混合編譯的文章并不多,infoq上有一篇翻譯文章:Android N混合使用AOT編譯,解釋和JIT三種運(yùn)行時(shí)。混合編譯運(yùn)行主要指AOT編譯,解釋執(zhí)行與JIT編譯,它主要解決的問題有以下幾個(gè):

  • 應(yīng)用安裝時(shí)間過長;在N之前,應(yīng)用在安裝時(shí)需要對(duì)所有ClassN.dex做AOT機(jī)器碼編譯,類似微信這種比較大型的APP可能會(huì)耗時(shí)數(shù)分鐘。但是往往我們只會(huì)使用一個(gè)應(yīng)用20%的功能,剩下的80%我們付出了時(shí)間成本,卻沒帶來太大的收益。
  • 降低占ROM空間;同樣全量編譯AOT機(jī)器碼,12M的dex編譯結(jié)果往往可以達(dá)到50M之多。只編譯用戶用到或常用的20%功能,這對(duì)于存儲(chǔ)空間不足的設(shè)備尤其重要。
  • 提升系統(tǒng)與應(yīng)用性能;減少了全量編譯,降低了系統(tǒng)的耗電。在boot.art的基礎(chǔ)上,每個(gè)應(yīng)用增加了base.art(這塊后面會(huì)詳細(xì)解析), 通過預(yù)加載與緩存提升應(yīng)用性能。
  • 快速的系統(tǒng)升級(jí);以往廠商ota時(shí),需要對(duì)安裝的所有應(yīng)用做全量的AOT編譯,這耗時(shí)非常久。事實(shí)上,同樣只有20%的應(yīng)用是我們經(jīng)常使用的,給不常用的應(yīng)用,不常用的功能付出的這些成本是不值得的。
  • Android N為了解決這些問題,通過管理解釋,AOT與JIT三種模式,以達(dá)到一種運(yùn)行效率、內(nèi)存與耗電的折中。簡(jiǎn)單來說,在應(yīng)用運(yùn)行時(shí)分析運(yùn)行過的代碼以及“熱代碼”,并將配置存儲(chǔ)下來。在設(shè)備空閑與充電時(shí),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種編譯模式按照排列次序逐漸增強(qiáng),那系統(tǒng)默認(rèn)采用了哪些編譯模式呢?我們可以在在手機(jī)上執(zhí)行g(shù)etprop | 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]

    其中有幾個(gè)我們是特別關(guān)心的,

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

    Android N的Profile文件

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

    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的存儲(chǔ)格式在offline\_profiling\_info.h中定義,我們也可以通過profman命令查看profile文件中的數(shù)據(jù),命令如下:

    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中的第一個(gè)dex。其他dex會(huì)使用類似base.apk:classes2.dex方式命名。后面的methods與classes代表的是它們?cè)赿ex格式中的index,只有這些類與方法是我們需要在[speed-profile]模式中需要編譯。

    Android N的dex2oat編譯

    在這里我們比較關(guān)心系統(tǒng)究竟是什么時(shí)候會(huì)去對(duì)應(yīng)用做類似增量的編譯,還有具體的編譯流程是怎么樣的?

    dex2oat編譯的時(shí)機(jī)

    首先我們來看系統(tǒng)在什么時(shí)候會(huì)對(duì)各個(gè)應(yīng)用做這種漸進(jìn)式編譯呢?手機(jī)在充電+空閑+四個(gè)小時(shí)間隔等多個(gè)條件下,通過BackgroundDexOptService.java中的JobSchedule下觸發(fā)編譯優(yōu)化。

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

    dex2oat編譯的流程

    對(duì)于[speed-profile]模式,dex2oat編譯命令的核心參數(shù)如下:

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

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

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

    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文件主要記錄已經(jīng)編譯好的類的具體信息以及函數(shù)在oat文件的位置,一個(gè)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

    那么我們就剩下最后一個(gè)問題,app image文件是什么時(shí)候被加載,并且為什么它會(huì)影響熱補(bǔ)丁的機(jī)制?

    App image文件的加載

    在apk啟動(dòng)時(shí)我們需要加載應(yīng)用的oat文件以及可能存在的app image文件,它的大致流程如下:

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

    無論是使用插入pathlist還是parent classloader的方式,若補(bǔ)丁修改的class已經(jīng)存在與app image,它們都是無法通過熱補(bǔ)丁更新的。它們?cè)趩?dòng)app時(shí)已經(jīng)加入到PathClassLoader的ClassTable中,系統(tǒng)在查找類時(shí)會(huì)直接使用base.apk中的class。

    instant run為什么沒有影響

    對(duì)于instant run來說,它的目標(biāo)是快速debug。從上面的編譯條件看來,它是不太可能可以觸發(fā)[speed-profile]編譯的。事實(shí)上,它在dex2oat上面?zhèn)魅肓?span style="font-weight:600">--debugable參數(shù), 不過dex2oat并沒有單獨(dú)處理這個(gè)參數(shù)。感興趣的同學(xué),可以再詳細(xì)研究這一塊。

    最后我們?cè)賮砜偨Y(jié)一下Android N混合編譯運(yùn)行的整個(gè)流程,它就像一個(gè)小型生態(tài)系統(tǒng)那樣和諧。

    Android N上熱補(bǔ)丁的出路

    假設(shè)base.art文件在補(bǔ)丁前已經(jīng)存在,這里存在三種情況:

  • 補(bǔ)丁修改的類都不app image中;這種情況是最理想的,此時(shí)補(bǔ)丁機(jī)制依然有效;
  • 補(bǔ)丁修改的類部分在app image中;這種情況我們只能更新一部分的類,此時(shí)是最危險(xiǎn)的。一部分類是新的,一部分類是舊的,app可能會(huì)出現(xiàn)地址錯(cuò)亂而出現(xiàn)crash。
  • 補(bǔ)丁修改的類全部在app image中;這種情況只是造成補(bǔ)丁不生效,app并不會(huì)因此造成crash。
  • 如何解決這個(gè)問題呢?下面根據(jù)當(dāng)時(shí)我的一些思路分別說明:

    插樁?

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

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

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

    miniloader方案

    假設(shè)我們實(shí)現(xiàn)一個(gè)最小化的loader,這部分代碼我們補(bǔ)丁時(shí)是不會(huì)去改變。然后其他代碼都通過動(dòng)態(tài)方式加載,這套方案的確是可行的,但是并不會(huì)被采用,因?yàn)樗鼤?huì)帶來以下幾個(gè)代價(jià):

  • 對(duì)Android N之前,由于不使用ClassN方式,帶來首次加載過慢甚至黑屏的問題;
  • 對(duì)于Android N,不僅存在第一點(diǎn)問題,同時(shí)將混合編譯的好處完全廢掉了(因?yàn)閯?dòng)態(tài)加載的代碼是相當(dāng)于完全編譯的);
  • 在微信中,補(bǔ)丁方案的原則應(yīng)該是不能影響運(yùn)行時(shí)的性能,所以這套方案也是不可取的。

    運(yùn)行時(shí)替換PathClassLoader方案

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

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

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

    事實(shí)上,在Android N上我們不會(huì)出現(xiàn)完整編譯一個(gè)應(yīng)用的base.odex與base.art的情況。base.art的作用是加快類與方法的第一次查找速度,所以在啟動(dòng)時(shí)這個(gè)數(shù)據(jù)是影響最大的。在這種情況,廢棄base.art大約帶來15%左右的性能損耗。在其他情況下,這個(gè)數(shù)字應(yīng)該是遠(yuǎn)遠(yuǎn)小于這個(gè)數(shù)字。

    Tinker的后續(xù)計(jì)劃

    在Android N上,Tinker全量合成方案帶來了一個(gè)較為嚴(yán)重的問題。即將Android N的混合編譯退化了,因?yàn)閯?dòng)態(tài)編譯的代碼采用的是[speed]方式完整編譯,它會(huì)占用比較多Rom空間。所以未來我們計(jì)劃根據(jù)平臺(tái)區(qū)分合成的方式,在Dalvik平臺(tái)我們合成一個(gè)完整的dex,但在Art平臺(tái)只合成需要的類,它的規(guī)則如下:

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

    所以Tinker最終期望的結(jié)構(gòu)圖應(yīng)該如下,在art上面僅僅合成mini.dex即可:?

    結(jié)語

    建議大家通過"閱讀全文"查看,以獲得更好的閱讀體驗(yàn)。盡管當(dāng)前Tinker還沒有開啟內(nèi)測(cè),我們會(huì)盡力在開源前做的更好。讓Tinker無論在Dalvik還是Art上,都有著最好的表現(xiàn),同時(shí)也懇請(qǐng)大家繼續(xù)耐心等候我們。


    原文地址: 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

    總結(jié)

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

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。