如何使用 RunLoop

runloop结构

本文主要以翻译为主,有翻译or理解错的地方请提出,3Q

使用 RunLoop 对象

Apple 框架中是如何描述 RunLoop 的:

框架RunLoop线程安全
CocoaNSRunLoop 实例对象不安全
Core FoundationCFRunLoopRef指针安全

带着问题?

  1. runloop 使用需要注意什么?
  2. 什么时候使用 runloop
  3. 系统框架哪些地方使用了 runloop?
  4. runloop 运行时的内部实体结构分析?

获得一个 runloop 对象

swift

1
2
var a = RunLoop.current
var b = CFRunLoopGetCurrent()

oc

1
2
id a = [NSRunLoop currentRunLoop];
id b = CFRunLoopGetCurrent();
__CFRunLoop 结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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; // 当前 runloop 要监听的 mode
CFMutableSetRef _commonModeItems; // 所有模式下 要监听的 source0,source1,observer, timer
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
app 运行时 CFRunLoop 内部样子简版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
CFRunLoop {
current mode = kCFRunLoopDefaultMode
common modes = {
UITrackingRunLoopMode
kCFRunLoopDefaultMode
}

common mode items = {
// source0 (manual)
CFRunLoopSource {order =-1, {
callout = _UIApplicationHandleEventQueue}}
CFRunLoopSource {order =-1, {
callout = PurpleEventSignalCallback }}
CFRunLoopSource {order = 0, {
callout = FBSSerialQueueRunLoopSourceHandler}}

// source1 (mach port)
CFRunLoopSource {order = 0, {port = 17923}}
CFRunLoopSource {order = 0, {port = 12039}}
CFRunLoopSource {order = 0, {port = 16647}}
CFRunLoopSource {order =-1, {
callout = PurpleEventCallback}}
CFRunLoopSource {order = 0, {port = 2407,
callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_}}
CFRunLoopSource {order = 0, {port = 1c03,
callout = __IOHIDEventSystemClientAvailabilityCallback}}
CFRunLoopSource {order = 0, {port = 1b03,
callout = __IOHIDEventSystemClientQueueCallback}}
CFRunLoopSource {order = 1, {port = 1903,
callout = __IOMIGMachPortPortCallback}}

// Ovserver
CFRunLoopObserver {order = -2147483647, activities = 0x1, // Entry
callout = _wrapRunLoopWithAutoreleasePoolHandler}
CFRunLoopObserver {order = 0, activities = 0x20, // BeforeWaiting
callout = _UIGestureRecognizerUpdateObserver}
CFRunLoopObserver {order = 1999000, activities = 0xa0, // BeforeWaiting | Exit
callout = _afterCACommitHandler}
CFRunLoopObserver {order = 2000000, activities = 0xa0, // BeforeWaiting | Exit
callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
CFRunLoopObserver {order = 2147483647, activities = 0xa0, // BeforeWaiting | Exit
callout = _wrapRunLoopWithAutoreleasePoolHandler}

// Timer
CFRunLoopTimer {firing = No, interval = 3.1536e+09, tolerance = 0,
next fire date = 453098071 (-4421.76019 @ 96223387169499),
callout = _ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv (QuartzCore.framework)}
},

modes = {
CFRunLoopMode {
sources0 = { /* same as 'common mode items' */ },
sources1 = { /* same as 'common mode items' */ },
observers = { /* same as 'common mode items' */ },
timers = { /* same as 'common mode items' */ },
},

CFRunLoopMode {
sources0 = { /* same as 'common mode items' */ },
sources1 = { /* same as 'common mode items' */ },
observers = { /* same as 'common mode items' */ },
timers = { /* same as 'common mode items' */ },
},

CFRunLoopMode {
sources0 = {
CFRunLoopSource {order = 0, {
callout = FBSSerialQueueRunLoopSourceHandler}}
},
sources1 = (null),
observers = {
CFRunLoopObserver >{activities = 0xa0, order = 2000000,
callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
)},
timers = (null),
},

CFRunLoopMode {
sources0 = {
CFRunLoopSource {order = -1, {
callout = PurpleEventSignalCallback}}
},
sources1 = {
CFRunLoopSource {order = -1, {
callout = PurpleEventCallback}}
},
observers = (null),
timers = (null),
},

CFRunLoopMode {
sources0 = (null),
sources1 = (null),
observers = (null),
timers = (null),
}
}
}

可以看到,系统默认注册了5个Mode:

  1. kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
  2. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
  3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
  4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
  5. kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。
app 运行时 CFRunLoop 内部样子,打印 runloop 对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
<CFRunLoop 0x6000008b0000 [0x7fff80615350]>{
wakeup port = 0x2703,
stopped = false,
ignoreWakeUps = true,
current mode = (none),
common modes = <CFBasicHash 0x600003afbb70 [0x7fff80615350]>{
type = mutable set,
count = 2,
entries =>
0 : <CFString 0x7fff86743f40 [0x7fff80615350]>{
contents = "UITrackingRunLoopMode"}
2 : <CFString 0x7fff80628740 [0x7fff80615350]>{
contents = "kCFRunLoopDefaultMode"}
},
common mode items = <CFBasicHash 0x600003aa7690 [0x7fff80615350]>{
type = mutable set,
count = 13,
entries =>
0 : <CFRunLoopSource 0x6000001bc180 [0x7fff80615350]>{
signalled = No,
valid = Yes,
order = -1,
context = <CFRunLoopSource context>{
version = 0,
info = 0x0,
callout = PurpleEventSignalCallback (0x7fff384399f5)
}
}
1 : <CFRunLoopSource 0x6000001b0240 [0x7fff80615350]>{
signalled = Yes,
valid = Yes,
order = 0,
context = <CFRunLoopSource context>{
version = 0,
info = 0x6000010b8e40,
callout = FBSSerialQueueRunLoopSourceHandler (0x7fff365a092a)
}
}
2 : <CFRunLoopSource 0x6000001b40c0 [0x7fff80615350]>{
signalled = No,
valid = Yes,
order = -1,
context = <CFRunLoopSource context>{
version = 1,
info = 0x510b,
callout = PurpleEventCallback (0x7fff38439a01)
}
}
5 : <CFRunLoopSource 0x6000001b80c0 [0x7fff80615350]>{
signalled = No,
valid = Yes,
order = 0,
context = <MSHRunLoopSource 0x600003ae95f0> {
port = 430b, callback = 0x0
}
}

7 : <CFRunLoopObserver 0x6000005b0820 [0x7fff80615350]>{
valid = Yes,
activities = 0xa0,
repeats = Yes,
order = 2147483647,
callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff47848c8c),
context = <CFArray 0x600003ae9bf0 [0x7fff80615350]>{
type = mutable-small,
count = 0,
values = ()
}
}
8 : <CFRunLoopObserver 0x6000005b0780 [0x7fff80615350]>{
valid = Yes,
activities = 0x1,
repeats = Yes,
order = -2147483647,
callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff47848c8c),
context = <CFArray 0x600003ae9bf0 [0x7fff80615350]>{
type = mutable-small,
count = 0,
values = ()
}
}
9 : <CFRunLoopObserver 0x6000005b06e0 [0x7fff80615350]>{
valid = Yes,
activities = 0xa0,
repeats = Yes,
order = 2001000,
callout = _afterCACommitHandler (0x7fff47879310),
context = <CFRunLoopObserver context 0x7fc5adc01e40>
}
10 : <CFRunLoopObserver 0x6000005b0640 [0x7fff80615350]>{
valid = Yes,
activities = 0xa0,
repeats = Yes,
order = 1999000,
callout = _beforeCACommitHandler (0x7fff478792a7),
context = <CFRunLoopObserver context 0x7fc5adc01e40>
}

12 : <CFRunLoopSource 0x6000001b8b40 [0x7fff80615350]>{
signalled = No,
valid = Yes,
order = -1,
context = <CFRunLoopSource context>{
version = 0,
info = 0x6000001b0180,
callout = __handleEventQueue (0x7fff478e3efb)
}
}
16 : <CFRunLoopSource 0x6000001b89c0 [0x7fff80615350]>{
signalled = No,
valid = Yes,
order = 0,
context = <MSHRunLoopSource 0x600003ae9650> {
port = 3f0b,
callback = 0x0
}
}
18 : <CFRunLoopObserver 0x6000005b0140 [0x7fff80615350]>{
valid = Yes,
activities = 0x20,
repeats = Yes,
order = 0,
callout = _UIGestureRecognizerUpdateObserver (0x7fff473eda72), context = <CFRunLoopObserver context 0x600001fb5180>
}
21 : <CFRunLoopSource 0x6000001b8a80 [0x7fff80615350]>{
signalled = No,
valid = Yes,
order = 0,
context = <MSHRunLoopSource 0x6000034a8160> {
port = 4207,
callback = 0x7fff2e3fdd33
}
}
22 : <CFRunLoopSource 0x6000001b8cc0 [0x7fff80615350]>{
signalled = No,
valid = Yes,
order = -2,
context = <CFRunLoopSource context>{
version = 0,
info = 0x600003a9fe70,
callout = __handleHIDEventFetcherDrain (0x7fff478e3f07)
}
}
},
modes = <CFBasicHash 0x600003afbba0 [0x7fff80615350]>{
type = mutable set,
count = 3,
entries =>
0 : <CFRunLoopMode 0x600000fb8270 [0x7fff80615350]>{
name = UITrackingRunLoopMode,
port set = 0x2c03,
queue = 0x600001ab9180,
source = 0x600001ab9200 (not fired),
timer port = 0x5303,
sources0 = <CFBasicHash 0x600003aa76f0 [0x7fff80615350]>{
type = mutable set,
count = 4,
entries =>
0 : <CFRunLoopSource 0x6000001bc180 [0x7fff80615350]>{
signalled = No,
valid = Yes,
order = -1,
context = <CFRunLoopSource context>{
version = 0,
info = 0x0,
callout = PurpleEventSignalCallback (0x7fff384399f5)
}
}
1 : <CFRunLoopSource 0x6000001b8b40 [0x7fff80615350]>{
signalled = No,
valid = Yes,
order = -1,
context = <CFRunLoopSource context>{
version = 0,
info = 0x6000001b0180,
callout = __handleEventQueue (0x7fff478e3efb)
}
}
2 : <CFRunLoopSource 0x6000001b0240 [0x7fff80615350]>{
signalled = Yes,
valid = Yes,
order = 0,
context = <CFRunLoopSource context>{
version = 0,
info = 0x6000010b8e40,
callout = FBSSerialQueueRunLoopSourceHandler (0x7fff365a092a)
}
}
3 : <CFRunLoopSource 0x6000001b8cc0 [0x7fff80615350]>{
signalled = No,
valid = Yes,
order = -2,
context = <CFRunLoopSource context>{
version = 0,
info = 0x600003a9fe70,
callout = __handleHIDEventFetcherDrain (0x7fff478e3f07)
}
}
},
sources1 = <CFBasicHash 0x600003aa7720 [0x7fff80615350]>{
type = mutable set,
count = 4,
entries =>
0 : <CFRunLoopSource 0x6000001b40c0 [0x7fff80615350]>{
signalled = No,
valid = Yes,
order = -1,
context = <CFRunLoopSource context>{
version = 1,
info = 0x510b,
callout = PurpleEventCallback (0x7fff38439a01)
}
}
4 : <CFRunLoopSource 0x6000001b89c0 [0x7fff80615350]>{
signalled = No,
valid = Yes,
order = 0,
context = <MSHRunLoopSource 0x600003ae9650> {
port = 3f0b,
callback = 0x0
}
}
5 : <CFRunLoopSource 0x6000001b8a80 [0x7fff80615350]>{
signalled = No,
valid = Yes,
order = 0,
context = <MSHRunLoopSource 0x6000034a8160> {
port = 4207,
callback = 0x7fff2e3fdd33
}
}
6 : <CFRunLoopSource 0x6000001b80c0 [0x7fff80615350]>{
signalled = No,
valid = Yes,
order = 0,
context = <MSHRunLoopSource 0x600003ae95f0> {
port = 430b,
callback = 0x0
}
}
} ,
observers = (
"<CFRunLoopObserver 0x6000005b0780 [0x7fff80615350]>{
valid = Yes,
activities = 0x1,
repeats = Yes,
order = -2147483647,
callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff47848c8c),
context = <CFArray 0x600003ae9bf0 [0x7fff80615350]>{type = mutable-small, count = 0, values = ()
}
}",
"<CFRunLoopObserver 0x6000005b0140 [0x7fff80615350]>{
valid = Yes,
activities = 0x20,
repeats = Yes,
order = 0,
callout = _UIGestureRecognizerUpdateObserver (0x7fff473eda72),
context = <CFRunLoopObserver context 0x600001fb5180>
}",
"<CFRunLoopObserver 0x6000005b0640 [0x7fff80615350]>{
valid = Yes,
activities = 0xa0,
repeats = Yes,
order = 1999000,
callout = _beforeCACommitHandler (0x7fff478792a7),
context = <CFRunLoopObserver context 0x7fc5adc01e40>
}",
"<CFRunLoopObserver 0x6000005b06e0 [0x7fff80615350]>{
valid = Yes,
activities = 0xa0,
repeats = Yes,
order = 2001000,
callout = _afterCACommitHandler (0x7fff47879310),
context = <CFRunLoopObserver context 0x7fc5adc01e40>
}",
"<CFRunLoopObserver 0x6000005b0820 [0x7fff80615350]>{
valid = Yes,
activities = 0xa0,
repeats = Yes,
order = 2147483647,
callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff47848c8c),
context = <CFArray 0x600003ae9bf0 [0x7fff80615350]>{
type = mutable-small,
count = 0,
values = ()
}
}"
),
timers = (null),
currently 600930818 (56642367633481) / soft deadline in: 1.84466874e+10 sec (@ -1) / hard deadline in: 1.84466874e+10 sec (@ -1)
},
1 : <CFRunLoopMode 0x600000fb8340 [0x7fff80615350]>{
name = GSEventReceiveRunLoopMode,
port set = 0x5203,
queue = 0x600001ab9280,
source = 0x600001ab9380 (not fired),
timer port = 0x2e03,
sources0 = <CFBasicHash 0x600003aa77b0 [0x7fff80615350]>{
type = mutable set,
count = 1,
entries =>
0 : <CFRunLoopSource 0x6000001bc180 [0x7fff80615350]>{
signalled = No,
valid = Yes,
order = -1,
context = <CFRunLoopSource context>{
version = 0,
info = 0x0,
callout = PurpleEventSignalCallback (0x7fff384399f5)
}
}
},
sources1 = <CFBasicHash 0x600003aa77e0 [0x7fff80615350]>{
type = mutable set,
count = 1,
entries =>
2 : <CFRunLoopSource 0x6000001b4180 [0x7fff80615350]>{
signalled = No,
valid = Yes,
order = -1,
context = <CFRunLoopSource context>{
version = 1,
info = 0x510b,
callout = PurpleEventCallback (0x7fff38439a01)
}
}
},
observers = (null),
timers = (null),
currently 600930818 (56642369572615) / soft deadline in: 1.84466874e+10 sec (@ -1) / hard deadline in: 1.84466874e+10 sec (@ -1)
},
2 : <CFRunLoopMode 0x600000fb01a0 [0x7fff80615350]>{
name = kCFRunLoopDefaultMode,
port set = 0x2603,
queue = 0x600001ab0880,
source = 0x600001ab0980 (not fired),
timer port = 0x1f03,
sources0 = <CFBasicHash 0x600003aa7750 [0x7fff80615350]>{
type = mutable set,
count = 4,
entries =>
0 : <CFRunLoopSource 0x6000001bc180 [0x7fff80615350]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x7fff384399f5)}}
1 : <CFRunLoopSource 0x6000001b8b40 [0x7fff80615350]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x6000001b0180, callout = __handleEventQueue (0x7fff478e3efb)}}
2 : <CFRunLoopSource 0x6000001b0240 [0x7fff80615350]>{signalled = Yes, valid = Yes, order = 0, context = <CFRunLoopSource context>{version = 0, info = 0x6000010b8e40, callout = FBSSerialQueueRunLoopSourceHandler (0x7fff365a092a)}}
3 : <CFRunLoopSource 0x6000001b8cc0 [0x7fff80615350]>{signalled = No, valid = Yes, order = -2, context = <CFRunLoopSource context>{version = 0, info = 0x600003a9fe70, callout = __handleHIDEventFetcherDrain (0x7fff478e3f07)}}
},
sources1 = <CFBasicHash 0x600003aa7780 [0x7fff80615350]>{
type = mutable set,
count = 4,
entries =>
0 : <CFRunLoopSource 0x6000001b40c0 [0x7fff80615350]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 1, info = 0x510b, callout = PurpleEventCallback (0x7fff38439a01)}}
4 : <CFRunLoopSource 0x6000001b89c0 [0x7fff80615350]>{signalled = No, valid = Yes, order = 0, context = <MSHRunLoopSource 0x600003ae9650> {port = 3f0b, callback = 0x0}}
5 : <CFRunLoopSource 0x6000001b8a80 [0x7fff80615350]>{signalled = No, valid = Yes, order = 0, context = <MSHRunLoopSource 0x6000034a8160> {port = 4207, callback = 0x7fff2e3fdd33}}
6 : <CFRunLoopSource 0x6000001b80c0 [0x7fff80615350]>{signalled = No, valid = Yes, order = 0, context = <MSHRunLoopSource 0x600003ae95f0> {port = 430b, callback = 0x0}}
},
observers = (
"<CFRunLoopObserver 0x6000005b0780 [0x7fff80615350]>{
valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff47848c8c), context = <CFArray 0x600003ae9bf0 [0x7fff80615350]>{type = mutable-small, count = 0, values = ()}}",
"<CFRunLoopObserver 0x6000005b0140 [0x7fff80615350]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x7fff473eda72), context = <CFRunLoopObserver context 0x600001fb5180>}",
"<CFRunLoopObserver 0x6000005b0640 [0x7fff80615350]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x7fff478792a7), context = <CFRunLoopObserver context 0x7fc5adc01e40>}",
"<CFRunLoopObserver 0x6000005b06e0 [0x7fff80615350]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x7fff47879310), context = <CFRunLoopObserver context 0x7fc5adc01e40>}",
"<CFRunLoopObserver 0x6000005b0820 [0x7fff80615350]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff47848c8c), context = <CFArray 0x600003ae9bf0 [0x7fff80615350]>{type = mutable-small, count = 0, values = ()}}"
),
timers = <CFArray 0x6000010b6400 [0x7fff80615350]>{
type = mutable-small, count = 1, values = (
0 : <CFRunLoopTimer 0x6000001b86c0 [0x7fff80615350]>{valid = Yes, firing = No, interval = 0, tolerance = 0, next fire date = 600930808 (-10.16959 @ 56632202715119), callout = (Delayed Perform) UIApplication _accessibilitySetUpQuickSpeak (0x7fff2574c7d2 / 0x7fff46da0bb6) (/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore), context = <CFRunLoopTimer context 0x6000021ef300>}
)
},
currently 600930818 (56642369637217) / soft deadline in: 1.84467441e+10 sec (@ 56632202715119) / hard deadline in: 1.84467441e+10 sec (@ 56632202715119)
},
}
}

