OC RunLoop應用例子

韋家冰發表於2017-12-13

知識點: 1、RunLoop的基礎知識 2、RunLoop 與 NSTimer 3、RunLoop 與 Perform Selector 4、RunLoop、執行緒、AutoreleasePool三者聯絡 5、RunLoop 與 執行緒通訊 6、RunLoop 的各種狀態監聽 7、RunLoop 與 NSNotificationQueue

####什麼是RunLoop?

NSRunLoop蘋果官方文件 CoreFoundation原始碼

RunLoop入門 看我就夠了 - 簡書 RunLoop已入門?不來應用一下? - 簡書

深入理解RunLoop | Garan no dou

基於runloop的執行緒保活、銷燬與通訊 IOS---例項化講解RunLoop

RunLoop結構圖

RunLoop跑圈圖

RunLoop 有五種執行模式,其中常見的有1.2兩種

1. kCFRunLoopDefaultMode:App的預設Mode,通常主執行緒是在這個Mode下執行
2. UITrackingRunLoopMode:介面跟蹤 Mode,用於 ScrollView 追蹤觸控滑動,保證介面滑動時不受其他 Mode 影響
3. UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成後就不再使用
4. GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到
5. kCFRunLoopCommonModes: 這是一個佔位用的Mode,作為標記kCFRunLoopDefaultMode和UITrackingRunLoopMode用,並不是一種真正的Mode
複製程式碼

####RunLoop應用 #####1、NSTimer 主執行緒下:

- (void)timer
{
    NSTimer *aTimer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

    // 1、定時器只執行在NSDefaultRunLoopMode下,一旦RunLoop進入其他模式,這個定時器就不會工作
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    // 2、定時器只執行在UITrackingRunLoopMode下(滑動UIScrollView),一旦RunLoop進入其他模式,這個定時器就不會工作
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
    // 3、標記為NSRunLoopCommonModes的模式:UITrackingRunLoopMode和NSDefaultRunLoopMode相容
    [[NSRunLoop mainRunLoop] addTimer:aTimer forMode:NSRunLoopCommonModes];
}
複製程式碼

scheduledTimerWithTimeInterval

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //主執行緒
    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        // 這種方法是直接加入RunLoop:[[NSRunLoop currentRunLoop] addTimer: t forMode: NSDefaultRunLoopMode];
        [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(timerFired:)userInfo:nil repeats:YES];
        // 需要確保當前currentRunLoop run起來
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    });
    
}

複製程式碼

#####RunLoop的run與stop 1、CFRunLoopStop能直接停止掉所有用CFRunloop執行起runloop

- (void)testTimer
{
    NSTimer *aTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(runrun) userInfo:nil repeats:YES];
    // 手動獲取
    NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
    // 由於UI都是在主執行緒,子執行緒不會有UITrackingRunLoopMode干擾,所以使用NSDefaultRunLoopMode就可以了
    [currentRunLoop addTimer:aTimer forMode:NSDefaultRunLoopMode];
    // run起來的方式,有
    // 方式1
    // 方式2
    // 方式3
}

複製程式碼

方式1:run

    /*方式1:
     永久性的執行在NSDefaultRunLoopMode模式
     停止runloop方式:
     1、使用CFRunLoopStop無效
     2、(1)停止移除timer或者移除port;(2)暴力強制退出執行緒(不是解決辦法)
     */
    [currentRunLoop run];
複製程式碼

方式2:runUntilDate

    /*方式2:
     規定時間執行在NSDefaultRunLoopMode模式
     停止runloop方式:
     1、使用CFRunLoopStop無效
     2、(1)停止移除timer或者移除port;(2)到達指定時間
     */
    [currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];//
複製程式碼

方式3:runMode: beforeDate: (其實呼叫CFRunLoopRunInMode)

    // returnAfterSourceHandled引數為YES,當觸發一個非timer事件後,runloop就終止了
    CFRunLoopRunInMode (mode, seconds, returnAfterSourceHandled);returnAfterSourceHandled = YES的封裝;
複製程式碼
    /*方式3:
     規定runMode、規定時間,執行完返回執行狀態
     停止runloop方式:
        (1)使用CFRunLoopStop;
        (2)停止移除timer或者移除port;
        (3)到達指定時間
     */

    BOOL result =  [currentRunLoop runMode:UITrackingRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:20]];
    if (result) {
        // 如果是PerfromSelector*事件或者其他Input Source事件觸發處理後,Run Loop結束時候返回YES,其他返回NO。
    }else {
        // NO
    }

}
複製程式碼

一定時間內監聽某種事件,或執行某種任務的執行緒,如在30分鐘內,每隔30s執行onTimerFired:

@autoreleasepool {
    
    NSTimer * udpateTimer = [NSTimer timerWithTimeInterval:30
                                                    target:self
                                                  selector:@selector(onTimerFired:)
                                                  userInfo:nil
                                                   repeats:YES];

    NSRunLoop * runLoop = [NSRunLoop currentRunLoop];
    [runLoop addTimer:udpateTimer forMode:NSRunLoopCommonModes];
    [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:60*30]];
}
複製程式碼

