iOS 模块分解—「Runloop 面试工作」看我就 🐒 了 ^_^.
Posted plainboiledwater
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 模块分解—「Runloop 面试工作」看我就 🐒 了 ^_^.相关的知识,希望对你有一定的参考价值。
释义:
Run loops 是线程相关底层基础的一部分。它的本质和字面意思一样运行着的循环(事件处理的循环),作用:接受循环事件和安排线程的工作。目的:让线程在有任务的时候忙于工作,而没任务的时候处于休眠状态。
Run loop 的管理并非完全自动。你仍然需要设置线程代码在合适的时候启动
run loop
来帮助你处理输入事件。ios 中 Cocoa 和 CoreFoundation 框架中各有完整的一套关于runloop
对象的操作api,在主线程中run loop
是自动创建并运行(在子线程开启RunLoop 需要手动创建且手动开启
)。
译文&源码
Runloop 尽管在平时多数开发者很少直接使用,但是理解 RunLoop 可以帮助开发者更好的利用多线程编程模型,同时也可以帮助开发者解答面试套路的一些疑惑,对于 iOS 编程 熟知它是必不可少的,下面是我对 Runloop 的整理,并且带有几个开发场景。 --> 大神可选择性路过「思想」。
目录
- Runloop 概念 & 作用
- Runloop 开启&退出
- Runloop 和线程关系
- Runloop 获取 & 源码
- Runloop 相关5个类
- Runloop 场景
1.Runloop 经典应用:常驻线程
2.AutoreleasePool 自动释放池
3.UI更新
4.UIImageView 延迟加载图片
5.UITableView 与 NSTimer 冲突- Runloop 模块博文推荐(??数量较多)
- Demo 重要的部分代码中都有相应的注解和文字打印,运行程序可以很直观的表现。
- Runtime 模块详解「面试、工作」看我就 ?? 了 ^_^.
Runloop 概念
- 【Runloop 释义】:"运行循环"、
"跑圈" - 【注解1】:iOS 中通常所说的
RunLoop
指的是 NSRunloop(Foundation框架)
或者 CFRunloopRef(CoreFoundation 框架)
,CFRunloopRef
是纯C的函数,而NSRunloop
仅仅是CFRunloopRef
的一层OC封装,并未提供额外的其他功能,因此要了解RunLoop
内部结构,需要多研究 CFRunLoopRef API(Core Foundation 更底层
)。 - 【注解2】:CFRunloopRef 其实就是 __CFRunloop 这个结构体指针(
按照OC的思路我们可以将RunLoop看成一个对象
),这个对象的运行才是我们通常意义上说的运行循环,核心方法是__CFRunloopRun() 查看下(附:源码)
。
Runloop 作用
- 1、保持程序的持续运行(
如:程序一启动就会开启一个主线程(中的 runloop 是自动创建并运行),runloop 保证主线程不会被销毁,也就保证了程序的持续运行
)。 - 2、处理App中的各种事件(
如:touches 触摸事件、NSTimer 定时器事件、Selector事件(选择器 performSelector)
)。 - 3、节省CPU资源,提高程序性能(
有事情就做事情,没事情就休息 (其资源释放)
)。 - 4、负责渲染屏幕上的所有UI。
附:CFRunLoop.c 源码
#【用DefaultMode启动,具体实现查看 CFRunLoopRunSpecific Line2704】
#【RunLoop的主函数,是一个死循环 dowhile】
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
/*
参数一:CFRunLoopRunSpecific 具体处理runloop的运行情况
参数二:CFRunLoopGetCurrent() 当前runloop对象
参数三:kCFRunLoopDefaultMode runloop的运行模式的名称
参数四:1.0e10 runloop默认的运行时间,即超时为10的九次方
参数五:returnAfterSourceHandled 回调处理
*/
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
//【判断】:如果runloop没有停止 且 没有结束则继续循环,相反侧退出。
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
#【直观表现】
RunLoop 其实内部就是do-while循环,在这个循环内部不断地处理各种任务(`比如Source、Timer、Observer`),
通过判断result的值实现的。所以 可以看成是一个死循环。
如果没有RunLoop,UIApplicationMain 函数执行完毕之后将直接返回,就是说程序一启动然后就结束;
Runloop 开启&退出
我们来验证 Runloop 是在那开启的?答案:UIApplicationMain 中开启;
#【验证 Runloop 的开启】。
# int 类型返回值
UIKIT_EXTERN int UIApplicationMain(int argc, char *argv[], NSString * __nullable principalClassName, NSString * __nullable delegateClassName);
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"开始");
int number = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"结束");
return number;
}
}
#【验证结果】:只会打印开始,并不会打印结束。
----
#【Runloop 的退出条件】。
App退出;线程关闭;设置最大时间到期;
【注解】:说明在UIApplicationMain函数内部开启了一个和主线程相关的RunLoop (保证主线程不会被销毁),导致 UIApplicationMain 不会返回,一直在运行中,也就保证了程序的持续运行。
Runloop和线程关系
【附】:CFRunLoop.c 源码
# NOTE: 获得runloop实现 (创建runloop)
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {// ??【主线程相关联的RunLoop创建】,如果为空,默认是主线程
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) { // 如果 RunLoop 不存在
__CFUnlock(&loopsLock);
// 创建字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 创建主线程对应的runloop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 使用字典保存(KEY:线程 -- Value:线程对应的runloop), 以保证一一对应关系。
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// ??【创建与子线程相关联的RunLoop】,从字典中获取 子线程的runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
// 如果子线程的runloop不存在,那么就为该线程创建一个对应的runloop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
// 把当前子线程和对应的runloop保存到字典中
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don‘t release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
- 【由上源码可得】:RunLoop 和 线程关系
- 1.每条线程都有唯一的一个与之对应的RunLoop对象。
- 2.主线程的RunLoop已经自动创建,子线程的RunLoop需要主动创建。
- 3.RunLoop在第一次获取时创建,在线程结束时销毁。
- 【注解】:Runloop 对象是利用字典来进行存储,而且 Key:线程 -- Value:线程对应的 runloop。
- iOS开发过程中对于开发者而言更多的使用的是NSRunloop,它默认提供了三个常用的run方法
如何创建子线程对应的 Runloop ?
- 【解决】:开一个子线程创建
runloop
,不是通过[alloc init]
方法创建,而是直接通过调用currentRunLoop
方法来创建。 - 【原因】:
currentRunLoop
本身是懒加载的,当第一次调用currentRunLoop
方法获得该子线程对应的Runloop
的时候,它会先去判断(去字典中查找)这个线程的Runloop
是否存在,如果不存在就会自己创建并且返回,如果存在直接返回。
Runloop 获取
// Foundation框架
NSRunLoop *mainRunloop = [NSRunLoop mainRunLoop]; // 获得主线程对应的 runloop对象
NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop]; // 获得当前线程对应的runloop对象
// Core Foundation框架
CFRunLoopRef maiRunloop = CFRunLoopGetMain(); // 获得主线程对应的 runloop对象
CFRunLoopRef maiRunloop = CFRunLoopGetCurrent(); // 获得当前线程对应的runloop对象
// NSRunLoop <--> CFRunLoopRef 相互转化
NSLog(@"NSRunLoop <--> CFRunloop == %p--%p",CFRunLoopGetMain() , [NSRunLoop mainRunLoop].getCFRunLoop);
#【打印结果】:内存地址相同
0000-00-13 00:30:16.527 MultiThreading[57703:1217113] NSRunLoop <--> CFRunloop == 0x60000016a680--0x60000016a680
Runloop 源码
Runloop 相关内部实现源码,代码量甚多,其核心方法是 【__CFRunLoopRun】 ,为了不影响文章的可读性,这里就不再直接贴源代码,放一段伪代码方便大家阅读【转】:
int32_t __CFRunLoopRun()
{
// 通知即将进入runloop
__CFRunLoopDoObservers(KCFRunLoopEntry);
do
{
// 通知将要处理timer和source
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
// 执行被加入的Block(处理非延迟的主线程调用)
__CFRunLoopDoBlocks();
// 处理Source0事件
__CFRunLoopDoSource0();
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks();
}
// 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort();
if (hasMsg) goto handle_msg;
}
// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
// GCD dispatch main queue
CheckIfExistMessagesInMainDispatchQueue();
// 即将进入休眠
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
// 等待内核mach_msg事件
mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
// 等待。。。
// 从等待中醒来
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
// 处理因timer的唤醒
if (wakeUpPort == timerPort)
__CFRunLoopDoTimers();
// 处理异步方法唤醒,如dispatch_async
else if (wakeUpPort == mainDispatchQueuePort)
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
// 处理Source1
else
__CFRunLoopDoSource1();
// 再次确保是否有同步的方法需要调用
__CFRunLoopDoBlocks();
} while (!stop && !timeout);
// 通知即将退出runloop
__CFRunLoopDoObservers(CFRunLoopExit);
}
线程执行了这个函数 (__CFRunLoopRun) 后,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束函数才返回,当然Runloop精华在于在休眠时几乎不会占用系统资源(系统内核负责)。
面对上面的一段伪代码不知道做什么的函数调用 , 这里你如果想结合上段的伪代码看源码的话,可以 CFRunLoop.c 源码 (c) 2015 上面的每一步都有相应的注解,打开对照查看可以很直观的表现。
【NOTE】:
当然我 也整理了一张图,描述了 Runloop 内部实现流程(版1 & 版2 基本描述了 Runloop 的核心流程,当然可还是对照查看官方文档或源码)。
【注意】:尽管 CFRunLoopPerformBlock 在上图中作为唤醒机制有所体现,但事实上执行 CFRunLoopPerformBlock 只是入队,下次 RunLoop 运行才会执行,而如果需要立即执行则必须调用 CFRunLoopWakeUp 。
Runloop 相关类
Core Foundation 中关于 RunLoop 的5个类
- 1、CFRunloopRef【RunLoop本身】
- 2、CFRunloopModeRef【Runloop的运行模式】
- 3、CFRunloopSourceRef【Runloop要处理的事件源】
- 4、CFRunloopTimerRef【Timer事件】
- 5、CFRunloopObserverRef【Runloop的观察者(监听者)】
CFRunLoop 的5个相关类关系图解:
【图解直观得知】:
- 一条线程 对应一个 Runloop,Runloop 总是运行在某种特定的CFRunLoopModeRef(
运行模式
)下。 - 每个 Runloop 都可以包含若干个 Mode ,每个 Mode 又包含Source源 / Timer事件 / Observer观察者。
- 在 Runloop 中有多个运行模式,每次调用 RunLoop 的主函数【__CFRunloopRun()】时,只能指定其中一个 Mode(
称 CurrentMode
)运行, 如果需要切换 Mode,只能是退出 CurrentMode 切换到指定的 Mode 进入,目的以保证不同 Mode 下的 Source / Timer / Observer 互不影响。 Runloop 有效,mode 里面 至少 要有一个timer(定时器事件) 或者是source(源);
附:源码
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name; // mode名
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0; // source0 源
CFMutableSetRef _sources1; // source1 源
CFMutableArrayRef _observers; // observer 源
CFMutableArrayRef _timers; // timer 源
CFMutableDictionaryRef _portToV1SourceMap;// mach port 到 mode的映射,为了在runloop主逻辑中过滤runloop自己的port消息。
__CFPortSet _portSet;// 记录了所有当前mode中需要监听的port,作为调用监听消息函数的参数。
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;// 使用 mk timer, 用到的mach port,和source1类似,都依赖于mach port
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR timer触发的理想时间*/
uint64_t _timerHardDeadline; /* TSR timer触发的实际时间,理想时间加上tolerance(偏差*/
};
Runloop 相关类(Mode)
CFRunLoopModeRef 代表 RunLoop 的运行模式;系统默认提供了5个 Mode 。
1.【kCFRunLoopDefaultMode (NSDefaultRunLoopMode)】: App的默认Mode,通常主线程是在这个Mode下运行。
2.【UITrackingRunLoopMode】: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
3.【UIInitializationRunLoopMode】: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
4.【GSEventReceiveRunLoopMode】: 接受系统事件的内部 Mode,通常用不到。
5.【kCFRunLoopCommonModes (NSRunLoopCommonModes)】: 这个并不是某种具体的 Mode, 可以说是一个占位用的Mode(一种模式组合)。
- CFRunLoop 对外暴露的管理 Mode 接口:
# CFRunLoop
CF_EXPORT void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFRunLoopMode mode);
CF_EXPORT CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);
# NSRunLoop.h
FOUNDATION_EXPORT NSRunLoopMode const NSDefaultRunLoopMode;// (默认):同一时间只能执行一个任务
FOUNDATION_EXPORT NSRunLoopMode const NSRunLoopCommonModes NS_AVAILABLE(10_5, 2_0); // (公用):可以分配一定的时间处理定时器
【注】:对照上面贴的源码,关于 CommonModes ;
【关于 _commonModes】:一个
mode
可以标记为common
属性(用于 CFRunLoopAddCommonMode函数
),然后它就会保存在_commonModes
。主线程CommonModes
默认已有两个modekCFRunLoopDefaultMode 和 UITrackingRunLoopMode
,当然你也可以通过调用CFRunLoopAddCommonMode()
方法将自定义mode
放到kCFRunLoopCommonModes
组合)。【关于 _commonModeItems】:
_commonModeItems
里面存放的source, observer, timer
等,在每次runLoop
运行的时候都会被同步到具有Common
标记的Modes
里。如:[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
就是把timer放到commonModeItems
里。更多系统或框架 Mode查看这里
Runloop 相关类(Source)
CFRunloopSourceRef 事件源 输入源,有两种分类模式
【官方版】:
【Port-Based Sources】: 基于端口的源
(对应的是source1)
:与内核端口相关,只需要简单的创建端口对象,并使用NSPort
的方法将端口对象加入到runloop
,端口对象会处理创建以及配置输入源;。【Custom Input Sources】:自定义源:使用
CFRunLoopSourceRef
类型相关的函数 (线程) 来创建自定义输入源。【Perform Selector Sources】:performSelector:OnThread:delay:
- 【源码版】:按照函数调用栈的分类 source0 和 source1
- Source0:非基于端口Port的事件;(用于用户主动触发的事件,如:点击按钮 或点击屏幕)。
- Source1:基于端口Port的事件;(通过内核和其他线程相互发送消息,与内核相关)。
- 补充:Source1 事件在处理时会分发一些操作给 Source0 去处理。
Runloop 相关类(Timer)
CFRunLoopTimerRef是基于时间的触发器。
基本上说的就是NSTimer(CADisplayLink也是加到RunLoop),它受RunLoop的Mode影响。
而与NSTimer相比,GCD定时器不会受Runloop影响。
Runloop 相关类(Observer)
相对来说CFRunloopObserverRef理解起来并不复杂,它相当于消息循环中的一个监听器,随时通知外部当前RunLoop的运行状态(它包含一个函数指针_callout_将当前状态及时告诉观察者
)。具体的Observer状态如下:
/* jianshu:白开水ln Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即将进入Runloop
kCFRunLoopBeforeTimers = (1UL << 1), //即将处理NSTimer
kCFRunLoopBeforeSources = (1UL << 2), //即将处理Sources
kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //从休眠装填中唤醒
kCFRunLoopExit = (1UL << 7), //退出runloop
kCFRunLoopAllActivities = 0x0FFFFFFFU //所有状态改变
};
Runloop 休眠
摘录:http://www.cnblogs.com/kenshincui/p/6823841.html
其实对于 Event Loop 而言 RunLoop 最核心的事情就是保证线程在没有消息时休眠以避免占用系统资源,有消息时能够及时唤醒。 RunLoop 的这个机制完全依靠系统内核来完成,具体来说是苹果操作系统核心组件 Darwin 中的 Mach 来完成的。可以从下图最底层 Kernel 中找到 Mach:
Mach 是 Darwin 的核心,可以说是内核的核心,提供了进程间通信(IPC)、处理器调度等基础服务。在 Mach 中,进程、线程间的通信是以消息的方式来完成的,消息在两个 Port 之间进行传递(这也正是 Source1 之所以称之为 Port-based Source 的原因,因为它就是依靠系统发送消息到指定的Port来触发的)。消息的发送和接收使用<mach/message.h>中的mach_msg()
函数(事实上苹果提供的Mach API 很少,并不鼓励我们直接调用这些API):
/*
* Routine: mach_msg
* Purpose:
* Send and/or receive a message. If the message operation
* is interrupted, and the user did not request an indication
* of that fact, then restart the appropriate parts of the
* operation silently (trap version does not restart).
*/
__WATCHOS_PROHIBITED __TVOS_PROHIBITED
extern mach_msg_return_t mach_msg(
mach_msg_header_t *msg,
mach_msg_option_t option,
mach_msg_size_t send_size,
mach_msg_size_t rcv_size,
mach_port_name_t rcv_name,
mach_msg_timeout_t timeout,
mach_port_name_t notify);
而 mach_msg() 的本质是一个调用 mach_msg_trap()
,这相当于一个系统调用,会触发内核状态切换。当程序静止时,RunLoop停留在__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy)
,而这个函数内部就是调用了mach_msg
让程序处于休眠状态。
Runloop 相关5个类代码示例
Mode-Runloop的运行模式、Source-Runloop要处理的事件源、Timer-定时器事件
Runloop 场景
1、NSTimer
2、ImageView显示:控制方法在特定的模式下可用
3、PerformSelector
4、常驻线程:在子线程中开启一个runloop
5、AutoreleasePool 自动释放池
6、UI更新
Runloop 经典应用:常驻线程
【注解】:常驻线程:线程创建出来就处于等待状态(有或无任务),想用它的时候就用它执行任务,不想用的时候就处于等待状态。
【场景】:如:1.聊天发送语音消息,可能会专门开一个子线程来处理;2.在后台记录用户的停留时间或某个按钮点击次数,这些用主线程做可能不太方便,可能会开启一个子线程后台默默收集;
【需求】:让线程持续存在,可以切换执行其他任务。
【解决】:开启 Runloop循环。
Demo & 效果图:
AutoreleasePool 自动释放池
AutoreleasePool
是另一个与 RunLoop
相关讨论较多的话题。其实从RunLoop
源代码分析,AutoreleasePool
与 RunLoop
并没有直接的关系,之所以将两个话题放到一起讨论最主要的原因是因为在iOS应用启动后会注册两个 Observer
管理和维护 AutoreleasePool。不妨在应用程序刚刚启动时打印 currentRunLoop
可以看到系统默认注册了很多个Observer
,其中有两个Observer
的 callout 都是 _ wrapRunLoopWithAutoreleasePoolHandler
,这两个是和自动释放池相关的两个监听。
<CFRunLoopObserver 0x6080001246a0 [0x101f81df0]>{valid =
Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce),
context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
‘‘ <CFRunLoopObserver 0x608000124420 [0x101f81df0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647,
callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>
{type = mutable-small, count = 0, values = ()}}
第一个 Observer 会监听 RunLoop 的进入,它会回调objc_autoreleasePoolPush() 向当前的 AutoreleasePoolPage 增加一个哨兵对象标志创建自动释放池。这个 Observer 的 order 是 -2147483647 优先级最高,确保发生在所有回调操作之前。
第二个 Observer 会监听 RunLoop 的进入休眠和即将退出 RunLoop 两种状态,在即将进入休眠时会调用 objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根据情况从最新加入的对象一直往前清理直到遇到哨兵对象。而在即将退出 RunLoop 时会调用objc_autoreleasePoolPop() 释放自动自动释放池内对象。这个Observer 的 order 是 2147483647 ,优先级最低,确保发生在所有回调操作之后。
主线程的其他操作通常均在这个 AutoreleasePool 之内(main函数中),以尽可能减少内存维护操作(当然你如果需要显式释放【例如循环】时可以自己创建 AutoreleasePool 否则一般不需要自己创建)。
UI更新
如果打印App启动之后的主线程RunLoop可以发现另外一个callout为
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv
的 Observer,这个监听专门负责UI变化后的更新,比如修改了frame、调整了UI层级(UIView/CALayer)或者手动设置了setNeedsDisplay/setNeedsLayout 之后就会将这些操作提交到全局容器。而这个Observer监听了主线程RunLoop的即将进入休眠和退出状态,一旦进入这两种状态则会遍历所有的UI更新并提交进行实际绘制更新。通常情况下这种方式是完美的,因为除了系统的更新,还可以利用 setNeedsDisplay 等方法手动触发下一次 RunLoop 运行的更新。但是如果当前正在执行大量的逻辑运算可能UI的更新就会比较卡,因此facebook 推出了 AsyncDisplayKit 来解决这个问题。AsyncDisplayKit 其实是将UI排版和绘制运算尽可能放到后台,将UI的最终更新操作放到主线程(这一步也必须在主线程完成),同时提供一套类 UIView 或 CALayer 的相关属性,尽可能保证开发者的开发习惯。这个过程中 AsyncDisplayKit 在主线程 RunLoop 中增加了一个Observer 监听即将进入休眠和退出 RunLoop 两种状态,收到回调时遍历队列中的待处理任务一一执行。
UIImageView 延迟加载图片
Demo & 效果图
UITableView 与 NSTimer 冲突
【描述】:由于 UItabelView 在滑动的时候,会从当前的 RunLoop 默认的模式 kCFRunLoopDefaultMode (NSDefaultRunLoopMode)
自动切换到 UITrackingRunLoopMode
界面追踪模式。这个时候,处于 NSDefaultRunLoopMode
里面的 NSTimer
由于切换了模式造成计时器无法继续运行。
【解决】:
- 1、更改RunLoop运行Mode(
NSRunLoopCommonModes
)
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
- 2、将NSTimer放到新的线程中
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
[thread start];
- (void)newThread{
@autoreleasepool{
//在当前Run Loop中添加timer,模式是默认的NSDefaultRunLoopMode
timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(incrementCounter:) userInfo: nil repeats:YES];
//开始执行新线程的Run Loop,如果不启动run loop,timer的事件是不会响应的
[[NSRunLoop currentRunLoop] run];
}
}
Runloop 模块博文推荐(??数量较多)
分享者 | Runloop 模块推荐阅读博文 |
---|---|
xx_cc | 充满灵性的死循环 http://www.jianshu.com/p/b9426458fcf6 |
WeiHing | 原理探究及基本使用 http://www.jianshu.com/p/911549ae4bf8 |
续更 | -- |
参考文章:
- http://www.cnblogs.com/kenshincui/p/6823841.html
- http://www.jianshu.com/p/b9426458fcf6
- http://www.jianshu.com/p/911549ae4bf8
- http://www.jianshu.com/p/ac05ac8428ac
- 附上写的小样 Demo,重要的部分代码中都有相应的注解和文字打印,运行程序可以很直观的表现
Reading
- 各位厂友,由于「时间 & 知识」有限,总结的文章难免有「未全、不足」,该模块将系统化学习,后替换、补充文章内容 ~
- 熬夜写者不易,不知名开发者。
以上是关于iOS 模块分解—「Runloop 面试工作」看我就 🐒 了 ^_^.的主要内容,如果未能解决你的问题,请参考以下文章