iOS开发之LLDB常用命令
iOS开发之LLDB常用命令
一、前言1、简介LLDB是新一代高性能调试器。它构建为一组可重用的组件,可以高度利用较大的LLVM项目中的现有库,例如Clang表达式解析器和LLVM反汇编程序。LLDB是Mac OS X上Xcode的默认调试器,支持在桌面和iOS设备和模拟器上调试C,Objective-C和C ++。LLDB项目中的所有代码都是在标准LLVM许可证下提供的,这是一种开源的“BSD风格”许可证。
对于开发测试同学来说,学习iOS代码调试LLDB命令,能更好地辅助我们通过各种手段如修改变量返回值创造实际难以模拟的环境进行测试,甚至能协助开发同学定位bug。下面小编来分享一些平时常用到的iOS代码调试LLDB命令,希望能对做iOS测试的同学有所帮助。
2、指令格式LLDB命令的各部分由空格分割,如果参数中包含空格,则需要使用双引号括起参数,如果参数中包含双引号或者反斜杠,则需要使用反斜杠进行转义。
<command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument [argument...]]
command:命令subcommand:子命令action:命令操作options:命令选项argument:命令参数比如给test函数设置断点:
breakpoint set -n test
breakpoint 是命令set 是命令操作-n 是命令选项test 是命令参数二、常用指令1、help(帮助)LLDB命令有非常多的功能,完全背下来不太容易,也没必要。开发者可以使用help命令查看相关命令的用法,甚至可以查看help命令的用法。
所有命令可以通过
help <command> [<subcommand>]
查看详细文档。比如
help breakpoint
、
help breakpoint set
(lldb) help breakpoint Commands for operating on breakpoints (see 'help b' for shorthand.)Syntax: breakpoint <subcommand> [<command-options>]The following subcommands are supported: clear -- Delete or disable breakpoints matching the specified source file and line. command -- Commands for adding, removing and listing LLDB commands executed when a breakpoint is hit. delete -- Delete the specified breakpoint(s). If no breakpoints are specified, delete them all. disable -- Disable the specified breakpoint(s) without deleting them. If none are specified, disable all breakpoints. enable -- Enable the specified disabled breakpoint(s). If no breakpoints are specified, enable all of them. list -- List some or all breakpoints at configurable levels of detail. modify -- Modify the options on a breakpoint or set of breakpoints in the executable. If no breakpoint is specified, acts on the last created breakpoint. With the exception of -e, -d and -I, passing an empty argument clears the modification. name -- Commands to manage name tags for breakpoints read -- Read and set the breakpoints previously saved to a file with "breakpoint write". set -- Sets a breakpoint or set of breakpoints in the executable. write -- Write the breakpoints listed to a file that can be read in with "breakpoint read". If given no arguments, writes all breakpoints.For more help on any particular subcommand, type 'help <command> <subcommand>'.(lldb)
2、p和po(打印)
p
命令:
print
命令的简写,使用p 命令可以查看基本数据类型的值;如果使用p命令查看的是对象,那么只会返回对象的指针地址。 p命令后面除了可以接变量、常量,还可以接表达式。
po
命令:
print object
的缩写,可以理解为打印对象。功能与p命令类似,也可以打印常量、变量,打印表达式返回的对象等。p 和 po 的区别在于使用 po 只会输出对应的值,而 p 则会返回值的类型以及命令结果的引用名。
图1:p和po的使用.png在输出结果中有类似于1这样的符号,它是指向对象的一个引用,在控制面板中可以直接使用这个符号来操作对应的对象,它们存在于LLDB的全名空间中,目的是为了辅助调试。$后面的数值是递增的,每打印一个与对象相关的命令,这个值都会加1。打印对象除了以上命令外,也可以在控制台左侧区域,点击变量右键点击 Print Deion of “xxx”,或者选中变量后,点击下边栏的i按钮,即可在控制台区看到打印结果。
另外,开发者可以按照print/的语法为print命令指定打印格式:
p //!< 默认打印十进制p/x //!< 以十六进制打印整数 p/d //!< 以带符号的十进制打印整数 p/u //!< 以无符号的十进制打印整数 p/o //!< 以八进制打印整数 p/t //!< 以二进制打印整数 p/a //!< 以十六进制打印地址 p/c //!< 打印字符常量 p/f //!< 打印浮点数 p/s //!< 打印字符串 p/r //!< 格式化打印
3、expression(执行表达式)
expression
命令是调试过程中最有价值有命令了,既可以打印值也可以修改值,简写为
expr
或者
e
。它能够在调试时,动态的修改变量的值,同时打印出结果,在调试想要让应用执行异常路径(如执行某else情况)或者修改某些中间变量值如(如修改返回状态码以查看客户端相关响应等)非常有用,可以创造各种实际中难以遇到的测试环境辅助测试。用法举例如下图:
图2:expression的使用.pngexpression命令是动态修改变量的值,Xcode还支持动态调用函数。在控制台执行call命令,可以在不修改代码,不重新编译的情况下,在断点调用某个方法,并输出此方法的返回值。
expression、expression --和指令print、p、call的效果一样expression -O --和指令po的效果一样
4、thread和frame(线程和栈帧)在进程停止后,LLDB会选择一个当前线程和线程中当前帧(frame)。很多检测状态的命令可以用于这个线程或帧。
1)thread list (列出所有线程)用于列出所有线程,如下所示,其中星号(*)表示
thread#1
为当前线程。
(lldb) thread listProcess 18671 stopped* thread #1: tid = 0x5fe79, 0x000000010a4f3e28 LLDBTest`-[ViewController viewDidLoad](self=0x00007fb21c1091f0, _cmd="viewDidLoad") at ViewController.m:21:18, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 thread #2: tid = 0x5ff6c, 0x00007fff5e73c458 libsystem_pthread.dylib`start_wqthread thread #3: tid = 0x5ff6d, 0x00007fff5e70253e libsystem_kernel.dylib`__workq_kernreturn + 10 thread #4: tid = 0x5ff6e, 0x00007fff5e73c458 libsystem_pthread.dylib`start_wqthread thread #5: tid = 0x5ff6f, 0x00007fff5e73c458 libsystem_pthread.dylib`start_wqthread thread #6: tid = 0x5ff70, 0x00007fff5e700e7e libsystem_kernel.dylib`mach_msg_trap + 10, name = 'com.apple.uikit.eventfetch-thread' thread #7: tid = 0x5ff71, 0x00007fff5e70253e libsystem_kernel.dylib`__workq_kernreturn + 10 thread #8: tid = 0x5ff72, 0x00007fff5e73c458 libsystem_pthread.dylib`start_wqthread thread #9: tid = 0x5ff74, 0x00007fff5e70253e libsystem_kernel.dylib`__workq_kernreturn + 10(lldb)
2)thread backtrace(堆栈打印,简写bt)bt命令可以打印出线程的堆栈信息,bt命令是打印当前线程的堆栈信息,如下图所示。该信息比左侧的Debug Navigator 看到的还要详细一些。如果嫌堆栈打印太长,可以加一个值限制,如
bt 10
。
bt all
命令可以打印所有线程的堆栈信息。
图3:bt指令.png3)thread return(跳出当前方法的执行)
thread return
主要用于控制程序流程,如想要直接跳过执行某个方法,或者直接让某方法返回一个想要的值,thread return后有一个可选参数,在执行时它会把可选参数加载进返回寄存器里,然后立刻执行返回命令,跳出当前栈帧(如:numberOfSectionsInTableView:方法直接thread return 10,就可以直接跳过方法执行,返回10)。
4)frame variable(查看帧变量)
frame variable
命令显示当前帧的变量,默认输出当前帧所有参数和所有局部变量,如下所示:
(lldb) frame variable(ViewController *) self = 0x00007fd09f405ed0(SEL) _cmd = "viewDidLoad"(__NSArrayI *) temArr = 0x0000600002094ff0 @"3 elements"(lldb)
5)frame variable(流程控制)thread continue?/continue?/c: 程序继续运行thread step-over/next/n:单步运行(源码),不会进入子函数thread step-in/step/s: 单步运行(源码),会进入子函数thread step-out/finish: 直接执行完当前函数的所有代码,返回到调用的地方thread return [value]/thread r:让当前函数直接返回,不执行当前断点后面代码thread step-inst-over/nexti/ni:单步运行(汇编),不会进入子函数thread step-inst/stepi/si:单步运行(汇编),会进入子函数前面四个对应Xcode工具栏的前四个:
图4:流程控制.png5、breakpoint(断点)1)breakpoint set(设置断点,可以简写为
br
)
breakpoint set -a 函数地址
breakpoint set -n 函数符号
(lldb) breakpoint set -a 0x000000010189bde7Breakpoint 3: where = LLDBTest`-[ViewController test] + 23 at ViewController.m:26:5, address = 0x000000010189bde7(lldb) breakpoint set -n "-[ViewController viewDidLoad]"Breakpoint 4: where = LLDBTest`-[ViewController viewDidLoad] + 30 at ViewController.m:17:5, address = 0x000000010189bcfe(lldb) breakpoint set -n touchesBegan:withEvent:Breakpoint 5: 97 locations.(lldb)
2)breakpoint list(列出所有的断点)下面有4个断点,每个断点都有一个整数编号
(lldb) breakpoint listCurrent breakpoints:1: file = '/Users/zhangjh48/Desktop/LLDBTest/LLDBTest/ViewController.m', line = 21, exact_match = 0, locations = 1, resolved = 1, hit count = 1 1.1: where = LLDBTest`-[ViewController viewDidLoad] + 152 at ViewController.m:21:18, address = 0x000000010189bd78, resolved, hit count = 1 3: address = LLDBTest[0x0000000100001de7], locations = 1, resolved = 1, hit count = 0 3.1: where = LLDBTest`-[ViewController test] + 23 at ViewController.m:26:5, address = 0x000000010189bde7, resolved, hit count = 0 4: name = '-[ViewController viewDidLoad]', locations = 1, resolved = 1, hit count = 0 4.1: where = LLDBTest`-[ViewController viewDidLoad] + 30 at ViewController.m:17:5, address = 0x000000010189bcfe, resolved, hit count = 0 6: file = '/Users/zhangjh48/Desktop/LLDBTest/LLDBTest/ViewController.m', line = 30, exact_match = 0, locations = 1, resolved = 1, hit count = 1 6.1: where = LLDBTest`-[ViewController touchesBegan:withEvent:] + 70 at ViewController.m:30:6, address = 0x000000010189be46, resolved, hit count = 1 (lldb)
3)设置断点状态
breakpoint disable 断点编号
: 禁用断点
breakpoint enable 断点编号
: 启用断点
breakpoint delete 断点编号
: 删除断点
(lldb) breakpoint disable 11 breakpoints disabled.(lldb) breakpoint enable 11 breakpoints enabled.(lldb) breakpoint delete 31 breakpoints deleted; 0 breakpoint locations disabled.(lldb)
4)断点命令
breakpoint command add 断点编号
: 给断点预先设置需要执行的命令,当触发断点时候,就会执行
(lldb) breakpoint set -n "-[ViewController test]"Breakpoint 3: where = LLDBTest`-[ViewController test] + 23 at ViewController.m:26:5, address = 0x0000000100cf8de7(lldb) breakpoint command add 3Enter your debugger command(s). Type 'DONE' to end.> print "断点命中了"> print "断点参数为"> po self> DONE2021-03-31 08:54:15.029736+0800 LLDBTest[2178:59045] ( "\U5317\U4eac", "\U4e0a\U6d77", "\U5e7f\U5dde") print "断点命中了"(const char [16]) $0 = "断点命中了" print "断点参数为"(const char [16]) $1 = "断点参数为" po self<ViewController: 0x7fe7f3107810>(lldb)
breakpoint command list 断点编号
: 列出断点的绑定的命令
breakpoint command delete 断点编号
: 删除断点绑定的命令
(lldb) breakpoint command list 3Breakpoint 3: Breakpoint commands: print "断点命中了" print "断点参数为" po self(lldb) breakpoint command delete 3(lldb) breakpoint command list 3Breakpoint 3 does not have an associated command.(lldb)
6、watchpoint(内存断点)相比较于
breakpoint
是对方法生效的断点,
watchpoint
则是对地址生效的断点。
watchpoint
类似于
KVO
的工作原理,当观察到属性地址里面的东西改变时,就让程序中断。
watchpoint set variable 变量
图5:内存断点.png
watchpoint set expression 地址
watchpoint list
watchpoint diable 断点编号
watchpoint enable 断点编号
watchpoint delete 断点编号
watchpoint command add 断点编号
watchpoint command list 断点编号
watchpoint command delete 断点编号
7、image(镜像寻址)
image
命令主要用于寻址,一般是通过各种组合命令实现不同功能。
1)image list(列出所有模块信息)
image list -o -f
打印出模块的偏移地址、全路径。可以使用
grep
过滤
(lldb) image list[ 0] D87C3A17-73C3-3DFE-8A60-B423A0E92269 0x00000001059bf000 /Users/zhangjh48/Library/Developer/Xcode/DerivedData/LLDBTest-fkdakpzatewwmvdojsurqlcdeomb/Build/Products/Debug-iphonesimulator/LLDBTest.app/LLDBTest [ 1] DEA51514-B4E8-3368-979B-89D0F8397ABC 0x000000010b7ba000 /usr/lib/dyld (lldb) image list -o -f[ 0] 0x00000000059bf000 /Users/zhangjh48/Library/Developer/Xcode/DerivedData/LLDBTest-fkdakpzatewwmvdojsurqlcdeomb/Build/Products/Debug-iphonesimulator/LLDBTest.app/LLDBTest[ 1] 0x000000010b7ba000 /usr/lib/dyld(lldb) image list -o -f | grep LLDBTest[ 0] 0x00000000059bf000 /Users/zhangjh48/Library/Developer/Xcode/DerivedData/LLDBTest-fkdakpzatewwmvdojsurqlcdeomb/Build/Products/Debug-iphonesimulator/LLDBTest.app/LLDBTest(lldb)
可以使用
image list
命令查看
ASLR
偏移地址,上面表示
LLDBTest
文件的偏移地址为0x00000000059bf000,MachO的TEXT段的偏移地址为0x00000000059bf000(ASLR+PAGEZERO)
2)image lookup(查找模块)
image lookup -t 类型
:查找某个类型的信息
image lookup -a 地址
:根据内存地址查找在模块中的位置
image lookup -n 符号或函数名
:查找某个符号或者函数的位置
(lldb) image lookup -t AppDelegateBest match found in /Users/zhangjh48/Library/Developer/Xcode/DerivedData/LLDBTest-fkdakpzatewwmvdojsurqlcdeomb/Build/Products/Debug-iphonesimulator/LLDBTest.app/LLDBTest:id = {0x200000033}, name = "AppDelegate", byte-size = 8, decl = AppDelegate.h:10, compiler_type = "@interface AppDelegate : UIResponder@end"(lldb) image lookup -n "-[ViewController test]"1 match found in /Users/zhangjh48/Library/Developer/Xcode/DerivedData/LLDBTest-fkdakpzatewwmvdojsurqlcdeomb/Build/Products/Debug-iphonesimulator/LLDBTest.app/LLDBTest: Address: LLDBTest[0x0000000100001df0] (LLDBTest.__TEXT.__text + 144) Summary: LLDBTest`-[ViewController test] at ViewController.m:25(lldb) image lookup -a 0x0000000100a68e07 Address: LLDBTest[0x0000000100001e07] (LLDBTest.__TEXT.__text + 167) Summary: LLDBTest`-[ViewController test] + 23 at ViewController.m:26:5(lldb)
其中
image lookup -a 地址
经常用于查询崩溃位置,通过地址查询崩溃的地方位于哪个模块哪个文件哪个位置
8、register(寄存器)
register
指令能够获取和修改各个寄存器的信息
1)register read (读取所有寄存器信息)注意:真机调试和模拟器调试,打印的值可能不一样,因为他们寄存器不一样
(lldb) register readGeneral Purpose Registers: x0 = 0x0000000105109d20 x1 = 0x0000000104bbe5f0 "testWithA:b:" x2 = 0x0000000000000003 x3 = 0x0000000000000004 x4 = 0x000000016b2459c8 x5 = 0x0000000000000040 x6 = 0x0000000000000044 x7 = 0x0000000000000800 x8 = 0x0000000104bc52b8 "testWithA:b:" x9 = 0x0000000000000303 x10 = 0x000000010681ddb0 x11 = 0x000000000007b700 x12 = 0x000000000000003b x13 = 0x0000000000000000 x14 = 0x0000000000000000 x15 = 0xffffffffffffffff x16 = 0x0000000104bc52f2 (void *)0xf3f00000000104bc x17 = 0x0000000104bbdd18 LLDBTest`-[ViewController testWithA:b:] at ViewController.m:27 x18 = 0x0000000000000000 x19 = 0x0000000105109d20 x20 = 0x0000000000000000 x21 = 0x00000001e5f70000 (void *)0x00000001d5623100 x22 = 0x00000001d1d341fc x23 = 0x0000000000000001 x24 = 0x0000000000000001 x25 = 0x00000001dbd44000 UIKitCore`_UIPreviewInteractionController._currentCommitTransition x26 = 0x000000028301ff80 x27 = 0x00000001e1d384a8 CoreFoundation`__NSArray0__struct x28 = 0x00000001d24cdffc fp = 0x000000016b245b30 lr = 0x0000000104bbdcfc LLDBTest`-[ViewController viewDidLoad] + 192 at ViewController.m:24:5 sp = 0x000000016b245b00 pc = 0x0000000104bbdd34 LLDBTest`-[ViewController testWithA:b:] + 28 at ViewController.m:28:43 cpsr = 0x40000000(lldb)
2)register read/格式 寄存器名称po $x0:打印方法调用者x/s $x1:打印方法名po $x2:打印参数(以此类推,x3、x4也可能是参数)如果是非arm64,寄存器就是r0、r1、r2图6:打印寄存器的值.png3)register write 寄存器名称 数值如下,修改寄存器
$x3
的值为5
(lldb) register read $x3 x3 = 0x0000000000000004(lldb) register write $x3 5(lldb) register read $x3 x3 = 0x0000000000000005(lldb) po $x35(lldb)
9、缩写lldb大部分命令都支持缩写:
命令:expression 缩写:exp命令:thread backtrace 缩写:bt命令:breakpoint list 缩写:br l命令:process continue 缩写:continue, c命令:thread step-over 缩写:next, n命令:thread step-in 缩写:step, s命令:thread step-out 缩写:finish, f命令:thread step-inst-over 缩写:nexti, ni命令:thread step-inst 缩写:stepi, si更多命令可参考:https://lldb.llvm.org/use/map.html
参考链接:https://www.dllhook.com/post/51.html#toc_11https://blog.bombox.org/2019-08-06/ios-lldb/https://www.sohu.com/a/301595611_216613https://zhuanlan.51cto.com/art/201904/595593.htm
iOS RunLoop
什么是RunLoop
从字面意思来看,就是运行循环的意思,其实就是在程序运行过程中循环做一些事情
RunLoop的应用范畴
下面几个技术都需要在RunLoop下才能进行
定时器(Timer)、PerformSelector- GCD Async Main Queue- 事件响应、手势识别、界面刷新- 网络请求- AutoreleasePool在main函数中,如果没有RunLoop,那么下面代码执行完第三行后就会退出程序
获取RunLoop对象
iOS中有2套API来访问和使用RunLoop
Foundation:NSRunLoopCore Foundation:CFRunLoopRefNSRunLoop是基于CFRunLoopRef的一层OC包装,两者都代表着RunLoop对象
CFRunLoopRef的开源代码地址:https://opensource.apple.com/tarballs/CF/
在代码中的调用方法:
NSRunLoop *runloop = [NSRunLoop currentRunLoop];CFRunLoopRef runloop2 = CFRunLoopGetCurrent();
RunLoop与线程
每条线程都有唯一的一个与之对应的RunLoop对象RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建RunLoop会在线程结束时销毁下面两种方式都可以获取到主线程的RunLoop,主线程的RunLoop会在main函数执行UIApplicationMain的时候自动创建,所以调用[NSRunLoop currentRunLoop]时已经有了RunLoop对象
NSLog(@"%p %p", [NSRunLoop currentRunLoop], [NSRunLoop mainRunLoop]);NSLog(@"%p %p", CFRunLoopGetCurrent(), CFRunLoopGetMain());
在子线程里一开始是没有创建RunLoop对象的,需要调用[NSRunLoop currentRunLoop]才会创建
我们可以从源码CFRunloop.c文件中查看对应实现
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { if (pthread_equal(t, kNilPthreadT)) {t = pthread_main_thread_np(); } __CFLock(&loopsLock); // 如果没有__CFRunLoops字典,先创建一个 if (!__CFRunLoops) { __CFUnlock(&loopsLock);CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); // 然后创建主线程的runloop,对应主线程的key存储到字典中CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) { CFRelease(dict);}CFRelease(mainLoop); __CFLock(&loopsLock); } // 从字典__CFRunLoops里面获取runloop // key: pthreadPointer CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFUnlock(&loopsLock); // 如果没有与之对应的runloop对象,就创建一个新的runloop if (!loop) {CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFLock(&loopsLock); // 再次判断是否有该runloop对象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的底层结构
我们通过源码可以看到CFRunLoopRef的本质是一个__CFRunLoop结构体类型
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;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;};
再来查看里面的_currentMode类型为CFRunLoopModeRef,表示RunLoop的运行模式,本质是__CFRunLoopMode的结构体类型
typedef struct __CFRunLoopMode *CFRunLoopModeRef;struct __CFRunLoopMode { CFRuntimeBase _base; pthread_mutex_t _lock;/* must have the run loop locked before locking this */ CFStringRef _name; Boolean _stopped; char _padding[3]; // _sources0、_sources1就是平时需要处理的事情 CFMutableSetRef _sources0; CFMutableSetRef _sources1; CFMutableArrayRef _observers; // 监听器 CFMutableArrayRef _timers; // 定时器 CFMutableDictionaryRef _portToV1SourceMap; __CFPortSet _portSet; 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; Boolean _mkTimerArmed;#endif#if DEPLOYMENT_TARGET_WINDOWS DWORD _msgQMask; void (*_msgPump)(void);#endif uint64_t _timerSoftDeadline; /* TSR */ uint64_t _timerHardDeadline; /* TSR */};
CFRunLoopRef的内部结构可以用下图来表述
CFRunLoopModeRef在RunLoop中的关系可以用下图表示
CFRunLoopModeRef
Mode的作用
一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer- RunLoop启动时只能选择其中一个Mode,作为currentMode如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入 - 不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响切换Mode程序不会退出,也是在内部做的切换如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出RunLoop里面常见的Mode
kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响kCFRunLoopCommonModes默认包括kCFRunLoopDefaultMode、UITrackingRunLoopModeMode中成员的用途
Source0 - 触摸事件处理 - performSelector:onThread:- Source1 - 基于Port的线程间通信 - 系统事件捕捉(比如捕捉到点击事件,然后再交给Source0来做处理)- Timers - NSTimer定时器 - performSelector:withObject:afterDelay:- Observers - 用于监听RunLoop的状态(监听RunLoop是否没有事件要处理了) - UI刷新(在RunLoop将要进入睡眠时唤醒刷新UI) - Autorelease pool(在RunLoop将要进入睡眠时唤醒进行内存释放)RunLoop的运行逻辑
通过代码监听
我们可以通过添加观察者Observer的方式来监听RunLoop的状态以及模式变化
1.通过函数调用的方式来监听
// 创建Observer// kCFRunLoopAllActivities:监听所有状态变化CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);// 添加Observer到RunLoop中CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);// 释放(C语言中create创建后要对应释放观察者)CFRelease(observer);void observeRunLoopActicities(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){ switch (activity) { case kCFRunLoopEntry: NSLog(@"kCFRunLoopEntry"); break; case kCFRunLoopBeforeTimers: NSLog(@"kCFRunLoopBeforeTimers"); break; case kCFRunLoopBeforeSources: NSLog(@"kCFRunLoopBeforeSources"); break; case kCFRunLoopBeforeWaiting: NSLog(@"kCFRunLoopBeforeWaiting"); break; case kCFRunLoopAfterWaiting: NSLog(@"kCFRunLoopAfterWaiting"); break; case kCFRunLoopExit: NSLog(@"kCFRunLoopExit"); break; default: break; }}
2.通过block回调来监听
// 创建ObserverCFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { switch (activity) { case kCFRunLoopEntry: { CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()); NSLog(@"kCFRunLoopEntry - %@", mode); CFRelease(mode); break; } case kCFRunLoopBeforeTimers: { CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()); NSLog(@"kCFRunLoopBeforeTimers - %@", mode); CFRelease(mode); break; } case kCFRunLoopBeforeSources: { CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()); NSLog(@"kCFRunLoopBeforeSources - %@", mode); CFRelease(mode); break; } case kCFRunLoopBeforeWaiting: { CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()); NSLog(@"kCFRunLoopBeforeWaiting - %@", mode); CFRelease(mode); break; } case kCFRunLoopAfterWaiting: { CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()); NSLog(@"kCFRunLoopAfterWaiting - %@", mode); CFRelease(mode); break; } case kCFRunLoopExit: { CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()); NSLog(@"kCFRunLoopExit - %@", mode); CFRelease(mode); break; } default: break; }});// 添加Observer到RunLoop中CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);// 释放CFRelease(observer);
下面是RunLoop状态变化的枚举注释
通过源码分析
我们新建一个项目并运行,然后可以通过LLDB的命令bt来查看详细的调用过程
可以看到RunLoop的调用是从CFRunLoopRunSpecific函数开始的,那么我们在源码可以找到该函数的实现来进行分析
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ CHECK_FOR_FORK(); if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished; __CFRunLoopLock(rl); CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false); if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {Boolean did = false;if (currentMode) __CFRunLoopModeUnlock(currentMode);__CFRunLoopUnlock(rl);return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished; } volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl); CFRunLoopModeRef previousMode = rl->_currentMode; rl->_currentMode = currentMode; int32_t result = kCFRunLoopRunFinished; // 通知observer进入Loopif (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); // 具体要做的事情result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);if (currentMode->_observerMask & kCFRunLoopExit ) // 通知observer,退出Loop __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); __CFRunLoopModeUnlock(currentMode); __CFRunLoopPopPerRunData(rl, previousPerRun);rl->_currentMode = previousMode; __CFRunLoopUnlock(rl); return result;}
我们可以看到核心代码是在__CFRunLoopRun函数中
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { uint64_t startTSR = mach_absolute_time(); if (__CFRunLoopIsStopped(rl)) { __CFRunLoopUnsetStopped(rl);return kCFRunLoopRunStopped; } else if (rlm->_stopped) {rlm->_stopped = false;return kCFRunLoopRunStopped; } mach_port_name_t dispatchPort = MACH_PORT_NULL; Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ))); if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF(); #if USE_DISPATCH_SOURCE_FOR_TIMERS mach_port_name_t modeQueuePort = MACH_PORT_NULL; if (rlm->_queue) { modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue); if (!modeQueuePort) { CRASH("Unable to get port for run loop mode queue (%d)", -1); } }#endif dispatch_source_t timeout_timer = NULL; struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context)); if (seconds <= 0.0) { // instant timeout seconds = 0.0; timeout_context->termTSR = 0ULL; } else if (seconds <= TIMER_INTERVAL_LIMIT) {dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground();timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); dispatch_retain(timeout_timer);timeout_context->ds = timeout_timer;timeout_context->rl = (CFRunLoopRef)CFRetain(rl);timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of contextdispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout); dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel); uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL); dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL); dispatch_resume(timeout_timer); } else { // infinite timeout seconds = 9999999999.0; timeout_context->termTSR = UINT64_MAX; } Boolean didDispatchPortLastTime = true; int32_t retVal = 0; // ---- 循环具体做的事情 do...while ---- do {#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI voucher_mach_msg_state_t voucherState = VOUCHER_MACH_MSG_STATE_UNCHANGED; voucher_t voucherCopy = NULL;#endif uint8_t msg_buffer[3 * 1024];#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI mach_msg_header_t *msg = NULL; mach_port_t livePort = MACH_PORT_NULL; // windows平台#elif DEPLOYMENT_TARGET_WINDOWS HANDLE livePort = NULL; Boolean windowsMessageReceived = false;#endif__CFPortSet waitSet = rlm->_portSet; __CFRunLoopUnsetIgnoreWakeUps(rl); // 通知observer:即将处理timer if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); // 通知observer:即将处理sources if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); // 处理blocks__CFRunLoopDoBlocks(rl, rlm); // 处理source0 Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); if (sourceHandledThisLoop) { // 处理blocks __CFRunLoopDoBlocks(rl, rlm);} Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR); if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI msg = (mach_msg_header_t *)msg_buffer; // 判断有无source1 if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) { // 如果有就跳转到handle_msg goto handle_msg; }#elif DEPLOYMENT_TARGET_WINDOWS if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) { goto handle_msg; }#endif } didDispatchPortLastTime = false;if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) // 通知observer:即将休眠 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);__CFRunLoopSetSleeping(rl);// do not do any user callouts after this point (after notifying of sleeping) // Must push the local-to-this-activation ports in on every loop // iteration, as this mode could be run re-entrantly and we don't // want these ports to get serviced. __CFPortSetInsert(dispatchPort, waitSet); __CFRunLoopModeUnlock(rlm);__CFRunLoopUnlock(rl); CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI#if USE_DISPATCH_SOURCE_FOR_TIMERS do { if (kCFUseCollectableAllocator) { // objc_clear_stack(0); // <rdar://problem/16393959> memset(msg_buffer, 0, sizeof(msg_buffer)); } msg = (mach_msg_header_t *)msg_buffer; // 等待别的消息来唤醒当前线程 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) { // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer. while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue)); if (rlm->_timerFired) { // Leave livePort as the queue port, and service timers below rlm->_timerFired = false; break; } else { if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg); } } else { // Go ahead and leave the inner loop. break; } } while (1);#else if (kCFUseCollectableAllocator) { // objc_clear_stack(0); // <rdar://problem/16393959> memset(msg_buffer, 0, sizeof(msg_buffer)); } msg = (mach_msg_header_t *)msg_buffer; __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);#endif #elif DEPLOYMENT_TARGET_WINDOWS // Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages. __CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived);#endif __CFRunLoopLock(rl); __CFRunLoopModeLock(rlm); rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart)); // Must remove the local-to-this-activation ports in on every loop // iteration, as this mode could be run re-entrantly and we don't // want these ports to get serviced. Also, we don't want them left // in there if this function returns. __CFPortSetRemove(dispatchPort, waitSet); __CFRunLoopSetIgnoreWakeUps(rl); // user callouts now OK again__CFRunLoopUnsetSleeping(rl);if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) // 通知observer:结束休眠 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); // 调用handle_msg handle_msg:; __CFRunLoopSetIgnoreWakeUps(rl);#if DEPLOYMENT_TARGET_WINDOWS if (windowsMessageReceived) { // These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after __CFRunLoopModeUnlock(rlm); __CFRunLoopUnlock(rl); if (rlm->_msgPump) { rlm->_msgPump(); } else { MSG msg; if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) { TranslateMessage(&msg); DispatchMessage(&msg); } } __CFRunLoopLock(rl); __CFRunLoopModeLock(rlm); sourceHandledThisLoop = true; // To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced // Use 0 for the mask so windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later. // NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling. __CFRunLoopSetSleeping(rl); __CFRunLoopModeUnlock(rlm); __CFRunLoopUnlock(rl); __CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL); __CFRunLoopLock(rl); __CFRunLoopModeLock(rlm); __CFRunLoopUnsetSleeping(rl); // If we have a new live port then it will be handled below as normal } #endif // 判断是怎么醒来的 if (MACH_PORT_NULL == livePort) { CFRUNLOOP_WAKEUP_FOR_NOTHING(); // handle nothing } else if (livePort == rl->_wakeUpPort) { CFRUNLOOP_WAKEUP_FOR_WAKEUP(); // do nothing on Mac OS#if DEPLOYMENT_TARGET_WINDOWS // Always reset the wake up port, or risk spinning forever ResetEvent(rl->_wakeUpPort);#endif } #if USE_DISPATCH_SOURCE_FOR_TIMERS // 被timer唤醒 else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) { CFRUNLOOP_WAKEUP_FOR_TIMER(); if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) { // Re-arm the next timer, because we apparently fired early // 处理timers __CFArmNextTimerInMode(rlm, rl); } }#endif#if USE_MK_TIMER_TOO // 被timer唤醒 else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) { CFRUNLOOP_WAKEUP_FOR_TIMER(); // On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled. // In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754 if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) { // Re-arm the next timer // 处理timers __CFArmNextTimerInMode(rlm, rl); } }#endif // 被gcd唤醒 else if (livePort == dispatchPort) { CFRUNLOOP_WAKEUP_FOR_DISPATCH(); __CFRunLoopModeUnlock(rlm); __CFRunLoopUnlock(rl); _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);#if DEPLOYMENT_TARGET_WINDOWS void *msg = 0;#endif // 处理gcd相关事情 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL); __CFRunLoopLock(rl); __CFRunLoopModeLock(rlm); sourceHandledThisLoop = true; didDispatchPortLastTime = true; } else { // 被source1唤醒 CFRUNLOOP_WAKEUP_FOR_SOURCE(); // If we received a voucher from this mach_msg, then put a copy of the new voucher into TSD. CFMachPortBoost will look in the TSD for the voucher. By using the value in the TSD we tie the CFMachPortBoost to this received mach_msg explicitly without a chance for anything in between the two pieces of code to set the voucher again. voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release); // Despite the name, this works for windows handles as well CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort); if (rls) {#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI // 处理source1mach_msg_header_t *reply = NULL;sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;if (NULL != reply) { (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL); CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);}#elif DEPLOYMENT_TARGET_WINDOWS sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;#endif } // Restore the previous voucher _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release); } #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);#endif // 处理block__CFRunLoopDoBlocks(rl, rlm); // 根据retVal返回值来决定要做什么if (sourceHandledThisLoop && stopAfterHandle) { retVal = kCFRunLoopRunHandledSource; } else if (timeout_context->termTSR < mach_absolute_time()) { retVal = kCFRunLoopRunTimedOut;} else if (__CFRunLoopIsStopped(rl)) { __CFRunLoopUnsetStopped(rl); retVal = kCFRunLoopRunStopped;} else if (rlm->_stopped) { rlm->_stopped = false; retVal = kCFRunLoopRunStopped;} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { retVal = kCFRunLoopRunFinished;} #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI voucher_mach_msg_revert(voucherState); os_release(voucherCopy);#endif } while (0 == retVal); // ---- 循环结束 ----- if (timeout_timer) { dispatch_source_cancel(timeout_timer); dispatch_release(timeout_timer); } else { free(timeout_context); } // 如果不等于0就退出loop return retVal;}
细节分析
在__CFRunLoopDoObservers函数里会调用通知observer的代码来执行__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity) {/* DOES CALLOUT */ CHECK_FOR_FORK(); CFIndex cnt = rlm->_observers ? CFArrayGetCount(rlm->_observers) : 0; if (cnt < 1) return; /* Fire the observers */ STACK_BUFFER_DECL(CFRunLoopObserverRef, buffer, (cnt <= 1024) ? cnt : 1); CFRunLoopObserverRef *collectedObservers = (cnt <= 1024) ? buffer : (CFRunLoopObserverRef *)malloc(cnt * sizeof(CFRunLoopObserverRef)); CFIndex obs_cnt = 0; for (CFIndex idx = 0; idx < cnt; idx++) { CFRunLoopObserverRef rlo = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx); if (0 != (rlo->_activities & activity) && __CFIsValid(rlo) && !__CFRunLoopObserverIsFiring(rlo)) { collectedObservers[obs_cnt++] = (CFRunLoopObserverRef)CFRetain(rlo); } } __CFRunLoopModeUnlock(rlm); __CFRunLoopUnlock(rl); for (CFIndex idx = 0; idx < obs_cnt; idx++) { CFRunLoopObserverRef rlo = collectedObservers[idx]; __CFRunLoopObserverLock(rlo); if (__CFIsValid(rlo)) { Boolean doInvalidate = !__CFRunLoopObserverRepeats(rlo); __CFRunLoopObserverSetFiring(rlo); __CFRunLoopObserverUnlock(rlo); // 这句才是真正处理observer的代码 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(rlo->_callout, rlo, activity, rlo->_context.info); if (doInvalidate) { CFRunLoopObserverInvalidate(rlo); } __CFRunLoopObserverUnsetFiring(rlo); } else { __CFRunLoopObserverUnlock(rlo); } CFRelease(rlo); } __CFRunLoopLock(rl); __CFRunLoopModeLock(rlm); if (collectedObservers != buffer) free(collectedObservers);}
在__CFRunLoopDoSources0函数中会调用处理Source0的代码__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
static Boolean __CFRunLoopDoSources0(CFRunLoopRef rl, CFRunLoopModeRef rlm, Boolean stopAfterHandle) {/* DOES CALLOUT */ CHECK_FOR_FORK(); CFTypeRef sources = NULL; Boolean sourceHandled = false; /* Fire the version 0 sources */ if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) {CFSetApplyFunction(rlm->_sources0, (__CFRunLoopCollectSources0), &sources); } if (NULL != sources) {__CFRunLoopModeUnlock(rlm);__CFRunLoopUnlock(rl);// sources is either a single (retained) CFRunLoopSourceRef or an array of (retained) CFRunLoopSourceRefif (CFGetTypeID(sources) == CFRunLoopSourceGetTypeID()) { CFRunLoopSourceRef rls = (CFRunLoopSourceRef)sources; __CFRunLoopSourceLock(rls); if (__CFRunLoopSourceIsSignaled(rls)) { __CFRunLoopSourceUnsetSignaled(rls); if (__CFIsValid(rls)) { __CFRunLoopSourceUnlock(rls); // 这句才是处理source0的真正的代码 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(rls->_context.version0.perform, rls->_context.version0.info); CHECK_FOR_FORK(); sourceHandled = true; } else { __CFRunLoopSourceUnlock(rls); } } else { __CFRunLoopSourceUnlock(rls); }} else { CFIndex cnt = CFArrayGetCount((CFArrayRef)sources); CFArraySortValues((CFMutableArrayRef)sources, CFRangeMake(0, cnt), (__CFRunLoopSourceComparator), NULL); for (CFIndex idx = 0; idx < cnt; idx++) {CFRunLoopSourceRef rls = (CFRunLoopSourceRef)CFArrayGetValueAtIndex((CFArrayRef)sources, idx);__CFRunLoopSourceLock(rls); if (__CFRunLoopSourceIsSignaled(rls)) { __CFRunLoopSourceUnsetSignaled(rls); if (__CFIsValid(rls)) { __CFRunLoopSourceUnlock(rls); __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(rls->_context.version0.perform, rls->_context.version0.info); CHECK_FOR_FORK(); sourceHandled = true; } else { __CFRunLoopSourceUnlock(rls); } } else { __CFRunLoopSourceUnlock(rls); }if (stopAfterHandle && sourceHandled) { break;} }}CFRelease(sources);__CFRunLoopLock(rl);__CFRunLoopModeLock(rlm); } return sourceHandled;}
__CFRunLoopServiceMachPort意味着RunLoop进入睡眠,也就是会阻塞当前线程,不再往后执行代码;只有当RunLoop被唤醒才会继续执行后面的代码。这种阻塞是真正的阻塞,线程会进入休息,不再做任何事情,和写个死循环的阻塞是不一样的,死循环还是会执行代码,线程还是要一直工作的
static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) { Boolean originalBuffer = true; kern_return_t ret = KERN_SUCCESS; for (;;) {/* In that sleep of death what nightmares may come ... */ mach_msg_header_t *msg = (mach_msg_header_t *)*buffer; msg->msgh_bits = 0; msg->msgh_local_port = port; msg->msgh_remote_port = MACH_PORT_NULL; msg->msgh_size = buffer_size; msg->msgh_id = 0; if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); } // 内核层面的api调用 ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV), 0, msg->msgh_size, port, timeout, MACH_PORT_NULL); // Take care of all voucher-related work right after mach_msg. // If we don't release the previous voucher we're going to leak it. voucher_mach_msg_revert(*voucherState); // Someone will be responsible for calling voucher_mach_msg_revert. This call makes the received voucher the current one. *voucherState = voucher_mach_msg_adopt(msg); if (voucherCopy) { if (*voucherState != VOUCHER_MACH_MSG_STATE_UNCHANGED) { // Caller requested a copy of the voucher at this point. By doing this right next to mach_msg we make sure that no voucher has been set in between the return of mach_msg and the use of the voucher copy. // CFMachPortBoost uses the voucher to drop importance explicitly. However, we want to make sure we only drop importance for a new voucher (not unchanged), so we only set the TSD when the voucher is not state_unchanged. *voucherCopy = voucher_copy(); } else { *voucherCopy = NULL; } } CFRUNLOOP_WAKEUP(ret); if (MACH_MSG_SUCCESS == ret) { *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL; return true; } if (MACH_RCV_TIMED_OUT == ret) { if (!originalBuffer) free(msg); *buffer = NULL; *livePort = MACH_PORT_NULL; return false; } if (MACH_RCV_TOO_LARGE != ret) break; buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE); if (originalBuffer) *buffer = NULL; originalBuffer = false; *buffer = realloc(*buffer, buffer_size); } HALT; return false;}
通过源码我们可以看到内部会调用内核层面的API来进行真正的休眠
调用API我们可以分为用户态和内核态的两种状态切换,RunLoop进入睡眠就是从用户态切换到内核态的调用,然后有消息唤醒线程时再切换到用户态来处理事件
总结
RunLoop的整个运行逻辑可以用下图来表述
以__CFRUNLOOP_IS_开头的大写函数都是真正去实行调用的函数
RunLoop对GCD的特殊处理
我们运行下面代码,并在GCD的回调里打断点,通过LLDB的指令bt打印来查看RunLoop的执行过程
@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; dispatch_async(dispatch_get_global_queue(0, 0), ^{ // 处理一些子线程的逻辑 // 回到主线程去刷新UI界面 dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"11111111111"); }); });}@end
可以看到RunLoop会处理GCD的回到主线程的调用情况,其他时候GCD都是有自己的逻辑去处理的,所以这是一个特殊情况
RunLoop的实际应用
解决NSTimer在滑动时停止工作的问题
如果定时器和滚动页面同时存在的情况下,可能会遇到因为页面滚动,而定时器停止的问题,见下面代码
// 通过这种方式添加定时器[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"%d", ++count);}];
原因是因为这样添加定时器会默认指定在RunLoop的NSDefaultRunLoopMode下进行,而处理页面滚动是在RunLoop的UITrackingRunLoopMode模式下进行的
解决办法:通过以下方式添加定时器,并选择NSRunLoopCommonModes模式
static int count = 0;NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"%d", ++count);}]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
NSRunLoopCommonModes并不是真的模式,而是一种标记,会让定时器同时能在这两种模式下都进行工作
选择通用模式会将这两种模式都添加到RunLoop底层__CFRunLoop结构体里的_commonModes成员变量里,_commonModes意味着在通用模式下可以执行的模式列表,而定时器timer也会被加入到_commonModeItems列表中;没有设置为通用模式的定时器timer只存在于__CFRunLoopMode里面的成员变量timers这个列表中
控制线程生命周期(线程保活)
线程在程序运行中执行完代码就会自动销毁了,见下面代码,可以利用RunLoop来进行线程保活
- (void)viewDidLoad { [super viewDidLoad]; Thread *thread = [[Thread alloc] initWithTarget:self selector:@selector(run) object:nil]; [thread start];}- (void)run { NSLog(@"%s %@", __func__, [NSThread currentThread]);}
完成实现代码如下
@interface Thread : NSThread@end@implementation Thread- (void)dealloc{ NSLog(@"%s", __func__);}@end@interface ViewController ()@property (strong, nonatomic) Thread *thread;@property (assign, nonatomic, getter=isStoped) BOOL stopped;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; __weak typeof(self) weakSelf = self; self.stopped = NO; self.thread = [[Thread alloc] initWithBlock:^{ NSLog(@"%@----begin----", [NSThread currentThread]); // 往RunLoop里面添加Source\Timer\Observer [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; while (weakSelf && !weakSelf.isStoped) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } NSLog(@"%@----end----", [NSThread currentThread]); }]; [self.thread start];}- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ if (!self.thread) return; [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];}// 子线程需要执行的任务- (void)test{ NSLog(@"%s %@", __func__, [NSThread currentThread]);}- (IBAction)stop { if (!self.thread) return; // 在子线程调用stop(waitUntilDone设置为YES,代表子线程的代码执行完毕后,这个方法才会往下走) [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];}// 用于停止子线程的RunLoop- (void)stopThread{ // 设置标记为YES self.stopped = YES; // 停止RunLoop CFRunLoopStop(CFRunLoopGetCurrent()); NSLog(@"%s %@", __func__, [NSThread currentThread]); // 清空线程 self.thread = nil;}- (void)dealloc{ NSLog(@"%s", __func__); [self stop];}@end
细节分析
1.控制器增加一个Thread *thread的属性来在多个方法能操作同一条线程
@property (strong, nonatomic) Thread *thread;
2.通过block的方式来创建线程是为了避免@selector里面会对Controller进行引用,Controller又持有thread,会造成循环引用,无法释放
self.thread = [[Thread alloc] initWithBlock:^{ }];[self.thread start];
3.在子线程里创建一个RunLoop对象,如果没有任何成员变量那么创建完就会马上退出,所以添加一个空白的任务Source1,也就是[[NSPort alloc] init],让RunLoop不会退出
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
添加的任务是个空的任务,RunLoop其实没有任何事情要执行,所以会进入到睡眠状态,所以会卡住线程不再往下执行代码
// 这句代码不会执行NSLog(@"%@----end----", [NSThread currentThread]);
4.采用循环创建RunLoop而不使用[[NSRunLoop currentRunLoop] run]是因为run方法的底层就是死循环进行runMode: beforeDate:的调用,那么就无法停止所有的RunLoop,所以我们自己实现while循环,并通过状态值来控制什么时候可以停止创建RunLoop
while (weakSelf && !weakSelf.isStoped) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];}
5.添加任务和结束RunLoop都要在同一个子线程来完成
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];// 子线程需要执行的任务- (void)test{ NSLog(@"%s %@", __func__, [NSThread currentThread]);}[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];// 用于停止子线程的RunLoop- (void)stopThread{ // 设置标记为YES self.stopped = YES; // 停止RunLoop CFRunLoopStop(CFRunLoopGetCurrent()); NSLog(@"%s %@", __func__, [NSThread currentThread]); // 清空线程 self.thread = nil;}
6.waitUntilDone:表示是否执行完selector的函数调用再继续往下执行代码,如果为NO,那么执行完performSelector,控制器就真的销毁了,这时再去调用到self就会报野指针的错误;设置为YES,代码会等子线程的stopThread执行完毕才会去真正销毁控制器
7.while循环里进行self的判断是为了防止执行了dealloc方法会将弱指针已经至为nil了,那么循环的判断就会为true,再次进入创建RunLoop;所以要确保self有值的情况下判断条件才成立
while (weakSelf && !weakSelf.isStoped) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];}
[NSDate distantFuture]表示的是未来无限长的时间
封装优化
我们对上述代码进行封装优化,以便更好的集成,如下所示
// PermenantThread.h typedef void (^PermenantThreadTask)(void);@interface PermenantThread : NSObject/** 开启线程 *///- (void)run;/** 在当前子线程执行一个任务 */- (void)executeTask:(PermenantThreadTask)task;/** 结束线程 */- (void)stop;@end// PermenantThread.m/** Thread **/@interface Thread : NSThread@end@implementation Thread- (void)dealloc{ NSLog(@"%s", __func__);}@end/** PermenantThread **/@interface PermenantThread()@property (strong, nonatomic) Thread *innerThread;@property (assign, nonatomic, getter=isStopped) BOOL stopped;@end@implementation PermenantThread#pragma mark - public methods- (instancetype)init{ if (self = [super init]) { self.stopped = NO; __weak typeof(self) weakSelf = self; self.innerThread = [[Thread alloc] initWithBlock:^{ [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; while (weakSelf && !weakSelf.isStopped) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } }]; [self.innerThread start]; } return self;}//- (void)run//{// if (!self.innerThread) return;//// [self.innerThread start];//}- (void)executeTask:(PermenantThreadTask)task{ if (!self.innerThread || !task) return; [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];}- (void)stop{ if (!self.innerThread) return; [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];}- (void)dealloc{ NSLog(@"%s", __func__); [self stop];}#pragma mark - private methods- (void)__stop{ self.stopped = YES; CFRunLoopStop(CFRunLoopGetCurrent()); self.innerThread = nil;}- (void)__executeTask:(PermenantThreadTask)task{ task();}@end
然后在Controller里就可以简单的集成使用了
@interface ViewController ()@property (strong, nonatomic) PermenantThread *thread;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; self.thread = [[PermenantThread alloc] init];}- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ [self.thread executeTask:^{ NSLog(@"执行任务 - %@", [NSThread currentThread]); }];}- (IBAction)stop { [self.thread stop];}- (void)dealloc{ NSLog(@"%s", __func__);}@end
也可以将PermenantThread的实现里RunLoop的创建用C语言的API来实现
/** Thread **/@interface Thread : NSThread@end@implementation Thread- (void)dealloc{ NSLog(@"%s", __func__);}@end/** PermenantThread **/@interface PermenantThread()@property (strong, nonatomic) Thread *innerThread;@end@implementation PermenantThread#pragma mark - public methods- (instancetype)init{ if (self = [super init]) { self.innerThread = [[Thread alloc] initWithBlock:^{ NSLog(@"begin----"); // 创建上下文(要初始化一下结构体) CFRunLoopSourceContext context = {0}; // 创建source CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context); // 往Runloop中添加source CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode); // 销毁source CFRelease(source); // 启动(第三个参数设置为false表示不退出,等同于加上while循环) CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false); // while (weakSelf && !weakSelf.isStopped) {// // 第3个参数:returnAfterSourceHandled,设置为true,代表执行完source后就会退出当前loop// CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);// } NSLog(@"end----"); }]; [self.innerThread start]; } return self;}//- (void)run//{// if (!self.innerThread) return;//// [self.innerThread start];//}- (void)executeTask:(PermenantThreadTask)task{ if (!self.innerThread || !task) return; [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];}- (void)stop{ if (!self.innerThread) return; [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];}- (void)dealloc{ NSLog(@"%s", __func__); [self stop];}#pragma mark - private methods- (void)__stop{ CFRunLoopStop(CFRunLoopGetCurrent()); self.innerThread = nil;}- (void)__executeTask:(PermenantThreadTask)task{ task();}@end
发表评论