解决 Swift + CocoaPods 因动态库导致启动时间过长

昨天遇到了一个棘手的问题,我发现我的 App (iOS 9.2/iPhone 5s/Swift/CocoaPods)启动巨慢,通过 device log 一看,我的天呐!7.9s?

Apr  2 21:33:27 Kittens-iPhone5S Cosmos[309] <Notice>: total time: 7.9 seconds (100.0%)  
Apr  2 21:33:27 Kittens-iPhone5S Cosmos[309] <Notice>: total images loaded:  239 (216 from dyld shared cache)  
Apr  2 21:33:27 Kittens-iPhone5S Cosmos[309] <Notice>: total segments mapped: 77, into 2811 pages with 236 pages pre-fetched  
Apr  2 21:33:27 Kittens-iPhone5S Cosmos[309] <Notice>: total images loading time: 6.8 seconds (85.7%)  
Apr  2 21:33:27 Kittens-iPhone5S Cosmos[309] <Notice>: total dtrace DOF registration time: 0.13 milliseconds (0.0%)  
Apr  2 21:33:27 Kittens-iPhone5S Cosmos[309] <Notice>: total rebase fixups:  61,981  
Apr  2 21:33:27 Kittens-iPhone5S Cosmos[309] <Notice>: total rebase fixups time: 63.21 milliseconds (0.8%)  
Apr  2 21:33:27 Kittens-iPhone5S Cosmos[309] <Notice>: total binding fixups: 260,964  
Apr  2 21:33:27 Kittens-iPhone5S Cosmos[309] <Notice>: total binding fixups time: 837.40 milliseconds (10.6%)  
Apr  2 21:33:27 Kittens-iPhone5S Cosmos[309] <Notice>: total weak binding fixups time: 0.38 milliseconds (0.0%)  
Apr  2 21:33:27 Kittens-iPhone5S Cosmos[309] <Notice>: total bindings lazily fixed up: 0 of 0  
Apr  2 21:33:27 Kittens-iPhone5S Cosmos[309] <Notice>: total time in initializers and ObjC setup: 213.3 milliseconds (2.7%)  
...

你可能会好奇如何查看启动时间,两步:

  • 添加环境变量 DYLD_PRINT_STATISTICS,设为 YES

  • 开启 lib 的 log 输出

随后通过 Device 窗口即可查看。

先来回顾一下App 的启动流程(WWDC 2012 Session 235):

1)链接和载入。

  • 可以在 Time Profile 中显示 dyld 载入库函数,库会被映射到地址空间,同时完成绑定以及静态初始化。
  • 不必要的Framework,不要链接,或标记为 optinal.

2)UIKit 初始化。

  • 字体、状态栏、user defaults、main nib会被初始化。
  • User defaults 本质上是一个 plist 文件,保存的数据是同时被反序列化的,不要在 user defaults 里面保存图片等大数据。

3)application:didFinishLaunchingWithOptions 回调。

  • 尽量减少这里面的操作。一些不必要的操作(网络请求/数据库查询)可以放在首屏显示之后。

4)第一次 Core Animation 调用。

  • 在启动后的方法 [UIApplication _resportAppLaunchFinished] 中调用 CA::Transaction::commit 实现第一帧画面的绘制。

我们知道在 OC 项目中,程序的主入口是 main 函数。

int main(int argc, char * argv[])  
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil,
                   NSStringFromClass([AppDelegate class]));
    }
}

通过传入的参数 NSStringFromClass([AppDelegate class] 指定了 AppDelegate 类作为应用的委托。

而在 Swift 项目中也是需要 main 函数的,只是不是显式的,而是通过 @UIApplicationMain 将被标注的类作为委托,自动创建被标记的类并插入 main 函数。

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {  
...

如果你想在 Swift 中使用像 OC 中一样的 main 函数,可以参考 这篇文章

另外,iOS 系统有个叫看门狗的机制,超过这个机制规定时间的操作系统会自动掐掉。

启动 20秒
恢复运行 10秒
悬挂进程 10秒
启动 20秒
退出应用 6秒
后台运行 10分钟

| 注:Xcode在Debug的时候,会禁止“看门狗”。

回到一开始打印出的 Log,我们发现时间基本都耗在了 image load 上。可是凭着过往的经验,比我项目里图片多得多的 App 都不存在这个问题。有人说是 CocoaPods 导入库太多的关系,我第一时间觉得不太可能。于是我新建了一个空项目,只是引入了一些常用的官方 framework,竟然依然存在这个问题。国外貌似讨论得也比较激烈。也有人说 production 版本就没个问题。 我们都知道 iOS8 之后 Xcode 允许开发者自己创建动态库,但这有一个性能问题,就是分散的动态库将会动态地一个接一个导入从而拖慢项目启动时间。 (其实上面的 image load 是 dynamic framework 本身,并不是图片)。参考了 johnno1962 大神(此人乃 injectionforxcode 作者)的思路,解决办法就是把 Pods 作为一个整体静态引入,从而 dyld 不需要在启动时引入动态库。

1.先把 linker 中的 -framework 替换成 -filelist 。如果使用 CocoaPods,可以在 Podfile 里添加这段代码自动完成上面的操作:

post_install do |installer|  
  pods_target = installer.aggregate_targets.detect do |target|
    # Target label is either `Pods` or `Pods-#{name_of_your_main_target}` based on how complex your dependency graph is.
    target.label == "Pods"
  end

  puts '+ Removing framework dependencies'

  pods_target.xcconfigs.each_pair do |config_name, config|
    next if config_name == 'Test'
    config.other_linker_flags[:frameworks] = Set.new
    config.attributes['OTHER_LDFLAGS[arch=armv7]'] = '$(inherited) -filelist "$(OBJROOT)/Pods.build/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)-armv7.objects.filelist"'
    config.attributes['OTHER_LDFLAGS[arch=arm64]'] = '$(inherited) -filelist "$(OBJROOT)/Pods.build/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)-arm64.objects.filelist"'
    config.attributes['OTHER_LDFLAGS[arch=i386]'] = '$(inherited) -filelist "$(OBJROOT)/Pods.build/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)-i386.objects.filelist"'
    config.attributes['OTHER_LDFLAGS[arch=x86_64]'] = '$(inherited) -filelist "$(OBJROOT)/Pods.build/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)-x86_64.objects.filelist"'
    config.save_as(Pathname.new("#{pods_target.support_files_dir}/#{pods_target.label}.#{config_name}.xcconfig"))
  end

end

2.把 frame link 入 Embedded Binaries 。

如果你使用 CocoaPods,在 Check Pods Manifest.lock 后创建一个 Run Script ,加入以下代码自动完成上述工作:

intermediates_directory = ENV['OBJROOT']  
configuration = ENV['CONFIGURATION']  
platform = ENV['EFFECTIVE_PLATFORM_NAME']  
archs = ENV['ARCHS']

archs.split(" ").each do |architecture|

Dir.chdir("#{intermediates_directory}/Pods.build") do

  filelist = ""

  Dir.glob("#{configuration}#{platform}/*.build/Objects-normal/#{architecture}/*.o") do |object_file|

    filelist += File.absolute_path(object_file) + "\n"

  end

  File.write("#{configuration}#{platform}-#{architecture}.objects.filelist", filelist)

end

end  

| 注:将 shell 的名字换成 !/usr/bin/ruby

不过,iOS 9.3 中貌似已经解决这个问题了。本文的意义其实也不大了,权当留个记录,万一你刚好遇到了同样的问题,兴许能帮到你。

参考链接

  1. Statically link CocoaPods frameworks for faster App startup https://github.com/johnno1962/Accelerator

  2. App launch time increased https://github.com/artsy/eigen/issues/586

  3. dyld-image-loading-performance https://github.com/stepanhruda/dyld-image-loading-performance

上一篇

在 OC 中实现消息的一箭双雕


下一篇

论互联网时代的个体突围