簡單談談iOS多線程之間的通信方式


一、進程與線程

1.1 進程

進程是系統進行資源分配和調度的基本單位,在iOS上,一個App運行起來的實例就是一個進程,每個進程在內存中都有自己獨立的地址段。

1.2 線程

線程是進程的基本執行單元,進程中的所有任務都在線程中執行,因此,一個進程中至少要有一個線程。iOS程序啟動后會默認開啟一個主線程,也叫UI線程。

1.3 進程與線程的關系

  • 地址空間:同一進程中的地址空間可以被本進程中的多個線程共享,但進程與進程之間的地址空間是獨立的
  • 資源擁有:同一進程中的資源可以被本進程中的所有線程共享,如內存、I/O、CUP等等,但進程與進程之間的資源是相互獨立的
  • 一個進程中的任一線程崩潰后,都會導致整個進程崩潰,但進程奔潰后不會影響另一個進程
  • 進程可以看做是線程的容器,每個進程都有一個程序運行的入口,但線程不能獨立運行,必須依存於進程

1.4 線程與Runloop的關系

  • 線程與Runloop是一一對應的,一個Runloop對應一個核心線程,為什么說是核心,因為Runloop是可以嵌套的,但核心的只有一個,他們的對應關系保存在一個全局字典里
  • Runloop是來管理線程的,線程執行完任務時會進入休眠狀態,有任務進來時會被喚醒開始執行任務(事件驅動)
  • Runloop在第一次獲取時被創建,線程結束時被銷毀
  • 主線程的Runloop在程序啟動時就默認創建好了
  • 子線程的Runloop是懶加載的,只有在使用時才被創建,因此在子線程中使用NSTimer時要注意確保子線程的Runloop已創建,否則NSTimer不會生效。

二、多線程

2.1 概念及原理

一個進程中可以並發多個線程同時執行各自的任務,叫做多線程。分時操作系統會把CPU的時間划分為長短基本相同的時間區間,叫“時間片”,在一個時間片內,CPU只能處理一個線程中的一個任務,對於一個單核CPU來說,在不同的時間片來執行不同線程中的任務,就形成了多個任務在同時執行的“假象”:

Thread

上圖中,CPU在時間片t3開始執行線程3中的任務,但任務還沒執行完,來到了t4,開始執行線程4中的任務,在t4這個時間片內就執行完了線程4的任務,到t5時接着執行線程3的任務。
現在都是多核CPU,每個核心都可以單獨處理任務,實現“真正”的多線程,但是一個App動輒幾十個並發線程,那么每個核心仍然以上述原理實現多線程。

2.2 iOS中的幾種多線程

iOS中,有下列幾種多線程的使用方式:

  1. pthread:即POSIX Thread,縮寫稱為Pthread,是線程的POSIX標准,是一套通用的多線程API,可以在Unix/Linux/Windows等平台跨平台使用。iOS中基本不使用。
  1. NSThread:蘋果封裝的面向對象的線程類,可以直接操作線程,比起GCDNSThread效率更高,由程序員自行創建,當線程中的任務執行完畢后,線程會自動退出,程序員也可手動管理線程的生命周期。使用頻率較低。
  1. GCD:全稱Grand Central Dispatch,由C語言實現,是蘋果為多核的並行運算提出的解決方案,CGD會自動利用更多的CPU內核,自動管理線程的生命周期,程序員只需要告訴GCD需要執行的任務,無需編寫任何管理線程的代碼。GCD也是iOS使用頻率最高的多線程技術。
  1. NSOperation:基於GCD封裝的面向對象的多線程技術,常配合NSOperationQueue使用,使用頻率較高。

三、線程池

  • 線程池(Thread Pool)
    顧名思義就是一個管理多個線程生命周期的池子。iOS開發中不會直接接觸到線程池,這是因為GCD已經包含了線程池的管理,我們只需要通過GCD獲取線程來執行任務即可。
  • 線程的生命周期
    一個線程的生命周期包括創建--就緒--運行--死亡這四個階段,我們可以通過阻塞、退出等來控制線程的生命周期。

四、線程間的通訊

4.1 幾種線程間的通訊方式

在面試中,經常被面試官問到線程間是如何通訊的,很多童鞋會回答在子線程獲取數據,切換回主線程刷新UI,那么請你回家等消息。蘋果的官方文檔給我們列出了線程間通訊的幾種方式:

Thread

上圖的表格是按照技術復雜度由低到高順序排列的,其中后兩種只能在OS X中使用。

  • Direct messaging:這是大家非常熟悉的-performSelector:系列。
  • Global variables...:直接通過全局變量、共享內存等方式,但這種方式會造成資源搶奪,涉及到線程安全問題。
  • Conditions:一種特殊的鎖--條件鎖,當使用條件鎖使一個線程等待(wait)時,該線程會被阻塞並進入休眠狀態,在另一個線程中對同一個條件鎖發送信號(single),則等待中的線程會被喚醒繼續執行任務。
  • Run loop sources:通過自定義Run loop sources來實現,后面的文章會單獨研究Run loop
  • Ports and sockets:通過端口和套接字來實現線程間通訊。

4.2 線程間通訊示例

前兩種我們太熟悉了,第三種條件鎖使用起來也不難,這里通過Port來實現一個線程間通訊的Demo。
新建一個iOS工程,新建類AvatarDownloader,模擬一個子線程中下載頭像,主線程刷新UI的過程

// AvatarDownloader.h
extern NSString * const AvatarDownloaderUrlKey;