#####2、performSelector: withObject: afterDelay: inModes

主執行緒

    // 延時performSelector在主執行緒會被RunLoopMode干擾
    // 延時performSelector其實裡面就是一個NSTimer要實現afterDelay
    [self performSelector:@selector(performSelector) withObject:nil afterDelay:2   inModes:@[NSDefaultRunLoopMode]];
複製程式碼

子執行緒

    BBThread *thread = [[BBThread alloc] initWithBlock:^{
        
        [self performSelector:@selector(performSelector) withObject:nil afterDelay:2 inModes:@[NSDefaultRunLoopMode]];
        // 需要手動currentRunLoop跑起來
        NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
        [currentRunLoop run];
         
    }];
    [thread start];
複製程式碼

#####3、常駐執行緒 AFNetworking 2.0中 建立了一條常駐執行緒專門處理所有請求的回撥事件

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
         // 這裡主要是監聽某個 port,目的是讓這個 Thread 不會回收
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; 
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread =
        [[NSThread alloc] initWithTarget:self
                                selector:@selector(networkRequestThreadEntryPoint:)
                                  object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}
複製程式碼

#####4、執行緒、RunLoop、AutoreleasePool三者關係 ######執行緒與RunLoop CFRunLoopGetMain() 和 CFRunLoopGetCurrent()的原始碼

/// 全域性的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
 
/// 獲取一個 pthread 對應的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次進入時,初始化全域性Dic,並先為主執行緒建立一個 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接從 Dictionary 裡獲取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到時,建立一個
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 註冊一個回撥,當執行緒銷燬時,順便也銷燬其對應的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}
複製程式碼

從上面的程式碼可以看出,執行緒和 RunLoop 之間是一一對應的,其關係是儲存在一個全域性的 Dictionary 裡。執行緒剛建立時並沒有 RunLoop,如果你不主動獲取,那它一直都不會有。RunLoop 的建立是發生在第一次獲取時,RunLoop 的銷燬是發生線上程結束時。你只能在一個執行緒的內部獲取其 RunLoop(主執行緒除外)。

######AutoreleasePool與RunLoop 1、一個執行緒對應一個RunLoop (主執行緒系統啟動,其他執行緒需要使用來載入獲取並啟動) 2、執行緒如果沒有啟動RunLoop,遇到autorelease物件會用下面情況:

1)、執行緒裡面如果有物件呼叫autorelease方法,系統就去找這個執行緒key對應的最棧頂poolpage;
2)、如果找到page.add(obj)OK;
3)、如果找不到就呼叫autoreleaseNoPage,新建這個執行緒的一個page;
4)、執行緒釋放,這個執行緒對應的page也pop。
複製程式碼

3、執行緒啟動了RunLoop,RunLoop內部會管理AutoreleasePool

1)、RunLoop開啟時會objc_autoreleasePoolPush;
2)、RunLoop休眠時會先objc_autoreleasePoolPop再objc_autoreleasePoolPush;
3)、RunLoop退出時會objc_autoreleasePoolPop;
複製程式碼

4、另外 在啟動RunLoop之前建議用 @autoreleasepool {...}包裹 意義:建立一個大釋放池,釋放{}期間建立的臨時物件,一般好的框架的作者都會這麼做 (上面的AFNetworking就是這麼做)

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
         // 這裡主要是監聽某個 port,目的是讓這個 Thread 不會回收
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; 
        [runLoop run];
    }
}
複製程式碼

######NSRunLoop與NSMachPort來執行緒通訊的例項:

- (void)testDemo3
{
    //宣告兩個埠   隨便怎麼寫建立方法,返回的總是一個NSMachPort例項
    NSMachPort *mainPort = [[NSMachPort alloc]init];
    NSPort *threadPort = [NSMachPort port];
    //設定執行緒的埠的代理回撥為自己
    threadPort.delegate = self;

    //給主執行緒runloop加一個埠
    [[NSRunLoop currentRunLoop]addPort:mainPort forMode:NSDefaultRunLoopMode];

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        //新增一個Port
        [[NSRunLoop currentRunLoop]addPort:threadPort forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

    });

    NSString *s1 = @"hello";

    NSData *data = [s1 dataUsingEncoding:NSUTF8StringEncoding];

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSMutableArray *array = [NSMutableArray arrayWithArray:@[mainPort,data]];
        //過2秒向threadPort傳送一條訊息,第一個引數:傳送時間。msgid 訊息標識。
        //components,傳送訊息附帶引數。reserved:為頭部預留的位元組數(從官方文件上看到的,猜測可能是類似請求頭的東西...)
        [threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0];

    });

}

//這個NSMachPort收到訊息的回撥,注意這個引數,可以先給一個id。如果用文件裡的NSPortMessage會發現無法取值
- (void)handlePortMessage:(id)message
{

    NSLog(@"收到訊息了,執行緒為:%@",[NSThread currentThread]);

    //只能用KVC的方式取值
    NSArray *array = [message valueForKeyPath:@"components"];

    NSData *data =  array[1];
    NSString *s1 = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"%@",s1);

//    NSMachPort *localPort = [message valueForKeyPath:@"localPort"];
//    NSMachPort *remotePort = [message valueForKeyPath:@"remotePort"];

}
複製程式碼

列印如下:

2016-11-23 16:50:20.604 TestRunloop3[1322:120162] 收到訊息了,執行緒為:<NSThread: 0x60800026d700>{number = 3, name = (null)}
2016-11-23 16:50:26.551 TestRunloop3[1322:120162] hello

複製程式碼

######自定義的輸入源來實現執行緒通訊

- (void)testDemo4
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        NSLog(@"starting thread.......");

        _runLoopRef = CFRunLoopGetCurrent();
        //初始化_source_context。
        bzero(&_source_context, sizeof(_source_context));
        //這裡建立了一個基於事件的源,繫結了一個函式
        _source_context.perform = fire;
        //引數
        _source_context.info = "hello";
        //建立一個source
        _source = CFRunLoopSourceCreate(NULL, 0, &_source_context);
        //將source新增到當前RunLoop中去
        CFRunLoopAddSource(_runLoopRef, _source, kCFRunLoopDefaultMode);

        //開啟runloop 第三個引數設定為YES,執行完一次事件後返回
        CFRunLoopRunInMode(kCFRunLoopDefaultMode, 9999999, YES);

        NSLog(@"end thread.......");
    });


    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        if (CFRunLoopIsWaiting(_runLoopRef)) {
            NSLog(@"RunLoop 正在等待事件輸入");
            //新增輸入事件
            CFRunLoopSourceSignal(_source);
            //喚醒執行緒,執行緒喚醒後發現由事件需要處理,於是立即處理事件
            CFRunLoopWakeUp(_runLoopRef);
        }else {
            NSLog(@"RunLoop 正在處理事件");
            //新增輸入事件,當前正在處理一個事件,當前事件處理完成後,立即處理當前新輸入的事件
            CFRunLoopSourceSignal(_source);
        }
    });

}

//此輸入源需要處理的後臺事件
static void fire(void* info){

    NSLog(@"我現在正在處理後臺任務");

    printf("%s",info);
}

複製程式碼

輸出結果如下:

2016-11-24 10:42:24.045 TestRunloop3[4683:238183] starting thread.......
2016-11-24 10:42:26.045 TestRunloop3[4683:238082] RunLoop 正在等待事件輸入 
2016-11-24 10:42:31.663 TestRunloop3[4683:238183] 我現在正在處理後臺任務
hello
2016-11-24 10:42:31.663 TestRunloop3[4683:238183] end thread.......

複製程式碼

#####CFRunLoopObserverRef是觀察者,能夠監聽RunLoop的狀態改變

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //建立監聽者
    /*
     第一個引數 CFAllocatorRef allocator:分配儲存空間 CFAllocatorGetDefault()預設分配
     第二個引數 CFOptionFlags activities:要監聽的狀態 kCFRunLoopAllActivities 監聽所有狀態
     第三個引數 Boolean repeats:YES:持續監聽 NO:不持續
     第四個引數 CFIndex order:優先順序,一般填0即可
     第五個引數 :回撥 兩個引數observer:監聽者 activity:監聽的事件
     */
    /*
     所有事件
     typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
     kCFRunLoopEntry = (1UL << 0),   //   即將進入RunLoop
     kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理Timer
     kCFRunLoopBeforeSources = (1UL << 2), // 即將處理Source
     kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠
     kCFRunLoopAfterWaiting = (1UL << 6),// 剛從休眠中喚醒
     kCFRunLoopExit = (1UL << 7),// 即將退出RunLoop
     kCFRunLoopAllActivities = 0x0FFFFFFFU
     };
     */
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"RunLoop進入");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"RunLoop要處理Timers了");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"RunLoop要處理Sources了");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"RunLoop要休息了");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"RunLoop醒來了");
                break;
            case kCFRunLoopExit:
                NSLog(@"RunLoop退出了");
                break;
                
            default:
                break;
        }
    });
    
    // 給RunLoop新增監聽者
    /*
     第一個引數 CFRunLoopRef rl:要監聽哪個RunLoop,這裡監聽的是主執行緒的RunLoop
     第二個引數 CFRunLoopObserverRef observer 監聽者
     第三個引數 CFStringRef mode 要監聽RunLoop在哪種執行模式下的狀態
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    /*
     CF的記憶體管理(Core Foundation)
     凡是帶有Create、Copy、Retain等字眼的函式,建立出來的物件,都需要在最後做一次release
     GCD本來在iOS6.0之前也是需要我們釋放的,6.0之後GCD已經納入到了ARC中,所以我們不需要管了
     */
    CFRelease(observer);
}


複製程式碼

CFRunLoopObserverRef使用demo: 利用主執行緒RunLoop空閒時候在處理,一些UI事情,減少卡頓(這就不需要多執行緒了)

 CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        
        if (activity == kCFRunLoopBeforeWaiting) {
            // RunLoop要休息了
        }
    });
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopDefaultMode);
    CFRelease(observer);
複製程式碼

NSNotificationQueue也與runloop有關係OC--NSNotificationCenter重新認知

相關文章