大家好,关于cf辅助网站源码分享购买很多朋友都还不太明白,今天小编就来为大家分享关于cf辅助官网sy的知识,希望对各位有所帮助!
RunLoop是iOS和OSX开发中非常基础的一个概念。
网上已经有很多源码分析和具体Demo的研究。
写这篇文章的目的是把目前网上能够找到的文章进行一个整体梳理。希望能帮到大家。
这篇文章将从CFRunLoop的源码入手,介绍RunLoop的概念以及底层实现原理。
具体目录如下:
一、RunLoop的基本概念
二、RunLoop相关类以及构成要素
三、RunLoop的运行逻辑
四、苹果官方RunLoop实际应用
五、第三方库RunLoop的运用
六、开发中常见RunLoop使用(如果对原理不感兴趣,可以直接跳到末尾看实际使用,这样有的放矢,阅读效率更高)
七、RunLoop可能的面试套路
一、基本概念
1、runloop是什么?————O
用一个字来形容runloop的话,runloop就是————圈。
或者说是英文字母——O。
这样的形象比喻,想要说明的是runloop的特性——runloop是一个事件循环对象。
一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑是这样的:
do{
//接受消息->等待->处理
}while(message!=quit)
线程执行了这个函数后,就会一直处于这个函数内部&34;的循环中,直到这个循环结束(比如传入quit的消息),函数返回。
2、没有runloop会怎样?
上面这样的运行机制通常被称作EventLoop。EventLoop在很多系统和框架里都有实现,
比如Node.js的事件处理,比如Windows程序的消息循环,再比如OSX/iOS里的RunLoop。
实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。
如果iOSAPP没有runloop机制会怎么样呢?
如果没有runloop的机制的话,我们的app,从一启动到退出程序时间会很短。
更加形象的说,app一启动就死了。
所以,runloop的存在,就是保住app的生命,让app可以随时待命,处理用户的操作以及其他事件。
runloop的重要性就在于:我们控制了runloop,就是控制了app的生死。
二、runloop的相关类以及构成要素
1、runloop相关类
在iOS中,RunLoop就是个对象。
众所周知,OC语言是对于C的封装。所以呢,会有两个框架,一个是CoreFoundation,一个是面向对象的Foundation。
全文在解释原理的时候,是直接使用底层CoreFoundation的源码解释。
在CoreFoundation框架为CFRunLoopRef对象,它提供了纯C函数的API,并且这些API是线程安全的;\n\n而在Foundation框架中用NSRunLoop对象来表示,它是基于CFRunLoopRef的封装,提供的是面向对象的API,但这些API不是线程安全的。
CFRunLoopRef的代码是开源的,我们可以在这里或这里找到CFRunLoop.c来查看RunLoop的源码。
2、构成元素
在CoreFoundation中关于RunLoop有5个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
一个Runloop对应一条线程,一个runloop里面可以有多个CFRunLoopModeRef(模式)。
同一个时刻,RunLoop只能是在一个mode上面的运行。如果需要切换mode,只能是退出currentMode,切换到指定的mode。
每一个mode又可以包含多个source/timer/observer。不同mode里面的子元素,互不影响。
5个类的对应关系大概是:
下面具体介绍这个CFRunLoop里面的4个类。CFRunLoopModeRef
官方文档介绍如下:
Arunloopmodeisacollectionofinputsourcesandtimerstobemonitoredandacollectionofrunloopobserverstobenotified.Youmustbesuretoaddoneormoreinputsources,timers,orrun-loopobserverstoanymodesyoucreateforthemtobeuseful.Youusemodestofilterouteventsfromunwantedsourcesduringaparticularpassthroughyourrunloop.
即一个runloopmode是若干个source、timer和observer的集合。它能帮我们过滤掉一些不想要的事件。
即一个RunLoop在某个mode下运行时,不会接收和处理其他mode的事件。
要保持一个mode活着,就必须往里面添加至少一个source、timer或observer。
这一点很容易理解,你想啊,如果一个mode里面什么东西都没有,那么他根本就没有活干,那mode活着还有什么意思。
苹果公开的mode有两个:kCFRunLoopDefaultMode(NSDefaultRunLoopMode)和UITrackingRunLoopMode。
前者是默认的模式,程序运行的大多时候都处于该mode下,后者是滑动tableView或scrollerView是为了界面流畅而用的mode。
还有个UIInitializationRunLoopMode是程序启动时进入的mode,一般用不上。
以上,两个公开常用的mode,在执行的时候,相互独立,互不干扰。
这样让不同的mode各司其职,对于程序运行的解耦很有好处。
但是如果偏偏想要让两个互相不干扰的mode,都做同一件事情可以实现么?
可以实现的。苹果爸爸,想到了这一点。
CFRunLoop里面有一个伪mode叫做kCFRunLoopCommonModes,它不是一个真正的mode,而是若干个mode的集合。
你可以把这个kCFRunLoopCommonModes理解成:折磨你改需求的产品经理。
kCFRunLoopDefaultMode就是写android的程序员,UITrackingRunLoopMode是写iOS的程序员。
产品经理一个需求,两个程序员必须都要实现这个功能。
我们往CommonModes里面加入任意的source/timer/observer。
就可以想象成是产品经理有新的需求。Android和iOS程序员都要把需求实现。
只要加入到了CommonModes里面,就相当于添加到了它里面所有的mode中(当然,根据各自的情况,可能不仅仅只要默认的两个mode)。
我们可以通过NSLog(@&34;,[NSRunLoopcurrentRunLoop])从打印结果看到CommonMode包含了上面的DefaultMode和TrackingRunLoopMode。
commonmodes=<CFBasicHash0x7fdaa0d00ae0[0x1084b57b0]>{type=mutableset,count=2,entries=>0:<CFString0x10939f950[0x1084b57b0]>{contents=&34;}
2:<CFString0x1084d5b40[0x1084b57b0]>{contents=&34;}}
CFRunLoopSourceRef
source是事件产生的地方(输入源),虽然官方文档在概念上把source分为三类:Port-BasedSources,CustomInputSources,CocoaPerformSelectorSources。
但在源码中source只有两个版本:source0和source1,它们的区别在于它们是怎么被标记(signal)的。
source0是app内部的消息机制,使用时需要调用CFRunLoopSourceSignal()来把这个source标记为待处理,然后调用CFRunLoopWakeUp()来唤醒RunLoop,让其处理这个事件。
source1是基于mach_ports的,用于通过内核和其他线程互相发送消息。
iOS/OSX都是基于Mach内核,Mach的对象间的通信是通过消息在两个端口(port)之间传递来完成。
很多时候我们的app都是处于什么事都不干的状态,在空闲前指定用于唤醒的machport端口,然后在空闲时被mach_msg()函数阻塞着并监听唤醒端口,mach_msg()又会调用mach_msg_trap()函数从用户态切换到内核态,这样系统内核就将这个线程挂起,一直停留在mac_msg_trap状态。直到另一个线程向内核发送这个端口的msg后,trap状态被唤醒,RunLoop继续开始干活。
其实,总结下来,事件产生的地方就是source(输入源),运用发消息的机制,让事件可以唤醒休眠的runloop执行。
具体细节,可以参见:RunLoop详解
CFRunLoopTimerRef
看到timer是不是很熟悉?
这里的timer看起来名字是用C语言的样式,其实是完全等价于我们在OC里面的计时器NSTimer。
所以,在平时编程的过程中,我们最开始意识到有runloop这回事,就是了使用NSTimer的时候。这一点在后面的具体场景应用会详细提到的。
另外,这个CFRunLoopTimerRef,还可以由方法performSelector:afterDelay:来触发。(因为,本质上afterDelay,底层就是启动了timer,不然怎么检测具体时间,然后调用回调方法呢。)
CFRunLoopObserverRef
看到名字就应该知道这就是一个观察者。它的主要用途就是监听RunLoop的状态变化。
它可以监听RunLoop的7种状态:
/*RunLoopObserverActivities*/
typedefCF_OPTIONS(CFOptionFlags,CFRunLoopActivity){
//即将进入loopkCFRunLoopEntry=(1UL<<0),
//即将处理timerkCFRunLoopBeforeTimers=(1UL<<1),
//即将处理sourcekCFRunLoopBeforeSources=(1UL<<2),
//即将sleepkCFRunLoopBeforeWaiting=(1UL<<5),
//刚被唤醒,退出sleepkCFRunLoopAfterWaiting=(1UL<<6),
//即将退出kCFRunLoopExit=(1UL<<7),
//全部的活动kCFRunLoopAllActivities=0x0FFFFFFFU
};
三、RunLoop的运行逻辑
1、获取RunLoop
苹果官方对于RunLoop的创建进行了封装,也就是说我们找不到像alloc或者new用这样的方法去手动创建RunLoop。
可以获取主线程或当前线程对应的RunLoop,只能通过CFRunLoopGetMain或CFRunLoopGetCurrent函数。
获取过程大概是:
\n>//全局的dictionary,key是pthread_t,value是CFRunLoopRef\nstaticCFMutableDictionaryRef__CFRunLoops=NULL;\n\nCF_EXPORTCFRunLoopRef_CFRunLoopGet0(pthread_tt){\n//第一次进入时,创建全局dictionary\nif(!__CFRunLoops){\n//创建可变字典\nCFMutableDictionaryRefdict=CFDictionaryCreateMutable();\n//先创建主线程的RunLoop\nCFRunLoopRefmainLoop=__CFRunLoopCreate(pthread_main_thread_np());\n//主线程的RunLoop存进字典中\nCFDictionarySetValue(dict,pthread_main_thread_np(),mainLoop);\n}\n\n//用传进来的线程作key,获取对应的RunLoop\nCFRunLoopRefloop=CFDictionaryGetValue(__CFRunLoops,t);\n\n//如果获取不到,则新建一个,并存入字典\nif(!loop){\nCFRunLoopRefnewLoop=__CFRunLoopCreate(t);\nCFDictionarySetValue(__CFRunLoops,pthreadPointer(t),newLoop);\n}\nreturnloop;\n}\n\n//获取主线程的RunLoop\nCFRunLoopRefCFRunLoopGetMain(void){\nif(!__main)__main=_CFRunLoopGet0(pthread_main_thread_np());\nreturn__main;\n}\n\n//获取当前线程的RunLoop\nCFRunLoopRefCFRunLoopGetCurrent(void){\nreturn_CFRunLoopGet0(pthread_self());\n}
从上面的代码可以看出,runloop和pthread_t(也就是线程)是一一对应的。
这样一一对应的关系是保存在一个全局的dictionary中的。
内部产生runloop的机制,与我们iOS开发中常用的懒加载很相似。
只有到了第一次要使用的时候,才会去创建。当线程销毁的时候,也销毁相应的runloop。
2、runloop运行逻辑
runloop整个的运行逻辑都是在于三个重要的对象如何运作:source(输入源)、timer(定时器)、observer(观察者)。
上面的关于runloop的相关类里面有过介绍,observer时刻监听,整个runloop的7种状态的变化。
在上面7种状态里面,对应着不同的处理方法。
网上的参考逻辑图如下:
其内部代码如下:(没有耐心的话,可以跳过不看)
>///用DefaultMode启动\nvoidCFRunLoopRun(void){\nCFRunLoopRunSpecific(CFRunLoopGetCurrent(),kCFRunLoopDefaultMode,1.0e10,false);\n}\n\n///用指定的Mode启动,允许设置RunLoop超时时间\nintCFRunLoopRunInMode(CFStringRefmodeName,CFTimeIntervalseconds,BooleanstopAfterHandle){\nreturnCFRunLoopRunSpecific(CFRunLoopGetCurrent(),modeName,seconds,returnAfterSourceHandled);\n}\n\n///RunLoop的实现\nintCFRunLoopRunSpecific(runloop,modeName,seconds,stopAfterHandle){\n\n///首先根据modeName找到对应mode\nCFRunLoopModeRefcurrentMode=__CFRunLoopFindMode(runloop,modeName,false);\n///如果mode里没有source/timer/observer,直接返回。\nif(__CFRunLoopModeIsEmpty(currentMode))return;\n\n///1.通知Observers:RunLoop即将进入loop。\n__CFRunLoopDoObservers(runloop,currentMode,kCFRunLoopEntry);\n\n///内部函数,进入loop\n__CFRunLoopRun(runloop,currentMode,seconds,returnAfterSourceHandled){\n\nBooleansourceHandledThisLoop=NO;\nintretVal=0;\ndo{\n\n///2.通知Observers:RunLoop即将触发Timer回调。\n__CFRunLoopDoObservers(runloop,currentMode,kCFRunLoopBeforeTimers);\n///3.通知Observers:RunLoop即将触发Source0(非port)回调。\n__CFRunLoopDoObservers(runloop,currentMode,kCFRunLoopBeforeSources);\n///执行被加入的block\n__CFRunLoopDoBlocks(runloop,currentMode);\n\n///4.RunLoop触发Source0(非port)回调。\nsourceHandledThisLoop=__CFRunLoopDoSources0(runloop,currentMode,stopAfterHandle);\n///执行被加入的block\n__CFRunLoopDoBlocks(runloop,currentMode);\n\n///5.如果有Source1(基于port)处于ready状态,直接处理这个Source1然后跳转去处理消息。\nif(__Source0DidDispatchPortLastTime){\nBooleanhasMsg=__CFRunLoopServiceMachPort(dispatchPort,&msg)\nif(hasMsg)gotohandle_msg;\n}\n\n///通知Observers:RunLoop的线程即将进入休眠(sleep)。\nif(!sourceHandledThisLoop){\n__CFRunLoopDoObservers(runloop,currentMode,kCFRunLoopBeforeWaiting);\n}\n\n///7.调用mach_msg等待接受mach_port的消息。线程将进入休眠,直到被下面某一个事件唤醒。\n///?一个基于port的Source的事件。\n///?一个Timer到时间了\n///?RunLoop自身的超时时间到了\n///?被其他什么调用者手动唤醒\n__CFRunLoopServiceMachPort(waitSet,&msg,sizeof(msg_buffer),&livePort){\nmach_msg(msg,MACH_RCV_MSG,port);//threadwaitforreceivemsg\n}\n\n///8.通知Observers:RunLoop的线程刚刚被唤醒了。\n__CFRunLoopDoObservers(runloop,currentMode,kCFRunLoopAfterWaiting);\n\n///收到消息,处理消息。\nhandle_msg:\n\n///9.1如果一个Timer到时间了,触发这个Timer的回调。\nif(msg_is_timer){\n__CFRunLoopDoTimers(runloop,currentMode,mach_absolute_time())\n}\n\n///9.2如果有dispatch到main_queue的block,执行block。\nelseif(msg_is_dispatch){\n__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);\n}\n\n///9.3如果一个Source1(基于port)发出事件了,处理这个事件\nelse{\nCFRunLoopSourceRefsource1=__CFRunLoopModeFindSourceForMachPort(runloop,currentMode,livePort);\nsourceHandledThisLoop=__CFRunLoopDoSource1(runloop,currentMode,source1,msg);\nif(sourceHandledThisLoop){\nmach_msg(reply,MACH_SEND_MSG,reply);\n}\n}\n\n///执行加入到Loop的block\n__CFRunLoopDoBlocks(runloop,currentMode);\n\n\nif(sourceHandledThisLoop&&stopAfterHandle){\n///进入loop时参数说处理完事件就返回。\nretVal=kCFRunLoopRunHandledSource;\n}elseif(timeout){\n///超出传入参数标记的超时时间了\nretVal=kCFRunLoopRunTimedOut;\n}elseif(__CFRunLoopIsStopped(runloop)){\n///被外部调用者强制停止了\nretVal=kCFRunLoopRunStopped;\n}elseif(__CFRunLoopModeIsEmpty(runloop,currentMode)){\n///source/timer/observer一个都没有了\nretVal=kCFRunLoopRunFinished;\n}\n\n///如果没超时,mode里没空,loop也没被停止,那继续loop。\n}while(retVal==0);\n}\n\n///10.通知Observers:RunLoop即将退出。\n__CFRunLoopDoObservers(rl,currentMode,kCFRunLoopExit);\n}
可以看到,这里runloop的整个运行逻辑就是一个do-while循环。
就是我们开篇说的一个——圈。
一旦调用了CFRunLoopRun,线程就会一直停留在这个循环里面;只有当超时或者被手动停止,函数才会返回,也就是退出了当前的runloop。
总结成一句话就是:runloop的运行逻辑就是do-while循环下运用观察者模式(或者说是消息发送),根据7种状态的变化,处理事件输入源和定时器。
四、苹果官方RunLoop实际应用
RunLoop作为iOSapp底层最重要的运行机制,基本运用在一个APP构成的方方方面。下面请大家跟我一起来看看,说起来那么晕头转向的RunLoop,到底可以解释哪些iOS的常见机制。
先来看一道面试题吧(这是我刚开始工作是一个JAVA面试官问的):
请问iOS的NSAutoreleasePool(自动释放池)在什么时候释放?
我当时的很纳闷,难道不是到了@autorelease的‘}’(放大括号)就释放了。
相对正确的答案应该是:退出runloop之前释放。
这样就引出了RunLoop的第一个应用——自动释放池。
1、自动释放池
这里的举例分析的是APP启动的时候,与主线程同时生成的自动释放池。
在打印[NSRunLoopcurrentRunLoop]的结果中我们可以看到与自动释放池相关的:
<CFRunLoopObserver>{activities=0x1,callout=\n_wrapRunLoopWithAutoreleasePoolHandler}\n\n<CFRunLoopObserver>{activities=0xa0,callout=\n_wrapRunLoopWithAutoreleasePoolHandler}\n
PP启动之后,苹果在主线程对应的RunLoop里面注册了两个Observer,其回调都是
_wrapRunLoopWithAutoreleasePoolHandler()。
第一个Observer监视的事件是Entry(即将进入Loop),其回调内会调用_objc_autoreleasePoolPush()创建自动释放池。其order是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个Observer监视了两个事件:BeforeWaiting(准备进入休眠)时调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop)时调用_objc_autoreleasePoolPop()来释放自动释放池。这个Observer的order是2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被RunLoop创建好的AutoreleasePool环绕着,所以不会出现内存泄漏,开发者也不必显示创建Pool了。
与主线程的RunLoop运行逻辑类似,在程序中自定义的自动释放池,也是在即将退出RunLoop的时候,释放创建的自动释放池。
2、NSTimer(定时器)
NSTimer其实就是CFRunLoopTimerRef,他们之间是toll-freebridged的。
上面有讲到RunLoop本身就是一个圈。更进一步说:不断地围着圈跑。
这个特性,很像城市里环城巴士。
一个NSTimer注册到RunLoop后,RunLoop会为其重复的时间点注册好事件。
例如10:00,10:10,10:20这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。
Timer有个属性叫做Tolerance(宽容度),标示了当时间点到后,容许有多少最大误差。
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果10:10时我忙着玩手机错过了那个点的公交,那我只能等10:20这一趟了。
3、PerformSelecter…
当调用NSObject的performSelecter:afterDelay:后,实际上其内部会创建一个Timer并添加到当前线程的RunLoop中。所以如果当前线程没有RunLoop,则这个方法会失效。
当调用performSelector:onThread:时,实际上其会创建一个Timer加到对应的线程去,同样的,如果对应线程没有RunLoop该方法也会失效。
4、事件响应
苹果注册了一个Source1(基于machport的)用来接收系统事件,其回调函数为__IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由IOKit.framework生成一个IOHIDEvent事件并由SpringBoard接收。
这个过程的详细情况可以参考这里。
SpringBoard只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种Event,随后用machport转发给需要的App进程。
随后苹果注册的那个Source1就会触发回调,并调用_UIApplicationHandleEventQueue()进行应用内部的分发。
_UIApplicationHandleEventQueue()会把IOHIDEvent处理并包装成UIEvent进行处理或分发,其中包括识别UIGesture/处理屏幕旋转/发送给UIWindow等。通常事件比如UIButton点击、touchesBegin/Move/End/Cancel事件都是在这个回调中完成的。
5、手势识别
当上面的_UIApplicationHandleEventQueue()识别了一个手势时,其首先会调用Cancel将当前的touchesBegin/Move/End系列回调打断。
随后系统将对应的UIGestureRecognizer标记为待处理。
苹果注册了一个Observer监测BeforeWaiting(Loop即将进入休眠)事件,这个Observer的回调函数是_UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的GestureRecognizer,并执行GestureRecognizer的回调。
当有UIGestureRecognizer的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
6、UI更新
在当前RunLoop的打印结果我们还可以看到
<CFRunLoopObserver>{activities=0xa0,callout=\n_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
即准备进入睡眠和即将退出loop两个时间点,会调用函数更新UI界面.
当在操作UI时,某个需要变化的UIView/CALayer就被标记为待处理,然后被提交到一个全局的容器去,再在上面的回调执行时才会被取出来进行绘制和调整。
所以如果在一次运行循环中想用如下方法设置一个view的两条移动路径是行不通的。因为它会把视图的属性变化汇总起来,直接让myView从起点移动到终点了:
CGRectframe=self.myView.frame;\n//先向下移动\nframe.origin.y+=200;\n[UIViewanimateWithDuration:1animations:^{\nself.myView.frame=frame;\n[self.myViewsetNeedsDisplay];\n}];\n
//再向右移动\nframe.origin.x+=200;\n[UIViewanimateWithDuration:1animations:^{\nself.myView.frame=frame;\n[self.myViewsetNeedsDisplay];\n}];
在仔细分析一下,上面代码的逻辑。
第一个动画是想要做到用1秒的时间,Y值增加200。第二个动画想要实现的是用1秒的时间,X值增加200.
想要实现的先下后右。
但是这样是无法实现的。因为,UI的绘制是拿到所有之后,在统一绘制的。
7、GCD
RunLoop底层会用到GCD的东西,GCD的某些API也用到了RunLoop。如当调用了dispatch_async(dispatch_get_main_queue(),block)时,主队列会把该block放到对应的线程(恰好是主线程)中,主线程的RunLoop会被唤醒,从消息中取得这个block,回调CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE()来执行这个block:
五、第三方库RunLoop的运用
1、AFNetWorking里面的常驻线程
(友情提醒:NSURLConnection在AFNetWorking3.0及以后已经弃用)
子线程默认是完成任务后结束。当要经常使用子线程,每次开启子线程比较耗性能。此时可以开启子线程的RunLoop,保持RunLoop运行,则使子线程保持不死。AFNetworking基于NSURLConnection时正是这样做的,希望在后台线程能保持活着,从而能接收到delegate的回调。
这一点充分体现了:我们控制了runloop,就是控制了app的生死。
具体做法是:
/*返回一个线程*/\n+(NSThread*)networkRequestThread{\nstaticNSThread*_networkRequestThread=nil;\nstaticdispatch_once_toncePredicate;\ndispatch_once(&oncePredicate,^{\n//创建一个线程,并在该线程上执行下一个方法\n_networkRequestThread=[[NSThreadalloc]initWithTarget:self\nselector:@selector(networkRequestThreadEntryPoint:)\nobject:nil];\n//开启线程\n[_networkRequestThreadstart];\n});\nreturn_networkRequestThread;\n}\n/*在新开的线程中执行的第一个方法*/\n+(void)networkRequestThreadEntryPoint:(id)__unusedobject{\n@autoreleasepool{\n[[NSThreadcurrentThread]setName:@&34;];\n//获取当前线程对应的RunLoop\nNSRunLoop*runLoop=[NSRunLoopcurrentRunLoop];\n//为RunLoop添加source,模式为DefaultMode\n[runLoopaddPort:[NSMachPortport]forMode:NSDefaultRunLoopMode];\n//开始运行RunLoop\n[runLooprun];\n}\n}
因为RunLoop启动前必须设置一个mode,而mode要存在则至少需要一个source/timer。所以上面的做法是为RunLoop的DefaultMode添加一个NSMachPort对象,虽然消息是可以通过NSMachPort对象发送到loop内,但这里添加的port只是为了RunLoop一直不退出,而没有发送什么消息。当然我们也可以添加一个超长启动时间的timer来既保持RunLoop不退出也不占用资源。
2、AsyncDisplayKit
AsyncDisplayKit是Facebook推出的用于保持界面流畅性的框架,其原理大致如下:
UI线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。
排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。绘制一般有文本绘制(例如CoreText)、图片绘制(例如预先解压)、元素绘制(Quartz)等操作。
UI对象操作通常包括UIView/CALayer等UI对象的创建、设置属性和销毁。
其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果(例如TextView创建时可能需要提前计算出文本的大小)。ASDK所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟(例如视图的创建、属性的调整)。
为此,ASDK创建了一个名为ASDisplayNode的对象,并在内部封装了UIView/CALayer,它具有和UIView/CALayer相似的属性,例如frame、backgroundColor等。所有这些属性都可以在后台线程更改,开发者可以只通过Node来操作其内部的UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的UIView/CALayer去。
ASDK仿照QuartzCore/UIKit框架的模式,实现了一套类似的界面更新的机制:即在主线程的RunLoop中添加一个Observer,监听了kCFRunLoopBeforeWaiting和kCFRunLoopExit事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。
六、开发中常见RunLoop使用
上面系统介绍了苹果官方对于RunLoop的使用和常见的第三方类库的使用。
但是,我们实际应用最多的可能会是下面这些具体场景。
1、UIImageView延迟加载图片
假设我们有一个UITableView,UITableView上面有很多UITableViewCell,UITableViewCell上面有一个UIImageView(你可以想象QQ的聊天页面)。这时候一般我们的需求都是那个UIImageView的图片需要你从网络上下载,并且异步,下载成功之后更新到UIImageView上。
实际上这个时候我们就会碰到问题,因为我们的UITableView是可以任意拖动的,所以如果不更改NSURLConnection的运行模式,那么只要UItableView出现滑动,NSURLConnection所在的DefaultMode就会退出,切换到UITrackingRunLoopMode。
给UIImageView设置图片可能耗时不少,如果此时要滑动tableView等则可能影响到界面的流畅。
解决是:使用performSelector:withObject:afterDelay:inModes:方法,将设置图片的方法放到DefaultMode中执行。
为了流畅性,把图片加载延迟。
2、UITableView与NSTimer冲突
由于UItabelView在滑动的时候,会从当前的RunLoop默认的模式kCFRunLoopDefaultMode(NSDefaultRunLoopMode)退出,进入到UITrackingRunLoopMode。
这个时候,处于NSDefaultRunLoopMode里面的NSTimer由于切换了模式造成计时器无法继续运行。
可以两个解决方法:(目的就是:即使mode切换,计时器依然工作)
此处参考:iOS下RunLoop的实际应用场景探究
七、RunLoop可能的面试套路
RunLoop面试小结
什么是RunLoop?
从字面上看:运行循环、跑圈
其实它内部就是do-while循环,在这个循环内部不断的处理各种任务(比如Source、Timer、Observer)
一个线程对应一个RunLoop,主线程的RunLoop默认已经启动,子线程的RunLoop需要手动启动(调用run方法)RunLoop只能选择一个Mode启动,如果当前Mode中没有任何Soure、Timer、Observer,那么就直接退出RunLoop
在开发中如何使用RunLoop?什么应用场景?\n开启一个常驻线程(让一个子线程不进入消亡状态,等待其他线程发来消息,处理其他事件)\n在子线程中开启一个定时器\n在子线程中进行一些长期监控
可以控制定时器在特定模式下执行可以让某些事件(行为、任务)在特定模式下执行可以添加Observer监听RunLoop的状态,比如监听点击事件的处理(在所有点击事件之前做一些事情)
所有参考链接:
1、ibireme
2、百度大神
3、视频-runloop线下分享
4、RunLoop实际应用
5、官方文档
6、CFRunLoop.c
头条热搜#2020生机大计鸣谢:吴佩在天涯
关于cf辅助网站源码分享购买的内容到此结束,希望对大家有所帮助。