extern NSString * const AvatarDownloaderPortKey;

@interface AvatarDownloader : NSObject

- (void)downloadAvatarInfo:(NSDictionary *)info;

@end

// AvatarDownloader.m
NSString * const AvatarDownloaderUrlKey = @"Url";

NSString * const AvatarDownloaderPortKey = @"Port";

@interface AvatarDownloader ()<NSMachPortDelegate>

@property (nonatomic, strong) NSPort *completePort;

@property (nonatomic, strong) NSMachPort *downloaderPort;

@end

@implementation AvatarDownloader

- (instancetype)init {
    if (self = [super init]) {
        self.downloaderPort = [[NSMachPort alloc] init];
        self.downloaderPort.delegate = self;
    }
    return self;
}

- (void)downloadAvatarInfo:(NSDictionary *)info {
    @autoreleasepool {
        NSLog(@"download thread: %@", [NSThread currentThread]);
        NSString *url = info[AvatarDownloaderUrlKey];
        NSLog(@"download url: %@", url);
        self.completePort = info[AvatarDownloaderPortKey];
        
        // 模擬下載
        sleep(2);
        UIImage *img = [UIImage imageNamed:@"avatar.jpg"];
        NSData *data = UIImageJPEGRepresentation(img, 1);
        NSLog(@"download complete");
        
        NSMutableArray *components = @[data].mutableCopy;
        [self.completePort sendBeforeDate:[NSDate date]
                                    msgid:1
                               components:components
                                     from:self.downloaderPort
                                 reserved:0];
    }
}

#pragma mark - NSMachPortDelegate
- (void)handlePortMessage:(NSPortMessage *)message {
    NSLog(@"downloader handlePortMessage: %@", [NSThread mainThread]);
    NSArray *components = [(id)message valueForKey:@"components"];
    NSData *data = components[0];
    NSString *msg = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"response msg from receiver: %@", msg);
}

根控制器代碼如下:

// ViewController.m
#import "ViewController.h"
#import "AvatarDownloader.h"

NSString * const AVATAR_URL = @"http://img3.imgtn.bdimg.com/it/u=1559309274,2399850183&fm=26&gp=0.jpg";

@interface RootViewController ()<NSMachPortDelegate>

@property (weak, nonatomic) IBOutlet UIImageView *imageView;

@property (nonatomic, strong) NSMachPort *mainPort;

@property (nonatomic, strong) AvatarDownloader *downloader;

@end

@implementation RootViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 創建Port對象,並添加到主線程的Runloop中
    self.mainPort = [[NSMachPort alloc] init];
    self.mainPort.delegate = self;
    [[NSRunLoop currentRunLoop] addPort:self.mainPort forMode:NSDefaultRunLoopMode];
    
    NSDictionary *info = @{AvatarDownloaderUrlKey : AVATAR_URL,
                           AvatarDownloaderPortKey : self.mainPort};
    
    self.downloader = [[AvatarDownloader alloc] init];
    [NSThread detachNewThreadSelector:@selector(downloadAvatarInfo:)
                             toTarget:self.downloader
                           withObject:info];
}

#pragma mark - NSPortDelegate
- (void)handlePortMessage:(NSPortMessage *)message {
    NSLog(@"handlePortMessage: %@", [NSThread currentThread]);
    NSArray *array = [(id)message valueForKey:@"components"];
    NSData *data = array[0];
    UIImage *avatar = [UIImage imageWithData:data];
    self.imageView.image = avatar;
    
    NSData *responseMsg = [@"頭像已收到" dataUsingEncoding:NSUTF8StringEncoding];
    NSMutableArray *components = @[responseMsg].mutableCopy;
    NSPort *remotePort = [(id)message valueForKey:@"remotePort"];
    
    // downloader線程已銷毀,因此要給remotePort發消息,就得把它添加到存活的runloop中
    [[NSRunLoop currentRunLoop] addPort:remotePort forMode:NSDefaultRunLoopMode];
    
    [remotePort sendBeforeDate:[NSDate date]
                         msgid:2
                    components:components
                          from:self.mainPort
                      reserved:0];
}

@end

NSPort的使用要點:

  1. NSPort對象必須添加到要接收消息的線程的Runloop
  1. 接收消息的對象實現NSPortDelegate協議的-handlePortMessage:方法來獲取消息內容

運行程序后,控制台輸出如下:

2020-02-23 00:11:43.448999+0800 TestObjC[3140:208871] download thread: <NSThread: 0x600001e1e740>{number = 6, name = (null)}
2020-02-23 00:11:43.449342+0800 TestObjC[3140:208871] download url: http://img3.imgtn.bdimg.com/it/u=1559309274,2399850183&fm=26&gp=0.jpg
2020-02-23 00:11:45.486259+0800 TestObjC[3140:208871] download complete
2020-02-23 00:11:45.486600+0800 TestObjC[3140:208701] handlePortMessage: <NSThread: 0x600001e49e00>{number = 1, name = main}
2020-02-23 00:11:45.492472+0800 TestObjC[3140:208701] downloader handlePortMessage: <NSThread: 0x600001e49e00>{number = 1, name = main}
2020-02-23 00:11:45.492666+0800 TestObjC[3140:208701] response msg from receiver: 頭像已收到

代碼中首先將self.mainPort添加到主線程的Runloop中,然后起新線程下載頭像,下載完成后通過mainPort發送消息,此時並沒有手動切換線程,但是controller中的回調卻是在主線程中的,如此便完成了線程間的通訊。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM