乡亲们, AFN 使用姿势不当引起的 Retain Cycle @ 青浦区 iOS 中学
知道存在,内存漏的飞快,用户日活成百上千的情况。
memory footprint 不佳,似乎也没什么。
程序一般也不会变慢。
内存问题,程序崩的,遇见的也挺少。
写在最后
老王猜想,对于一般的团队,
系统内存当然是,想怎么用怎么用,不用花钱的。
技术小组长,技术负责人,产品负责人,谁管这个呀,
产品负责人关心的是产品体验 (崩溃率 bugly , 还有性能( 卡顿,界面卡不卡 , 接入听云, 啥啥啥))、 产品运营 (PV / UV)
老王猜想,必须写在最后,否则就没人看了
修改源码,讲个故事,YYModel 做梦都要哔哔
先读一下语义
YYModel 非常优秀,本文没发现有改进的地方,
改动一下,贴近所做的项目。
怎么说,网上博客那么多
讲个故事
很敬仰上海的一位叫王大宇的 iOS 工程师,
做梦开始
醒来啦
僵尸对象
等内存错误
本文小结下,内存管理的语义:
需要该对象的时候,他就得在。不需要他的时候,他最好被释放了。
合理的利用资源。
需要该对象的时候,他不在,释放早了。
给他发消息,程序会崩,EXC_BAD_INSTRUCTION
不需要该对象的时候,他还在。内存可能泄漏了。一般是存在循环引用 ( retain cycle )
iOS 的内存分析,工具挺多
可以使用 Xcode 的 Debug 工具,内存图( 点一下就好,断点旁边 )
这么用,
在重点测试的界面,多操作,然后退出。
重复几次。确认系统缓存已初始化。
退出重点测的界面后,开内存图,
如果内存释放的干净,就没什么 retain cycle 等内存泄漏。
内存图自带断点效果,会暂停 app 的运行
可以看到此刻存在的所有对象。
环节短的循环引用,明显可见,找起来很快。
通过内存图,左边列表中,可以看到当前的所有对象,以及它们的数量。
最关心的就是感叹号,代表异常, 就是内存泄漏, 可能 Retain Cycle
本文 Demo ,可见系统的代理 AppDelegate 实例, 相关 ViewController . 可看到图片视图有 24个。
中间大片的区域是对象的内存图,他们是怎么关联的。可以参考下
左边栏的右下方按钮,可以直接筛选出内存有错误的对象,方便找出内存泄漏的对象
可看出本文 Demo 内存泄漏严重。左边栏,点开几个带感叹号的,看情况。 右边栏,有一些具体信息
photo 照片模型对象,持有一个 location 位置的模型对象,
location 位置的模型对象,持有一个对象,
那对象,又持有 photo 照片模型对象。
三个对象,构成了一个引用的圈, retain cycle
发现问题了,解决就是改代码
很熟悉,直接改。
可以全局搜关键字,本文 demo 搜 .location
可以根据右边栏的信息找,
知道是哪个类,又有一个 closure 对象
可找到错误代码
photoModel.location?.reverseGeocodedLocation(completion: { (locationModel) in
self.photoLocationLabel.attributedText = photoModel.locationAttributedString(withFontSize: 14.0)
})
}
photoModel 有一个 location 的属性,location 持有一个匿名函数 closure.
这个 closure 又引用了 photoModel。
不知道这个 closure 有没有 retain 该 photoModel,
点进方法看,
这是一个逃逸闭包,赋给了 LocationModel 的 placeMarkCallback 属性,强引用
func reverseGeocodedLocation(completion: @escaping ((LocationModel) -> Void)) {
if placemark != nil {
completion(self)
}
else {
// 查看 completion
placeMarkCallback = completion
if !placeMarkFetchInProgress {
beginReverseGeocodingLocationFromCoordinates()
}
}
}
与 Xcode 内存图检查到的一致。
解决循环引用,就是加 weak
ARC , 自动引用记数, iOS 用来管理内存的。
循环引用,retain cycle, 是 ARC 搞不定的地方
一个对象的引用记数, 就是有多少个其他的对象,持有对他的引用。( 就是有多少个其他的对象,有指针指向他)
当这个对象的引用计数为 0, iOS 的 ARC 内存机制知道这个对象不必存在了,会找一个合适的时机释放。
循环引用,多个对象相互引用,形成了一个圈( 引用的链路 )。
循环引用,问题很严重,内存泄漏了
( 打个比方: 你找 iOS 系统借了钱,少还一大截。人家系统没说什么, 心里都记着 )
加 weak, ARC 就明白了,
( 因为 weak 是弱引用,不会增加该对象的引用记数。
直接写,隐含了一个 strong 的语义,默认 retain , 该对象的引用记数 + 1 )
链路就断了,内存回收成功。
Swift 的 closure 中,可以添加一个弱引用列表。
这个捕获列表可以让指定的属性弱引用。
closure 使用弱引用,就好
func reverseGeocode(locationForPhoto photoModel: PhotoModel) {
photoModel.location?.reverseGeocodedLocation(completion: { [ weak photoModel] (locationModel) in
self.photoLocationLabel.attributedText = photoModel?.locationAttributedString(withFontSize: 14.0)
})
}
Xcode 的调试计量工具很强大,调试内存的时候,可切换调试视图层级等
左边栏的右上方的按钮,可以切换调试的选项,
内存转 UI, 内存转线程
通过使用 Xcode 内存图,内存泄漏少了很多。
重复操作三五次,又发现一个内存泄漏
对象结点很多,看图挺复杂的
可以用 Instruments 的 Leaks
Leaks 自带两个模版 Allocation 和 Leaks,
Allocation 模版对 app 运行过程中分配的所有对象的内存,都追踪到了。
上方的时间线展示了,已经分配了多少兆的内存。
All Heap & Anonymous VM, 所有堆上的内存,和虚拟内存 ( WWDC 2018/416 , 讲的比较详细)
下方的标记按钮,可以做分代标记
Leaks 模版会检查 app 所有的内存,找出泄漏的对象 ( 释放不了的对象 )
Instruments 的内存检查机制是,默认每隔 10 秒钟,自发的取一个内存快照分析
反复操作,找到第一个 Leaks, 可以暂停下
下方的 Leaks 详情表中,头部的 Leaks 按钮,有三个选项,
默认选项就是第一个, Leaks,
展示了所有内存泄漏的对象。
下方的右边栏就是更多信息,展示了详情界面每一列对象的进一步的资料
Leaks 详情表中,每一列对象,有一个灰色的箭头按钮,
点进去,可以看引用计数的增减日志
一般先看看第二个 Cycles & Roots, 又是一张内存图
photoModel 是循环圈的根结点,与左边的对象结点列表一致
有用的是第三个选项 Call Tree , 调用树
与 Time Profiler 的 Call Tree 不一样,
Time Profiler 的 Call Tree 采集的是应用中所有的方法调用,
Leaks 的 Call Tree 采集的是分配内存与内存泄漏相关的方法调用。
Call Tree 的选项一般勾选 Hide System Libraries 和 Separate by Thread.
Hide System Libraries , 隐藏系统的方法。系统的方法改不了,是黑盒,参考意义有限。
Separate by Thread. 将方法堆栈,按线程分开。一般出问题多在主线程,优先看 main thread.
按住 Alt 键,点击方法名称左边的小三角,可以展开调用栈。
又看到了这个方法 func reverseGeocode(locationForPhoto photoModel: PhotoModel)
再检查下
func reverseGeocode(locationForPhoto photoModel: PhotoModel) {
photoModel.location?.reverseGeocodedLocation(completion: { [ weak photoModel] (locationModel) in
self.photoLocationLabel.attributedText = photoModel?.locationAttributedString(withFontSize: 14.0)
})
}
self 是一个 CatPhotoTableViewCell 实例,self 持有 photoModel 属性,
( 函数里面的 photoModel, 使用的是 func updateCell(with photo: PhotoModel?) {
方法中传入的 self 的 photoModel 属性)
photoModel 持有 location 属性, location 属性持有一个逃逸闭包,
该逃逸闭包持有 self.
之前用 weak 处理了三对象的循环引用,现在有一个四对象的循环引用。
四对象的循环引用中 photoModel 在之前的处理中,已经弱引用了。本来好像没什么问题的。
估计系统没及时释放的 weak 的 photoModel,又泄漏了。
本文中,采用 Xcode 内存图,难以复现。有时候有。
解决就是再加一个 weak.
检查项目中的循环引用,通常使用分代式分析 ( Generational Analysis )
先记录一个内存使用的基线 A ( 当前使用场景, 建议用重点测的场景前的那一个 ),
进入一个场景 ( Controller 重点测的场景), 打个标 ( 记录现在的内存使用情况 ) B ,
再退出该场景,再打一个标 C。
如果 A < B , A = C , 正常,内存回收的不错。
如果 A < B <= C , 异常,内存很可能泄漏了
换句话,套路很简单,设立内存基线,点击进入新界面,(操作一下,滚一滚)
然后弹出,内存往往会先升后降。
这种操作,需要重复几次。找出必然。确认系统缓存已初始化,在运行。
( 有点类似苹果的单元测试算函数执行时间,跑一遍,就是运行了好几次的函数,取的平均值。 )
这里有一个很经典的面试题:
app 发布前,一般会系统检查循环引用,内存泄漏,怎么处理呢?
( 换个说法, 怎么分析 app 堆的快照? )
相关代码: https://github.com/BoxDengJZ/Instruments_Wen
更多资料: 视频教程,practical-instruments
同质博客: Memory
扩展阅读:
命令行工具 vmmap - 查看虚拟内存 : WWDC 2018:iOS 内存深入研究
hr>
hr>
hr>
hr>
hr>
hr>
hr>
hr>
iOS 电量消耗改善:一招套路及相关姿势
解决电量问题的工作流:
先使用 Xcode Energy Gauge 分析出哪一块耗电(网络和 motion , 还是定位 ), 用 Time Profiler 定位问题与解决 ( Instruments ), 得到用户好的反馈。
三个原则:
Do it never/do it less (能不做,就不做。少做的,好)
比如: 网络请求,先压缩数据Do it at a better time (合适的时机处理 )
网络请求,使用缓存机制,设置内容验证( 需要的数据是否更新了 ),或者缓存的失效时间Do it efficiently ( 有效处理 )
合并网络请求。一次请求大量数据,比多次请求少量数据省电
套路很简单,将手机放在桌子上,Xcode 里面启动 app, 并检测,啥也不干。如果电量消耗较高,很不合适了
本文解决的两个问题,给 CoreMotion 更新设置过滤,干掉频繁的日志上传
WWDC 推荐使用 Xcode Debug 栏的 Energy Debug Gauge。( 调性能,都是用真机。机器老一点,效果更好 )
Energy Debug Gauge 形象、直观
可看出,当前手机的耗电情况,耗电低、高、很高。苹果的三个阶段,有些不太细致。(左上的 Utilization, Current Impact)
app 的平均能耗,一目了然 ( 右上的 Average ) 。
每一个时刻,耗电的是什么。 CPU 、网络、文件 I/O 、定位,哪些消耗了。( 中间的 Energy Impact )
Xcode Energy Gauge 可以快速定位问题,想要进一步的细致分析,下面有各种选项,跳转到对应的 Instrumens 模版。
比如:
分析 CPU 使用的 time profile, (能够知道代码的执行情况了,根据函数的调用消耗。找出权重大的,干掉不必要的。)
分析网络活动的 network profile, 分析定位活动的 location profile
本文 Demo
这里电量消耗很高,很稳定
主要是 CPU 和网络请求在耗电。
使用 Instruments 的 Time Profiler 分析,
可以先放大上面的 time line,再选择一个时间段,在调用树 call tree 中,进一步分析。
Time Profiler 的选项默认是按线程划分的,再选一个隐藏系统的。(系统的,可以参考一下,到底发生了什么。系统的改不了。可以改自己的源代码 )
在调用树的表格中,按权重展开 ( weight ),要干掉的就是权重大的,耗时间的。
接着展开主线程 ( main thread 。看上图,其他线程的耗时,相比主线程的,可忽略 ),
按住 Option 键,点击 main thread 左边的小三角,可以一下子展开很多。
可清晰看出,耗时严重的是 450 毫秒左右的那一行 thunk for ... CMDeviceMotion? ...
里面调用了一个耗时的方法,CatPhotoTableViewCell.panImage
, 上图, 454 毫秒中,占 419 毫秒。
点击进入详情,就看到代码了。
在 CatFeedViewController 的 viewDidLoad 方法中,有一个倾斜的设置
motionManager.startDeviceMotionUpdates(to: .main, withHandler:{ deviceMotion, error in
guard let deviceMotion = deviceMotion else { return }
let xRotationRate = CGFloat(deviceMotion.rotationRate.x)
let yRotationRate = CGFloat(deviceMotion.rotationRate.y)
let zRotationRate = CGFloat(deviceMotion.rotationRate.z)
// y > z, 这个动作是翘起来
// y > x + z, 这个动作是斜着翘起来
if abs(yRotationRate) > (abs(xRotationRate) + abs(zRotationRate)) {
for cell in self.tableView.visibleCells as! [CatPhotoTableViewCell] {
cell.panImage(with: yRotationRate)
}
}
})
现在的代码显示栏 ( 原来的 Call Tree 表格 ), 右上角有一个 Xcode 的小图标,点击返回 Xcode 调试代码。
手机没动,老是调用 cell.panImage(with: yRotationRate)
, 根本就没效果。
设置一下,调用 cell.panImage
的最小手机幅度比较好。幅度小,根本就,没效果。
添加一个属性记录,过滤掉手机小的抖动。
private var lastY = 0.0
override func viewDidLoad() {
super.viewDidLoad()
......
motionManager.startDeviceMotionUpdates(to: .main, withHandler:{ deviceMotion, error in
guard let deviceMotion = deviceMotion else { return }
// 添加了这两行
guard abs(self.lastY - deviceMotion.rotationRate.y) > 0.1 else { return }
self.lastY = deviceMotion.rotationRate.y
let xRotationRate = CGFloat(deviceMotion.rotationRate.x)
let yRotationRate = CGFloat(deviceMotion.rotationRate.y)
let zRotationRate = CGFloat(deviceMotion.rotationRate.z)
if abs(yRotationRate) > (abs(xRotationRate) + abs(zRotationRate)) {
for cell in self.tableView.visibleCells as! [CatPhotoTableViewCell] {
cell.panImage(with: yRotationRate)
}
}
})
}
还有一个使用 Timer 定时发送日志的问题,CPU 根本没有空闲的时间,开销很大。
具体见文末的 Demo Code.
最后这样
会慢慢降下去,至于电量低消耗。
需要大约两分钟时间,一屏幕放不下。
Instruments 的 Energy Log 有问题,连着 Xcode 调试的部分 gg 了
Instruments 的 Energy Log 模版用途不大
因为不能手机在线调试。Energy 是空的, 或者提示 No Data
,
这是苹果的一个知名 bug .(参见 Apple Forum )
Energy Log 模版的模块挺丰富的,可以看屏幕亮度、定位、蓝牙、GPU 和网络等等的功耗情况,其中网络又包括 WiFi 和蜂窝网络。
想着一边给手机充电,一边调试电量损失,不靠谱。
试了下,无线用 Instruments 的 Energy Log 模版调试,结果一样。
连着 Xcode 调试耗电,也没有数据。
无线用 Instruments 调试,首先要设置 Xcode 无线 Debug ,
无线 debug 功能,隐藏在 Xcode 的 Window > Devices and Simulators 中。
实际上是,使用共享的无线网络,取代了数据线的连接,连上了。会有一个网络的 Icon . 上面还有提示语 ( connected , 连上了 )
如下图:
更多参见博客 How to use Wireless Debugging on Xcode 9
然后就可以设置 Instruments 无线设备调试了,
更多参见苹果文档 Energy Efficiency Guide for iOS Apps
instruments 的 Energy 模版,将电量消耗的程度划分为 20 个级别。
0 代表不耗电,自然 app 没做什么
20 代表耗电严重
现在要看到,只能导入离线的 log。
在手机的设置中,开发者选项中的 Logging, 选中 Energy, 点击开始录制:
之后,使用你的 app 一段时间,(可以重点测耗电功能)
开发者选项中的 Logging, 点击完成录制,
导入电量消耗 log 数据,到 Instruments 的 Energy Log 模版.
老版本的不行( 11.4 ), 没数据。操作的时候,手机的设置 app ,还老是闪退。
手机升级到最新版(12.1 , 20181127),试了多次,也不行, 本文觉得是彻底挂了
(本文中,重启手机,升级手机。没有重启电脑。也有可能是,我的手机和电脑坏了)
湿一点,好消化
耗电是不好的。
写入硬盘与网络请求,都是高耗电操作。
网络请求特别耗电,每一个网络请求,手机设备需要使用他的蜂窝网络天线,发送无线电波。
网络的质量与类型,对于耗电的影响也很大。
使用 Wi-Fi 比 3G , 4G 要省电得多。 使用 4G 比 3G 要省电,因为 4G 的信号更强。
计时器,能不用就不用。( NSTimer )
一般情况下,app 都用 Timer 做了很多无用功。
比如, 一个列表屏幕, 上方 banner 计时器,往下滑到看不见 banner ,就可以暂停计时器。上滑,看得见 banner 了,又可以恢复。同样的,进入子界面,又可以暂停,或者释放,...
例子: 定时做重复的大量工作不好,可能每当系统休眠(系统要降低能级了),系统又被唤醒了,开始功耗。
定位
与网络请求类似,手机设备定位通过 GPS 天线发送信号,也挺耗电的。
如果 app 经常去获取手机设备的精确定位,定位精度越高,能耗越严重。
建议使用策略,手机的负担会小很多。
( ,Deferred location updates, 位置更新延迟(直到移动了 x 米或者时间超过了 xx 秒 )、
significant location change, 定位变化比较大的时候,唤醒、
region monitoring, 监测用户进入或离开特定地理区域)
Motion 物理引擎,动态效果更新状态,挺耗电的。
使用罗盘、陀螺仪、加速计,都消耗不小。
相关代码: https://github.com/BoxDengJZ/Instruments_Wen
更多资料:
WWDC 2015 Debugging Energy Issues
视频教程,practical-instruments
本文 Demo 使用的是 500 px 的 API .
后来发现有人都写过了,
好尴尬
想了一下,可以写他没交代的。苹果更新太快,人是物非
本文例子,有些单薄。
苹果的有一期将性能优化的 WWDC ,好像例子感觉也不怎么样。( 还记得,那讲师像是咱村里的 )
手机性能优化的重点,就是界面渲染。一般,计算任务都交给服务端。
界面渲染慢,就不好了。
常见问题,就是离屏渲染。 这里用 NSShadow 处理掉 CALayer 的阴影属性带来的离屏渲染。
常见的离屏渲染代码:
绘制阴影,
var label = UILabel()
label.layer.shadowColor = UIColor.lightGray.cgColor
label.layer.shadowOffset = CGSize(width: 0.0, height: 5.0)
label.layer.shadowOpacity = 1.0
label.layer.shadowRadius = 5.0
label.text = "离屏渲染"
写完以后,CPU 和 GPU 都没有充足的信息绘制阴影效果。
过程是, CPU 会先把文本传出去,请求 GPU (CPU 把文本传给 GPU ),创建一个内存中的位图上下文(GPU 把文本放进去 ),离屏渲染这就开始了。
这个上下文缺信息,不在屏幕上, 不是帧缓冲。(渲染出来的图形上下文,不属于当前帧)。
之前 CPU 把文本处理好了,现在 CPU 处理文本效果(这里是阴影)。
然后,CPU 拿到渲染好的文字,基于渲染出来的每一个像素的透明度,计算出阴影的形状。
最后,CPU 把最新计算出来的文字阴影形状的信息,传给 GPU .
GPU 有阴影信息,有之前图形上下文的渲染文本,GPU 就渲染好了最终的文字及其阴影,交给帧缓冲 (Frame Buffer), 我们就看到了。
这段代码要渲染两次,出现了离屏渲染。对 GPU 的性能有影响。他需要等待 CPU 来算出阴影的形状。
因为我们要 60 的帧数 ( FPS ), GPU 准备帧缓冲,渲染出当前帧,只有 17 微秒。拖累了主线程,屏幕刷新不过来。
对于图形阴影, 用 layer 的 shadowPath. 对于文字阴影,用 NSShadow .
layer 的四个属性 shadowColor , shadowOffset ,shadowOpacity ,shadowRadius ,一般性能不好。
对于一个视图框, 通过 layer 在周边加阴影。用 layer 的 shadowPath,创建一个 UIBezierPath,
UIBezierPath(rect: CGRect(x: 0, y: 0, width: 50, height: 50))
// size of your label
与之前不同,图层不用渲染两次。现在 GPU 有了足够的信息绘制阴影效果, 就不用离屏渲染了。
对于文字阴影,用 NSShadow ,用 path 就比较难。
UI 性能优化主要用的是 Instruments 的 Core Animation 模版。
过去,Core Animation 模版几个调试选项非常强大,正常的绿色, 异常的红色,离屏渲染的黄色。
(光栅化有效,光栅化后缓存的内容成功复用。界面会显示绿色。
光栅化失效的部分,呈红色。光栅化后缓存的内容没有复用,光栅化隐式创建的位图浪费了。直接重新绘制。 )
现在这些利器都在 Xcode 里了,可以直接使用。
真机运行直接选择,
本文 Demo 使用的是 500 px 的 API .
调试界面的离屏渲染,用的是 Color Offscreen-Rendered Yellow
选项。
调试目标是,出现大片的绿色。
这里可以用 Apple 的 UIKit 框架下的 NSShadow 对象。
let shadow = NSShadow()
shadow.shadowColor = UIColor.lightGray
shadow.shadowOffset = CGSize(width: 0.0, height: 5.0)
if let mutableAttributedString = label.attributedText as? NSMutableAttributedString{
let range = NSRange(location: 0, length: mutableAttributedString.string.count)
mutableAttributedString.addAttribute(NSAttributedString.Key.shadow, value: shadow, range: range)
}
用背景色处理掉混色。
界面图层混色,就是多个视图的位置有重叠,他们又不是透明的。重叠区域的每一个像素,GPU 需要算出一种新的颜色(混色)。
如果这种效果,不是 UI 设计的,尽量避免。
调试界面的图层混色,用的是 Color Blended Layer
选项。
UILabel 建议设置背景色。UILabel 有文字,那就不透明,Label 默认的背景色是透明色,与父视图的背景色,混杂了。GPU 对相关位置的颜色,需要重新计算。
override func awakeFromNib() {
super.awakeFromNib()
[userNameLabel, photosLikeLabel, photosDescriptionLabel, photoTimeIntevalSincePostLabel].forEach {
$0?.backgroundColor = UIColor.white
}
}
设置后, 效果明显
混色,如果不是 UI 指定的效果,建议处理掉。
界面图层混色,常见的影响因素是 view 的 alpha 属性。 alpha 小于 1, 一般自带混色效果。
其他知识点介绍: 卡顿(丢帧)。
Instruments 的 Core Animation 模版的时间线,就是 FPS. 显示随着时间,app 的帧率波动。
可以方便的读取帧数,直观的了解那些界面要改善。
用代码检测卡顿,自然是 CADisplayLink ,网上相关博客很多。
我之前是在小公司,每天的工作内容就是和策划美术老板撕逼,催策划案子,催美术资源, 然后照着那个永远在改的案子边做边改,反过来被策划和老板催实现,改bug。做完以后老板大手一挥,这个功能去掉,这个功能要改,嗯嗯。。。还要和服务器讨论接口设计。有时候对美术给的资源不满意还会自己PS或者开3DMAX弄一下。。。反正是真正写代码的时候少,需要的功能都直接网上找,每天都很忙又不知道在忙些什么。
我想了解一下iOS开发的工作内容大概是怎样呢?
也会和产品、UI撕逼吗?
是写代码的时候多,还是处理资源的时候多?
对于UI实现是鼠标拖拖拖吗,是程序来做还是设计来做?
开发过程中案子改动频繁吗?
周围有话语权的人多吗,是不是老板产品设计谁都可以对你指手画脚?
创业小公司和大公司有何不同吗?
对数据(比如游戏的次日留存,流水这些)迷信或者说崇拜吗? 公司会对同行各种鄙视,对自己数据各种吹逼吗?
老板爱画大饼的多么?
像游戏行业梦想一夜暴富的多么?
加班多么,加班的原因多是什么?