iOS15系統(tǒng)啟動時間加速

WWDC21中最有趣的特性被深深地隱藏在 Xcode 13發(fā)布說明中:

部署在 macOS 12 或 iOS 15 及更高版本操作系統(tǒng)上的所有程序及 dylibs現在都使用鏈式修復格式。這種格式使用不同的加載命令和 LINKEDIT 數據,不能在低版本的操作系統(tǒng)上運行或加載。

目前還沒有任何文獻或會議可以了解更多有關于此更改的信息,但我們可以對其進行逆向工程,以了解 Apple 在新版本上有何不同,它是否優(yōu)化了App的啟動時間。首先,了解控制App啟動的程序的一些背景知識。

認識dyld

dyld是蘋果的動態(tài)鏈接器,是蘋果操作系統(tǒng)一個重要組成部分,是每個App的入口點。它負責讓APP的代碼做好運行準備,因此對dyld的任何改進都會使得App啟動時間縮短。在調用main、運行靜態(tài)初始化程序或設置 Objective-C運行時間之前,dyld負責執(zhí)行修正操作,包括變基和綁定操作,這些操作修改App二進制文件中的指針以包含在運行時有效的地址。想要了解它們如何運行,可以使用?dyldinfo 命令行工具。

% xcrun dyldinfo -rebase -bind Snapchat.app/Snapchatrebase information (from compressed dyld info):segment section address type__DATA __got 0x10748C0C8 pointer...bind information:segment section address type addend dylib symbol__DATA __const 0x107595A70 pointer 0 libswiftCore _$sSHMp

這意味著地址?0x10748C0C8 位于?__DATA/__got,需要按一個常量值進行移位。地址?0x107595A70?在?__DATA/__const,?應該指向 Hashable[1] 的協(xié)議描述符在libswiftCore.dylib

dyld 使用?LC_DYLD_INFO?載入命令和?dyld_info_command?結構確定二進制文件中變基、綁定和導出符號[2]的位置和大小?。Emerge (聲明:我是創(chuàng)始人),解析這些數據,直觀了解它們對二進制大小的貢獻,建議鏈接器標志使它們變得更小

一種新的格式

當第一次上傳一個為iOS15構建的App時,通過 Emerge,并沒有看到dyld修正的效果。因為缺少LC_DYLD_INFO_ONLY加載命令,它已被替換為LC_DYLD_CHAINED_FIXUPS 和?LC_DYLD_EXPORTS_TRIE 。

% otool -l iOS14Example.app/iOS14Example |?grep LC_DYLD??????cmd LC_DYLD_INFO_ONLY% otool -l iOS15Example.app/iOS15Example |?grep LC_DYLD??????cmd LC_DYLD_CHAINED_FIXUPS??????cmd LC_DYLD_EXPORTS_TRIE

導出數據與之前完全相同,樹的每個節(jié)點代表符號名稱的一部分。?

iOS 15 中唯一的變化是數據現在由?linkedit_data_command 引用,該命令包含第一個節(jié)點的偏移量。為了驗證這一點,我寫了一個簡短的 Swift App來解析 iOS 15 二進制文件并打印每個符號:

letbytes = (try!Data(contentsOf: url)asNSData).bytesbytes.processLoadComands { load_command, pointerinifload_command.cmd ==LC_DYLD_EXPORTS_TRIE{letdataCommand = pointer.load(as: linkedit_data_command.self)bytes.advanced(by:Int(dataCommand.dataoff)).readExportTrie()? ? }? ? ? }? ? ? ? ? extensionUnsafeRawPointer{funcreadExportTrie(){varfrontier = readNode(name:"")guard!frontier.isEmptyelse{return}? ? ? repeat{let(prefix, offset) = frontier.removeFirst()letchildren = advanced(by:Int(offset)).readNode(name:prefix)for(suffix, offset)inchildren {frontier.append((prefix+ suffix, offset))? ? ? ? }? ? }while!frontier.isEmpty? ? }? ? ? ? ? // Returns an array of child nodes and their offset? ? funcreadNode(name: String)-> [(String,UInt)] {guardload(as:UInt8.self) ==0else{// This is a terminal node? ? print("symbol name \(name)")return[]? ? ? }? ? letnumberOfBranches =UInt(advanced(by:1).load(as:UInt8.self))varmutablePointer =self.advanced(by:2)varresult = [(String,UInt)]()for_in0..

一種新的格式

當第一次上傳一個為iOS15構建的App時,通過 Emerge,并沒有看到dyld修正的效果。因為缺少LC_DYLD_INFO_ONLY加載命令,它已被替換為LC_DYLD_CHAINED_FIXUPS 和?LC_DYLD_EXPORTS_TRIE 。

% otool -l iOS14Example.app/iOS14Example |?grep LC_DYLD??????cmd LC_DYLD_INFO_ONLY% otool -l iOS15Example.app/iOS15Example |?grep LC_DYLD??????cmd LC_DYLD_CHAINED_FIXUPS??????cmd LC_DYLD_EXPORTS_TRIE

導出數據與之前完全相同,樹的每個節(jié)點代表符號名稱的一部分。?

iOS 15 中唯一的變化是數據現在由?linkedit_data_command 引用,該命令包含第一個節(jié)點的偏移量。為了驗證這一點,我寫了一個簡短的 Swift App來解析 iOS 15 二進制文件并打印每個符號:

letbytes = (try!Data(contentsOf: url)asNSData).bytesbytes.processLoadComands { load_command, pointerinifload_command.cmd ==LC_DYLD_EXPORTS_TRIE{letdataCommand = pointer.load(as: linkedit_data_command.self)bytes.advanced(by:Int(dataCommand.dataoff)).readExportTrie()? ? }? ? ? }? ? ? ? ? extensionUnsafeRawPointer{funcreadExportTrie(){varfrontier = readNode(name:"")guard!frontier.isEmptyelse{return}? ? ? repeat{let(prefix, offset) = frontier.removeFirst()letchildren = advanced(by:Int(offset)).readNode(name:prefix)for(suffix, offset)inchildren {frontier.append((prefix+ suffix, offset))? ? ? ? }? ? }while!frontier.isEmpty? ? }? ? ? ? ? // Returns an array of child nodes and their offset? ? funcreadNode(name: String)-> [(String,UInt)] {guardload(as:UInt8.self) ==0else{// This is a terminal node? ? print("symbol name \(name)")return[]? ? ? }? ? letnumberOfBranches =UInt(advanced(by:1).load(as:UInt8.self))varmutablePointer =self.advanced(by:2)varresult = [(String,UInt)]()for_in0..

真正的變化在 LC_DYLD_CHAINED_FIXUPS。?在 iOS 15 之前的版本,變基、綁定和延遲綁定分別存儲在單獨的表中。現在它們已組合成鏈,在這個新的加載命令中,包含鏈起點的指針:?

App二進制文件被分解成多個段,每個段都包含一個可以綁定或變基的修復鏈(不再有延遲綁定)。二進制文件中的每個 64 位 rebase[3] 定位,對它指向的偏移量以及到下一個修正的偏移量進行編碼,如以下結構所示:

structdyld_chained_ptr_64_rebase{uint64_ttarget :36,high8 :8,reserved :7,// 0snext :12,bind :1;// Always 0 for a rebase};

指針對象使用36位,足以容納 23???= 64GB 的二進制文件,12 位用于提供下一個修正的偏移量(步幅 = 4)。因此,它可以指向 2 12?* 4 = 16kb范圍內的任何位置——正是 iOS 上的頁面大小。

這種非常緊湊的編碼意味著遍歷鏈的整個過程可以包含在二進制的現有大小內。?在我的測試中,超過 50% 的 dyld 數據對二進制大小的貢獻被保存,因為只保留了少量元數據用來指示每個頁面上的第一個修正。最終結果是Swift App的大小減少了 1mb 以上。

這個過程的源代碼在?MachOLoaded.cpp 中?,二進制設計在 /usr/include/macho-o/fixup-chains.h


排序問題

要理解這種改變背后的動機,我們必須注意App啟動時開銷最大的操作——缺頁異常。在App啟動期間訪問文件系統(tǒng)上的代碼時,需要通過缺頁異常將其從文件寫入到內存。App二進制文件中的每個 16kb區(qū)間都映射到內存中的一個頁面。一旦頁面被修改,它就需要在App運行期間一直保留在 RAM 中(稱為臟頁面)。iOS 通過壓縮最近未使用的頁面來優(yōu)化這一點。

App啟動時的修正需要更改App二進制文件中的地址,因此整個頁面都被標記為臟頁面。讓我們看看在app啟動期間修正程序使用了多少頁面:

% xcrun dyldinfo -rebaseSnapchat.app/Snapchat> rebases% ruby -e 'putsIO.read("rebases").split("\n").drop(2).map{ |a| a.split(" ")[2].to_i(16) /16384}.uniq.count'1554% xcrun dyldinfo -bindSnapchat.app/Snapchat> binds450

對于表的格式,首先解析變基,然后是綁定。這意味著變基需要許多缺頁異常,并且最終主要是 IO 綁定 [4]。另一方面,綁定訪問了30% 的變基使用的頁面,有效地進行了第二次內存?zhèn)鬟f。

現在在 iOS 15版本中,鏈式修正將每個內存頁面的所有更改組合在一起。dyld 現在可以通過一次遍歷內存來更快地處理它們,同時完成變基和綁定。這使得諸如內存壓縮器之類的操作系統(tǒng)功能能夠利用眾所周知的排序,而無需在綁定期間返回并解壓縮舊頁面。由于這些改變,dyld中的變基函數變成了一個空操作:

?https://opensource.apple.com/source/dyld/dyld-851.27/src/ImageLoaderMachOCompressed.cpp.auto.html

總的來說,這種改變主要影響對 iOS App進行逆向工程和探索動態(tài)鏈接器細節(jié),這很好地提醒了大家,低級的內存管理會影響App性能。雖然這種改變僅在iOS 15版本上的App有效,但請記住,仍然可以做很多事情來優(yōu)化App啟動時間:

減少動態(tài)框架的數量

減少應用程序大小,從而減少內存頁面的使用(這就是我制作 Emerge 的原因?。?/p>

將代碼移出 +加載以及靜態(tài)初始化程序

使用?更少的類

將工作推遲到繪制第一個框架后

參考鏈接:

[1] The symbol from?dyldinfo?is mangled, you can get the human readable name with?xcrun swift-demangle '_$sSHMp'.

[2] Exports are the second piece of a bind. One binary binds to symbols exported from its dependencies.

[3] The same goes for binds, a pointer is actually a union of rebase and bind (dyld_chained_ptr_64_bind) with a single bit used to differentiate the two. Binds also require the imported symbol name which isn’t discussed here.

[4]?https://asciiwwdc.com/2016/sessions/406

原文鏈接:https://medium.com/geekculture/how-ios-15-makes-your-app-launch-faster-51cf0aa6c520

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容