NSRunLoop 是对CFRunLoopRef指针的封装,虽然两者之间类型转换不消耗性能,但是NSRunLoop class 定义一个得到 CFRunLoopRef 的方法 getCFRunLoop

配置 RunLoop

在子线程上运行 RunLoop,你一定要向 runloop 中至少添加一个 input source 或者 timer,如果 runloop 中没有可监听的 sources,那么在 runloop 运行的时候他就会立刻退出。详情看下文 配置 RunLoop 事件源(sources)

除了添加事件源 sources,你可能还要给 runloop 添加 observers(使用它来监听当前 runloop 的执行阶段)。

创建 run loop observer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (void)threadMain{
// The application uses garbage collection, so no autorelease pool is needed.
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];

// Create a run loop observer and attach it to the run loop.
CFRunLoopObserverContext context = {0, self, NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(
kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&myRunLoopObserver,
&context);

if (observer){
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}

// Create and schedule the timer.
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(doFireTimer:) userInfo:nil repeats:YES];

NSInteger loopCount = 10;
do{
// Run the run loop 10 times to let the timer fire.
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--;
}
while (loopCount);
}

给 thread 配置 runloop 让处于保活状态,给runloop 添加一个 input sources来接受消息会比较好。虽然只有一个 timer sources 也可以进入 runloop,但是一旦 timer fires,timer 就会失效,导致 runloop 退出。添加一个 repeating timer 可以让 runloop 保活,可是反复调用 timer fire,会反复唤醒thread(这实际上是轮询的另一种形式),相对来说,使用 input source 来等待 event 发生,没发生前thread 都处于睡眠状态。

开启 RunLoop

  • 只有子线程才需要开启 RunLoop
  • 一个 RunLoop 实例至少有一个 input source or timer 来监听事件,如果没有可监听的sources,RunLoop开启后会立即退出。

启动 RunLoop 的方法:

方式方法名(NSRunLoop)解释
无条件run最简单但也最不可取的方案。会让线程进入无限循环,对 run loop 很难控制。可以添加和移除 input source 和 timer,但只能通过 kill 的方式停止 run loop。无法在自定义模式下运行 runloop。
设定时间限制runUntilDate:run loop 在收到事件或超时前会一直运行。run loop 结束后可以重启,并处理接下来的事情。比上一种方式更好,提供了时间限制。
处于特定模式runMode:beforeDate:相比上一种方式,增加了在特定模式下运行 run loop
Running a run loop
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)skeletonThreadMain {
// Set up an autorelease pool here if not using garbage collection.
BOOL done = NO;

// Add your sources or timers to the run loop and do any other setup.

do{
// Start the run loop but return after each source is handled.
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);

// If a source explicitly stopped the run loop, or if there are no
// sources or timers, go ahead and exit.
if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
done = YES;

// Check for any other exit conditions here and set the
// done variable as needed.
}
while (!done);
// Clean up code here. Be sure to release any allocated autorelease pools.
}

可以递归启动 run loop。也就是说可以在 input source 或 timer 的回调处理函数中调用 CFRunLoopRun, CFRunLoopRunInMode 或上面提到的 NSRunLoop 的三个方法,而且嵌套的 run loop 可以在任意 Mode 下运行。

退出 RunLoop

在处理事件之前,有两种方法可以让 RunLoop退出:

  • 给 run loop 配置 timeout 值
  • 告诉 run loop 停止

使用timeout的方式:RunLoop 退出之前会执行完所有正常情况下的处理,包括向 observers 发送通知。

使用 CFRunLoopStop():和 timeout方式相似。RunLoop 会把剩下的所有状态发送给 observers,然后退出。不同的是:你只能使用 CFRunLoopStop 停止以 Unconditionally 方式开启的 RunLoop。

要注意的是 CFRunLoopStop 只会停止对 CFRunLoopRun 和 CFRunLoopRunInMode 的调用。对于 Cocoa 框架相当于只停止一次 runMode:beforeDate: 的调用,而不是退出 run loop。stop 一次运行和 exit 整个 run loop 是不一样的。

虽然移除 RunLoop 的 input source 和 timer 也会导致其退出,但这种方法不可靠。因为有些系统程序会向 RunLoop 中添加 input source,开发者根本不知道有这回事,移除的时候就会漏掉,自然就不会导致 RunLoop 退出。

配置 RunLoop 事件源(sources)

定义 Custom Input Source

创建自定义输入源涉及定义以下内容:

  • 你想让 input source 处理的信息
  • A scheduler routine:用于让外部 client 获知如何联系 input source
  • A handler routine:执行任何 client 发出请求的
  • A cancellation routine:使 input source 失效

因为你使用 custom input source 来处理 custom information,配置的过程会相当灵活。The scheduler, handler, and cancellation routines,是你创建 custome input source 的关键。但是,input source 剩下的大部分行为都没有在这几个历程中,eg:

  • 向 input source 传递数据的方式需要你自己实现
  • 让其他线程知道这个 input source 的存在

下图给出了一个 custom input source 配置 demo。在这个例子中,
app 的主线程维护 :

  • 引用 input source
  • 引用 input source 自定义的 command buffer
  • 引用 runloop[已经把input source 加到里面了]

当 main thread 有一个task想要 worker thread 处理,main thread 会post 一个 command 到 command buffer 中,顺带把 worker thread 需要的信息发给它,然后 main thread 开启 task(因为main thread 和 worker thread 都有可以访问 command buffer,所以访问 command buffer 的过程要 sync)。一旦命令发布,main thread 向 input source 发送消息,唤起 worker thread 的runloop。worker thread 的 runloop 一旦接受到唤起命令,他就会调用 input source 的 handler 程序,handler 会处理 command buffer 中的命令。

操作 custom input source
 Operating a custom input source

定义 Input Source

定义 custome input source 需要 Core Foundation,接口都是基于 C 的!

下面代码封装成 OC:RunLoopSource 封装了 CFRunLoopSourceRef,并管理一个 command buffer,并使用 buffer 接收其他线程的消息。RunLoopContext 封装了 CFRunLoopRef 和 RunLoopSource 指针,用于向应用主线程传递 source 对象和 run loop 引用。

Custom Input Source 对象定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// These are the CFRunLoopSourceRef callback functions.
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
void RunLoopSourcePerformRoutine (void *info);
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);

@interface RunLoopSource : NSObject{
CFRunLoopSourceRef runLoopSource;
NSMutableArray* commands;
}

- (id)init;
- (void)addToCurrentRunLoop;
- (void)invalidate;

// Handler method
- (void)sourceFired;

// Client interface for registering commands to process
- (void)addCommand:(NSInteger)command withData:(id)data;
- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop;

@end

// RunLoopContext is a container object used during registration of the input source.
@interface RunLoopContext : NSObject{
CFRunLoopRef runLoop;
RunLoopSource* source;
}
@property (readonly) CFRunLoopRef runLoop;
@property (readonly) RunLoopSource* source;

- (id)initWithSource:(RunLoopSource*)src andLoop:(CFRunLoopRef)loop;
@end

虽然 Objective-C 代码管理 input source 的自定义数据,但是把 input source 添加到 runloop 中需要基于 c 的方法。 第一个被调用的例程是 scheduler,当你把 source 添加到 runloop 时就会调用。 input source 只有一个 client,也就是主线程。这里 scheduler 做的事情就是用 application delegate 的 registerSource: 方法将 RunLoopContext 对象中的信息传递过去,以便之后 application delegate 与 input source 通信时使用。

Scheduling a run loop source

1
2
3
4
5
6
7
8
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode){
RunLoopSource* obj = (RunLoopSource*)info;
AppDelegate* del = [AppDelegate sharedAppDelegate];
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];

[del performSelectorOnMainThread:@selector(registerSource:)
withObject:theContext waitUntilDone:NO];
}

当向 input source 发送消息的时候,用于处理数据的 perform 函数会被调用,它是最重要的回调之一。下面的这个方法只是把请求转发给了 RunLoopSource 的 sourceFired 方法。后面会列出 sourceFired 处理 command buffer 的逻辑。

1
2
3
4
5
//Performing work in the input source
void RunLoopSourcePerformRoutine (void *info){
RunLoopSource* obj = (__bridge RunLoopSource*)info;
[obj sourceFired];
}

如果你以前使用 CFRunLoopSourceInvalidate() 方法把 input source 从runloop中移除,系统会调用 input source 的 cancellation routine. 你可以使用这个 routine 通知 clients,你的input source 失效,需要移除他们对 input source 的引用。下面的 cancellation routine 体用系统传入的 RunLoopSource,runloop,runloopMode 创建一个新的 RunLoopContext,并传给 application delegate。

Invalidating a run loop source:

1
2
3
4
5
6
7
8
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode){
RunLoopSource* obj = (__bridge RunLoopSource*)info;
AppDelegate* del = [AppDelegate sharedAppDelegate];
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];

[del performSelectorOnMainThread:@selector(removeSource:)
withObject:theContext waitUntilDone:YES];
}

注意 application delegate’s 的 registerSource: and removeSource: 方法在下面

把 Input Source 添加到 Run Loop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (id)init{
CFRunLoopSourceContext context = {0, self, NULL, NULL, NULL, NULL, NULL,
&RunLoopSourceScheduleRoutine,
RunLoopSourceCancelRoutine,
RunLoopSourcePerformRoutine};

_runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
commands = [[NSMutableArray alloc] init];

return self;
}

- (void)addToCurrentRunLoop{
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopAddSource(runLoop, _runLoopSource, kCFRunLoopDefaultMode);
}

当 worker 线程调用 addToCurrentRunLoop 方法时,才会将 input source 放到 runloop 中,并在此时调用 RunLoopSourceScheduleRoutine 。input source 加到 runloop 之后,thread 就可以跑起他的 runloop ,等待他派发 task 。

  • CFRunLoopSourceContext 结构体描述了 custom input source(source0)的上下文
  • CFRunLoopSourceContext1 结构体描述了 port-based input source(source1)的上下文
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);

void (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
void (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
void (*perform)(void *info);

} CFRunLoopSourceContext;

typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
mach_port_t (*getPort)(void *info);
void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
void * (*getPort)(void *info);
void (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;

Input Source 与 Client 的协作

想使用 input source,你需要从其他 thread 发送消息。input source 的全部目的是使其关联 thread 处于休眠状态,直到有事要做。这需要让你的应用程序中的其他线程知道输入源并有一种与之通信的方法。

当 input source 第一次装载到 run loop 的时候,可以向 client 发送注册 input source 的请求。可以将一个 input source 直接或间接注册到多个 client 中。下面的代码展示了 application delegate 提供的注册方法 registerSource:,之前提到过回调函数 RunLoopSourceScheduleRoutine 的实现会调用 registerSource:。也就是说执行流程是 install(addToCurrentRunLoop) -> schedule(RunLoopSourceScheduleRoutine) -> register(registerSource:)。对应地,当 input source 从 run loop 中被移除,回调函数 RunLoopSourceCancelRoutine 中会调用 removeSource: 方法。

scheduler an input sourceInvalidating an input source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)registerSource:(RunLoopContext*)sourceInfo;{
[sourcesToPing addObject:sourceInfo];
}

- (void)removeSource:(RunLoopContext*)sourceInfo{
id objToRemove = nil;

for (RunLoopContext* context in sourcesToPing) {
if ([context isEqual:sourceInfo]) {
objToRemove = context;
break;
}
}

if (objToRemove)
[sourcesToPing removeObject:objToRemove];
}

向 Input Source 发送信号

在将数据交给 Input Source 后,客户端必须发信号通知源并唤醒其 runloop。消息传达 input source 以后,thread 可以处于休眠状态,你应该显示的唤起 runloop,如果处理完了可能会导致错误结果!

Waking up the run loop:

1
2
3
4
- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop {
CFRunLoopSourceSignal(runLoopSource);
CFRunLoopWakeUp(runloop);
}

配置 Timer Sources

创建 Timer Sources:创建一个 timer 对象,然后在 runloop中调度它。
Cocoa 中使用 NSTimer,Core Foundation 中使用 CFRunLoopTimerRef 类型。虽然二者是 toll-free bridged 的,但是 NSTimer 提供的 API 更便捷:

1
2
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
scheduledTimerWithTimeInterval:invocation:repeats:

上面这2个方法会创建 timer 并添加到当前线程 runloop 的 DefaultMode 中。也可以手动创建一个 NSTimer 对象并用 NSRunLoop 的 addTimer:forMode: 方法将其添加到 runloop 的指定 Mode 中。

下面的代码展示了两种添加 timer 的方式:第一个 timer 延迟 1 秒触发并每隔 0.1 秒重复触发,第二个 timer 延迟 0.2 秒触发然后每隔 0.2 秒重复触发。

Creating and scheduling timers using NSTimer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];

