iOS 15 如何让 App 启动更快?

译者 | 无阻我飞扬     责编 | 晋兆雨

出品 | CSDN(ID:CSDNnews)

WWDC21中最有趣的特性被深深地隐藏在 Xcode 13发布说明中:

部署在 macOS 12 或 iOS 15 及更高版本操作系统上的所有程序及 dylibs现在都使用链式修复格式。这种格式使用不同的加载命令和 LINKEDIT 数据,不能在低版本的操作系统上运行或加载。

目前还没有任何文献或会议可以了解更多有关于此更改的信息,但我们可以对其进行逆向工程,以了解 Apple 在新版本上有何不同,它是否优化了App的启动时间。首先,了解控制App启动的程序的一些背景知识。

认识dyld

dyld是苹果的动态链接器,是苹果操作系统一个重要组成部分,是每个App的入口点。它负责让APP的代码做好运行准备,因此对dyld的任何改进都会使得App启动时间缩短。在调用main、运行静态初始化程序或设置 Objective-C运行时间之前,dyld负责执行修正操作,包括变基和绑定操作,这些操作修改App二进制文件中的指针以包含在运行时有效的地址。想要了解它们如何运行,可以使用 dyldinfo 命令行工具。

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

这意味着地址 0x10748C0C8 位于 __DATA/__got,需要按一个常量值进行移位。地址 0x107595A70 在 __DATA/__const, 应该指向 Hashable[1] 的协议描述符在libswiftCore.dylib

dyld 使用 LC_DYLD_INFO 载入命令和 dyld_info_command 结构确定二进制文件中变基、绑定和导出符号[2]的位置和大小 。Emerge (声明:我是创始人),解析这些数据,直观了解它们对二进制大小的贡献,建议链接器标志使它们变得更小:

 

一种新的格式

当第一次上传一个为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

导出数据与之前完全相同,树的每个节点代表符号名称的一部分。 

iOS 15 中唯一的变化是数据现在由 linkedit_data_command 引用,该命令包含第一个节点的偏移量。为了验证这一点,我写了一个简短的 Swift App来解析 iOS 15 二进制文件并打印每个符号:

let bytes = (try! Data(contentsOf: url) as NSData).bytes    
  bytes.processLoadComands { load_command, pointer in    
    if load_command.cmd == LC_DYLD_EXPORTS_TRIE {    
      let dataCommand = pointer.load(as: linkedit_data_command.self)    
      bytes.advanced(by: Int(dataCommand.dataoff)).readExportTrie()    
    }    
  }    
      
  extension UnsafeRawPointer {    
    func readExportTrie() {    
      var frontier = readNode(name: "")    
      guard !frontier.isEmpty else { return }    
      
      repeat {    
        let (prefix, offset) = frontier.removeFirst()    
        let children = advanced(by: Int(offset)).readNode(name: prefix)    
        for (suffix, offset) in children {    
          frontier.append((prefix + suffix, offset))    
        }    
      } while !frontier.isEmpty    
    }    
      
    // Returns an array of child nodes and their offset    
    func readNode(name: String) -> [(String, UInt)] {    
      guard load(as: UInt8.self) == 0 else {    
        // This is a terminal node    
        print("symbol name \(name)")    
        return []    
      }    
      let numberOfBranches = UInt(advanced(by: 1).load(as: UInt8.self))    
      var mutablePointer = self.advanced(by: 2)    
      var result = [(String, UInt)]()    
      for _ in 0..<numberOfBranches {    
        result.append(    
          (mutablePointer.readNullTerminatedString(),    
           mutablePointer.readULEB()))    
      }    
      return result    
    }    
  }


真正的变化在 LC_DYLD_CHAINED_FIXUPS。 在 iOS 15 之前的版本,变基、绑定和延迟绑定分别存储在单独的表中。现在它们已组合成链,在这个新的加载命令中,包含链起点的指针: 

App二进制文件被分解成多个段,每个段都包含一个可以绑定或变基的修复链(不再有延迟绑定)。二进制文件中的每个 64 位 rebase[3] 定位,对它指向的偏移量以及到下一个修正的偏移量进行编码,如以下结构所示:

struct dyld_chained_ptr_64_rebase
{
uint64_t target : 36,
high8 : 8,
reserved : 7, // 0s
next : 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 通过压缩最近未使用的页面来优化这一点。

App启动时的修正需要更改App二进制文件中的地址,因此整个页面都被标记为脏页面。让我们看看在app启动期间修正程序使用了多少页面:

% xcrun dyldinfo -rebase Snapchat.app/Snapchat > rebases
% ruby -e 'puts IO.read("rebases").split("\n").drop(2).map { |a| a.split(" ")[2].to_i(16) / 16384 }.uniq.count'
1554% xcrun dyldinfo -bind Snapchat.app/Snapchat > binds
450

对于表的格式,首先解析变基,然后是绑定。这意味着变基需要许多缺页异常,并且最终主要是 IO 绑定 [4]。另一方面,绑定访问了30% 的变基使用的页面,有效地进行了第二次内存传递。

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

 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

作者:Noah Martin

声明:本文由CSDN翻译,转载请注明来源。 





☞微信自动抢红包软件被判赔 475 万;日本科学家打破网速全球纪录;JavaScript蝉联最受欢迎编程语言|极客头条☞培训机构出来的程序员进不了大厂?☞绝密邮件曝光!看乔布斯如何拯救濒危的苹果?
  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
<p> <span style="font-size:14px;color:#337FE5;">【为什么学爬虫】</span> </p> <p> <span style="font-size:14px;">       1、爬虫入手容易,但是深入较难,如何写出高效率的爬虫,如何写出灵活性高可扩展的爬虫都是一项技术活。另外在爬虫过程中,经常容易遇到被反爬虫,比如字体反爬、IP识别、验证码等,如何层层攻克难点拿到想要的数据,这门课程,你都能学到!</span> </p> <p> <span style="font-size:14px;">       2、如果是作为一个其他行业的开发者,比如app开发,web开发,学习爬虫能你加强对技术的认知,能够开发出加安全的软件和网站</span> </p> <p> <br /> </p> <span style="font-size:14px;color:#337FE5;">【课程设计】</span> <p class="ql-long-10663260"> <span> </span> </p> <p class="ql-long-26664262" style="font-size:11pt;color:#494949;"> 一个完整的爬虫程序,无论大小,总体来说可以分成三个步骤,分别是: </p> <ol> <li class="" style="font-size:11pt;color:#494949;"> 网络请求:模拟浏览器的行为从网上抓取数据。 </li> <li class="" style="font-size:11pt;color:#494949;"> 数据解析:将请求下来的数据进行过滤,提取我们想要的数据。 </li> <li class="" style="font-size:11pt;color:#494949;"> 数据存储:将提取到的数据存储到硬盘或者内存中。比如用mysql数据库或者redis等。 </li> </ol> <p class="ql-long-26664262" style="font-size:11pt;color:#494949;"> 那么本课程也是按照这几个步骤循序渐进的进行讲解,带领学生完整的掌握每个步骤的技术。另外,因为爬虫的多样性,在爬取的过程中可能会发生被反爬、效率低下等。因此我们又增加了两个章节用来提高爬虫程序的灵活性,分别是: </p> <ol> <li class="" style="font-size:11pt;color:#494949;"> 爬虫进阶:包括IP代理,多线程爬虫,图形验证码识别、JS加密解密、动态网页爬虫、字体反爬识别等。 </li> <li class="" style="font-size:11pt;color:#494949;"> Scrapy和分布式爬虫:Scrapy框架、Scrapy-redis组件、分布式爬虫等。 </li> </ol> <p class="ql-long-26664262" style="font-size:11pt;color:#494949;"> 通过爬虫进阶的知识点我们能应付大量的反爬网站,而Scrapy框架作为一个专业的爬虫框架,使用他可以速提高我们编写爬虫程序的效率和速度。另外如果一台机器不能满足你的需求,我们可以用分布式爬虫多台机器帮助你速爬取数据。 </p> <p style="font-size:11pt;color:#494949;">   </p> <p class="ql-long-26664262" style="font-size:11pt;color:#494949;"> 从基础爬虫到商业化应用爬虫,本套课程满足您的所有需求! </p> <p class="ql-long-26664262" style="font-size:11pt;color:#494949;"> <br /> </p> <p> <br /> </p> <p> <span style="font-size:14px;background-color:#FFFFFF;color:#337FE5;">【课程服务】</span> </p> <p> <span style="font-size:14px;">专属付费社群+定期答疑</span> </p> <p> <br /> </p> <p class="ql-long-24357476"> <span style="font-size:16px;"><br /> </span> </p> <p> <br /> </p> <p class="ql-long-24357476"> <span style="font-size:16px;"></span> </p>
©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值