最近在调研 iOS app 中存在的各种卡顿现象以及解决方法。
iOS App 出现卡顿(stall)的概率可能超出大部分人的想象,尤其是对于大公司旗舰型 App。一方面是由于业务功能不停累积,各个产品团队之间缺乏协调,大家都忙着增加功能,系统资源出现瓶颈。另一方面的原因是老设备更新换代太慢,iOS 设备的耐用度极好,现在还有不少 iPhone 4S 在服役,iPhone 6 作为问题设备持有量很高,据估计,现在 iPhone 6s 以前的设备占有比高达 40%。
所以,如果尝试在线上 App 加入卡顿检测的工具,你会发现卡顿出现的概率高的惊人。但卡顿的检测就修复并不简单,主要是因为难以在开发设备上复现。
之前写过一篇介绍主线程卡顿监控的文章,好像现在主流的做法都是通过监控 Runloop 事件回调,检查进入回调的时间间隔是否超过 Threshold,超过则记录当前 App 所有线程的 call stack。
我前段时间从后台上报的卡顿日志里看到这样一个 call stack:
> 0 libsystem_kernel.dylib __workq_kernreturn
> 1 libsystem_pthread.dylib _pthread_workqueue_addthreads
> 2 libdispatch.dylib _dispatch_queue_wakeup_global_slow
> 3 libdispatch.dylib _dispatch_queue_wakeup_with_qos_slow
> 4 libdispatch.dylib dispatch_async
也就是说卡顿出现在 dispatch_async,以我现有对于 GCD 的认知,dispatch_async 是绝无可能出现卡顿的。dispatch_async 的主要任务是从系统线程池里取出一个工作线程,并将 block 放到该线程里去执行。
上述 call stack 确确实实的出现了,而且样本数量还不少,最后一个函数明显是一个内核调用。从函数名字猜测,可能是 GCD 尝试从线程池里获取线程,但已有线程都在执行状态,所以向系统内核申请创建新的线程。但创建线程的内核调用会很慢吗?会慢到让主线程出现卡顿的程度?带着疑问我搜索了大量相关资料,最后比较相关的有这样一篇文章:http://newosxbook.com/articles/GCD.html
其中有这样一段话:
This isn't due to 10.9's GCD being different – rather, it demonstrates the true asynchronous nature of GCD: The main thread has yet to return from requesting the worker (which it does by pthread_workqueue_addthreads_np, as I'll describe later), and already the worker thread has spawned and is mid execution, possibly on another CPU core. The exact state of the main thread with respect to the worker is largely unpredictable.
作者认为,GCD 申请到的线程有可能是一个正在处理其他任务的 thread,main thread 需要等待这个忙碌的线程返回才能继续执行,我对这种说法存疑。
最后求助无门的状况下,我决定使用一次宝贵的 TSL 机会,直接向 Apple 的工程师求教。这里不得不提下,向 Apple 寻求 technical support 是非常宝贵而且可行的方案,每个开发者账号每年都有 2 次机会,不用非常可惜。
我把问题抛过去后,得到一位 Apple 内核团队工程师的回复,我将精简过的回复以问答的形式展示和大家分享:
Q: looks like even if it's async dispatching, the main thread still has to wait for the other thread to return, during which time, the other thread happen to be in mid execution of sth. this confuses me, what exactly is the main thread waiting for?
为什么主线程需要等待 dispatch_async 返回,主线程到底在等待什么?
A: It's hard to say with just a user space backtrace. Frame 0 has clearly sent the current thread into the kernel, and this specific kernel call is /way/ too complex to analyse from outside [1].
从用户态调用栈无法得出答案,内核可能的状态过于复杂。
Q: I know it's suggested that we create limited amount of serial queue,and use target queue probably. but what could happen if we don't follow that rule?
Apple 一直推荐自己创建 serial GCD queue 的时候,一定要控制数量,而且最好设置 target queue,否则会出现问题,但会出现什么问题我一直很好奇,这次借着机会一起问了。
A: