149

即刻Swift静态库实践

 6 years ago
source link: https://zhuanlan.zhihu.com/p/32178522?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

即刻Swift静态库实践

记录我,遇见你

背景

即刻是国内较早全面拥抱Swift的iOS开发团队,目前即刻100%的业务代码(第三方库依赖除外)都通过Swift实现。随着业务的发展,即刻做了多次架构的拆分,项目按模块划分成多个target,依赖的第三方库也日渐增多。

在Xcode 9以前,由于官方不支持Swift静态库,开发团队不得不将项目的依赖打包成动态库。然而,随着项目里动态库越来越多,app的启动速度不可避免地受到影响(参考Optimizing App Startup Time)。

终于在Xcode 9时代,swift带来对静态库的原生支持。即刻一直以来都关注着swift静态库这个feature,因此在Xcode 9到来的时候,我们就决定将项目改造成静态库打包的方式。

遇到的问题和方法

静态库是Swift社区比较关注的点,因此在Xcode 9 出来的同时,即刻便开始着手支持。

在手动简单尝试(改target编译项)后,我们决定自己来尝试做这件事情,原因是一来对比测试后,发现静态库打包对我们当前项目有所优化,二是改动并不大,做完资源管理后,代码改动不大。

我们了解到cocoapod 也在着手支持,但在我们开始实践时 cocoapods的支持还存在些问题,而且我们所做的改造即使未来切换为cocoapod的方式也是必要的。于是我们决定先行,并将手动改造脚本化,使用Xcodeproj 来替代手动改造

main_project = Xcodeproj::Project.open('Ruguo.xcodeproj')
    main_target  = main_project.targets.first
    # add complie flag
    # 1. 静态库链接对于无引用的代码会优化掉,为binary增加 -all_load,加载全部,防止部分OC符号找不到
    main_target.build_configurations.each do |config|
        config.build_settings['OTHER_LDFLAGS'] += ['-all_load'] unless config.build_settings['OTHER_LDFLAGS'].include?('-all_load')
    end
    
    # main target build phases
    # main target 的不同阶段
    main_embed_frameworks_phase = main_target.build_phases.select {|phase| (phase.respond_to? :name) && phase.name == "Embed Frameworks" }.first
    main_copy_resources_phase = main_target.build_phases.select {|phase| phase.kind_of? Xcodeproj::Project::Object::PBXResourcesBuildPhase }.first
    main_linkPhase = main_target.build_phases.select { |phase| phase.kind_of? Xcodeproj::Project::Object::PBXFrameworksBuildPhase }.first


    excludeTargets = ['主project,有些target,比如拓展程序,不转成静态库']
    main_project.targets.each do |target|
        if not excludeTargets.include? target.name then
            target_linkPhase = target.build_phases.select { |phase| phase.kind_of? Xcodeproj::Project::Object::PBXFrameworksBuildPhase }.first
            target_copy_resources_phase = target.build_phases.select {|phase| phase.kind_of? Xcodeproj::Project::Object::PBXResourcesBuildPhase }.first
            
            # find dynamic library references
            # 1. 静态库是一个个Object文件的集合,对于其依赖的动态库加到binary最终的依赖就可以
            # 2. tbd 是一种动态库的 stub library,拥有相应动态库一样的链接符号,但没有相关代码,能加速编译等
            target_linkPhase.files_references.each do |file_reference|
                if file_reference.path.end_with?(".tbd") or file_reference.path.end_with?(".dylib") or file_reference.path.end_with?(".framework") then
                  main_linkPhase.add_file_reference(file_reference, true)
                end
                
                # stub library is not allow in static library
                if file_reference.path.end_with?(".tbd") then
                    target_linkPhase.remove_file_reference(file_reference)
                end
            end
            
            # add OTHER_LDFLAGS
            target.build_configurations.each do |config|
                config.build_settings['MACH_O_TYPE'] = 'staticlib'
                # 保留静态库符号信息
                config.build_settings['STRIP_INSTALLED_PRODUCT'] = 'NO'
            end
    
            # copy frameworks resources into main bundle
            target_copy_resources_phase.files_references.each do |file_reference|
                main_copy_resources_phase.add_file_reference(file_reference, true)
            end
        end
    end
        
    main_project.save
  • all_load

打包成静态库后,第一个问题就是一些OC的Selector 没有找到,解决方法便是增加all_load 编译选项,参考文献

与动态framework 不同,静态库的Object最终都将打到main binary,因此,项目里各个framework和target资源的路径将会有所不同。为了规范资源使用,对各个模块的资源,我们都建立了单独的bundle来管理,并修正了项目里资源的使用方式

成功将项目改造成静态库打包后,在测试阶段,我们发现采集上来的崩溃日志信息上符号信息错乱的情况。(如下:堆栈函数地址偏移过大,显然是符号问题)

通过Hopper 工具,发现jike可执行文件里,大量符号丢失。于是立刻将静态库的编译配置改为

- Strip Debug Symbols During Copy: No
- Strip Style: Debugging Symbols
- Strip Linked Product: No

编译后,打包发现符号恢复了,但包大小增加了近10M。因此,一度怀疑静态库带来的10来M的优化是因为符号的丢失,仔细一想觉得不太合理,release 下符号信息应该存在于单独的dsym文件,由dsym 文件就可以重新符号化,因此不应该将符号打到最终的main binary。通过排查,发现静态库确实需要保留符号,因为静态库最终将被打到main binary,所以需要保留静态库的符号,这样生成main binary的时候其dsym的符号信息就能比较完整。

与此同时,设置main binary,因为符号最终无须存在于binary内。

Strip Debug Symbols During Copy: Yes
Strip Style: All Symbols
Strip Linked Product: Yes

这样我们就能获得完整符号信息的dsym和符号瘦身的main binanry。而原来我们的项目是由动态库构成,动态库和main binary一样,是独立的macho文件,丢弃符号的同时,也有其对应的dsym文件,因此也能正常符号化。

进展

  • 目前即刻将项目内的pods 依赖和模块target全部打包成静态库,并以及上线,体验地址
  • premain 消耗从800+ms 降低到 500+ms, iPhone 6s
  • 包大小48M 减少到35M
    • 当前没有找到静态库打包和动态库打包对app bundle 大小影响的直接资料
    • 但根据实践和我们推测,与动态库保持相对独立和通用不同,静态库打包后其代码都将拷贝成为main binary的一部分,因此编译器生成的main binary能获得更多的优化, 比如无用代码消除等。
    • 在iOS 平台,动态库是PIC(position independent code),相比较静态库no-PIC。

CocoaPods

  • 在我们着手做静态库打包的时候,cocoapods 也正进行相关支持,目前Cocoapods 1.4.0.2-beta 已经支持
  • Cocoapods 1.4.0.2-beta支持打包静态库,但需要在podspec 中添加属性static_framework = true,由于pod spec 一般由pod owner 提供,绝大部分pod目前都没有提供静态库打包的podspec。因此如果我们想依赖cocoapod完成打包工作,那么一个可行的方式就是自己提供项目依赖的pod对应的静态库 podspec

最好的方式是建立自己私有的repo,并将上传对应版本的podspec ,注意为每个 podspec 添加static_framework属性。对于podspec 里dependency 也需要为其创建静态库版本。通过这个方式,只要切换podfile指定的版本就能切换动态库打包和静态库打包

Note: 有些混编的库,目前Cocoapods 1.4.0.2-beta 尚未能很好支持
issue: https://github.com/CocoaPods/CocoaPods/issues/7213
e.g: Rxswift、RxCocoa等
  • 目前新的版本,对于Pod依赖 我们已经使用CocoaPods 来完成静态库打包, 主要考虑更小的维护成本,和能更灵活切换动/静 库打包, 带-static是即刻私有repo上相应pod 的静态库版本,例如:
pod 'AsyncSwift', '2.0.4-static'
    pod 'Alamofire',  '4.2.0-static'
    pod 'RxSwift', '4.0.0'
    pod 'RxCocoa', '4.0.0'
    pod 'AsyncDisplayKit', '2.0.2-static'
    pod 'SwiftyUserDefaults', '3.0.0-static'
    pod 'ObjectMapper', '3.1.0-static'

结语&广告

探索和跟进新技术是即刻iOS一直在前进的方向,欢迎?更多优秀的同学加入即刻,一起努力打造更酷的技术和应用。

招聘链接: 即刻船票

[Linker and Libraries Guide ]

[Reliable Software Technologies]

[Framework Programming Guide]

How are static libraries linked and how are dynamic libraries loaded?


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK