笔者由于在iOS开发过程中做过一些优化的工作,对iOS性能优化有一些粗浅的认识,一直想把自己这些经验,简单总结一下。于是最近在工作闲暇时间,准备针对iOS开发的性能优化写一系列文章。
作为整个系列的第一篇,我打算针对iOS的优化中的一些总体原则做一些总结。因为我觉得无论列表流畅度优化也好、启动时间优化也好还是说其他方面的优化,都有一些共性的原则,只有掌握了这些总体性的原则,才能够更好的做优化,给我们具体的优化任务指明方向,让我们少绕弯路。后面如果时间允许,我可能会写一些关于列表流畅度、启动时间和内存优化等方面的文章。
我对优化总体原则总结出包括不要提前过度优化、要找到性能瓶颈、要在不同性能指标间权衡、要理解优化任务的底层运行机制和要有技术保障体系五大原则,其中具体阐述每一个原则的时候并不局限于性能优化方面,会发散到其他的相关领域,会对一些延伸的领域做一些简单的探讨,希望能够对读者有一些启示。以下是主要内容。
一、不要提前过度优化
这个原则包括优化的过程中需要避免的两个陷阱,即提前优化和过度优化。
提前优化指的是在开发的起始阶段就把性能优化作为一个重要的任务来考虑,在没有实际数据指标的基础上,为了性能提前做的些盲目优化工作。
当然这个观点可能会引起争议,因为在某些开发领域,一些性能指标以历史的经验来说,的确有很大概率甚至必然会有性能瓶颈的问题,因此在架构初期就需要考虑性能的问题。 因此如果把“不要提前优化“这个观点推广所有开发领域上的话,我认为可能不一定合适。但是如果把此观点约束在iOS开发这一领域内,我个人认为还是成立的。因为在目前阶段iOS平台设备性能普遍较好,苹果无论是硬件层面还是系统层面对性能方面都做了大量的优化。所以我认为性能方面并不是iOS开发过程中需要首要考虑的因素。相比性能, 我个人认为在iOS开发的初始阶段,以下几个方面是更重要的,是需要首先考虑的。
首先需要考虑的是架构的选择,这里的架构指的是Native架构、web架构、Native和web混合架构和跨平台的架构。这里面我个人的意见是首先应该尽量避免使用web架构,从Facebook早期的失败经验可以看出,web和Native相比的确存在诸多性能、体验等方面的问题。连大厂都无法彻底改善webview的问题,何况我们。但是在一些和用户体验相比,对动态化需求更加迫切的应用场景下,是可以选择web架构的,比如大家都一直在吐槽某铁路售票软件。Native架构的优势是产品体验好,对大多数iOS开发者技术栈友好,缺点是由于苹果对热更新做了严格限制,导致一些动态化的方案无法使用。Native和web混合架构主要是在Native架构上,在一些运营需求十分强烈的场景下(如电商等场景),某些模块使用web开发,这样既可以在App大部分场景下使用Native架构,保证用户体验,又满足了部分场景动态化的需求。跨平台的架构主要是可以减少多端开发的成本,使用一套代码完成iOS和Android两个平台的开发,目前主流的框架有ReactNative、Weex和Xamarin等。这些跨平台架构的愿景都很美好,但实际使用过程中,个人觉得现阶段并不比使用Native架构节省人力,其中会遇到许多已知的未知的坑,当然作为新的技术我们应该持开发的心态,但在使用时候也需要全面的评估,尤其作为一个可能有很长生命周期的应用,在使用非官方推荐的开发架构也好、开源库也好,如果后期无人维护的话,自己团队是不是有实力去接盘,如果不能,使用苹果官方推荐的技术栈则更稳妥些。
其次需要考虑的是开发语言的选择,这个方面其实在选择了架构之后,也将可选的开发语言范围缩小到几个。而且实际项目开发中并不难选,因为团队开发人员的技术栈几乎决定了使用的开发语言,如果使用了大部分团队成员都不熟悉的语言,我相信即使所选语言在很多方面都有压倒性的优势,项目的推进也不会十分顺利。但是排除团队技术栈的因素,不同的开发语言的确是各有千秋。大家都知道iOS开发过程中,如果使用Native架构,官方的开发语言是objc和swift。objc是早期iOS开发的官方推荐语言,优点是其动态性,十分灵活,可以实现许多“黑魔法”,缺点语法略怪异(当然对于iOS开发者,使用久了也不觉得怪了),另外是对一些高级的语言特性支持的不是很好(记得最初使用objc开发iOS应用的时候,因为一些特殊的需求。由于objc不支持namespace,给团队造成了很大的困扰)。swift是苹果近年来主推的开发语言, 其吸收了许多其他语言的先进特性,也比较容易上手。关于两种开发语言的具体技术细节,大家有兴趣可以自己查看一些资料了解下。虽然苹果一直在力推swift,但是目前在国内iOS开发领域,由于一些用户基数大的主流App,均是在swift出现前使用objc编写的,而且大多经过了数年的版本迭代,加之早起swift ABI的不稳定和版本之间升级需要较多工作,还有swift和objc混编的一些问题,导致目前国内主流App大多仍使用objc作为开发语言。在一些创业公司,或者新的项目中,才有部分开发者使用swift,当然如果目光放长远的话,未来一定是swift的天下,这两年objc在每年的语言排名中逐年下降也侧面印证了这一点。 除了官方推荐的objc和是swift之外,如果使用跨平台等其他架构,还可以使用如js、c#等语言,有兴趣的可以自行了解下。
再次需要考虑的是开发过程中具体的代码架构的选择,这里只简单谈谈Native架构下的代码架构选择。 目前iOS开发中常用的架构有MVC 、MVVM、VIPER、MVP等。关于这些架构,网上目前有很多的介绍,大家如果对具体细节有兴趣可以自行查阅。这里我只想补充一点,大家在学习和实践时,不要盲目跟风新技术,比如MVVM等架构未必比MVC好很多,MVC也未必是一个过时的框架。要知道很多新架构带来的扩展性和解耦行都是通过引入间接层来实现的,随之而来的可能是更多的胶水代码和更复杂的代码结构。希望大家在选择的时候能够根据项目的特点和团队自身的状况,选择最适合自己团队和项目的代码架构。
除了上面说的三点,还有一些其他的关键点需要大家在项目初期考虑,比如如何在团队内部达成统一的代码风格?一些关键的技术如何选型?如何保证代码结构清晰、简单、扩展性好等等。性能问题可以在项目后期开始考虑,如果真的发现明显的性能问题再优化也来得及。比如项目一开始凭直觉感觉某一个模块可能会有性能问题,就盲目使用多线程,而不是根据实际情况具体问题具体分析。会导致程序复杂且容易出现线程安全问题。
过度优化是指为了优化性能,过度增加系统复杂度和维护成本,使得开发周期变长。虽然可能性能上带来了一定的提升,但是和过度优化而导致的这些缺点来比,这么做显而易见是得不偿失的。
笔者在工作过程中,发现许多同学在性能优化过程中,都容易陷入这种过度优化的陷阱。比如这一个简单的设置界面,一共只有十几个静态的cell, 如果去考虑圆角性能、离屏渲染、图片缓存、高度缓存、异步渲染甚至缓存布局信息,这些无疑是陷入了过度优化的陷阱,在这个应用背景下,简单快速的实现功能才是第一要务。在目前苹果的开发框架和平台上,一般如果出现性能问题,以我的实际经验来说,问题大部分是出在业务逻辑上面,所以遇到问题首先需要在业务逻辑上找问题,一些过度的极限的优化,完全是没有必要的。
其实不光是性能优化,我发现许多同学在日常开发中,处处都有过度设计的情况。比如设计模式中的design happy这一陷阱,许多初学者在刚开始学习设计模式的时候,十分痴迷设计模式在解决不同问题时,对代码的解耦性和可扩展性上的威力,在开发过程中会时时刻刻想着应该用什么设计模式。结果导致很多的过度设计,其实我们写代码过程中,如果能遵守基本的SOLID原则,大部分情况下就可以写出高质量的代码。
另外一个例子是组件化。近期iOS组件化是一个十分流行的话题,有许多团队提出了不同的组件化方案。实际项目中,团队在是否采用组件化方式开发的选择上,我希望要结合项目特点和团队组织架构形式具体问题具体分析,不要盲目跟风。在产品功能相对单一、开发人员较少、并行开发需求不强烈的情况下,推行组件化,不但增加系统复杂度,而且增加开发人员学习成本高,使得开发成本变大,我个人觉得这种规模的应用初期需要更多考虑的是如何快速上线、快速迭代和保证App质量。因此如果能够进行清晰的分层,严格遵守简单统一的架构模式即可。组件化比较适合从功能形态上可以清晰划分若干模块的产品,比如美团、58同城、淘宝和携程等产品,内部有多个业务模块,而且这些公司开发此类“航母”App的时候,会从组织架构把不同业务划分给不同的开发团队,为了能够保证不同团队之间能够独立并行开发和发版,最大程度上减少代码的依赖程度,这个时候应用组件化则是最佳实践。
上面是对不要提前过度优化原则的详细阐述,并引申到相关开发领域,做了一些不成熟的探讨。下面介绍性能优化总体原则的第二个。
二、要找到性能瓶颈
在做优化前,一定要首先找到性能瓶颈有哪些,依性能严重程度逐个解决。不要盲目优化,否则最后可能花了很大的力气,优化掉的可能只是性能损耗很小的一部分。这一原则我觉得尤为重要,因为我在工作中遇见过包括我在内,很多不进行性能瓶颈查找,全凭主观猜测进行性能优化的情况。 在寻找性能瓶颈过程中,也需要注意以下问题。
不要主观猜测,让性能评测数据说话。
这一点十分重要,要时刻记住要以事实说话,不要以为某个函数使用的算法的时间复杂度是O(n2)就觉得一定会有性能问题,非要费很大力气优化到O(n*logn), 殊不知你的输入数据可能只有几十或者几百个,即使O(n2)也不会有多大的性能问题。也不要以为某个方法仅仅调用了系统库的一个简单get方法,就不会有什么性能问题,殊不知这个get方法里可能包含一些十分耗时的操作(比如磁盘IO)。因此在遇到性能问题的时候,一定不要凭主观猜测,实地跑一下性能数据,让数据告诉我们性能瓶颈究竟在哪里。
要使用恰当的性能评测工具。
对于开发版本的性能优化,Xcode提供的instruments绝对是最好的寻找性能瓶颈的工具,没有之一。instruments有丰富的性能评测工具,包括常用的Core Animation、Time Profiler、Leaks和Allocations等等。这些工具在分析函数执行时间、fps和内存等方面给我提供了十分便捷的功能。在使用instruments过程中需要注意:
要使用真机,而不是模拟器。模拟器的CPU比iOS机器要快很多,所以在模拟器上,CPU相关的操作会更快。因为Mac的GPU和iOS设备上的GPU不同,所以模拟器需要在CPU上通过软件去模拟iOS设备上的GPU,所以GPU相关的操作会更慢。因此如果使用模拟器去进行性能优化的话,评测设备和真实用户设备性能表现的不一致,会导致优化的效果大打折扣。这里面内存是一个例外,在做内存优化的时候,使用模拟器和真机一般差别不大,可以使用模拟器进行内存的优化。
要使用Release配置而不是Debug配置,因为在release包的时候,编译器会做一些优化以提高性能。自己的工程代码可能也会在release下做一些优化,比如去除log信息和一些debug功能等。我们关心的是release下的性能,因为用户最终也是使用的release的安装包。所以测试的时候要一定要记住在release配置下进行,instruments进行性能评测的时候,默认是在release下进行的。但是工程代码里面的优化则需要自己注意。笔者就曾经为此付出过很大代价,因为没有注意工程代码里面的一些debug功能,导致优化过程中错误的认为动态库是影响启动时间的罪魁祸首,花了很大力气把动态库修改为静态库,白白浪费了很多时间。
要使用性能相对差的机器进行评测,因为我们需要保证的是我们的应用在性能差的机器上也有良好的表现。如果有条件,最好能够覆盖多个机型,和我们传统上的认识不同,机型越新性能不一定越高,例如iPad3在动画和渲染性能上比iPad2差。
要覆盖不同系统版本,因为在iOS系统上,一般系统版本越高,同一机器性能大体上趋于差,所以如果只覆盖低版本的机型,可能在高版本上表现的性能会不尽如人意。
除了使用instruments,还可以使用log等方式进行查找性能瓶颈。
对于线上的App,查找性能瓶颈的工具主要是Application Performance Management(APM)。目前各大公司基本都有自己的APM,主要是对线上App进行性能监控以及预警。因为在开发和测试阶段,由于使用人数相对比较有限,很难覆盖所有的业务场景。而在App发布出去后,大量用户在使用过程中的性能表现,会给我们的App带来更全面的性能评测数据,因此线上App的性能监控是十分重要的。
要抓重点,有的放矢。找到性能损耗大的前N个问题,依重要程度和解决的难易程度解决,这样才能花最少的精力,解决最大问题。
三、要在不同性能指标间权衡,达到总体最优
在已经找到性能瓶颈的时候,解决性能问题的方法则需要具体问题具体分析,要在不同性能指标间权衡,以达到总体最优。
这需要我们要有整体的意识,App的性能可以分为很多类,不同的性能指标对用户体验造成的影响也不尽相同,比如fps主要影响的是用户的滑动体验,页面加载时间和应用启动时间影响的是用户等待时间上的体验。我们在优化的过程中,要牢记我们的目标是希望App的整体体验最优,而不是某一单项的性能指标最优。不同优化指标之间可能是呈正相关,比如优化了滑动过程中大量函数的耗时时间,使得fps性能提升,可能会导致App的耗电量变少。同时,不同优化指标也可能是负相关、相互制约的,比如为了流畅性做了过多的cache,会导致内存性能下降,甚至导致因为memory warning导致被系统kill掉,这无疑对App的整体体验造成了负面的影响。因此实际优化过程中需要我们反复权衡利弊和取舍,达到整体的性能最优
四、要理解优化任务的底层运行机制
如果不理解优化任务的底层运行机制,可能很难达到更好的优化效果。
比如在做启动时间优化的时候,如果你不知道iOS中App的启动时间是由main之前和main之后两部分时间组成的,此时如果你的App是因为main函数之前的部分占用了过多的启动时间,可能你花了大量的精力去优化main之后的时间却没有达到好的优化效果。如果你不知道App启动过程的运行机制,就无法知道去检查是否链接了过多的自定义的动态库或者去load函数里面确认是否有耗时的操作等等。还有在做fps优化的时候,如果不了解卡顿的底层原因是什么、一个view从创建到显示过程中经历那些步骤、CPU和GPU在这个过程中都扮演什么角色,则很难做到丝滑般的顺畅?还有在做内存优化的时候,如果不了解内存分为哪几类、系统对App和不同类型extension的内存限制机制的不同、超过限制系统会采取什么操作等等,也很难把内存优化做好。因此只有深入了解底层机制才能更好的有针对性的提出更优的解决方案。
其实不只是在性能优化方面,在开发过程中很多情况下,了解底层的原理会让你变得更高效,更容易解决遇到的各种问题。在这里分享一个我印象比较深的一次经历。记得有一次在开发某个功能的时候,需要用到level db数据库,在开发过程中做单元测试的时候发现,level db的本地存储文件在不断删除和写入的过程中,越变越大,甚至达到1G大小。当时第一印象以为是在使用上出了问题,所以上层业务逻辑上查找问题,结果查了很久都没有找到问题。但如果我在使用level db的时候去多了解一下其底层的实现原理,了解LSM(Log-Structured-Merge Tree)的原理,遇到这个问题的时候就不会认为这是一个bug,也不会浪费了大把的时间来做无用功。所以建议大家不要抱怨每天的工作过于简单枯燥,在开发过程中多去挖掘一些深层次的东西,不但让你的技术的深度不断加深,也会对你的编码效率有很大的提升。
五、要有技术保障体系
性能优化不能一劳永逸,我个人觉得更是一场持久战。不仅需要你能够在某个特定时期做专项优化的攻坚战,还要为打好持久战做出好的后勤补给,为了能使App长期保持好的性能,不仅仅需要开发人员有良好的开发技能,还需要有一些技术保障和体系。下面简单罗列我能想到的几个方面。
要有好的测试保障,这里的测试保障不仅仅指的是测试人员的手动测试,更需要的自动化测试。要建立针对不同性能指标的专项自动化测试,建立一套从定时运行测试到测试结果的输出等一套完整的自动化测试体系,能够为性能的保证提供坚实的数据支撑。
引入关键性能指标上线准入制度。在开发阶段,为了不将有性能问题的代码带到线上,可以将比如启动时间、FPS、安装包大小等指标作为关键指标,上线前进行自动化测试,如指标不达标,不允许上线。
对于线上App使用APM进行监控,发现线上的性能问题,有及时的预警机制,能够随时解决线上的性能问题。
开发过程中,代码中需要有对性能保障的设计。 比如可以设计可复用的高性能控件,这样其他开发人员在开发类似功能时,可以简单复用,不仅提升了性能,而且大大节省了开发的时间。还有比如为了防止App随着版本迭代导致启动时加载的服务越来约多,导致启动变慢,可以设计App启动器,把这些任务统一放到主界面加载完成后再执行,并且在组内开发人员中形成硬性的规范,但凡启动时期不必须的服务,要么不要执行,要么统一放到启动器的主界面加载完成的回调中执行。
定期做一些性能优化方面的技术分享,不仅仅可以提高组内同学的开发技能,还可以活跃组内的技术气氛。
以上我对iOS性能优化的总体原则做的总结,希望能够对大家有一点点启示。其中可能很多想法并不成熟,也希望大家能够多多批评互相探讨,共同进步。