作者:段继统 & 夏磊

调试断点是与开发体验关系最为亲密点之一,优酷iOS团队在内部调研时候发现,大量国内的iOS APP研发团队也遇到了相似的问题。思考到国内Swift热火朝天的现状,咱们尽快整顿了该计划并通过本文分享进去,心愿能在这个问题上帮忙到大家。

前言

家喻户晓,Swift是苹果公司于2014年苹果开发者年会(WWDC2014)上公布的编译式新开发语言,反对多编程范式,能够用来撰写基于macOS、iOS、iPadOS、watchOS和tvOS上的APP。对于宽广iOS开发同学来说,这也是研发将来iOS APP开发必须要把握的语言技能。Swift语言在公布后的数年里失去了飞速发展,在2019年苹果公布了Swift5.0版本并宣告Swift ABI稳固。

在Swift5.0版本的ABI稳固后,Swift正式具备了欠缺的生产研发根底,优酷iOS研发团队也开始进行优酷iOS、iPadOS版本的Swift迁徙。优酷在被阿里巴巴收买后,取得了大量团体挪动基建和中间件的反对,因而优酷iOS App在继续演变数年后,根本成为规范的大型组件化工程,由数十个垂直团队负责各自业务并行开发。其中,优酷播放详情页场景是最重要的视频内容生产场景,也率先在2020年初开始业务页面框架、播放器框架及业务模块的Swift迁徙。

2020年底,优酷iOS生产团队实现了业务页面框架和播放器框架的Swift化,这两个框架代码量较少,外部代码后果正当清晰,而且对外部依赖较少。因而在齐全Swift化后,性能上失去了晋升,并且得益于Swift的优良语法,团队开发业务需要代码行数降落,团队效力也取得了增幅。整个过程都比拟顺畅,也并未遇到显著的工程开发或者品质问题。

进入2021年后,在业务页面框架及播放器框架Swift版本的根底上,优酷iOS团队全面启动了业务层代码Swift迁徙,而在这个阶段,Swift调试断点慢的问题开始呈现并日趋严重。 在视频内容场景,外围主业务模块代码7万多行,内部依赖各种模块达200以上,在这个业务模块里,首次断点的工夫顽劣状况下能够达到180秒以上,团队研发效率被重大制约。

2022年初优酷iOS团队实现了80%以上业务代码的Swift迁徙,调试首次断点慢的问题曾经成为业务场的效率瓶颈。在外部的研发幸福感问卷调查里,97%的iOS开发同学认为调试首次断点慢是目前研发过程的最大痛点,这个问题给iOS研发同学带来的挫败感,足以打消Swift的其余劣势。因而,解决这个问题也成为优酷iOS团队年度首要指标。

调试首次断点慢景象及初步剖析

Swift调试断点慢次要景象是,当Xcode工程运行起来之后,咱们进行首次断点的等待时间会特地漫长。大部分状况下,工程首次断点失效后,第二次及后续断点的等待时间都非常短暂,根本能够认为无等待时间。不过从团队外部收集的状况来看,不同Mac电脑开发设施和不同的iOS设施体现不全统一,局部同学首次断点之后进行断点的等待时间也极其迟缓。

这个景象或者说问题在团队外部频繁呈现后,咱们首先向苹果中国开发者关系团队反馈,并附上了具体的工程文档等信息。苹果方面也基于反馈在外部进行了考察和验证,并最终给咱们回答,示意外部并没有相似问题的发现。在交换过程中咱们发现,苹果外部的大型APP工程模式都是传统的单工程模式,与国内的组件化多个工程模式截然不同。基于各方面汇总信息,咱们对这个问题开始进行初步剖析和解决。

从下表中能够剖析,播放器框架模块和播放主业务模块状况联合断点工夫来看,断点工夫仿佛与内部依赖数量出现等比关系,所以能够初步判定断点工夫和内部依赖数量存在较强的相关性。

另外还有一个景象,如果子工程和壳工程所依赖SDK的module没有对齐,lldb会很快断点失效,然而打印报错信息,同时无奈po任何值。通过此景象也能够初步剖析进去,在断点时lldb对子工程依赖的module进行了扫描。

