转iOS开发已二月有余, 整体的模式与android没有什么非常大的不同, 可能唯一不是很适应的就是UI界面,由于是初创项目,UI效果非常多,这对于一个极少开发ui的客户端研发确实感到比较头大, 总算上手状况良好.
作用自己的iOS的第一篇文章,主要是记录一下自己在排查一次主线程卡顿问题。
0x00 问题
我做的应用是一个短视频相关的应用,其首页是一个不规则的无限瀑布流,基本技控件使用的就是UICollectionViewFlowLayout+UICollectionView,但因为每个视频的大小尺寸不一致,采用的是不规则的布局方式,每一个cell就是一个视频的封面,此乃背景。
基本操作包括上拉和下拉,上拉加载更多,下拉重新加载数据,都是常规操作没啥毛病,有毛病的是应用在上下拉的时候有可能会整个应用卡住,所有操作都不能再操作,但应用又没有因此退出。
0x10 分析
按以前做android时的经验来看,这肯定是主线程被卡住了,但iOS与android不一样的是,android有ANR,可以非常容易地看出主线程阻塞的问题,但iOS要验证的话还需要做一些其他的事情。
0x11 定时器
由于初接触iOS不久,很多调试工具都不熟悉,只能采取一些简单通用的办法,那就是定时器。
在AppDelegate.mm的application方法里(保证时机够早,而且是在主线程内),设置了一个定时器,在主线程定时打印一些日志。
1 | [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { |
结果如我所料,当发生卡顿时,日志果然不再输出来了。这起码证明初期的想法是对的,就是主线程不能继续跑了。
0x12 工具分析
需要知道是主线程跑不动了,队列的任务都不能继续了,但依然不知道因为死锁还是其真的有非常耗时的任务在执行,因为其实在卡的时候应用还没死,所以我想知道应用究竟执行到什么地方。
xcode比as方便的一点就是无论什么时候,只要程序没死你都可以点击下方的暂停按钮来中断应用执行,当然也可以看执行到哪里。
此时大多数时候都能够定位到这个位置
从堆栈上来看,卡在layoutSubviews上可能性很大,而这个方法一般是子view发生位置大小变化时才会触发,但我在不同的时候点暂停都几乎可以定位到这里,但看旁边的内存位置,事实上指令还是在继续执行的,因为每一次的暂停中断的内存位置都不一样,起码告诉了我其实是没卡死的,只是有一个任务一直在执行,没能出来导致后面的任务都在排队。
然而,排查了一次,事实上此时的确没有没什么大型的任务在主线程里操作,并将写日志等IO相关操作都丢去异步线程后,情况依旧。
此时又有了一种想法,既然是出不来,除了有大任务外,还有没可能是死锁,导致上一任务结束不了;或者是死循环,此时有个大胆的不成熟想法,就是子view变化导致触发父view的layoutSubviews,可能因此又导致了子view的变化……此以反复(毕竟没读过多少iOS的书,允许我天马行空)。
那么此时我就需要看一下执行栈了。
0x13 Instruments
这工具是无意中听到小伙伴说的,当时是想查内存泄漏,但后来发现这工具用于分析应用性能等都是十分不错的,而且也可以满足我查找执行堆栈的需求。
工具的入口就在xcode左边
有不同的分析内容,包括CPU,内存等,由于是想看执行指令,那就看CPU项,点击CPU项,可以看到当前的CPU使用情况。右上角就可以看到Instruments的入口
(其实此时我应该就能看出CPU存在异常了,经验不够啊)
点击”Profile in Instruments” 会问你是要转移数据还是重启应用重新监测,点”Transfer”就把当前的执行调试信息转移到Instruments中。
此时可以比较清楚地看到CPU的战占用已经到了100%,这绝对是不正常的,再看看主要都是Main Thread在全负载运行,也更加证实了开始时的猜测,就是主线程卡住了。
在CPU Usage内选择一段100%的时间段,然后看下面的执行分析,就能看到执行堆栈的。
可以按照Weight来一层层看下来,可以知道不同的操作任务所耗费的CPU占比,从中挑出大头。但事实上,从上图可以看出,并不存在什么大头,也不存在循环调用的问题,这一个假设可以排除了;而且从CPU占用100%上来看,死锁也是可以排除的,因为死锁只是资源占用冲突,一般来说CPU都不会满载占用。
0x14 替换
这就尴尬了,招都使完了,除了知道主线程被卡啥假设都没中。没办法,只好一个个替换了,因为问题发生在瀑布流上,所以就先从瀑布流上下手。因为瀑布流是不规则的,所以瀑布流是经过修改的,先把自定义的UICollectionViewFlowLayout替换回系统的试试。结果不试不知道,尼玛的居然还真的没问题了。那至少可以缩小问题范围,就是自定义的瀑布流影响的。
接下来就是针对自定义的瀑布流而已进行一个个排查了,当我把所有的cell的高度都统一后,问题同样也消失了,因为应用首页是不规则的,所以item的高度都是按照视频封面的大小,以一半屏幕宽度作为封面的新宽度来重新算封面的高度的,难道是这里出问题?
newItemHeight = (screenWidth/2) / (image.width/image.height)
在排版时把这四个量每次都打印出来,看是否有什么异常。
当界面卡,CPU开始满载时,查看日志,发现打印了一个Nan的数据,这明显是非法的。再往后看一下其余的项,结果发现image.width与image.height都为0!!每次有Nan打印时,都会发生卡,看来是八九不离十了,也能解析之前固定高度后就没问题的现象。
由于image.width与image.height数据是服务器返回的,的确是有可能是错误的,但客户端没考虑这种情况,直接把它们当除数去除了,小学的教育教我们,除0这种操作是异教的,是要被烧死的,但程序偏偏这样执行了,还没崩溃,只是返回了一个Nan出来。
0x20 修正
知道出问题的地方后,就可以修复了,修复的方法也很简单,使用isnan判断一个值是否Nan,如果是的话就给一个默认值,这事就算结束了。
同时服务端也需要作相应修改,客户端这样做只能算是容错而已,体验效果也会打个折扣。
0x30 延伸
问题的表面是除0导致的,得到一个没意义的Nan值,至于iOS拿这个值做什么会做到满载我也不知道,但至于为什么除0会像啥事都没的呢?
0x31 C的除0操作
oc based on C,所以我们可以先看一下C在除0上是怎么处理的。
1 |
|
执行后输出
result=inf
在C里,inf表示”infinite”,也用于表示无法定义或无法表示的浮点数。但它不会崩溃,这点与oc的结果是一致的。
0x32 除0的本质
从小我们的教育就告诉我们,在做除法时不能做”除0”这种操作,因为是错误的,但至于为什么是错误的没说。后来我想了一下这个问题,感觉”除0”其实没想象中那么异教。
下面说说我个人不成熟的看法。
先来想想除法是怎么理解。以10/2来说,可以理解为
- 10里面有多少个2组成
- 把10分成2份一份多少
如果按这种思路思考,那除0操作是不是也能这么理解,如10/0是不是可以理解为
- 10里面有多少个0组成
- 把10分成0份一份有多少
看上去语法感觉也说得通,但仔细一想这两句话都是没意义的,10里面有多少个0,1个,2个,……,还是很多个,无论多少个0都不可能加出个10出来,同理,10分成0份那是分还是不分?意义是不明的。
换一个角度来想,我们在做除法的时候,是不是除数越小,最终除出来的数越大?
以正数为例1
2
3
4
51/1=1
1/0.1=10
1/0.01=100
.......
1/0.000......0001=1000...000
当数无限趋向于0时,作为除数的结果是商会越来越大,那如果除数为0,是不是表示商是无限大了?
再拿斜率来说,我们都知道tan90°是不存在的,按斜率的定义是角对边/邻边,当邻边无限趋向于0,与x轴夹角无限趋向于90度时,斜率指数上升,当夹角为90度时,斜率不存在了。那如果按上面的说法,我们是不是也可以说有一个”无限大”的斜率?
事实上”趋向0”与”等于0”到底不是一回事,有点用经典力学解析微观世界的意味,并不能以这种说法试图将除0这种操作合法化。
后来在TED上看到一个视频,解析了以上的一些问题,虽然也是相对浅显,但对于合法化除0操作的想法,最终还是异教。
0x33 计算机的处理
听到同事说,他说在计算机里处理除法,也是将其当乘法来处理的,所以 10/0 等于 10×(0的倒数),然后结果就是0了。
首先,说计算机里除法是用乘法来算的也不对,因为计算机里只有加减,并没有乘除;其次,这种计算方式完全是按照人类的计算方式来算的,就算计算机真的这么算,那0的倒数不就是1/0吗,除0的问题没解决,那谈何算乘?所以,0的倒数也是不存在的。