iOS底層原理 - 常駐執行緒

極客學偉發表於2018-10-16

iOS底層原理 - 常駐執行緒

在 AFN 2.0 時代,會經常看到 AFN 建立一個常駐執行緒的方式:

0️⃣ AFN 2.0 時代的常駐執行緒

+ (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;
}

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

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
複製程式碼

我們知道 RunLoop 是App執行的基礎,並且每個執行緒都有其對應的唯一的 RunLoop 保證其正常執行,而且我們還知道 RunLoop 必須在特定模式下才能保持活躍狀態,更多關於 RunLoop 底層的問題可參閱 iOS - Runloop 詳解

1️⃣ 新時代常駐執行緒初試

在專案中我們也會有類似建立一個常駐執行緒進行某些耗時操作的需求,若每次均建立一個新的非同步執行緒效率很低,作為優化我們需要一個常駐的非同步執行緒。 如下程式碼可滿足我們建立一個常駐執行緒的需求:

1. 宣告常駐執行緒屬性並對其強引用
```objc
@property (nonatomic, weak) NSThread *p_thread;
```
複製程式碼
2. 例項化常駐執行緒,且獲取當前 RunLoop 設定一個 source 使其保活
```objc
+ (void)p_creatThreadMethod {

    void (^creatThreadBlock)(void) = ^ {
        NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
        [currentRunLoop addPort:[NSPort new] forMode:NSDefaultRunLoopMode];
        [currentRunLoop run];
    };
    
    if (@available(iOS 10.0, *)) {
        self.p_thread = [[NSThread alloc] initWithBlock:creatThreadBlock];
    } else {
        self.p_thread = [[NSThread alloc] initWithTarget:self selector:@selector(class_creatThreadMethod:) object:creatThreadBlock];
    }
}

+ (void)class_creatThreadMethod:(void (^)(void))block {
    block();
}
```
複製程式碼
3. 使用:在需要常駐執行緒執行某操作時:
```objc
- (void)threakTaskMethod:(void (^)(void))task {
    [self performSelector:@selector(taskMethod:) onThread:self.p_thread withObject:task waitUntilDone:NO];
 }
```
複製程式碼
弊端:此時我們建立的常駐執行緒無法跟隨當前物件的釋放而釋放:因為

Apple 對 [[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. Manually removing all known input sources and timers from the run loop is not a guarantee that the run loop will exit. macOS can install and remove additional input sources as needed to process requests targeted at the receiver’s thread. Those sources could therefore prevent the run loop from exiting. If you want the run loop to terminate, you shouldn't use this method. Instead, use one of the other run methods and also check other arbitrary conditions of your own, in a loop. A simple example would be:

大概意思就是: 如果 RunLoop 沒有定時器或輸入源,這個方法會立即結束。否則他會反覆呼叫 runMode:beforeDate: 方法,換句話說就是這個方法實際上是開啟了一個無限迴圈用以處理 runloop 的輸入源和定時器。 手動移除所有輸入源和定時器並不能保證 RunLoop 會退出。MacOS 可以安裝和移除額外的輸入源以處理接收者執行緒的請求。所以這種方式可組織 RunLoop 退出。 如果你想終結 RunLoop ,你不能使用這種方式。相反,使用其他的執行方法在 RunLoop 中檢測他們的狀態,一個簡單的?:

這時 Apple 為我們提供了一個控制 RunLoop 宣告週期的例子:

BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

複製程式碼

所以我們可以對以上程式碼進行改進!

2️⃣ 控制常駐執行緒的生命週期

1. 宣告常駐執行緒屬性和保活狀態變數
@property (nonatomic, strong) XWThread *p_thread;
@property (nonatomic, assign, getter=isShouldKeepRunning) BOOL shouldKeepRunning;
複製程式碼
2. 在管理類中建立常駐執行緒
- (NSThread *)thread {
    NSThread *thread = nil;
    __weak typeof(self) weakSelf = self;
    void (^creatThreadBlock)(void) = ^ {
        NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
        [currentRunLoop addPort:[NSPort new] forMode:NSDefaultRunLoopMode];
        while (weakSelf && weakSelf.isShouldKeepRunning) {
            // runloop 只有在外部標識 shouldKeepRunning 為 YES 時才繼續 run
            [currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
    };
    
    self.shouldKeepRunning = YES;
    if (@available(iOS 10.0, *)) {
        thread = [[NSThread alloc] initWithBlock:creatThreadBlock];
    } else {
        thread = [[NSThread alloc] initWithTarget:weakSelf selector:@selector(creatThreadMethod:) object:creatThreadBlock];
    }
    [thread start];
    return thread;
}

- (void)creatThreadMethod:(void (^)(void))creatThreadBlock {
    creatThreadBlock();
}
複製程式碼
3. 在使用常駐執行緒執行某些操作時
/**
 在預設常駐執行緒中執行操作 (執行緒需隨當前物件建立或銷燬)
 
 @param task 操作
 */
- (void)executeTask:(XWLivingThreadTask)task {
    if (!task || !self.p_thread) return;
    [self performSelector:@selector(threakTaskMethod:) onThread:self.p_thread withObject:task waitUntilDone:NO];
}

- (void)threakTaskMethod:(void (^)(void))task {
    task ? task() : nil;
}
複製程式碼
4. 在物件銷燬時手動銷燬常駐執行緒
- (void)dealloc {
    [self performSelector:@selector(clearThreadMethod) onThread:self.p_thread withObject:nil waitUntilDone:YES];
}

- (void)clearThreadMethod {
    self.shouldKeepRunning = NO; // runloop 結束標記
    CFRunLoopStop(CFRunLoopGetCurrent());
}
複製程式碼

至此,我們就封裝了一個可控制其生命週期的常駐執行緒。美滋滋。

其中 RunLoop 保活部分程式碼也可使用底層 CoreFoundation 的程式碼實現:

    void (^creatThreadBlock)(void) = ^ {
        CFRunLoopSourceContext content = {0};
        CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &content);
        CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
        CFRelease(source);
        CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
    };
複製程式碼

3️⃣ 開源

你若問我有沒有演示 Demo, 答案是有的,封裝了一個小工具類用以快速建立全域性常駐執行緒和區域性常駐執行緒的方法。 這是程式碼 ↓

XWLivingThread

相關文章