但仅仅依赖表象剖析还不够,所以后续的工作咱们从两个方向着手,第一是从播放主业务模块的解耦测试,疾速解耦播放主业务模块的内部依赖,测试耦合数量的缩小对断点工夫是否能有帮忙;第二是从lldb本身断点原理的剖析,看首次断点如此长的工夫中lldb到底在做什么动作。

通过业务模块解耦动手

咱们通过删除及整顿工程依赖援用代码的形式,疾速清理内部模块依赖,最终将播放主业务模块的内部依赖降到90个左右。整顿结束后,播放主业务首次调试断点工夫也从200秒左右降到120秒左右,对团队开发艰难现状有所缓解。然而通过理论验证和利用后,咱们也发现这种依赖业务层解耦的形式是对于团队来说不可行的,根本原因有二:

1、革新老本高

播放主业务模块从200多个模块依赖降到了90多个,一方面来说说对于避免工程腐化起到了踊跃帮忙,另一方面在业务需要的压力下,研发人员须要投入了微小的精力来进行代码重构和解耦。长期来看,不同垂直业务团队面临的状况不同,将来的业务技术需要复杂度也不尽相同,这个计划是无奈做到疾速复用。从人力老本来说,这个计划只能短期进行工程治理,无奈长期坚持下去。

2、理论收益低

从取得的收益来看,播放主业务模块内部依赖升高到90多个后,咱们原来的预期是调试首次断点工夫能升高50%甚至更低,然而后果来看,在内部依赖曾经无奈解除的状况下,首次断点等待时间仍然长达120秒以上,这样的收益后果是咱们无奈承受的。因而也得进去论断,在优酷iOS这样大型组件化多工程的模式下,咱们用业务模块解耦的形式是无奈根治该问题的。

通过LLDB剖析动手

通过工程治理后,咱们感觉还是应该从侧面攻克该问题,从LLDB剖析来查看根本原因并且解决。如果要剖析LLDB动手,对于工程师来说最好的方法还是查看Swift源码,跑起来看一看外部的原型机制。咱们首先依据苹果的文档将源码下载下来,而后进行配置,具体文档能够参考 How to Set Up an Edit-Build-Test-Debug Loop,一步一步的跟着做就能够。

因为Swift是依赖于LLVM,并且在其根底上做了本人的定制化开发,所以切换分支不能只切换Swift源码的,须要将LLVM一起切到对应的分支上, 保障代码同步。正好Swift提供了相应的工具来帮忙咱们切换对应分支,只须要运行Swift文件下的utils/update-checkout相干命令即可。优酷iOS团队目前应用的是Swift5.4版本,对应Xcode版本为13.2.1。

1、应用LLVM自带耗时工具

想要看到底在断点命中后,到底哪块最耗时,就须要应用工具来计算耗时,而这块LLVM有自带的工具类TimeProfiler,外面封装了计时办法,并且输入相干json文件,而后能够用chrome自带的tracing工具解析后事实相干图表

//TimeProfiler.h void timeTraceProfilerBegin(StringRef Name, StringRef Detail); void timeTraceProfilerBegin(StringRef Name,                             llvm::function_ref<std::string()> Detail); void timeTraceProfilerEnd();

2、耗时最多的两个中央

通过TimeProfiler对要害函数进行耗时埋点,发现有两个函数耗时较多,如下代码:

// SwiftASTContext.cppbool SwiftASTContext::GetCompileUnitImportsImpl(    SymbolContext &sc, lldb::StackFrameWP &stack_frame_wp,    llvm::SmallVectorImpl<swift::AttributedImport<swift::ImportedModule>>        *modules,    Status &error)
// SymbolFileDWARF.cppvoid SymbolFileDWARF::FindTypes(    ConstString name, const CompilerDeclContext &parent_decl_ctx,    uint32_t max_matches,    llvm::DenseSet<lldb_private::SymbolFile *> &searched_symbol_files,    TypeMap &types)

一个是SwiftASTContext类的GetCompileUnitImportsImpl办法,这个办法次要是解析以后编译单元与Module相干的操作,另一个则是在某一个变量如果是Any类型,则须要对其进行解析,找到其类型相干的操作,而最终这两个函数的操作都与以后工程的二进制依赖剖析有关系,所以,如果能缩小在断点命中后对依赖的剖析,那么断点工夫就会越快。

有效的解决方案

依据上面对源码的剖析,咱们最开始的思考是否可能通过编译器的一些选项,跳过对一些module的扫描,从而晋升首次断点速度,以比拟小的老本来尽快解决。

有效计划1 - 对编译选项的批改

通过对编译日志的剖析,在构建的时候发现一个参数-serialize-debugging-options,从名字判断是用于debug调试的时候序列化生成调试关联产物,接着咱们再通过swiftc -frontend --help命令发现了以下这个选项:

针对这个参数,咱们进行了尝试,在Xcode构建设置里的Other Swift Flags里加上这个参数,然而从后果发现也没失效。于是咱们再次查内外部材料,并且在官网Swift论坛发帖进行征询,这其中有个外国的iOS开发者回复示意须要增加自定义flag SWIFT_SERIALIZE_DEBUGGING_OPTIONS=NO。随后咱们立即在Xcode工程里加上该选项后并进行验证,从理论后果来说,首次断点速度取得了显著的晋升,但也同时发现了重大的缺点。当团队同学想要po打印相干变量的时候,却什么都打不进去,lldd间接无奈解析,从理论开发角度来说该计划不行。

有效计划2 - 对依赖库的批改

在咱们本人构建的lldb去调试工程的时候,因为编译的lldb是debug包,当命中断点后,lldb会打印一些debug的log信息。这其中有一堆log十分引人注目,会继续地打好几十秒,因而咱们立即对这部份log俩进行剖析,上面是局部截取的log:

warning: (arm64) /Users/ray/workspace/YouKuUniversal/Pods/SOME/SOME.framework/SOME(SOME9999999.o) 0x00004c50: unable to locate module needed for external types: /Users/remoteserver/build/14695183/workspace/iphone-out/ModuleCache.noindex/2YQ3UYLF0BE3R/UIKit-1XGSPECLTDLOB.pcmerror: '/Users/remoteserver/build/14695183/workspace/iphone-out/ModuleCache.noindex/2YQ3UYLF0BE3R/UIKit-1XGSPECLTDLOB.pcm' does not existDebugging will be degraded due to missing types. Rebuilding the project will regenerate the needed module files.

这块log是其中某一个依赖库的报错,大略问题是说在找这个库的modulecache的时候无奈找到其门路。因为优酷iOS的二进制依赖库都是通过阿里近程编译集群生成,因而在生成这个库的debug调试信息的时候,其门路指向的是近程机器的门路。因而,在咱们本地机器下来搜寻这个近程服务器的地址必定是找不到的,而后报错。

通过这个景象,咱们猜想是否是因为无奈找到正确的modulecache,导致咱们以后工程的整个工程Swift依赖库的cache都无奈正确的构建起来,所以每次断点都得从新搜寻依赖库,而后构建cache。

那么,这个门路是哪儿带进来的呢?通过钻研发现,这个门路是卸载Mach-O文件DWARF的debug信息里的:

那外围就在于怎么解决这个信息,想要批改相对来说有点麻烦,还得弄个Mach-O批改工具,那最快的形式就是去掉这个section。编译设置外面恰好有这个选项能够间接去掉,叫做Generate Debug Symbol

因为报错这个log波及到几百个库,即便改这个选项有用,那改一个必定是看不出成果的,所以咱们间接批改了一百来个库,将这些库在release编译环境下把这个选项都改为NO,试试是否有成果。

后果令人悲观,通过咱们的测试,即便改了这么多库的状况,对首次断点速度也毫无晋升,问题仍旧存在。

既然这两种路都走不通,那lldb本身有相干设置吗?如果有的话那是否lldb的设置能够失效呢?

无效的解决方案 - LLDB配置优化

从上述咱们对lldb的剖析上曾经能够晓得,调试首次断点开始,从执行到断点正式失效蕴含的工夫次要蕴含两局部,其中大部分是模块依赖的module化解析构建,另一部分是本身Any类型的解析。既然业务解耦的工程化以及对编译选项的配置批改明确不可行,那咱们就思考从lldb本身着手,通过setting list命令找到所有与Swift调试无关的设置项,在这其中发现最要害的有两个:

memory-module-load-level

在调试时从内存加载module信息的级别,默认为complete,另外还有partial和minimal两种,其中minimal最快。

memory-module-load-level            -- Loading modules from memory can be                                         slow as reading the symbol tables and                                         other data can take a long time                                         depending on your connection to the                                         debug target. This setting helps users                                         control how much information gets                                         loaded when loading modules from                                         memory.'complete' is the default value                                         for this setting which will load all                                         sections and symbols by reading them                                         from memory (slowest, most accurate).                                         'partial' will load sections and                                         attempt to find function bounds                                         without downloading the symbol table                                         (faster, still accurate, missing                                         symbol names). 'minimal' is the                                         fastest setting and will load section                                         data with no symbols, but should                                         rarely be used as stack frames in                                         these memory regions will be                                         inaccurate and not provide any context                                         (fastest).

use-swift-clangimporter

Swift调试时是否从新构建所依赖的module,默认值为true。

use-swift-clangimporter      -- Reconstruct Clang module dependencies from                                 headers when debugging Swift code

所以咱们从以上两个配置项着手,在命中任意断点时执行以下两个命令:

settings set target.memory-module-load-level minimalsettings set symbols.use-swift-clangimporter false

执行后发现断点速度显著晋升,首次断点从180秒缩短到40秒,两条命令独自测试,memory-module-load-level设置优化约6秒左右,其余工夫优化来源于use-swift-clangimporter设置。在论证这个形式后,咱们在此配置根底上,征集优酷及团体外部iOS同学试用。验证不同的开发环境后,咱们惊喜地发现,首次断点工夫均有大幅度晋升,根本达到可用水平。

阿里巴巴团体外部验证后果如图:

配置优化后存在的问题及解决

当然,在在进行上述优化设置后,咱们也发现了问题,会呈现局部OC属性无奈po的状况,例如Swift继承OC基类的状况:

//oc@interface OPVideo : NSObject@property (nonatomic, strong) NSString *sid;@end//swift@objc public class DetailVideoSwift: OPVideo {    @objc public var desc: String?}

此时“po video.sid”无奈输入,然而“po video.desc”失常,这样就导致调试时有很大的局限性。通过查阅lldb文档发现,lldb能够把指定代码绑定到自定义命令,所以咱们能够应用这个机制解决局部属性无奈po的问题。

首先新建Swift代码库,内部同学参考时能够放入到本身工程的相干根底库中,在库里实现办法:

public func aliprint(_ target:Any?,selector:String?){    if let target = target as AnyObject?{        if let selector = selector {            let returnValue = target.perform(NSSelectorFromString(selector))            print("(String(describing: returnValue?.takeUnretainedValue()))")        }else{            print("(String(describing: target))")        }    }}

打包后将蕴含该代码的模块SDK退出主工程依赖,再通过命令

command regex px 's/(.+) (.+)/expr -l Swift -O -- import AliOneUtils; aliprint(%1,selector:%2);/'

将px命令绑定到aliprint办法,留神此处px为自定义命令,这样就解决了局部属性无奈po 的问题,经测试齐全可用:

总结

优酷iOS团队在作为阿里外部Swift迁徙的先驱,在Swift迁徙过程中遇到了不少问题,也总结了大量的教训。调试断点是与开发体验关系最为亲密点之一,咱们在内部调研时候发现,大量国内的iOS APP研发团队也遇到了相似的问题。

思考到国内Swift热火朝天的现状,咱们尽快整顿了该计划并分享内部,心愿能在这个问题上帮忙到大家。同时,如果有iOS团队和大神有更加优良的解决方案,也心愿可能分享进去,独特帮忙国内iOS Swift开发生态的蓬勃发展。

目前,优酷iOS团队在此方向上做的投入和钻研只是一个开始,后续在性能体验、编译速度、包大小优化等方向上也将积极探索,心愿通过开发效力和技术的变革,为用户带来更好的优质服务体验。

关注【阿里巴巴挪动技术】,阿里前沿挪动干货&实际给你思考!