iOS10定时消息的改动

前言

     iOS10已经发布了一段时间,iOS10的各种适配相信大家已经完成。本文将讲述的是关于iOS10内核的一个小改动,惯例,本文属于进阶性技术文,不会讲解API的使用,要求读者对RunLoop有一定的认知,感谢网友@送你的独白么 提供的SDK。

定时器

     当我们的程序需要定时处理一些事件时,我们就会用到定时器,常用的定时器有NSTimer,CADisplayLink,GCD Timer,本文主要针对NSTimer和CADisplayLink进行讲述,因为这两者跟你的Application更为密切。

     NSTimer和CADisplayLink都是建立在CFRunLoopTimer之上的抽象物,但有趣的是,苹果只提供了NSTimer和CFRunLoopTimer互转的Toll-Free Bridge,并没有提供CADisplayLink和CFRunLoopTimer互转的接口,因此一些开发者对此产生了一些猜想,有的人认为,CADisplayLink是用GCD Dispatch Source来实现的,有的人认为,CADisplayLink是用RunLoopSource来实现的,但这些猜想的依据都太容易被推翻了。如果CADisplayLink是用GCD Dispatch Source来实现的,那么CADisplayLink是怎么在你所创建的子线程中工作的呢?如果CADisplayLink是用RunLoopSource来实现的,会不会多此一举?

     CFRunLoopTimer是RunLoop的定时源,与Source1(Port)一样,都属于端口事件源,但不同的是,每一个Source1都有与之对应的端口,而一个RunLoopMode中的所有CFRunLoopTimer共用一个端口(Mode Timer Port),CFRunLoopTimer在RunLoop中的工作原理如下图。

定时源工作

     从定时源在RunLoop中的工作原理我们得知,只要符合条件的定时器都会被触发,也就是说,在同一次Loop中,可能会执行几个定时器的回调。

     很多讲述定时器的技术文中都有这么一个观点,如果一个定时器错过了本次可以触发的时间点,那么定时器将跳过这个时间点,等待下一个时间点的到来,这个观点似乎是从官方文档中得来的,但这个观点跟定时器在RunLoop中的工作原理并不符。定时消息从内核发出,消息在消息中心等待被处理,RunLoop每次Loop都会去消息中心查找相应的端口消息,若找到相应的端口消息就会进行处理,所以,即使当前RunLoop正在执行一个耗时很长的任务,当任务执行完进入下一次Loop时,那些未被处理的消息仍然会被处理。经过大量测试表明,定时消息并不会因延迟而掉失。

     关于RunLoop,官方文档在这一部份的勘误比较多,经常会出现文档的介绍跟源码不同的情况,所以想学习RunLoop的同学,建议看源码和自己做测试,特别是自己做测试。

     NSTimer和CADisplayLink最大的区别在于信号的发射频率不同,CADisplayLink的发射频率固定在16.67ms一次,而NSTimer则可以自由定义。我在页面间跳转的性能优化(一)中曾经提到过,不是必要的情况下,都不要选择使用CADisplayLink作为定时器,因为它会使目标RunLoop一直处理活跃状态。下面通过一个例子来看看实际的效果,创建一个CADisplayLink定时器,设置为100秒后触发,然后观察目标RunLoop的状态。

CADisplayLink

     从实际效果我们可以看到,目标RunLoop一直处于活跃状态,不断地处理内核发出的信号,直到RunLoop Stop或CADisplayLink定时器被移除。同样的条件,我们把定时器换成NSTimer来观察实际情况。

NSTimer

     与CADisplayLink的固定信号不同,NSTimer的信号间隔完全是由使用者来定义。所以,除非你需要实现定时动画,不然都不要选择使用CADisplayLink作为定时器,它不仅会损耗大量的CPU资源,还会影响目标RunLoop处理其它事件源。

改动

     前面介绍了定时器的工作原理,现在来看看实际的改动,从一个例子入手进行讲述。现在有页面A,B,页面A,B各有一个按钮,页面A的按钮用来进入页面B,进入页面B后创建一个子线程,然后向子线程添加一个定时器并启动RunLoop,页面B的按钮用于停止定时器,并返回页面A,页面B被释放时会在dealloc方法里输出dealloc,编译环境是ARC,下图为页面B的代码,Gif图分别是iOS10与iOS9的实际运行效果。

