谈谈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中的回调却是在主线程中的,如此便完成了线程间的通讯。

Guess you like

Origin juejin.im/post/7066776992800047117
Recommended