王尘宇王尘宇

研究百度干SEO做推广变成一个被互联网搞的人

iOS15系统启动时间加速


iOS15系统启动时间加速

iOS 15 中唯一的变化是数据现在由 linkedit_data_command 引用,该命令包含第一个节点的偏移量。为了验证这一点,我写了一个简短的 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 之前的版本,变基、绑定和延迟绑定分别存储在单独的表中。现在它们已组合成链,在这个新的加载命令中,包含链起点的指针: 

iOS15系统启动时间加速

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位,足以容纳 2³ ⁶ = 64GB 的二进制文件,12 位用于提供下一个修正的偏移量(步幅 = 4)。因此,它可以指向 2 ¹² * 4 = 16kb范围内的任何位置——正是 iOS 上的页面大小。

这种非常紧凑的编码意味着遍历链的整个过程可以包含在二进制的现有大小内。 在我的测试中,超过 50% 的 dyld 数据对二进制大小的贡献被保存,因为只保留了少量元数据用来指示每个页面上的第一个修正。最终结果是Swift App的大小减少了 1mb 以上。

这个过程的源代码在 MachOLoaded.cpp 中 ,二进制设计在 /usr/include/macho-o/fixup-chains.h

排序

要理解这种改变背后的动机,我们必须注意App启动时开销最大的操作——缺页异常。在App启动期间访问文件系统上的代码时,需要通过缺页异常将其从文件写入到内存。App二进制文件中的每个 16kb区间都映射到内存中的一个页面。一旦页面被修改,它就需要在App运行期间一直保留在 RAM 中(称为脏页面)。iOS 通过压缩最近未使用的页面来优化这一点。

iOS15系统启动时间加速

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% 的变基使用的页面,有效地进行了第二次内存传递。

现在在 iOS 15版本中,链式修正将每个内存页面的所有更改组合在一起。dyld 现在可以通过一次遍历内存来更快地处理它们,同时完成变基和绑定。这使得诸如内存压缩器之类的操作系统功能能够利用众所周知的排序,而无需在绑定期间返回并解压缩旧页面。由于这些改变,dyld中的变基函数变成了一个空操作:

iOS15系统启动时间加速

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

总的来说,这种改变主要影响对 iOS App进行逆向工程和探索动态链接器细节,这很好地提醒了大家,低级的内存管理会影响App性能。虽然这种改变仅在iOS 15版本上的App有效,但请记住,仍然可以做很多事情来优化App启动时间:

减少动态框架的数量

减少应用程序大小,从而减少内存页面的使用(这就是我制作 Emerge 的原因!)

将代码移出 +加载以及静态初始化程序

使用 更少的类

将工作推迟到绘制第一个框架后

参考链接:

[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

相关文章

评论列表

发表评论:
验证码

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。