个人总结的一些通用的优化思路:
合并: 将多个操作进行合并,例如draw call, network request
压缩: 例如纹理的压缩,网络请求响应里使用压缩格式
延迟: 延迟创建,按需创建。
对象池:反复使用,不要反复的创建和销毁。
如果只是简单的设置了shadowColor, shadowOffset, shadowOpacity等属性,那么Core Animation就得自己去计算阴影的形状。这会导致在渲染的时候,发生离屏渲染,降低性能。
可以利用Simulator或Instruments去测试Core Animation的性能。如:
Color Blended Layers
Color Copied Images
Color Misaligned Images
Color Off-screen Rendered
如果为UIImageView的layer设置了cornerRadius和masksToBounds两个属性,在iOS9以下的系统会导致离屏渲染问题。解决办法有:
- 使用中间为圆形透明的图片,盖在image view上,这样会将离屏渲染问题,转化为blended layers问题,但是性能要好一些。 不过此种办法适用的场合有限。
- layer.shouldRasterize = YES;使用光栅化,但是只能用于图片不变的场合。记得要同时设置rasterizationScale为当前屏幕的缩放系数。
- 利用Core Graphics绘制圆角图片,用到了UIBezierPath。
卡顿监控的实现一般有两种方案:
(1)主线程卡顿监控。通过子线程监测主线程的runLoop,判断两个状态区域之间的耗时是否达到一定阈值。具体原理和实现,这篇文章介绍得比较详细。
实现思路:NSRunLoop调用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间,还有kCFRunLoopAfterWaiting之后,也就是如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿. 要监控NSRunLoop的状态,需要添加观察者。
当检测到了卡顿,下一步需要做的就是记录卡顿的现场,即此时程序的堆栈调用,可以借助开源库 PLCrashReporter 来实现。
在xcode里运行APP时,可以通过运行script,调用xcrun dsymutil工具,产生符号信息文件。有了符号文件,再加上崩溃日志,就可以解析出完整的调用栈。
(2)FPS监控。要保持流畅的UI交互,APP刷新率应当努力保持在60FPS。监控实现原理比较简单,通过记录两次刷新时间间隔,就可以计算出当前的FPS。
微信读书团队在实际应用过程中,发现上面两种方案,抖动都比较大。因此提出了一套综合的判断方法,结合了主线程监控,FPS监控,以及CPU使用率等指标,作为判断卡顿的标准。
iOS实时卡顿监控
微信iOS卡顿监控系统
调研和整理
简单监测iOS卡顿的demo
如果想在线上产品中加入监控系统,有些问题是需要考虑的:
对客户手机的性能影响(运行速度,流量),流量影响,磁盘占用影响。
对服务器的压力
一般公司可以使用大厂的APM产品,大厂一般自己研发。
经实验,callStackSymbols方法,有以下局限:
- 只能返回当前线程的调用堆栈,如果想在其它线程获取主线程的调用堆栈,是不行的。即使使用
dispatch_async
函数切换到主线程也不行。 - 只能定位到函数级别,不能定位到行号,显示的是未符号化的结果。但是光有这点儿堆栈信息又无法去符号化。
Last Exception Backtrace:
0 CoreFoundation 0x189127100 __exceptionPreprocess + 132
1 libobjc.A.dylib 0x1959e01fc objc_exception_throw + 60
2 CoreFoundation 0x189127040 +[NSException raise:format:] + 128
3 MedicalRecordsFolder 0x100a8666c 0x10003c000 + 10790508
4 libsystem_platform.dylib 0x19614bb0c _sigtramp + 56
5 MedicalRecordsFolder 0x1006ef164 0x10003c000 + 7024996
6 MedicalRecordsFolder 0x1006e8580 0x10003c000 + 6997376
7 MedicalRecordsFolder 0x1006e8014 0x10003c000 + 6995988
8 MedicalRecordsFolder 0x1006e7c94 0x10003c000 + 6995092
9 MedicalRecordsFolder 0x1006f2460 0x10003c000 + 7038048
10 libdispatch.dylib 0x195fb8014 _dispatch_call_block_and_release + 24
11 libdispatch.dylib 0x195fb7fd4 _dispatch_client_callout + 16
12 libdispatch.dylib 0x195fbe4a8 _dispatch_queue_drain + 640
13 libdispatch.dylib 0x195fba4c0 _dispatch_queue_invoke + 68
14 libdispatch.dylib 0x195fbf0f4 _dispatch_root_queue_drain + 104
15 libdispatch.dylib 0x195fbf4fc _dispatch_worker_thread2 + 76
16 libsystem_pthread.dylib 0x19614d6bc _pthread_wqthread + 356
17 libsystem_pthread.dylib 0x19614d54c start_wqthread + 4
基地址:应用,以及引用到的每个库,都有自己的地址范围。这个在崩溃日志的Binary Images部分可以看到一个列表。
偏移量:+号后面的十进制数字,是被调用函数的地址相对于基地址的偏移。
栈地址:第3列的数字,就是被调用函数的地址。
如果没有ASLR,那么栈地址=基地址+偏移量。用栈地址,就能在符号文件中找到符号信息。但是实际上iOS是有ALSR的。
ALSR: Address space layout randomization,ASLR通过将系统可执行程序随机装载到内存里,从而防止缓冲区溢出攻击。
从Xcode9开始,诊断选项里有个叫"Main Thread checker"的,默认是打开的,在程序运行期间,如果检测到了主线程之外的线程中更新UI,那么会在控制台中打出警告。但问题是,很多开发者选择无视,需要依赖于开发者的自觉,才能避免之类的问题。
也可以自己去实现一套机制,原理是通过hook UIView的-setNeedsLayout, -setNeedsDisplay, -setNeedsDisplayInRect三个方法,确保它们都是在主线程中执行。如果不是,那么让程序发生崩溃,可以强制开发者去修改。
可以看看这两篇文章:
网易iOS App运行时Crash自动防护实践
XXShield实现防止iOS APP Crash和捕获异常状态下的崩溃信息
unrecognized selector: 可以利用运行时的消息转发机制,重写forwardingTargetForSelector方法,做以下几步的处理:
- 为桩类添加相应的方法,方法的实现是一个具有可变数量参数的C函数
- 该C函数只是简单的返回0,类似于返回nil
- 将消息直接转发到这个桩类对象上。
KVO:
容易出现崩溃的地方:忘记了移除观察者;没有添加就去移除;重复添加后导致添加/移除次数不匹配;
定时器:
由于定时器对target进行了强引用,容易造成循环引用,一是造成内存不释放,二是不释放的对象在定时器被触发时执行代码,很有可能导致崩溃。
使用weak proxy解决。
其实我们自己的safe cast宏也是可以防止一些崩溃的。
图像渲染工作原理
由CPU计算好显示内容,GPU渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 HSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。如下图:
屏幕渲染有以下两种方式:
On-Screen Rendering
当前屏幕渲染,指的是在当前用于显示的屏幕缓冲区中进行渲染操作。
Off-Screen Rendering
离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。由上面的一个结论视图和圆角的大小对帧率并没有什么卵影响,数量才是伤害的核心输出啊。可以知道离屏渲染耗时是发生在离屏这个动作上面,而不是渲染。为什么离屏这么耗时?原因主要有创建缓冲区和上下文切换。创建新的缓冲区代价都不算大,付出最大代价的是上下文切换。
哪些API会引起离屏渲染:
- iOS9之下的layer.cornerRadius + layer.masksToBounds
- layer.mask
- layer.shadowOffset
- layer.shouldRasterize = YES
那么为什么上面会引起离屏渲染呢?
当图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制,这种情况就需要屏幕外渲染。屏幕外渲染并不意味着软件绘制。
不知道业界有没有这样的系统级方案存在,但我觉得可以尝试这样向面试官回答:
消除内存泄漏
消除后台线程更新UI
使用安全转换宏, 特别要注意将不确定类型的对象放到容器中要做防空处理.
判断一个对象的类是不是期望的类型
使用断言,在开发期间尽量多的暴露出问题
通过Xcode的静态分析工具,查找出代码中的隐患
通过XCode的诊断工具,检测出不容易暴露的问题
最佳实践:在析构或其它合适的时机,将delegate或datasource置为nil,会比较安全. 在iOS早期的framework中有一些类是在ARC之前写的, 要特别注意它的delegate不是用weak修饰的.比如,UIWebView的delgate,SKRequest的delegate等等
在UITableView的cellForRowAtIndexPath方法最后,判断cell是否为空,如为空则创建一个空的cell并记录日志;同理适用于UICollectionView.
在最后,可以使用防崩溃大招: unrecognized selector, KVO
在事后,通过崩溃收集系统,收集崩溃日志,修复崩溃。
官方文档在这里:Energy Efficiency Guide for iOS Apps
一些要点:
让CPU不间歇的做一些零碎的工作,不如集中的做完,这样CPU可以得到休息的机会。这里涉及到dynamic cost和fixed cost的概念,集中的做完的情况下,因为持续时间短,fixed cost会比较低。
- 在需要定位时调用一次CLLocationManager类的requestLocation方法,这个方法在获取到定位信息后就会关闭定位服务。
- 不使用时要及时的关闭定位服务。
- 使用尽可能低的定位精度,只要能满足需要即可。
- 设置location manager的pausesLocationUpdatesAutomatically和activityType两个属性,可以让location manager做适当的优化。
- 在后台运行时,允许延期的位置更新。
- 将定位更新限制在特定的区域或位置。
- 以上都不适合时,考虑注册Significant-Change Location Updates.
-
停止设备方向变化的通知
如果当前APP或是界面只会停留在一个方向,可以临时关闭硬件。// Turn on the accelerometer [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; // Turn off the accelerometer [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications];
-
通过设置更新间隔,降低更新的次数
使用时要注意优化。
imageNamed这个方法,会配合缓存使用。当要加载一个图片时,会去缓存中查找,如果有,则返回;否则去bundle或asset里面去加载图片,并存缓存。
imageWithContentOfFile这个方法,则只是会去加载图片,并不会存缓存。
结论:
对于可能反复使用的图片,使用imageNamed方法可以提高性能,不用反复加载。
对于很确定只使用一次,且比较大的图片,强烈建议用imageWithContentOfFile这个方法,这样不必将加载出来的图片加入缓存,降低内存的使用。
过度的创建NSDateFormatter用于NSDate与NSString之间转换,会导致App卡顿,打开Profile工具查一下性能,你会发现这种操作占CPU比例是非常高的。据官方说法,创建NSDateFormatter代价是比较高的,如果你使用的非常频繁,那么建议你缓存起来,缓存NSDateFormatter一定能提高效率。
该工具提供了内存泄漏检测更好的解决方案。只需要引入MLeaksFinder,就可以自动在App运行过程检测到内存泄漏的对象并立即提醒,无需打开额外的工具,也无需为了检测内存泄漏而一个个场景去重复地操作。它能自动检测UIViewController和UIView对象的内存泄漏,而且也可以扩展以检测其它类型的对象。
检测只在debug构建时才开启,完全不影响release包。
具备以下优点:
- 使用简单,不侵入业务逻辑代码,不用打开 Instrument
- 不需要额外的操作,你只需开发你的业务逻辑,在你运行调试时就能帮你检测
- 内存泄露发现及时,更改完代码后一运行即能发现(这点很重要,你马上就能意识到哪里写错了)
- 精准,能准确地告诉你哪个对象没被释放
那这个工具的原理是什么呢?
当一个VC被pop或dismiss后,该VC包括它的view,view的subviews等等将很快被释放(除非你把它设计成单例,或者持有它的强引用,但一般很少这样做)。于是,我们只需要在一个VC被pop或dismiss一小段时间后,看看该VC,它的view,view的subviews等等是否还存在。
具体的方法是,为基类NSObject添加一个-willDealloc
方法,该方法的作用是,先用一个弱指针指向self,并在一小段时间(3秒)后,通过这个弱指针调用一个方法。如果对象被释放了,就调用不到这个方法;否则的话,就说明发生了泄漏。
当一个VC被pop或dismiss时,遍历该VC上的所有view,依次调-willDealloc
,若3秒后没被释放,就会调到指定的方法。
查找到泄漏的对象以后,利用FBRetainCycleDetector检测该对象有没有被循环引用。
需要解决的问题:
- 不入侵开发代码
hook掉 UIViewController 和 UINavigationController的pop跟dismiss方法 - 遍历相关对象
- 构建堆栈信息
- 例外机制:对于有些VC,在被pop或dismiss后,不会被释放,因此需要提供机制让开发者指定哪个对象不会被释放。这里可以通过重载上面的
-willDealloc
方法,直接return NO即可。 - 特殊情况:比如系统手势返回时,在滑到一半时hold住,虽然已被pop,但这时还不会被释放。
- 系统View:某些系统的私有View,不会被释放,因此需要建立白名单。
- 手动扩展:从VC和View出发,去检测其它类型的对象的内存泄漏。
常规的写文件操作:
调用用户态的写接口->触发内核态的sys_write
->文件系统将数据写回磁盘。
这种方式存在两个非常明显的问题:
- 文件系统处于效率不会立刻将数据写回到磁盘(比如磁道寻址由于机械操作的原因相对非常耗时),而是以Block块的形式缓存在队列中,经过排序、合并到达一定比例之后再写回磁盘。
- 这种方式在将数据写回到磁盘时,需要经历两次拷贝。一次是把数据从用户态拷贝到内核态,需要经历上下文切换;还有一次是内核空间到硬盘上真正的数据拷贝。当切换次数过于频繁,整体性能也会下降。
基于上述问题,xlog采用了mmap的方案进行日志系统的设计:
mmap 是使用逻辑内存对磁盘文件进行映射,中间只是进行映射没有任何拷贝操作,避免了写文件的数据拷贝。操作内存就相当于在操作文件,避免了内核空间和用户空间的频繁切换。
除了性能外,它还能保证日志的完整性,因为如下这些情况下会自动回写磁盘:
- 内存不足
- 进程crash
- 调用msync或者munmap
- 不设置MAP_NOSYNC情况下30-60s(仅限FreeBSD)