页面B代码
iOS10
iOS9

     一般情况下,从页面B返回到页面A后,页面B会被释放,页面B的dealloc方法会输出dealloc,但从实际的运行效果可以看到,在iOS10环境下页面B并没有被释放,WTF,为什么iOS10环境下会这样?要回答这个问题,我们需要先知道iOS10的改动是什么。

     若目标RunLoop当前没有定时源需要处理(像上面的例子那样,子线程RunLoop只有一个定时器,该定时器移除后,则子线程RunLoop没有定时源需要处理),则通知内核不需要再向当前Timer Port发送定时消息并移除该Timer Port。在iOS10环境下,当移除Timer Port后,内核会把消息列表中与该Timer Port相应的定时消息移除,而iOS10以前的环境下,当移除Timer Port后,内核不会把消息列表中与该Timer Port相应的定时消息移除。iOS10的处理是更为合理的,iOS10以前的处理可能是历史遗留问题吧。

     看回上面的例子,例子中遇到的问题是页面B返回后并没有被释放,即页面B的内存被强制保留了,所以我们现在需要知道的是页面B为什么被强制保留了。在页面B中我们创建了一个子线程,子线程的主函数是页面B的对象函数,这可能是导致页面B被强制保留的原因,所以,我们需要知道子线程开启前后,页面B对象的引用计数是否有增加。

创建并开启子线程
页面B的引用计数

     从输出的信息我们得知,创建子线程后,Target会被强制保留,直到子线程的主函数返回。引用计数在很多时候可以帮助我们了解内存的使用情况,但在ARC编译环境下,我们无法直接使用retainCount方法来获取一个对象的引用计数,所以,我们需要做额外的处理。

获取对象的引用计数

     回到例子中,我们知道了页面B被强制保留的原因后,就知道了怎么解决,只需要退出子线程即可,子线程之所以可以一直存活,是因为启动了RunLoop,所以,我们只需要退出RunLoop,子线程的主函数就会返回。例子中涉及到线程异步的问题,定时器是在子线程RunLoop中注册的,但定时器的移除操作却是在主线程,由于子线程RunLoop处理完一次定时信号后,就会进入休眠状态。在iOS10以前的环境下,定时器被移除后,内核仍然会向对应的Timer Port发送一次信号,所以子线程RunLoop接收到信号后会被唤醒,由于没有定时源需要处理,所以RunLoop会直接跳转到判断阶段,判断阶段会检测当前RunLoopMode是否有事件源需要处理,若没有事件源需要处理,则会退出RunLoop。由于例子中子线程RunLoop的当前RunLoopMode只有一个定时器,而定时器被移除后,RunLoopMode就没有了需要处理的事件源,所以会退出RunLoop,子线程的主函数也因此返回,页面B对象被释放。

     但在iOS10环境下,当定时器被移除后,内核不再向对应的Timer Port发送任何信号,所以子线程RunLoop一直处于休眠状态并没有退出,而我们只需要手动唤醒RunLoop即可。

更改页面B代码
iOS10

     例子中所遇到的问题已经解决,但看完这个例子,可能你会有疑问,这个例子讲述的情况有实战意义?这个例子是从一个国外成熟产品所提供的配套SDK中简化而来,配套的SDK用于与产品进行对接。额......实话说,当我看到这个处理方式的时候,我被震惊了,没想到一个成熟产品所提供的配套SDK会出现这样的问题,让我更震惊的是,随后在其它SDK中也发现了这个问题,这......

     我们回头来看看例子中的处理方式,例子中,子线程RunLoop的退出依赖于RunLoopMode的事件源为空,这种RunLoop的退出方式是极不稳定的,因为系统有很多API会向目标RunLoopMode添加额外的事件源来处理系统事件的,所以这种方式是不能确保一定可以退出RunLoop的。正确的方式应该是配对调用CFRunLoopRun( ),CFRunLoopStop( )来启动和退出RunLoop,需要注意的是,除非你要创建一个单例线程,不然不要使用[runloop run]方法来启动RunLoop,因为使用run方法启动RunLoop后,唯一退出RunLoop的方式是当前RunLoopMode的事件源为空,而我们知道这种方式本身是极不稳定的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 姓名:雷潇 16030110083 转载自:http://www.lai18.com/content/246314...
    babyL_f449阅读 209评论 0 0
  • 之前要做一个发送短信验证码的倒计时功能,打算用NSTimer来实现,做的过程中发现坑还是有不少的。 基本使用 NS...
    WeiHing阅读 4,377评论 1 8
  • Run loop 剖析:Runloop 接收的输入事件来自两种不同的源:输入源(intput source)和定时...
    Mitchell阅读 12,407评论 17 111
  • 深入理解RunLoop 由ibireme| 2015-05-18 |iOS,技术 RunLoop 是 iOS 和 ...
    橙娃阅读 847评论 1 2
  • 1 在学妹第五遍跟我诉苦水,说前男友喜欢上了一个整过容的女孩子,她心里很不平衡,而且很想不通,为什么前任会爱上一个...
    徐徐来阅读 8,718评论 87 267