iOS多线程机制

1、概述

    当一个程序进入到内存中开始运行时,即变成了一个进程。进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立的单位。每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存。在iOS开发中,一个APP在内存中就是一个进程,且相互独立,只能访问自己的沙盒控件,这也是苹果运行能够流畅安全的一个主要原因。在学习iOS的多线程机制之前,先了解下几个基本概念。

1.1 进程特点

  • 独立性:是系统独立存在的实体,拥有自己独立的资源,有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户的进程不可以直接访问其他进程的地址空间。
  • 动态性:进程与程序的区别在于:程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集和,进程中加入了时间的概念。进程具有自己的生命周期和不同的状态,这些都是程序不具备的。
  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会相互影响。
  • 1.2 线程

        线程的特点如下:
    • 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行
    • 进程要想执行任务,必须得有线程,进程至少要有一条线程
    • 程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程

    1.3 进程和线程的关系

    • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
    • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间 的资源是独立的。
    • 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程 都死掉。所以多进程要比多线程健壮。
    • 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。 同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
    • 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
    • 线程是处理器调度的基本单位,但是进程不是。

    1.4 多进程

         打开mac的活动监视器,可以看到很多个进程同时运行
    • 进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然程序是死的(静态的),进程是活动的(动态的)。
    • 进程可以分为系统进程用户进程
      • 系统进程:凡是用于完成操作系统的各种功能的进程就是系统进程,他们就是出于运行状态下的操作系统本身
      • 用户进程:运行用户程序时创建的运行在用户态下的进程。
    • 进程又被细化为线程,也就是一个进程下有多个能独立运行的更小的单位。在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态,这便是多进程。

    1.5 多线程

    • 同一时间,CPU 只能处理1条线程,只有1条线程执行。多线程并发执行,其实是CPU快速地在多条线程之间调度(切换)。如果CPU的调度线程的时间足够快,就造成了多线程并发执行的假象。

    • 如果线程非常至多(N条),CPU会在这些(N条)线程之间调度,消耗大量的CPU资源,每条线程被调用执行的频率会降低(线程的执行效率降低),因此是有最佳线程数的计算的。
    • 多线程的优点:

      • 适当提高程序的执行效率
      • 适当提高资源的利用率(CPU、内存利用率)
    • 多线程的缺点:

      • 开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512kb),如若开启大量线程,会占用大量的内存空间,就会降低程序的性能

      • 线程越多,CPU在调度线程的开销就越大

      • 程序设计更加复杂:如线程之间的通信、多线程之间的数据共享等

    1.6 任务

         就是执行操作的意思,也就是在线程中执行的那段代码。在DCD中是放在block 中的将要执行的代码。

         执行任务的方式有两种:同步执行(sync)和异步执行(async)

    • 同步(Sync): 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列中的任务都完成之后再执行,所以会阻塞线程。 只能在当前线程中执行任务(是当前的线程,不一定是主线程),不具备开启新线程的能力
    //创建同步任务(queue是队列)
    dispatch_sync(queue, ^{
            
    });
    • 异步(Async): 线程会立即返回,无需等待就会继续执行下面的任务,不会阻塞当前线程。可以在线的线程中执行任务,具备开启线程的能力(注:具备这个能力,但并不代表一定开启新的线程)。如果不是添加在主队列上,异步会在子线程中执行任务。
    //创建异步任务(queue是队列)
    dispatch_async(queue, ^{
            
    });
         同步和异步的区别在于:是否等待队列的任务执行完成,以及是否具备开启新线程的能力。

    1.7 队列

         队列(Dispatch Queue): 这里的队列是指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,遵循 FIFO(先进先出) 的原则,即只能在队尾插入,队头删除。 没读取一个任务,则从队列中释放(删除)一个任务。

    在GCD中有两种队列:串行队列和并发队列。两者都符合 FIFO 的原则,二者的主要区别是:执行的顺序不同开启的线程数不同

    • 串行队列(Serial Dispatch Queue):

      同一时间内,队列中只能执行一个任务,只有当前的任务执行完成之后,才能执行下一个任务。(只能开启一个线程,一个线程执行完毕后,再执行下一个任务)。
    //第一个参数是队列的名字,自定义;第二个参数表示是串行还是并发
    //DISPATCH_QUEUE_SERIAL = NULL,因此DISPATCH_QUEUE_SERIAL可以用NULL代替创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("shifx", DISPATCH_QUEUE_SERIAL);
    • 主队列:是主线程上的一个串行队列,是系统自动为程序创建的。在主队列中调用不能在主线程再次调用同步任务,否则会造成堵塞,但是可以在其他线程中调用
    • 并行队列(Concurrent Dispatch Queue):

      同时允许多个任务同时执行。(可以开启多个线程,并且同时执行)。并发队列的并发功能只有在异步(dispatch_async) 函数下才有效
    //第一个参数是队列的名字,自定义;第二个参数表示是串行还是并发
    dispatch_queue_t queue = dispatch_queue_create("shifx", DISPATCH_QUEUE_CONCURRENT);
    • 全局队列:系统提供的并发队列



    1.8 任务和队列

           任务和队列两两组合,就可以等得到四种组合,再加上两个特殊队列,即:
    • 串行队列+同步执行
    • 并发队列+同步执行
    • 串行队列+异步执行
    • 并发队列+异步执行
    • 串行主队列
    • 全局并发队列

    1.8.1串行队列+同步执行

    -(void)serialSysncText {
        //串行队列
        dispatch_queue_t serial = dispatch_queue_create("shifx", DISPATCH_QUEUE_SERIAL);
        NSLog(@"串行同步任务开始");
        dispatch_sync(serial, ^{
            sleep(1);
            for (int i = 0; i<3; i++) {
                NSLog(@"串行同步任务1 = %d",i);
            }
        });
        dispatch_sync(serial, ^{
            sleep(1);
            for (int i = 0; i<3; i++) {
                NSLog(@"串行同步任务2 = %d",i);
            }
        });
        NSLog(@"串行同步任务结束");
    }
    

       从执行结果可以看出,串行同步任务是顺序执行的,任务2只有等任务1完成之后才会开始。

    1.8.2 并发队列+同步任务

    -(void)concurrentAsysncText {
        //并发队列
        dispatch_queue_t concurrent = dispatch_queue_create("shifx", DISPATCH_QUEUE_CONCURRENT);
        NSLog(@"并发同步任务开始");
        dispatch_sync(concurrent, ^{
            sleep(1);
            for (int i = 0; i<3; i++) {
                NSLog(@"并发同步任务1 = %d",i);
            }
        });
        dispatch_sync(concurrent, ^{
            sleep(1);
            for (int i = 0; i<3; i++) {
                NSLog(@"并发同步任务2 = %d",i);
            }
        });
        NSLog(@"并发同步任务结束");
    }
    
       从执行结果来看。并发队列执行的同步任务也是按照顺序依次执行的,因为它们都是在主线程中执行的,没有开启新的线程。

    1.8.3 串行队列+异步执行

    -(void)serialAsysncText {
        //串行队列
        dispatch_queue_t serial = dispatch_queue_create("shifx", DISPATCH_QUEUE_SERIAL);
        NSLog(@"串行异步任务开始");
        dispatch_async(serial, ^{
            
            for (int i = 0; i<3; i++) {
                sleep(1);
                NSLog(@"串行异步任务1 = %d",i);
                NSLog(@"当前线程:%@",[NSThread currentThread]);
            }
        });
        dispatch_async(serial, ^{
            for (int i = 0; i<3; i++) {
                sleep(1);
                NSLog(@"串行异步任务2 = %d",i);
                NSLog(@"当前线程:%@",[NSThread currentThread]);
            }
        });
        NSLog(@"串行异步任务结束");
    }
    

      从执行结果来看:
    • 所有的异步任务都是在打印串行异步任务结束后才开始执行的;这意味着,异步任务是主线程开启的子线程中进行的。
    • 从线程信息可以看到,开启了新的线程,但也只开启了一个线程;
    • 在串行队列中的异步任务还是按照先后顺序执行的;

    1.8.4 并发队列+异步执行

    -(void)concurrentAsysncText {
        //并发队列
        dispatch_queue_t concurrent = dispatch_queue_create("shifx", DISPATCH_QUEUE_CONCURRENT);
        NSLog(@"并发异步任务开始");
        dispatch_async(concurrent, ^{
            for (int i = 0; i<3; i++) {
                sleep(1);
                NSLog(@"并发异步任务1 = %d",i);
                NSLog(@"当前线程:%@",[NSThread currentThread]);
            }
        });
        dispatch_async(concurrent, ^{
            
            for (int i = 0; i<3; i++) {
                sleep(1);
                NSLog(@"并发异步任务2 = %d",i);
                NSLog(@"当前线程:%@",[NSThread currentThread]);
            }
        });
        NSLog(@"并发异步任务结束");
    }
    
       从执行结果可以看到:
    • 所有的异步任务都是在打印并发异步任务结束后开始执行,这还是意味着,主线程开启型线程去执行异步任务。
    • 由于有两个异步任务,所以并发队列就开了两个新的线程去执行,这一点从打印的线程编号也可以看出。
    • 两个异步任务的执行是没有先后顺序的。

    1.8.5 主队列+同步任务

          首先,要注意一种错误调用,那就是“在主线程中调用主队列同步任务”,由于主队列是串行队列,它不能在主线程中调用主队列的同步任务:

       出现崩溃的原因就在于同步任务放在主线程中去调用,需要等待主线程执行完毕才可以执行,而主线程则需要等待该方法执行完成,两者互相等待造成了死锁,所以就崩溃了。正确的情形是“其他线程调用主队列同步任务”,示例代码如下:
    //使用 NSThread 的 detachNewThreadWithBlock 方法会创建线程
    [NSThread detachNewThreadWithBlock:^{
        [self mainSysncText];
    }];
        
    -(void)mainSysncText {
        NSLog(@"当前所在的线程:%@",[NSThread currentThread]);
        dispatch_sync(dispatch_get_main_queue(), ^{
            for (int i = 0; i<2; i++) {
                sleep(1);
                NSLog(@"主队列同步任务1 = %d",i);
                NSLog(@"当前线程:%@",[NSThread currentThread]);
            }
        });
        dispatch_sync(dispatch_get_main_queue(), ^{
            for (int i = 0; i<2; i++) {
                sleep(1);
                NSLog(@"主队列同步任务2 = %d",i);
                NSLog(@"当前线程:%@",[NSThread currentThread]);
            }
        });
    }
    

    1.8.6 主队列+异步任务

        由于主队列是串新队列,所以等同于:串行队列+异步任务
    -(void)mainAsysncText {
        NSLog(@"当前所在的线程:%@",[NSThread currentThread]);
        NSLog(@"主队列异步任务开始");
        dispatch_async(dispatch_get_main_queue(), ^{
            for (int i = 0; i<2; i++) {
                sleep(1);
                NSLog(@"主队列异步任务1 = %d",i);
                NSLog(@"当前线程:%@",[NSThread currentThread]);
            }
        });
        dispatch_async(dispatch_get_main_queue(), ^{
            for (int i = 0; i<2; i++) {
                sleep(1);
                NSLog(@"主队列异步任务2 = %d",i);
                NSLog(@"当前线程:%@",[NSThread currentThread]);
            }
        });
        NSLog(@"主队列异步任务结束");
    }
    

    1.9 多线程生命周期


    多线程的生命周期是:新建 - 就绪 - 运行 - 阻塞 - 死亡
    • 新建:实例化线程对象
    • 就绪:向线程对象发送start消息,线程对象被加入可调度线程池等待CPU调度。
    • 运行:CPU 负责调度可调度线程池中线程的执行。线程执行完成之前,状态可能会在就绪和运行之间来回切换。就绪和运行之间的状态变化由CPU负责,程序员不能干预。
    • 阻塞:当满足某个预定条件时,可以使用休眠或锁,阻塞线程执行。sleepForTimeInterval(休眠指定时长),sleepUntilDate(休眠到指定日期),@synchronized(self):(互斥锁)。
    • 死亡:正常死亡,线程执行完毕。非正常死亡,当满足某个条件后,在线程内部中止执行/在主线程中止线程对象

    1.10 线程池原理


  • 若线程池大小小于核心线程池大小时
    • 创建线程执行任务
  • 若线程池大小大于等于核心线程池大小时
    1. 先判断线程池工作队列是否已满
    2. 若没满就将任务push进队列
    3. 若已满时,且maximumPoolSize>corePoolSize,将创建新的线程来执行任务
    4. 反之则交给饱和策略去处理
           饱和策略:
    • AbortPolicy直接抛出RejectedExecutionExeception异常来阻止系统正常运行
    • CallerRunsPolicy将任务回退到调用者
    • DisOldestPolicy丢掉等待最久的任务
    • DisCardPolicy直接丢弃任务

    参数意义
  • 2、iOS中的多线程机制

         为了提高资源利用率,从而提升系统整体效率,系统通常会将耗时的操作放在后台执行,避免阻塞主线程,等待子线程执行完毕通知主线程更新UI,在iOS中UI绘制和用户响应都是主线程。iOS中多线程机制有以下几种:

    2.1 NSThread

         此技术方案是一种轻量级别的多线程技术。是程序员手动开辟的子线程,如果使用的是初始化方式就需要手动启动新线程,如果使用的是构造器方式则会自行启动。但是,无论是何种启动方式,都是需要我们自己管理的,即要负责线程使用完毕之后的资源回收,故而NSThread创建的线程的生命周期是由程序员管理的。
         NSThread是苹果官方提供的,使用起来相对于pthread更加面向对象,简单易用,在开发中偶尔会使用NSThread,比如经常会调用[[NSThread currentThread]来显示当前的进程(线程)信息。
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(testThread:) object:@"参数"];//使用初始化方法出来的线程需要手动启动
    [thread start] //启动线程
    thread.name = @"NsThread线程";
    thread.threadPriority = 1; //设置优先级。范围0~1,表示执行概率,其值越大,表示该线程越容易被执行。由于是概率,并不能完全按照此实现想要的执行顺序
    [thread cancel];//取消已经启动的线程
    [NSThread detachNewThreadSelector:@selector(testThread:) toTarget:self withObject:@"构造器方法"];//使用构造器方式新建并启动一个线程

    2.2.1 相关用法

    // 获得主线程
    + (NSThread *)mainThread;    
    
    // 判断是否为主线程(对象方法)
    - (BOOL)isMainThread;
    
    // 判断是否为主线程(类方法)
    + (BOOL)isMainThread;    
    
    // 获得当前线程
    NSThread *current = [NSThread currentThread];
    //打印当前线程信息: NSLog(@"%@", [NSThread currentThread]); // 线程的名字——setter方法
    - (void)setName:(NSString *)n;    
    
    // 线程的名字——getter方法
    - (NSString *)name;    
    

    2.2.2 状态控制

    //1.启动线程
    - (void)start;
    // 线程进入就绪状态 -> 运行状态。当线程任务执行完毕,自动进入死亡状态
    //2.阻塞线程
    + (void)sleepUntilDate:(NSDate *)date;
    + (void)sleepForTimeInterval:(NSTimeInterval)ti;
    // 线程进入阻塞状态
    //3.强制停止线程,杀死线程
    + (void)exit;
    // 线程进入死亡状态
    

    2.2.3 线程通信

           在开发中,我们经常会在子线程进行耗时操作,操作结束后再回到主线程去刷新 UI。这就涉及到了子线程和主线程之间的通信。我们先来了解一下官方关于 NSThread 的线程间通信的方法。
    // 在主线程上执行操作
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray<NSString *> *)array;
      // equivalent to the first method with kCFRunLoopCommonModes
    
    // 在指定线程上执行操作
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
    
    // 在当前线程上执行操作,调用 NSObject 的 performSelector:相关方法
    - (id)performSelector:(SEL)aSelector;
    - (id)performSelector:(SEL)aSelector withObject:(id)object;
    - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
    

    下面通过一个经典的下载图片 DEMO 来展示线程之间的通信。具体步骤如下:

    1. 开启一个子线程,在子线程中下载图片。
    2. 回到主线程刷新 UI,将图片展示在 UIImageView 中。

    DEMO 代码如下:

    /**
     * 创建一个线程下载图片
     */
    - (void)downloadImageOnSubThread {
        // 在创建的子线程中调用downloadImage下载图片
        [NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil];
    }
    
    /**
     * 下载图片,下载完之后回到主线程进行 UI 刷新
     */
    - (void)downloadImage {
        NSLog(@"current thread -- %@", [NSThread currentThread]);
        
        // 1. 获取图片 imageUrl
        NSURL *imageUrl = [NSURL URLWithString:@"https://ysc-demo-1254961422.file.myqcloud.com/YSC-phread-NSThread-demo-icon.jpg"];
        
        // 2. 从 imageUrl 中读取数据(下载图片) -- 耗时操作
        NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
        // 通过二进制 data 创建 image
        UIImage *image = [UIImage imageWithData:imageData];
        
        // 3. 回到主线程进行图片赋值和界面刷新
        [self performSelectorOnMainThread:@selector(refreshOnMainThread:) withObject:image waitUntilDone:YES];
    }
    
    /**
     * 回到主线程进行图片赋值和界面刷新
     */
    - (void)refreshOnMainThread:(UIImage *)image {
        NSLog(@"current thread -- %@", [NSThread currentThread]);
        
        // 赋值图片到imageview
        self.imageView.image = image;
    }
    

    2.2.4 线程安全和线程同步

       首先,来看线程安全和线程同步的概念:
    • 线程安全:
           如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

    若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。

    • 线程同步:

           可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。举个简单例子就是:两个人在一起聊天。两个人不能同时说话,避免听不清(操作冲突)。等一个人说完(一个线程结束操作),另一个再说(另一个线程再开始操作)。

          下面,我们模拟火车票售卖的方式,实现 NSThread 线程安全和解决线程同步问题。场景:总共有50张火车票,有两个售卖火车票的窗口,一个是北京火车票售卖窗口,另一个是上海火车票售卖窗口。两个窗口同时售卖火车票,卖完为止。

    (1)NSThread非线程安全的情况
    /**
     * 初始化火车票数量、卖票窗口(非线程安全)、并开始卖票
     */
    - (void)initTicketStatusNotSave {
        // 1. 设置剩余火车票为 50
        self.ticketSurplusCount = 50;
        
        // 2. 设置北京火车票售卖窗口的线程
        self.ticketSaleWindow1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
        self.ticketSaleWindow1.name = @"北京火车票售票窗口";
        
        // 3. 设置上海火车票售卖窗口的线程
        self.ticketSaleWindow2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
        self.ticketSaleWindow2.name = @"上海火车票售票窗口";
        
        // 4. 开始售卖火车票
        [self.ticketSaleWindow1 start];
        [self.ticketSaleWindow2 start];
    
    }
    
    /**
     * 售卖火车票(非线程安全)
     */
    - (void)saleTicketNotSafe {
        while (1) {
            //如果还有票,继续售卖
            if (self.ticketSurplusCount > 0) {
                self.ticketSurplusCount --;
                NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread].name]);
                [NSThread sleepForTimeInterval:0.2];
            }
            //如果已卖完,关闭售票窗口
            else {
                NSLog(@"所有火车票均已售完");
                break;
            }
        }
    }
    
            可以看到在不考虑线程安全的情况下,得到票数是错乱的,这样显然不符合我们的需求,所以我们需要考虑线程安全问题。
    (2)NSThread考虑线程安全的情况
    /**
     * 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
     */
    - (void)initTicketStatusSave {
        // 1. 设置剩余火车票为 50
        self.ticketSurplusCount = 50;
        
        // 2. 设置北京火车票售卖窗口的线程
        self.ticketSaleWindow1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketSafe) object:nil];
        self.ticketSaleWindow1.name = @"北京火车票售票窗口";
        
        // 3. 设置上海火车票售卖窗口的线程
        self.ticketSaleWindow2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketSafe) object:nil];
        self.ticketSaleWindow2.name = @"上海火车票售票窗口";
        
        // 4. 开始售卖火车票
        [self.ticketSaleWindow1 start];
        [self.ticketSaleWindow2 start];
        
    }
    
    /**
     * 售卖火车票(线程安全)
     */
    - (void)saleTicketSafe {
        while (1) {
            // 互斥锁
            @synchronized (self) {
                //如果还有票,继续售卖
                if (self.ticketSurplusCount > 0) {
                    self.ticketSurplusCount --;
                    NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread].name]);
                    [NSThread sleepForTimeInterval:0.2];
                }
                //如果已卖完,关闭售票窗口
                else {
                    NSLog(@"所有火车票均已售完");
                    break;
                }
            }
        }
    }
    
              可以看出,在考虑了线程安全的情况下,加锁之后,得到的票数是正确的,没有出现混乱的情况。我们也就解决了多个线程同步的问题。实质上就是使用互斥锁来保护临界资源在同一时刻只能被一个线程访问,从而实现线程安全。

    2.2.5 线程的状态转换

    •    当我们新建一个线程:
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    •    在内存中表现为:
    •     当调用[thread start]之后,系统把线程对象放入可调度线程池中,线程对象进入了就绪状态:

    • 如果CPU现在调度当前线程对象,则当前线程对象进入运行状态,如果CPU调度其他线程对象,则当前线程对象回到就绪状态。
    • 如果CPU在运行当前线程对象的时候调用了sleep方法\等待同步锁,则当前线程对象就进入了阻塞状态,等到sleep到时\得到同步锁,则回到就绪状态。
    • 如果CPU在运行当前线程对象的时候线程任务执行完毕\异常强制退出,则当前线程对象进入死亡状态。

    2.2 GCD

        Grand Central Dispatch(GCD)是Apple开发的一个多核编程的技术方案。它是一套纯c语言的api,主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并发任务。
          GCD的优点如下:
    • GCD支持多核并行计算
    • GCD自动管理线程的生命周期(线程的创建、调起、等待、销毁)
    • 使用者只需告知GCD执行任务
        pthread和NSThread实际上就是线程这个概念在iOS中的抽象,使用这两个库需要开发者自己去管理线程的声明周期(创建、调度和销毁),因此这两个库一般用于一些简单的场景,而GCD和NSOperation则是更高级的抽象,在iOS的日常开发中经常被用到。在多线程中,并发编程是可以让我们更加高效的利用多核多芯片的计算性能,但是也带来了资源竞争的问题。在深入研究学习GCD之前,先学习一下并发时的资源竞争问题。

    2.2.1 并发时的资源竞争

       我们知道线程在并发调用的时候是无法预估它们之间之间执行时序先后顺序的,当存在多个线程同时操作同一个资源的时候,这个资源的读写操作就变得不可预估了。比如,当存在一个整型数值17分别被两条并发线程访问,当没有对这个资源进行保护的时候,这个计数器的数值与预期不一致(计数器实际上被+2,但结果仍然是18)。这个问题被称为竞态条件,要解决这个问题需要保证多线程的执行顺序,也就是使用一些同步机制来保证并发线程的执行顺序。

        https://hello-david.github.io/archives/4397956c.html这里是ios系统中关于线程安全的框架。
    (1)同步手段
    1. 原子操作(Atomic Operations):一种简单的同步形式,适用于简单的数据类型。 原子操作的优点是它们不会阻塞竞争线程。 对于简单的操作(例如增加计数器变量),这比使用锁可以带来更好的性能。

    2. 内存壁垒和易失性变量(Memory Barriers and Volatile Variables):内存屏障是一种非阻塞同步工具,用于确保内存操作以正确的顺序发生。内存屏障的作用类似于围栏,迫使处理器在允许执行位于屏障之后的加载和存储操作之前,完成位于屏障前面的所有加载和存储操作。易失性变量将另一种类型的内存约束应用于单个变量,它具有“易变性”,声明为volatile变量编译器会强制要求读内存,相关语句不会直接使用上一条语句对应的的寄存器内容,而是重新从内存中读取。。由于内存屏障和易失性变量都会减少编译器可执行的优化次数,因此应谨慎使用它们,并且仅在需要确保正确性的地方使用它们。

    3. 锁(Locks):锁是最常用的同步工具之一,可以使用锁来保护代码的关键部分,锁一次只能允许一个线程访问。例如,关键代码可能操纵特定的数据结构或一次使用最多支持一个客户端的某些资源。通过在此部分周围加锁,可以排除其他线程进行可能影响代码正确性的更改。

    4. 条件(Conditions):条件是一种信号量,同时也可以看做一种特殊的锁。当某个条件为真时,它允许线程彼此发信号。条件通常用于指示资源的可用性或确保任务以特定顺序执行。当线程测试条件时,除非该条件已经为真,否则它将阻塞。它保持阻塞状态,直到其他线程显式更改并发出条件信号为止。条件和互斥锁之间的区别在于,可以允许多个线程同时访问该条件。条件更多的表现得像一个看门人,它根据某些指定的标准让不同的线程通过这个门。

    5. 执行选择器事务(Perform Selector Routines):Cocoa框架中可以使用performSelector:onThread:withObject:waitUntilDone:相关的方法来在主线程或者其他线程的Runloop中顺序执行代码。

    (2)锁
         锁是我们常常会使用的一种同步机制,在ios中它的分类有很多:

         对于应用开发而言最最最常接触到就是互斥锁、递归锁这两种锁了,它们在API层面上又被封装为NSLock、NSRecursiveLock或者@synchronized语法了。这些锁通过保证线程同步访问的方式保护了资源从而解决了资源竞争的问题,但是它并不能完美解决并发编程中的所有问题,可能还会引发几个问题:

    • 死锁(经典场景:使用互斥锁的线程加锁后又再次访问这个锁)
    • 资源饥饿
    • 线程优先级反转

    2.2.2 GCD中任务与调度队列

       GCD设计的目的就是提供系统级别的高效性能调度,通过提交任务到队列中,实现在多核硬件上同时执行代码。其中,GCD非常重要的两个概念就是“调度队列”和“任务”,通过封装”调度队列“和”任务“来隐藏更细节的线程管理、调度。基于任务和队列,GCD的一个使用步骤如下:
    • 创建/获取一个并发/串行的队列;
    • 创建任务:确定要执行的事情,就是写匿名函数block,也就是要执行的代码块;
    • 将任务加入到队列中:同时需要指定任务的执行方式,即同步还是异步。
        // 1.创建一个队列
        dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
        // 2.创建一个任务
        dispatch_block_t block = ^{
            NSLog(@"%@",[NSThread currentThread]);
        };
        // 3.将任务添加进队列中(同时指定任务的执行方式)  
        dispatch_async(queue, block);
    

       完成这三步之后,就是OS要完成的事情了。在GCD中,GCD会自动将队列中的任务取出,放到对应的线程中去执行。任务的取出遵循队列的FIFO原则,即先进先出,后进后出。GCD中,当要执行队列中的任务的时候,会自动开启一个新线程(不是一定会开启新线程的,比如串行队列,以及同步任务就不具有开启新线程的能力)。如果接下来还要继续执行任务的话就会从线程池中取出线程,而不是再去新建一个线程,这也就节省了创建线程所需要的时间。但是如果在一段时间内没有执行任务的话,该线程就会被销毁,再执行任务就再去创建新的线程。

    (1)任务:执行的操作
         任务很好理解,就是iOS中代码实现的行为事件,每个事件都可以被当作是一个任务,GCD中的任务有两种封装:dispatch_block_t 和 dispatch_function_t。

    ● dispatch_block_t(常用)

    提交给指定队列的 block,无参无返回值。

    typedef void (^dispatch_block_t)(void); 

    ● dispatch_function_t

    提交给指定队列的 function,void(*)()类型的函数指针。

    typedef void (*dispatch_function_t)(void *);
        封装成一个block是GCD的常见做法,任务封装好了之后,就是要把它放入到执行队列中等待执行。
    (2)队列:存放任务的载体
        dispatch_queue_t serial = dispatch_queue_create("A", DISPATCH_QUEUE_SERIAL);
        
        dispatch_queue_t conque = dispatch_queue_create("B", DISPATCH_QUEUE_CONCURRENT);
        
        dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
        dispatch_queue_t globQueue = dispatch_get_global_queue(0, 0);
    
        NSLog(@"%@-%@-%@-%@",serial,conque,mainQueue,globQueue);
        关于任务和队列的组合情况在1.8小节有详细的说明,这里就不重复了。我们接下来先看GCD执行任务的方式的区别,以及一些重点函数的说明。
    (3)dispatch_sync & dispatch_sync_f 
    void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block); void dispatch_sync_f(dispatch_queue_t queue, void *context, dispatch_function_t work);
         这两个都是将任务添加到队列中,并指定任务的执行方式为同步。dispatch_sync 提交了一个block对象到指定队列以同步执行,并在该block完成之后返回(阻塞),由于阻塞的特性,使用该函数需要注意死锁的问题。dispatch_sync _f则是提交一个function到指定队列以同步执行,并在该function完成后返回(阻塞)。
          可以看到,区别就在于传入的任务是block还是function。怎么选呢,一般我们采用匿名函数,就是当函数没有返回值,也没有参数的时候,那这种情况下我们就使用block来封装任务,block会被自动copy和release掉。function就是函数,如果有返回值以及参数的话,就使用这种方式。
    (4)dispatch_async & dispatch_async_f
    void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
         这两个是异步的,dispatch_async是提交一个block对象到指定队列中以异步执行,并直接返回(不会阻塞)。dispatch_async_f同上,这里不再赘述了。
    (5)dispatch_get_main_queue
    // @return 主队列
    dispatch_queue_main_t dispatch_get_main_queue(void);       
     系统创建主队列并与主线程进行关联的时机:
    ① 调用 dispatch_main();
    ② 调用 UIApplicationMain(iOS)或者 NSApplicationMain(macOS);
    ③ 在主线程使用 CFRunLoopRef。
    大多数情况下我们的应用程序会在 main() 函数里使用第 2 种方式
    (6)dispatch_get_global_queue
    /*!
     * @param identifier
     * 队列的服务质量,传0就是默认
     *
     * @param flags
     * 苹果留着以后用的,传0就行
     * 
     * @return dispatch_queue_global_t
     * 可以指定服务质量的系统定义的全局并发队列
     */
    dispatch_queue_global_t dispatch_get_global_queue(long identifier, unsigned long flags);
    
         一种特殊的并发队列,可以指定服务质量(服务质量有助于确定队列执行任务的优先级)。这里需要注意:对主队列和全局并发队列使用dispatch_suspenddispatch_resumedispatch_set_context是无效的。另外,这里再学习下全局并发队列与手动创建的并发队列的区别
  • 手动创建的并发队列可以设置唯一标识,可以跟踪错误,而全局并发队列没有;
  • 在 ARC(自动内存管理) 中不需要考虑释放内存,dispatch_release(q);不需要也不允许调用。而在 MRC (手动内存管理)中由于手动创建的并发队列是 create 出来的,所以需要调用dispatch_release(q);来释放内存,而全局并发队列不需要;
  • 全局并发队列可以指定服务质量(服务质量有助于确定队列执行的任务的优先级);
  • 一般我们使用全局并发队列。
  • (7)dispatch_queue_t
    typedef NSObject<OS_dispatch_queue> *dispatch_queue_t;
         应用程序向其提交block(任务,代码块)以进行后续执行的轻量级对象。首先,队列是一个对象,这也解释了为什么在MRC中要手动管理dispatch_queue_t的内存。这是一个队列,遵循FIFO原则,分为”串行“和”并发“两种队列。
         队列是通过调用dispatch_retain和dispatch_release来进行引用计数的。提交到队列中的待处理块也会保留着对该队列的引用,直到任务完成为止。一旦释放了对队列的所有引用,系统就会重新分配该队列。
    (8)dispatch_queue_create
    /*!
     * @param label
     * 给队列一个字符串标签进行唯一标识,以便在调试时区分队列
     * 建议使用反向DNS命名方式(com.example.myqueue)
     * 该参数可以为空(NULL)
     *
     * @param attr
     * 指定队列类型
     * DISPATCH_QUEUE_SERIAL     为串行队列
     * DISPATCH_QUEUE_CONCURRENT 为并发队列
     * 该参数可以为空(NULL),传空时默认为串行队列(在iOS4.3版本之前该参数只能传空)
     * 
     * @return dispatch_queue_t
     * 新创建的队列
     */
    dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
    
    // 创建一个串行队列 
    dispatch_queue_t queue = dispatch_queue_create("com.junteng.myqueue", DISPATCH_QUEUE_SERIAL);
    // 创建一个并发队列
    dispatch_queue_t queue = dispatch_queue_create("com.junteng.myqueue", DISPATCH_QUEUE_CONCURRENT);
    // 获取主队列
    dispatch_queue_t queue = dispatch_get_main_queue();
    // 获取全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    			
    (9) dispatch_queue_get_label
       获得队列的唯一标识label:
    /*!
     * @param queue
     * 需要获取label的队列;
     * 如果需要获取当前队列的label则使用 DISPATCH_CURRENT_QUEUE_LABEL
     * 
     * @return 
     * 创建队列时给队列设置的标签
     */
    const char * dispatch_queue_get_label(dispatch_queue_t queue);
    
    dispatch_queue_t queue = dispatch_queue_create("com.junteng.myqueue", NULL);
    dispatch_sync(queue, ^{
       NSLog(@"%s", dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL));
    });
    NSLog(@"%s", dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL));
    NSLog(@"%s", dispatch_queue_get_label(queue));
    /*
    com.junteng.myqueue
    com.apple.main-thread
    com.junteng.myqueue
     */
    

    2.2.3 死锁

    (1)死锁条件
  • 互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
  • 占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
  • 不可抢占:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
  • 循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。
  • (2)GCD中的死锁
              使用dispatch_sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁),死锁的原因就是队列引起的循环等待。
    /*
     队列的特点:FIFO (First In First Out) 先进先出
     以下将 block(任务2)提交到主队列,主队列将来要取出这个任务放到主线程执行。
     而主队列此时已经有任务,就是执行(viewDidLoad方法),
     所以主队列要想取出 block(任务2),就要等上一个任务(viewDidLoad方法)先执行完,才能取出该任务执行。
     而 dispatch_sync 函数必须执行完 block(任务2)才会返回,才能往下执行代码。
     所以(任务2)要等待(viewDidLoad方法)执行完,(viewDidLoad方法)要等待(任务2)执行完。互相等待,就产生了死锁。
     */
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        NSLog(@"执行任务1");
    
        dispatch_queue_t queue = dispatch_get_main_queue();
        dispatch_sync(queue, ^{
            NSLog(@"执行任务2");
        });
    
        NSLog(@"执行任务3");
    }
    /*
     打印:
     2020-01-19 00:16:26.980630+0800 多线程[25011:5507937] 执行任务1
     (lldb) 
     */
    
    /*
     解决方案:打破(使用`dispatch_sync`函数往`当前串行队列`中添加任务)这一条件即可
     以下将(任务2)异步执行,打印结果为:132
     */
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        NSLog(@"执行任务1");
    
        dispatch_queue_t queue = dispatch_get_main_queue();
        dispatch_async(queue, ^{
            NSLog(@"执行任务2");
        });
    
        NSLog(@"执行任务3");
    }
    /*
     打印:
     2020-01-19 03:16:47.472682+0800 多线程[25416:5603048] 执行任务1
     2020-01-19 03:16:47.472890+0800 多线程[25416:5603048] 执行任务3
     2020-01-19 03:16:47.474389+0800 多线程[25416:5603048] 执行任务2
     */
    

    2.3 NSOperationQueue

              NSOperation、NSOperationQueue 是苹果提供给我们的一套多线程解决方案。实际上 NSOperation、NSOperationQueue 是基于GCD 更高一层的封装,完全面向对象。但是比GCD 更简单易用、代码可读性也更高。
    • GCD是纯C语言的API,NSOperationQueue 则是基于GCD的OC版本封装。
    • GCD只支持FIFO的队列,NSOperationQueue可以添加任务依赖,方便控制执行顺序
    • 可以设定操作执行的优先级
    • 任务执行状态控制:isReady,isExecuting,isFinished,isCancelled
            如果只是重写 NSOperation的 main方法,那么就由底层控制变更任务执行及完成状态,以及任务退出。如果重写了 NSOperation的 start 方法,自行控制任务状态。系统通过 KVO的方式移除 isFinished==YES的 NSOperation。
    • 可以设置最大并发量
    • GCD的执行速度比NSOperationQueue 快
         那么,当任务之间不太相互依赖的时候,用GCD。反之,任务之间有相互依赖,或者要监听任务的执行情况的话,就用NSOperationQueue 。

    2.3.1 基本概念

             既然是基于 GCD 的更高一层的封装。那么,GCD 中的一些概念同样适用于 NSOperation、NSOperationQueue。在 NSOperation、NSOperationQueue 中也有类似的任务(操作)和队列(操作队列)的概念。
  • 操作(Operation
    • 执行操作的意思,换句话说就是你在线程中执行的那段代码。
    • 在 GCD 中是放在 block 中的。在 NSOperation 中,我们使用 NSOperation 子类NSInvocationOperation、NSBlockOperation,或者自定义子类来封装操作。
  • 操作队列(Operation Queues)
    • 这里的队列指操作队列,即用来存放操作的队列。不同于 GCD 中的调度队列 FIFO(先进先出)的原则。NSOperationQueue 对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。
    • 操作队列通过设置最大并发操作数(maxConcurrentOperationCount)来控制并发、串行。
    • NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。
  • NSOperation、NSOperationQueue 使用步骤
    • NSOperation 需要配合 NSOperationQueue 来实现多线程。因为默认情况下,NSOperation 单独使用时系统同步执行操作,配合 NSOperationQueue 我们能更好的实现异步执行。
    • NSOperation 实现多线程的使用步骤分为三步
      • 创建操作:先将需要执行的操作封装到一个 NSOperation 对象中。
      • 创建队列:创建 NSOperationQueue 对象。
      • 将操作加入到队列中:将 NSOperation 对象添加到 NSOperationQueue 对象中。
      • 之后呢,系统就会自动将 NSOperationQueue 中的 NSOperation 取出来,在新线程中执行操作。
  • 2.3.2 创建操作

    NSOperation 是个抽象类,不能用来封装操作。我们只有使用它的子类来封装操作。我们有三种方式来封装操作。

    • 使用子类 NSInvocationOperation
    • 使用子类 NSBlockOperation
    • 自定义继承 NSOperation 的子类,通过实现内部相应的方法来封装操作。
    (1)使用子类 NSInvocationOperation
    /**
     NSInvocationOperation : 创建操作 ---> 创建队列 ---> 操作加入队列
     */
    - (void)demo{
        //1:创建操作
        NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleInvocation:) object:@"123"];
        //2:创建队列
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        //3:操作加入队列 --- 操作会在新的线程中
        [queue addOperation:op];
    }
    
    - (void)handleInvocation:(id)op{
        NSLog(@"%@ --- %@",op,[NSThread currentThread]);
    } 复制代码

    打印:

    2020-01-29 15:43:11.012492+0800 001---NSOperation初体验[1628:78932] 123 --- <NSThread: 0x600001ea9080>{number = 3,

    NSInvocationOperation也可以手动调起,这样不会开启线程,会在当前队列执行任务

    /**
     手动调起操作
     */
    - (void)demo1{
        NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleInvocation:) object:@"123"];
        // 注意 : 如果该任务已经添加到队列,你再手动调回出错
        // NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        // [queue addOperation:op];
        // 手动调起操作,默认在当前线程
        [op start];
    }
    
    - (void)handleInvocation:(id)op{
        NSLog(@"%@ --- %@",op,[NSThread currentThread]);
    } 复制代码

    打印:

    2020-01-29 15:52:55.404726+0800 001---NSOperation初体验[1740:83188] 123 --- <NSThread: 0x60000024e940>{number = 1, name = main} 复制代码

    注意:我们尽量不要用这种方式去执行任务,如果该任务已经添加到队列,再手动调起任务程序会崩溃

    3、runloop和多线程

    3.1  概念

          Run loops 是与 threads 关联的基本基础结构的一部分。Run loop 是一个 event processing loop (事件处理循环),可用于计划工作并协调收到的事件的接收。Run loop 的目的是让 thread 在有工作要做时保持忙碌,而在没有工作时让 thread 进入睡眠状态。(官方解释初次看时显的过于生涩,不过我们仍然可以抓住一些关键点,原本我们的 thread 执行完任务后就要释放销毁了,但是在 run loop 的加持下,线程不再自己主动去销毁而是处于待命状态等待着我们再交给它任务,换句话说就是 run loop 使我们的线程保持了活性,下面我们试图对 run loop 的概念进行理解。)
          一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。但是,开启线程是很消耗性能的,开启主线程需要1M内存,开启一个后台线程需要消耗512K内存。因此,我们需要一个机制,让线程能随时处理事件但并不退出,这种模型通常被称作 Event Loop。 Event Loop 在很多系统和框架里都有实现,比如 Node.js 的事件处理,比如 Windows 程序的消息循环,再比如 OSX/iOS 里的 Run Loop、Android的looper机制。
          实现这种模型的关键点在于基于消息机制:管理事件/消息,让线程在没有消息时休眠以避免资源占用、在有消息到来时立刻被唤醒执行任务。 

           那什么是 run loop?顾名思义,run loop 就是在 “跑圈”,run loop 运行的核心代码是一个有状态的 do while 循环,每循环一次就相当于跑了一圈,线程就会对当前这一圈里面产生的事件进行处理,do while 循环我们可能已经写过无数次,当然我们日常在函数中写的都是会明确结束的循环,并且循环的内容是我们一开始就编写好的,我们并不能动态的改变或者插入循环的内容,而 run loop 则不同,只要不是超时或者故意退出状态下 run loop 就会一直执行 do while 循环,所以可以保证线程不退出,并且可以让我们根据自己需要向线程中添加任务。

    那么为什么线程要有 run loop 呢?前面提到了一点,是为了降低系统性能开销,其实还有一点也很重要。我们的 APP 可以理解为是靠 event 驱动的(包括 iOS 和 Android 应用)。我们触摸屏幕、网络回调等都是一个个的 event,也就是事件。这些事件产生之后会分发给我们的 APP,APP 接收到事件之后分发给对应的线程。通常情况下,如果线程没有 run loop,那么一个线程一次只能执行一个任务,执行完成后线程就会退出。要想 APP 的线程一直能够处理事件或者等待事件(比如异步事件),就要保活线程,也就是不能让线程早早的退出,此时 run loop 就派上用场了,其实也不是必须要给线程指定一个 run loop,如果需要我们的线程能够持续的处理事件,那么就需要给线程绑定一个 run loop。也就是说,run loop 能够保证线程一直可以处理事件。

          通常情况下,事件并不是永无休止的产生,所以也就没必要让线程永无休止的运行,run loop 可以在无事件处理时进入休眠状态,避免无休止的 do while 跑空圈,看到这里我们注意到线程和 run loop 都是能进入休眠状态的,这里为了便于理解概念我们看一些表示 run loop 运行状态的代码:

    /* Run Loop Observer Activities */
    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry = (1UL << 0), // 进入 Run Loop 循环 (这里其实还没进入)
        kCFRunLoopBeforeTimers = (1UL << 1), // Run Loop 即将开始处理 Timer
        kCFRunLoopBeforeSources = (1UL << 2), // Run Loop 即将开始处理 Source
        kCFRunLoopBeforeWaiting = (1UL << 5), // Run Loop 即将进入休眠
        kCFRunLoopAfterWaiting = (1UL << 6), // Run Loop 从休眠状态唤醒
        kCFRunLoopExit = (1UL << 7), // Run Loop 退出(和 kCFRunLoop Entry 对应)
        kCFRunLoopAllActivities = 0x0FFFFFFFU
    };
    

    3.2 runloop和线程的关系

            一个线程对应一个 run loop,线程和RunLoop之间是以键值对的形式一一对应的,其中key是thread,value是runLoop(这点可以从苹果公开的源码中看出来)。程序运行是主线程的 main run loop 默认启动了,所以我们的程序才不会退出,子线程的 run loop 按需启动(调用 run 方法)。run loop 是线程的事件管理者,或者说是线程的事件管家,它会按照顺序管理线程要处理的事件,决定哪些事件在什么时候提交给线程处理。

    3.2.1 main runloop

           前面我们学习线程时,多次提到主线程主队列都是在 app 启动时默认创建的,而恰恰主线程的 main run loop 也是在 app 启动时默认跟着创建并启动的,那么我们从 main.m 文件中找出一些端倪,使用 Xcode 创建一个 OC 语言的 Single View App 时会自动生成如下的 main.m 文件:
    #import <UIKit/UIKit.h>
    #import "AppDelegate.h"
    
    int main(int argc, char * argv[]) {
        NSString * appDelegateClassName;
        @autoreleasepool {
            // Setup code that might create autoreleased objects goes here.
            appDelegateClassName = NSStringFromClass([AppDelegate class]);
            NSLog(@"🏃🏃‍♀️🏃🏃‍♀️..."); // 这里插入一行打印语句
        }
        return UIApplicationMain(argc, argv, nil, appDelegateClassName);
        // return 0;
        
        // 把上面的 return UIApplicationMain(argc, argv, nil, appDelegateClassName); 语句拆开如下:
        // int result = UIApplicationMain(argc, argv, nil, appDelegateClassName);
        // return result; // ⬅️ 在此行打一个断点,执行程序会发现此断点是无效的,因为 main 函数根本不会执行到这里
    }
    
          main函数最后一行return语句是返回UIApplicationMain函数的执行结果,我们把此行注释,然后添加一行return 0;,运行程序后会看到执行NSLog语句后程序就结束了直接回到了手机桌面,而最后一行是return UIApplicationMain(argc, argv, nil, appDelegateClassName);的话运行程序后就进入了 app 的首页而并不会结束程序,那么我们大概想到了这个UIApplicationMain函数是不会返回的,它不会返回,所以main函数也就不会返回了,main函数不会返回,所以我们的 app 就不会自己主动结束运行回到桌面了(当然这里的函数不会返回是不同于我们线程学习时看到的线程被阻塞甚至死锁时的函数不返回)。下面看一下UIApplicationMain函数的声明,看到是一个返回值是 int 类型的函数。
    UIKIT_EXTERN int UIApplicationMain(int argc, char * _Nullable argv[_Nonnull], NSString * _Nullable principalClassName, NSString * _Nullable delegateClassName);
    

    UIApplicationMain Creates the application object and the application delegate and sets up the event cycle. Return Value Even though an integer return type is specified, this function never returns. When users exits an iOS app by pressing the Home button, the application moves to the background.  即使指定了整数返回类型,此函数也从不返回。当用户通过按 Home 键退出 iOS 应用时,该应用将移至后台。 Discussion ... It also sets up the main event loop, including the application’s run loop, and begins processing events. ... Despite the declared return type, this function never returns.  ... 它还设置 main event loop,包括应用程序的 run loop(main run loop),并开始处理事件。... 尽管声明了返回类型,但此函数从不返回。

         在开发者文档中查看UIApplicationMain函数,摘要告诉我们UIApplicationMain函数完成:创建应用程序对象和应用程序代理并设置 event cycle,看到 Return Value 一项 Apple 已经明确告诉我们UIApplicationMain函数是不会返回的,并且在 Discussion 中也告诉我们UIApplicationMain函数启动了 main run loop 并开始着手为我们处理事件。

         main函数是我们应用程序的启动入口,然后调用UIApplicationMain函数其内部帮我们开启了 main run loop,换个角度试图理解为何我们的应用程序不退出时,是不是可以理解为我们的应用程序自启动开始就被包裹在 main run loop 的 do while 循环 中呢?那么根据上面UIApplicationMain函数的功能以及我们对 runloop 概念的理解,大概可以书写出如下 runloop 的伪代码:
















    全部评论

    相关推荐

    qq乃乃好喝到咩噗茶:院校后面加上211标签,放大加粗,招呼语也写上211
    点赞 评论 收藏
    分享
    评论
    点赞
    收藏
    分享

    创作者周榜

    更多
    牛客网
    牛客企业服务