// Create and schedule the first timer.
NSDate* futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
NSTimer* myTimer = [[NSTimer alloc] initWithFireDate:futureDate
interval:0.1
target:self
selector:@selector(myDoFireTimer1:)
userInfo:nil
repeats:YES];
[myRunLoop addTimer:myTimer forMode:NSDefaultRunLoopMode];

// Create and schedule the second timer.
[NSTimer scheduledTimerWithTimeInterval:0.2
target:self
selector:@selector(myDoFireTimer2:)
userInfo:nil
repeats:YES];

Creating and scheduling a timer using Core Foundation:

1
2
3
4
5
6
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0, NULL, NULL, NULL, NULL};
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.3, 0, 0,
&myCFTimerCallback, &context);

CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);

配置 a Port-Based Input Source

Cocoa and Core Foundation 都提供了 port-based 对象用于线程 or 进程间通讯,下面会使用不同类型的 ports 实现建立 port 通讯

配置 NSMachPort Object

使用 NSMachPort 对象建立本地连接:

  • 创建 NSMachPort 对象并添加到 primary 线程的 runloop 中。
  • 当启动 secondary 线程时将这个 NSMachPort 对象传递给 secondary 线程的入口函数。
  • secondary 线程会用这个 NSMachPort 对象往 primary 线程发消息。
main thread 代码实现

下面的代码是启动 secondary worker 线程的主要代码。使用 Cocoa 框架的代码要比 Core Foundation 的少,效果几乎一样。有个不同点是 Cocoa 直接传递 NSPort 对象,而 Core Foundation 传递端口名字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)launchThread{
NSPort* myPort = [NSMachPort port];
if (myPort) {
// This class handles incoming port messages.
[myPort setDelegate:self];

// Install the port as an input source on the current run loop.
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];

// Detach the thread. Let the worker release the port.
[NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:)
toTarget:[MyWorkerClass class] withObject:myPort];
}
}

为了建立好 threads 间的通讯,你可能想要得到 worker thread 将他的本地端口号发送给 main thread,把这个当做校验消息。main thread 得到 worker thread 的端口号,校验完毕,就知道启动 secondary thread 的过程一切进展顺利。主线程会将 worker thread 的 portID 保存起来。

下面的 handlePortMessage: 方法会在线程自己的本地端口收到数据后被调用。NSPortMessage 持有两个端口对象:发送端口和接收端口。handlePortMessage: 方法中使用 [portMessage sendPort] 获取到了发送端口对象,也就是 secondary thread 拥有的本地端口。也就是 secondary thread 启动后会给 primary thread 发消息,告知自己的端口对象,主线程会将其存下来以备日后使用。msgid 标记了消息的唯一 ID。

Handling Mach port messages:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define kCheckinMessage 100
// Handle responses from the worker thread.
- (void)handlePortMessage:(NSPortMessage *)portMessage{
unsigned int message = [portMessage msgid];
NSPort* distantPort = nil;

if (message == kCheckinMessage) {
// Get the worker thread’s communications port.
distantPort = [portMessage sendPort];

// Retain and save the worker port for later use.
[self storeDistantPort:distantPort];
}
else{
// Handle other messages.
}
}
secondary thread 代码实现

对于 secondary worker thread, 你一定要明确他的端口号!用它与 primary thread 通讯。

下面代码:如何配置 worker thread,worker thread 的入口函数会被传入 primary thread 的端口对象。下面代码中的 MyWorkerClass 是个辅助类,它的 sendCheckinMessage: 方法负责创建worker thread的本地端口,并发消息给 primary thread 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+(void)LaunchThreadWithPort:(id)inData{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];

// Set up the connection between this thread and the main thread.
NSPort* distantPort = (NSPort*)inData;

MyWorkerClass* workerObj = [[self alloc] init];
[workerObj sendCheckinMessage:distantPort];
[distantPort release];

// Let the run loop process things.
do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]];
} while (![workerObj shouldExit]);

[workerObj release];
[pool release];
}

配置 NSMessagePort Object

使用 Core Foundation 配置 Port-Based Input Source

Threading Programming Guide-RunLoop
Threading Programming Guide(2)