上篇文章講了runtime的簡單應用,使用鉤子實現了對字典和陣列的賦值的校驗,順便隨手擼了一個簡單的jsonToModel
,iOS
除了runtime
還有一個東西的叫做runloop
,各位看官老爺一定都有了解,那麼今天這篇文章初識一下runloop
。
什麼是runloop
簡單來講runloop
就是一個迴圈,我們寫的程式,一般沒有迴圈的話,執行完就結束了,那麼我們手機上的APP是如何一直執行不停止的呢?APP就是用到了runloop
,保證程式一直執行不退出,在需要處理事件的時候處理事件,不處理事件的時候進行休眠,跳出迴圈程式就結束。用虛擬碼實現一個runloop
其實是這樣子的
int ret = 0;
do {
//睡眠中等待訊息
int messgae = sleep_and_wait();
//處理訊息
ret = process_message(messgae);
} while (ret == 0);
複製程式碼
獲取runloop
iOS中有兩套可以獲取runloop程式碼,一個是Foundation
、一個是Core Foundation
。
Foundation
其實是對Core Foundation
的一個封裝,
NSRunLoop * runloop1 = [NSRunLoop currentRunLoop];
NSRunLoop *mainloop1 = [NSRunLoop mainRunLoop];
CFRunLoopRef runloop2= CFRunLoopGetCurrent();
CFRunLoopRef mainloop2 = CFRunLoopGetMain();
NSLog(@"%p %p %p %p",runloop1,mainloop1,runloop2,mainloop2);
NSLog(@"%@",runloop1);
//列印
runlopp1:0x600001bc58c0
mainloop1:0x600001bc58c0
runloop2:0x6000003cc300
mainloop1:0x6000003cc300
runloop1:<CFRunLoop 0x6000003cc300 [0x10b2e9ae8]>.....
複製程式碼
runloop1
和mainloop1
地址一致,說明當前的runloop
是mainrunloop
,runloop1
作為物件輸出的結果其實也是runloop2
的地址,證明Foundation runloop
是對Core Foundation
的一個封裝。
RunLoop
底層我們猜測應該是結構體,我們都瞭解到其實OC
就是封裝了c/c++
,那麼c厲害之處就是指標和結構體基本解決常用的所有東西。我們窺探一下runloop
的真是模樣,通過CFRunLoopRef *runloop = CFRunLoopGetMain();
檢視CFRunloop
是typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
,我們常用的CFRunLoopRef
是__CFRunLoop *
型別的,那麼再在原始碼(可以下載最新的原始碼)中搜尋一下 struct __CFRunLoop {
在runloop.c 637行
如下所示:
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* model list 鎖 */
__CFPort _wakeUpPort; // 接受 CFRunLoopWakeUp的埠
Boolean _unused;//是否使用
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread; //執行緒
uint32_t _winthread;//win執行緒
CFMutableSetRef _commonModes; //modes
CFMutableSetRef _commonModeItems; //modeItems
CFRunLoopModeRef _currentMode; //當前的mode
CFMutableSetRef _modes; //所有的modes
struct _block_item *_blocks_head; //待執行的block列表頭部
struct _block_item *_blocks_tail; //待執行的block 尾部
CFAbsoluteTime _runTime; //runtime
CFAbsoluteTime _sleepTime; //sleeptime
CFTypeRef _counterpart; //
};
複製程式碼
經過簡化之後:
struct __CFRunLoop {
pthread_t _pthread; //執行緒
CFMutableSetRef _commonModes; //modes
CFMutableSetRef _commonModeItems; //modeItems
CFRunLoopModeRef _currentMode; //當前的mode
CFMutableSetRef _modes; //所有的modes
}
複製程式碼
runloop
中包含一個執行緒_pthread
,一一對應的CFMutableSetRef _modes
可以有多個mode
CFRunLoopModeRef _currentMode
當前mode
只能有一個
那麼mode裡邊有什麼內容呢?我們猜測他應該和runloop
類似,在原始碼中搜尋CFRuntimeBase _base
看到在runloop.c line 524
看到具體的內容:
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
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 */
};
複製程式碼
經過簡化之後是:
struct __CFRunLoopMode {
CFStringRef _name;//當前mode的名字
CFMutableSetRef _sources0;//souces0
CFMutableSetRef _sources1;//sources1
CFMutableArrayRef _observers;//observers
CFMutableArrayRef _timers;//timers
}
複製程式碼
一個mode
可以有多個timer
、souces0
、souces1
、observers
、timers
那麼使用圖更直觀的來表示:
runloop
包含多個mode
,但是同時只能執行一個mode
,這點和大家開車的駕駛模式類似,運動模式和環保模式同時只能開一個模式,不能又運動又環保,明顯相悖。多個mode
被隔離開有點是處理事情更專一,不會因為多個同時處理事情造成卡頓或者資源競爭導致的一系列問題。
souces0
- 觸控事件
- performSelector:onThread:
測試下點選事件處理源
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);//此處斷點
}
(LLDB) bt //輸出當前呼叫棧
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x000000010c5bb66d CFRunloop`::-[ViewController touchesBegan:withEvent:](self=0x00007fc69ec087e0, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x00006000012a01b0) at ViewController.mm:22:2
frame #1: 0x0000000110685a09 UIKitCore`forwardTouchMethod + 353
frame #2: 0x0000000110685897 UIKitCore`-[UIResponder touchesBegan:withEvent:] + 49
frame #3: 0x0000000110694c48 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 1869
frame #4: 0x00000001106965d2 UIKitCore`-[UIWindow sendEvent:] + 4079
frame #5: 0x0000000110674d16 UIKitCore`-[UIApplication sendEvent:] + 356
frame #6: 0x0000000110745293 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 3232
frame #7: 0x0000000110747bb9 UIKitCore`__handleEventQueueInternal + 5911
frame #8: 0x000000010d8eabe1 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
frame #9: 0x000000010d8ea463 CoreFoundation`__CFRunLoopDoSources0 + 243
frame #10: 0x000000010d8e4b1f CoreFoundation`__CFRunLoopRun + 1231
frame #11: 0x000000010d8e4302 CoreFoundation`CFRunLoopRunSpecific + 626
frame #12: 0x0000000115ddc2fe GraphicsServices`GSEventRunModal + 65
frame #13: 0x000000011065aba2 UIKitCore`UIApplicationMain + 140
frame #14: 0x000000010c5bb760 CFRunloop`main(argc=1, argv=0x00007ffee3643f68) at main.m:14:13
frame #15: 0x000000010f1cb541 libdyld.dylib`start + 1
frame #16: 0x000000010f1cb541 libdyld.dylib`start + 1
複製程式碼
#1
看到現在是在佇列queue = 'com.apple.main-thread'中,#10
Runloop
啟動,#9
進入到__CFRunLoopDoSources0
,最終__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
呼叫了__handleEventQueueInternal
->[UIApplication sendEvent:]
->[UIWindow sendEvent:]
->[UIWindow _sendTouchesForEvent:]
->[UIResponder touchesBegan:withEvent:]
->-[ViewController touchesBegan:withEvent:](self=0x00007fc69ec087e0, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x00006000012a01b0) at ViewController.mm:22:2
,可以看到另外一個知識點,手勢的傳遞是從上往下的,順序是UIApplication -> UIWindow -> UIResponder -> ViewController
。
Source1
- 基於Port的執行緒間通訊
- 系統事件捕捉
Timers
- NSTimer
- performSelector:withObject:afterDelay:
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
static int count = 5;
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
NSLog(@"-------:%d \n",count++);
});
dispatch_resume(timer);
//log
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x0000000101f26457 CFRunloop`::__29-[ViewController viewDidLoad]_block_invoke(.block_descriptor=0x0000000101f28100) at ViewController.mm:72:33
frame #1: 0x0000000104ac2db5 libdispatch.dylib`_dispatch_client_callout + 8
frame #2: 0x0000000104ac5c95 libdispatch.dylib`_dispatch_continuation_pop + 552
frame #3: 0x0000000104ad7e93 libdispatch.dylib`_dispatch_source_invoke + 2249
frame #4: 0x0000000104acfead libdispatch.dylib`_dispatch_main_queue_callback_4CF + 1073
frame #5: 0x00000001032568a9 CoreFoundation`__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
frame #6: 0x0000000103250f56 CoreFoundation`__CFRunLoopRun + 2310
frame #7: 0x0000000103250302 CoreFoundation`CFRunLoopRunSpecific + 626
複製程式碼
最終進入函式__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
呼叫了libdispatch的_dispatch_main_queue_callback_4CF
函式,具體實現有興趣的大佬可以看下原始碼的實現。
Observers
- 用於監聽RunLoop的狀態
- UI重新整理(BeforeWaiting)
- Autorelease pool(BeforeWaiting)
Mode
型別都多個,系統暴露在外的就兩個,
CF_EXPORT const CFRunLoopMode kCFRunLoopDefaultMode;
CF_EXPORT const CFRunLoopMode kCFRunLoopCommonModes;
複製程式碼
那麼這兩個Mode都是在什麼情況下執行的呢?
kCFRunLoopDefaultMode(NSDefaultRunLoopMode)
:App
的預設Mode
,通常主執行緒是在這個Mode
下執行UITrackingRunLoopMode
:介面跟蹤Mode
,用於ScrollView
追蹤觸控滑動,保證介面滑動時不受其他Mode
影響
進入到某個Mode
,處理事情也應該有先後順序和休息的時間,那麼現在需要一個狀態來表示此時此刻的status
,系統已經準備了CFRunLoopActivity
來表示當前的狀態
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即將進入loop
kCFRunLoopBeforeTimers = (1UL << 1),//即將處理timers
kCFRunLoopBeforeSources = (1UL << 2), //即將處理sourcs
kCFRunLoopBeforeWaiting = (1UL << 5),//即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6),//即將從休眠中喚醒
kCFRunLoopExit = (1UL << 7),//即將退出
kCFRunLoopAllActivities = 0x0FFFFFFFU//所有狀態
};
複製程式碼
1UL
表示無符號長整形數字1
,再次看到這個(1UL << 1)
我麼猜測用到了位域或者聯合體,達到省空間的目的。kCFRunLoopAllActivities = 0x0FFFFFFFU
轉換成二進位制就是28個1
,再進行mask
的時候,所有的值都能取出來。
現在我們瞭解到:
CFRunloopRef
代表RunLoop
的執行模式- 一個
Runloop
包含若干個Mode
,每個Mode
包含若干個Source0/Source1/Timer/Obser
Runloop
啟動只能選擇一個Mode
作為currentMode
- 如果需要切換
Mode
,只能退出當前Loop
,再重新選擇一個Mode
進入 - 不同組的
Source0/Source1/Timer/Observer
能分隔開來,互不影響 - 如果
Mode
沒有任何Source0/Source1/Timer/Observer
,Runloop
立馬退出。
runloop切換Mode
CFRunLoopObserverRef obs= CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:{
CFRunLoopMode m = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"即將進入 mode:%@",m);
CFRelease(m);
break;
}
case kCFRunLoopExit:
{
CFRunLoopMode m = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"即將退出 mode:%@",m);
CFRelease(m);
break;
}
default:
break;
}
});
CFRunLoopAddObserver(CFRunLoopGetMain(), obs, kCFRunLoopCommonModes);
CFRelease(obs);
//當滑動tb的時候log
即將退出 mode:kCFRunLoopDefaultMode
即將進入 mode:UITrackingRunLoopMode
即將退出 mode:UITrackingRunLoopMode
即將進入 mode:kCFRunLoopDefaultMode
複製程式碼
當runloop
切換mode
的時候,會退出當前kCFRunLoopDefaultMode
,加入到其他的UITrackingRunLoopMode
,當前UITrackingRunLoopMode
完成之後再退出之後再加入到kCFRunLoopDefaultMode
。
我們再探究下runloop
的迴圈的狀態到底是怎樣來變更的。
// //獲取loop
CFRunLoopRef ref = CFRunLoopGetMain();
//獲取obs
CFRunLoopObserverRef obs = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities, YES, 0, callback, NULL);
//新增監聽
CFRunLoopAddObserver(ref, obs, CFRunLoopCopyCurrentMode(ref));
CFRelease(obs);
int count = 0;//定義全域性變數來計算一個mode中狀態切換的統計資料
void callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
printf("- ");
count ++;
printf("%d",count);
switch (activity) {
case kCFRunLoopEntry:
printf("即將進入 \n");
count = 0;
break;
case kCFRunLoopExit:
printf("即將退出 \n");
break;
case kCFRunLoopAfterWaiting:
printf("即將從休眠中喚醒 \n");
break;
case kCFRunLoopBeforeTimers:
printf("即將進入處理 timers \n");
break;
case kCFRunLoopBeforeSources:
printf("即將進入 sources \n");
break;
case kCFRunLoopBeforeWaiting:
printf("即將進入 休眠 \n");
count = 0;
break;
default:
break;
}
}
//點選的時候 會出發loop來處理觸控事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
}
//log
- 1即將從休眠中喚醒
- 2即將進入處理 timers
- 3即將進入 sources
-[ViewController touchesBegan:withEvent:]
- 4即將進入處理 timers
- 5即將進入 sources
- 6即將進入處理 timers
- 7即將進入 sources
- 8即將進入處理 timers
- 9即將進入 sources
- 10即將進入 休眠
- 1即將從休眠中喚醒
- 2即將進入處理 timers
- 3即將進入 sources
- 4即將進入處理 timers
- 5即將進入 sources
- 6即將進入 休眠
- 1即將從休眠中喚醒
- 2即將進入處理 timers
- 3即將進入 sources
- 4即將進入 休眠
複製程式碼
runloop
喚醒之後不是立馬處理事件的,而是看看timer
有沒有事情,然後是sources
,發現有觸控事件就處理了,然後又迴圈檢視timer
和sources
一般迴圈2次進入休眠狀態,處理source
之後是迴圈三次。
RunLoop在不獲取的時候不存在,獲取才生成
RunLoop
是在主動獲取的時候才會生成一個,主執行緒是系統自己呼叫生成的,子執行緒開發者呼叫,我們看下CFRunLoopGetCurrent
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
複製程式碼
看到到這裡相信大家已經對runloop
有了基本的認識,那麼我們再探究一下底層runloop
是怎麼運轉的。
首先看官方給的圖:
那我又整理了一個表格來更直觀的瞭解狀態運轉步驟 | 任務 |
---|---|
1 | 通知Observers:進入Loop |
2 | 通知Observers:即將處理Timers |
3 | 通知Observers:即將處理Sources |
4 | 處理blocks |
5 | 處理Source0(可能再處理Blocks) |
6 | 如果存在Source1,跳轉第8步 |
7 | 通知Observers:開始休眠 |
8 | 通知Observers:結束休眠1.處理Timer2.處理GCD Asyn To Main Queue 3.處理Source1 |
9 | 處理Blocks |
10 | 根據前面的執行結果,決定如何操作1.返回第2步,2退出loop |
11 | 通知Observers:退出Loop |
檢視runloop原始碼中runloop.c
2333行
//入口函式
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 context
dispatch_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 {
#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;
#endif
__CFPortSet waitSet = rlm->_portSet;
__CFRunLoopUnsetIgnoreWakeUps(rl);
//通知即將處理Timers
if (rlm->_observerMask & kCFRunLoopBeforeTimers)
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
//通知即將處理Sources
if (rlm->_observerMask & kCFRunLoopBeforeSources)
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
//處理Blocks
__CFRunLoopDoBlocks(rl, rlm);
//處理Source0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
//處理Block
__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;
//y判斷是否有Source1
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
//有則去 handle_msg
goto handle_msg;
}
#endif
}
didDispatchPortLastTime = false;
//即將進入休眠
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
//開始休眠
__CFRunLoopSetSleeping(rl);
__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) {
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) {
(_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
if (rlm->_timerFired) {
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) {
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
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));
__CFPortSetRemove(dispatchPort, waitSet);
__CFRunLoopSetIgnoreWakeUps(rl);
// user callouts now OK again
__CFRunLoopUnsetSleeping(rl);
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting))
//結束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
//標籤 handle_msg
handle_msg:;
__CFRunLoopSetIgnoreWakeUps(rl);
if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
} else if (livePort == rl->_wakeUpPort) {
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
}
#if USE_DISPATCH_SOURCE_FOR_TIMERS
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
//被timer喚醒
CFRUNLOOP_WAKEUP_FOR_TIMER();
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
__CFArmNextTimerInMode(rlm, rl);
}
}
#endif
#if USE_MK_TIMER_TOO
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
__CFArmNextTimerInMode(rlm, rl);
}
}
#endif
//被GCD換醒
else if (livePort == dispatchPort) {
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
//處理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();
voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
mach_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);
}
#endif
}
_CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);
}
//處理bBlock
__CFRunLoopDoBlocks(rl, rlm);
//設定返回值
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);
}
return retVal;
}
複製程式碼
經過及進一步精簡
//入口函式
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;
}
Boolean didDispatchPortLastTime = true;
int32_t retVal = 0;
do {
__CFRunLoopUnsetIgnoreWakeUps(rl);
//通知即將處理Timers
if (rlm->_observerMask & kCFRunLoopBeforeTimers)
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
//通知即將處理Sources
if (rlm->_observerMask & kCFRunLoopBeforeSources)
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
//處理Blocks
__CFRunLoopDoBlocks(rl, rlm);
//處理Source0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
//處理Block
__CFRunLoopDoBlocks(rl, rlm);
}
msg = (mach_msg_header_t *)msg_buffer;
//y判斷是否有Source1
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
//有則去 handle_msg
goto handle_msg;
}
//即將進入休眠
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
//開始休眠
__CFRunLoopSetSleeping(rl);
do {
//等待訊息來喚醒當前執行緒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
} while (1);
#else
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting))
//結束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
//標籤 handle_msg
handle_msg:;
//被timer喚醒
CFRUNLOOP_WAKEUP_FOR_TIMER();
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
__CFArmNextTimerInMode(rlm, rl);
}
#if USE_MK_TIMER_TOO
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
__CFArmNextTimerInMode(rlm, rl);
}
}
#endif
//被GCD換醒
else if (livePort == dispatchPort) {
//處理GCD
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else {
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
//處理Source1
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
// Restore the previous voucher
_CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);
}
//處理bBlock
__CFRunLoopDoBlocks(rl, rlm);
//設定返回值
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;
}
} while (0 == retVal);
return retVal;
}
複製程式碼
精簡到這裡基本都能看懂了,還寫了很多註釋,基本和上面整理的表格一致。
這裡的執行緒休眠__CFRunLoopServiceMachPort
是呼叫核心函式mach_msg()進行休眠,和我們平時while(1)
大不同,while(1)
叫死迴圈,其實系統每時每刻都在判斷是否符合條件,耗費很高的CPU,核心則不同,Mach核心提供面向訊息,基於基礎的程式間通訊。
保活機制
一個程式執行完畢結束了就死掉了,timer
和變數也一樣,執行完畢就結束了,那麼我們怎麼可以保證timer
一直活躍和執行緒不結束呢?
timer保活和多mode執行
timer
可以新增到self
的屬性保證一直活著,只要self
不死,timer
就不死。timer
預設是新增到NSDefaultRunLoopMode
模式中,因為RunLoop
同時執行只能有一個模式,那麼在滑動scroller
的時候怎Timer
會卡頓停止直到再次切換回來,那麼如何保證同時兩個模式都可以執行呢?
Foundation
提供了一個API(void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode
新增上,mode
值為NSRunLoopCommonModes
可以保證同時兼顧2種模式。
測試程式碼:
static int i = 0;
NSTimer *timer=[NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d",++i);
}];
//NSRunLoopCommonModes 並不是一個真正的模式,它這還是一個標記
//timer在設定為common模式下能執行
//NSRunLoopCommonModes 能在 _commentModes中陣列中的模式都可以執行
//[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];//預設的模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
//log
2019-07-23 15:14:31 CFRunloop[62358:34093079] 1
2019-07-23 15:14:32 CFRunloop[62358:34093079] 2
2019-07-23 15:14:33 CFRunloop[62358:34093079] 3
2019-07-23 15:14:34 CFRunloop[62358:34093079] 4
2019-07-23 15:14:35 CFRunloop[62358:34093079] 5
2019-07-23 15:14:36 CFRunloop[62358:34093079] 6
2019-07-23 15:14:37 CFRunloop[62358:34093079] 7
2019-07-23 15:14:38 CFRunloop[62358:34093079] 8
複製程式碼
當滑動的時候timer
的時候,timer
還是如此絲滑,沒有一點停頓。
沒有卡頓之後我們VC -> dealloc
中timer
還是在執行,那麼需要在dealloc
中去下和刪除觀察者
-(void)dealloc{
NSLog(@"%s",__func__);
CFRunLoopRemoveObserver(CFRunLoopGetMain(), obs, m);
dispatch_source_cancel(timer);
}
複製程式碼
退出vc
之後dealloc
照常執行,日誌只有-[ViewController dealloc]
,而且數字沒有繼續輸出,說明刪除觀察者和取消source
都成功了。
那麼NSRunLoopCommonModes
是另外一種模式嗎?
通過原始碼檢視得知,在runloop.c line:1632 line:2608
if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) {
doit = CFEqual(curr->_mode, curMode) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
} else {
doit = CFSetContainsValue((CFSetRef)curr->_mode, curMode) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
}
複製程式碼
還有很多地方均可以看出,當是currentMode
需要和_mode
相等才去執行,當是kCFRunLoopCommonModes
的時候,只需要包含curMode
即可執行。可見kCFRunLoopCommonModes
其實是一個集合,不是某個特定的mode
。
執行緒保活
執行緒為什麼需要保活?效能其實很大的瓶頸是在於空間的申請和釋放,當我們執行一個任務的時候建立了一個執行緒,任務結束就釋放掉該執行緒,如果任務頻率比較高,那麼一個一直活躍的執行緒來執行我們的任務就省去申請和釋放空間的時間和效能。上邊已經講過了
runloop
需要有任務才能不退出,總不可能直接讓他執行while(1)
吧,這種方法明顯不對的,由原始碼得知,當有監測埠的時候,也不會退出,也不會影響應能。所以線上程初始化的時候使用
[[NSRunLoop currentRunLoop] addPort:[NSPort port]
forMode:NSRunLoopCommonModes];
複製程式碼
來保活。 在主執行緒使用是沒有意義的,系統已經在APP啟動的時候進行了呼叫,則已經加入到全域性的字典中了。
驗證執行緒保活
@property (nonatomic,strong) FYThread *thread;
- (void)viewDidLoad {
[super viewDidLoad];
self.thread=[[FYThread alloc]initWithTarget:self selector:@selector(test) object:nil];
_thread.name = @"test thread";
[_thread start];
}
- (void)test {
//新增埠
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
NSLog(@"%@",[NSThread currentThread]);
NSLog(@"--start--");
[[NSRunLoop currentRunLoop] run];
NSLog(@"--end--");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[self performSelector:@selector(alive) onThread:self.thread withObject:nil waitUntilDone:NO];
NSLog(@"執行完畢了子執行緒");//不執行 因為子執行緒保活了 不會執行完畢
}
//測試子執行緒是否還活著
- (void)alive{
NSLog(@"我還活著呢->%@",[NSThread currentThread]);
}
//log
//註釋掉新增埠程式碼
<FYThread: 0x6000013a9540>{number = 3, name = test thread}
--start--
--end--
-[ViewController touchesBegan:withEvent:]
執行完畢了子執行緒
//註釋放開的時候點選觸發log
<FYThread: 0x6000013a9540>{number = 3, name = test thread}
--start--
-[ViewController touchesBegan:withEvent:]
執行完畢了子執行緒
我還活著呢-><FYThread: 0x6000017e5c80>{number = 3, name = test thread}
複製程式碼
[[NSRunLoop currentRunLoop] addPort:[NSPort port]forMode:NSDefaultRunLoopMode]
新增埠註釋掉,直接執行了--end--
,執行緒雖然strong
強引用,但是runloop
已經退出了,所以函式alive
沒有執行,不註釋的話,alive
還會執行,end
一直不會執行,因為進入了runloop
,而且沒有退出,程式碼就不會向下執行。
那我們測試下該執行緒宣告週期多長?
- (void)viewDidLoad {
[super viewDidLoad];
self.thread=[[FYThread alloc]initWithTarget:self selector:@selector(test) object:nil];
_thread.name = @"test thread";
[_thread start];
}
- (void)test {
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
//獲取obs
NSLog(@"%@",[NSThread currentThread]);
NSLog(@"--start--");
/*
If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:. In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.
*/
[[NSRunLoop currentRunLoop] run];
NSLog(@"--end--");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[self performSelector:@selector(alive) onThread:self.thread withObject:nil waitUntilDone:NO];
NSLog(@"執行完畢了子執行緒");//不執行 因為子執行緒保活了 不會執行完畢
}
//返回上頁
- (IBAction)popVC:(id)sender {
[self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:NO];
}
//測試子執行緒是否還活著
- (void)alive{
NSLog(@"我還活著呢->%@",[NSThread currentThread]);
}
//停止子執行緒執行緒
- (void)stop{
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s",__func__);
}
- (void)dealloc{
NSLog(@"%s",__func__);
}
//log
<FYThread: 0x600003394780>{number = 3, name = test thread}
--start--
-[ViewController stop]
-[ViewController stop]
複製程式碼
擁有該執行緒的是VC
,點選pop
的時候,但是VC
和thread
沒釋放掉,好像thread
和VC
建立的迴圈引用,當self.thread=[[FYThread alloc]initWithTarget:self selector:@selector(test) object:nil];
註釋了,則VC
可以進行正常釋放。
通過測試瞭解到
這個執行緒達到了永生,就是你殺不死他,簡直了死待。查詢了不少資料才發現官方文件才是最穩的。有對這句[[NSRunLoop currentRunLoop] run]
的解釋
If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:. In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.
就是系統寫了以一個死迴圈但是沒有阻止他的引數,相當於一直在迴圈呼叫
runMode:beforeDate:
,那麼該怎麼辦呢?
官方文件給出瞭解決方案
BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
複製程式碼
將程式碼改成下面的成功將死待殺死了。
- (void)test {
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
//獲取obs
NSLog(@"%@",[NSThread currentThread]);
NSLog(@"--start--");
self.shouldKeepRunning = YES;//預設執行
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (_shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
NSLog(@"--end--");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[self performSelector:@selector(alive) onThread:self.thread withObject:nil waitUntilDone:NO];
NSLog(@"執行完畢了子執行緒");//不執行 因為子執行緒保活了 不會執行完畢
}
//返回上頁
- (IBAction)popVC:(id)sender {
self.shouldKeepRunning = NO;
[self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:NO];
}
//測試子執行緒是否還活著
- (void)alive{
NSLog(@"我還活著呢->%@",[NSThread currentThread]);
}
//停止子執行緒執行緒
- (void)stop{
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s",__func__);
[self performSelectorOnMainThread:@selector(pop) withObject:nil waitUntilDone:NO];
}
- (void)pop{
[self.navigationController popViewControllerAnimated:YES];
}
- (void)dealloc{
NSLog(@"%s",__func__);
}
//log
<FYThread: 0x600002699fc0>{number = 3, name = test thread}
--start--
-[ViewController stop]
--end--
-[ViewController dealloc]
-[FYThread dealloc]
複製程式碼
點選popVC:
首先將self.shouldKeepRunning = NO
,然後子執行緒執行CFRunLoopStop(CFRunLoopGetCurrent())
,然後在主執行緒執行pop
函式,最終返回上級頁面而且成功殺死VC
和死待。
當然這個死待其實也是有用處的,當使用單例模式作為下載器的時候使用死待也沒問題。這樣子處理比較複雜,我們可以放在VC
的dealloc
看看是否能成功。
關鍵函式稍微更改:
//停止子執行緒執行緒
- (void)stop{
if (self.thread == nil) {
return;
}
NSLog(@"%s",__func__);
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)stopThread{
self.shouldKeepRunning = NO;
CFRunLoopStop(CFRunLoopGetCurrent());
}
- (void)dealloc{
[self stop];
NSLog(@"%s",__func__);
}
複製程式碼
當點選返回按鈕VC
和執行緒都沒死,原來他們形成了強引用無法釋放,就是VC
始終無法執行dealloc
。將函式改成block
實現
__weak typeof(self) __weakSelf = self;
self.thread = [[FYThread alloc]initWithBlock:^{
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
NSLog(@"%@",[NSThread currentThread]);
NSLog(@"--start--");
__weakSelf.shouldKeepRunning = YES;//預設執行
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (__weakSelf && __weakSelf.shouldKeepRunning ){
[theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
};
NSLog(@"--end--");
}];
複製程式碼
測試下崩潰了,崩潰到了:
while (__weakSelf.shouldKeepRunning ){
[theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];//崩潰的地方
};
複製程式碼
怎麼想感覺不對勁啊,怎麼會不行呢?VC
銷燬的時候呼叫子執行緒stop
,最後打斷點發現到了崩潰的地方self
已經不存在了,說明是非同步執行的,往前查詢使用非同步的函式最後出現在了[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
,表示不用等待stopThread
函式執行時間,直接向前繼續執行,所以VC
釋放掉了,while (__weakSelf.shouldKeepRunning )
是true
,還真進去了,訪問了exe_bad_access
,所以改成while (__weakSelf&&__weakSelf.shouldKeepRunning )
再跑一下
//log
--start--
-[ViewController stop]
-[ViewController dealloc]
--end--
-[FYThread dealloc]
複製程式碼
如牛奶般絲滑,解決了釋放問題,也解決了複雜操作。本文章所有程式碼均在底部連結可以下載。 使用這個思路自己封裝了一個簡單的功能,大家可以自己封裝一下然後對比一下我的思路,說不定有驚喜!
資料參考
資料下載
最怕一生碌碌無為,還安慰自己平凡可貴。
廣告時間