图15 主线程与子线程混合UIPasteboard测试代码执行输出结果
这个结果在我们的意料之外。尽管将50次的UIPasteboard对象的操作放在了子线程,主线程仅执行了50次,但是单次执行时间又原来的0.5秒左右提高到了接近1秒。显然系统对于剪贴板的操作是做了线程同步限制。总时间是不变的。
尽管我们知道了剪贴板是同步操作的,依然并未复现卡死的情况。那么多线程并发到底会不会有可能触发剪贴板的卡死呢?继续验证。
多线程并发频繁调用UIPasteboard接口测试
图16 多线程并发UIPasteboard测试代码
创建1000个子线程并发任务,每个并发任务中获取100次UIPasteboard对象。同时在主线程调用1次UIPasteboard的存取操作。执行后输出如下:
图17 多线程并发UIPasteboard测试代码执行输出结果
从输出内容可以看出,子线程在执行了150次左右便不再执行下去了,通过上面的操作,成功将App卡死无法执行下去了。并且此时尝试打开京东、腾讯视频等其他App发现此时已经无法打开了,而且在所有App中使用剪贴板都会使App卡主。
所以多线程并发使用UIPasteboard相关接口的确会导致App卡死现象,并且会影响其他程序。
测试到这里,我们了解到了系统对于UIPasteboard不但做了线程同步的限制,而且做了进程同步限制。在高并发使用UIPasteboard接口的情况下,很容易使UIPasteboard出现卡主的问题,并且影响整个系统的App。
那么触发100次UIPasteboard的OpenUDID对App会带来多大的风险?
OpenUDID的影响
对App的影响范围
通过上述调研,OpenUDID会在第一次获取OpenUDID值的时候访问100次UIPasteboard的API。如果启动时使用了OpenUDID等于间接使用了UIPasteboard,由于UIPasteboard是进程间同步的,当系统UIPasteboard被卡死时,App便无法启动了。
另外,大家在使用OpenUDID的时候经常会把它私有化,尤其是在做SDK时,仅仅改个类名,然后便使用原有逻辑也是常有的事。出于用户体验优化的需要,很多开发者会将部分逻辑比如初始化等放在子线程执行。如果放到子线程的这部分逻辑首先访问了私有OpenUDID代码去获取OpenUDID,就发生了子线程连续访问UIPasteboard的情况。通常一个App会接入多个SDK,如果每个SDK都有一个OpenUDID,并且各自创建子线程访问那么就会发生并发访问剪贴板的情况。如下图所示:
图18 OpenUDID多线程并发频繁使用剪贴板
基于上述测试的结果,可以猜测:线程开的越多,则越有可能触发剪贴板被卡死的情况。
为了了解App无法启动的情况,我们在启动时添加了启动异常计数的埋点策略,当启动失败次数达到3次时就进行埋点并上报。同时为了优化用户体验,启动失败次数达到3次时则进入启动修复页面,提示用户去重启设备。通过对该策略的埋点数据分析,每天大约会有万分之二的用户会触发连续三次启动失败的问题。虽然App启动失败还有许多其他的原因,但剪贴板卡死这个问题的影响应该还是比较大的。
OpenUDID的使用现状调研
为了进一步扩大对OpenUDID剪贴板问题的影响范围的了解,对经常用到的SDK使用OpenUDID以及修复的情况进行了调研(由于SDK存在版本差异,实际情况可能与结果有些偏差):
SDK | OpenUDID类名 | 是否修改了 | 启动注册是否调用 |
滴滴打车 | DIOpenUDID | 否 | 否 |
讯飞 | IFlyOpenUDID | 否 | 否 |
微博 | WBSDKOpenUDID | 是(只修改了私有剪贴板标识,还是会访问100次剪贴板) | 否(但初始时会触发一次剪贴板的调用) |
支付宝 | UTDIDOpenUDID | 是(仅在App首次启动时会触发100次访问剪贴板的逻辑) | 否 |
微信 | WXOMTAOpenUDID | 是(不会触发100次访问剪贴板逻辑) | 否(但初始化时会触发一次剪贴板的调用) |
头条广告 | BUOpenUDID | 是(不会触发100次访问剪贴板逻辑) | 否 |
百度地图 | -- | -- | -- |
ZBar | -- | -- | -- |
听云 | -- | -- | -- |
表 2 常用SDK OpenUDID使用情况
从以上的调研结果中看出,目前OpenUDID使用还是非常广泛的,并且大多数情况下均保留了OpenUDID反复使用UIPasteboard接口的逻辑。
为了降低触发UIPasteboard卡死的概率,可以抛弃剪贴板保存OpenUDID的逻辑,将OpenUDID的值保存在钥匙串中。最初OpenUDID是利用系统自定义剪贴板,可以在不同App之前共享数据的特性来保证OpenUDID的值始终不变,但随着iOS系统对此特性的封锁,利用剪贴板保存OpenUDID反而会带来问题。