Osheep

时光不回头,当下最重要。

多线程在iOS 中的应用


一、进程

1.1 什么是进程

  • 进程是指在系统中正在运行的一个应用程序
  • 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内
  • 比如同时打开Xcode和QQ,就会开启两个线程
  • 通过活动“活动监视器”可以查看MAC系统中所有的进程

二、线程

2.1 什么是线程

  • 一个进程要想执行任务,必须得有线程(每一个进程至少要有一个线程)
  • 线程是进程的基本单元,一个程序所有的任务都做线程中执行
  • 使用迅雷下载电影、使用网易云听歌,都需要在线程中执行

2.2 线程的串行

  • 一个线程的任务是串行的(同一时间内,一个线程只能执行一个任务)

三、多线程

3.1 what

  • 一个进程中可以开启多条线程,每条线程可以同时(并行)执行不同的任务
  • 多线程可以提高程序的执行效率

3.2 多线程的原理

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

3.3 多线程的优点

  • 能适当提高程序的执行效率
  • 能适当提高资源的利用率(cpu 、内存)

3.4 多线程的缺点

  • 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
  • 线程越多,cpu在调度线程上的开销就越大
  • 使程序的设计更加复杂:比如多线程之间的通信,多线程之间的数据共享

四、多线程在iOS中的应用

4.1 什么是主线程

  • 一个ios程序运行后,默认会开启一条线程,称为“主线程”或“UI线程”

4.2 主线程的主要作用

  • 显示\刷新UI界面
  • 处理UI事件(比如点击事件、滚动事件、拖拽事件等)

4.3 主线程的使用注意

  • 别将比较耗时的操作放到主线程中,耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的体验。

4.4 实现方案

《多线程在iOS 中的应用》

4种方案.png

五、NSThread

5.1 what
一个NSThread对象就代表一个线程

  • 创建、启动线程
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"哈哈"];
    thread.name = @"线程A";//自己来区别的
    // 开启线程
    [thread start];
  • 获得当前线程
    NSThread *current = [NSThread currentThread];
    NSLog(@"btnClick---%@", current);
  • 其他创建线程的方式
    //创建完线程直接(自动)启动
    [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"我是参数"];
    // 在后台线程中执行 === 在子线程中执行
    [self performSelectorInBackground:@selector(run:) withObject:@"abc参数"];
    以上2种方式的优点:快捷方便 缺点:无法对线程进行更详细的设置

    六、线程的状态

    线程一共有5种状态,如图:

    《多线程在iOS 中的应用》

    线程的状态流程图.png

    6.1新建状态和就绪状态
    现在以NSThread为例,黄色为例。
    [tehread start]之前是新建状态,start之后进入了就绪状态(cpu在调度其他进程的时候也是处于调度状态),这个时候就进入了可调度线程池,放进可调度的线程池的线程是可以来回进行调度的。
    6.2运行状态
    线程经过cpu调度之后就进入了运行状态。
    6.3阻塞状态
    调用了sleep方法/等待同步锁的时候,就进入了阻塞状态。
    6.4死亡状态
    线程任务执行完毕,或者异常/强制退出等,就进入了死亡状态。

    七、多线程的安全隐患

    7.1 why

  • 资源共享
    一个资源可能被多个资源共享(多个线程可能访问同一个资源),比如多个线程访问同一个变量、同一个对象、同一个文件等,当多个线程访问同一个资源时,很容易引发数据安全和数据错乱问题
    如下图分析:
    《多线程在iOS 中的应用》

    安全隐患分析.png

    7.2解决方法

  • 互斥锁
    《多线程在iOS 中的应用》

    安全隐患解决.png

@synchronized(锁对象){
}
注意:锁定一份代码只用一把锁,用多把锁是无效的
*互斥锁的优点、缺点
优点:能有效防止因多线程抢夺资源造成的数据安全问题
缺点:需要消耗大量的cpu资源

  • 线程同步
    多条线程按顺序的执行任务
    互斥锁就是使用了线程同步技术

  • 原子性和非原子性
    oc在定义属性时有2种选择

    • atomic: 原子属性,为setter方法加锁(默认就是atomic),线程安全,需要消耗大量的资源
    • nonatmic: 非原子属性,不会为setter方法加锁,非线程安全,适合内存小的移动设备
    • ios开发建议:所有的属性都声明为nonatmic,尽量避免多线程抢夺同一块资源,尽量将加锁、抢夺资源的业务逻辑交给服务器端处理,减小移动端的压力。

八、线程之间的通信

8.1 what
在一个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信
8.2 体现

  • 一个线程传递数据给另外一个线程
  • 在一个线程中执行完一个特定的任务后,转到另一个线程继续执行任务。

import “ViewController.h”

@interface HMViewController ()
@property (weak, nonatomic) IBOutlet UIImageView imageView;
@end
@implementation HMViewController
-(void)viewDidLoad{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
-(void)touchesBegan:(NSSet
)touches withEvent:(UIEvent )event
{
// 在子线程中调用download方法下载图片
[self performSelectorInBackground:@selector(download) withObject:nil];
}
/*

下载图片 : 子线程
/
-(void)download
{
// 1.根据URL下载图片
NSURL
url = [NSURL URLWithString:@”http://news.baidu.com/z/resource/r/image/2014-06-22/2a1009253cf9fc7c97893a4f0fe3a7b1.jpg“];
NSLog(@”——-begin”);
NSData data = [NSData dataWithContentsOfURL:url]; // 这行会比较耗时
NSLog(@”——-end”);
UIImage
image = [UIImage imageWithData:data];
// 2.回到主线程显示图片
// [self.imageView performSelector:@selector(setImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:NO];
// setImage: 1s
[self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO];
// [self performSelectorOnMainThread:@selector(settingImage:) withObject:image waitUntilDone:NO];
}
/*
设置(显示)图片: 主线程
/
//- (void)settingImage:(UIImage *)image
//{
// self.imageView.image = image
//}
@end

九、GCD

9.1 what

  • 全称是Grand Central Dispatch,可译为“牛逼的中枢调度器”。
  • 纯c语言,提供了非常多、强大的函数

9.2 优势

  • GCD是苹果公司为多核的并行运算提出的解决法案
  • GCD会自动利用更多的CPU内核,会自动管理线程的生命周期(创建线程、调度线程、销毁线程)
  • 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码

9.3 队列和任务

  • 任务:执行什么操作
  • 队列:用来存放任务
    • 并发队列:可以让多个任务同时执行,只有在异步函数下才有效
    • 串行队列:让任务一个接着一个的执行
  • 同步:在当前线程中执行,不具备开启线程的能力
  • 异步:在另一条线程中执行,具备开启线程的能力

GCD会自动将队列中的任务取出,放到对应的线程中执行,任务的取出遵循队列的FIFO原则:先进先出,后进后出
9.4 并发对列
GCD默认已经提供了全局的并发对列,供整个应用使用,不需要手动创建
// 1.获得全局的并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

9.5串行队列
GCD中获得串行有2中途径
// 1.创建串行队列
dispatch_queue_t queue = dispatch_queue_create("com.baidu.queue", NULL);

//2.使用主队列(跟主线程相关联的队列)
dispatch_queue_t queue = dispatch_get_main_queue();

各队列的执行效果

《多线程在iOS 中的应用》

e.png

上代码

#import "ViewController.h"

@interface HMViewController ()

@end

@implementation HMViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self performSelectorInBackground:@selector(test) withObject:nil];

//    [self syncMainQueue];
}

- (void)test
{
    NSLog(@"test --- %@", [NSThread currentThread]);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"任务 --- %@", [NSThread currentThread]);
    });
}

/**
 * 使用dispatch_async异步函数, 在主线程中往主队列中添加任务
 */
- (void)asyncMainQueue
{
    // 1.获得主队列
    dispatch_queue_t queue = dispatch_get_main_queue();

    // 2.添加任务到队列中 执行
    dispatch_async(queue, ^{
        NSLog(@"----下载图片1-----%@", [NSThread currentThread]);
    });
}

/**
 * 使用dispatch_sync同步函数, 在主线程中往主队列中添加任务 : 任务无法往下执行
 */
- (void)syncMainQueue
{
    // 1.获得主队列
    dispatch_queue_t queue = dispatch_get_main_queue();

    // 2.添加任务到队列中 执行
    dispatch_sync(queue, ^{
        NSLog(@"----下载图片1-----%@", [NSThread currentThread]);
    });
//    dispatch_sync(queue, ^{
//        NSLog(@"----下载图片2-----%@", [NSThread currentThread]);
//    });
//    dispatch_sync(queue, ^{
//        NSLog(@"----下载图片3-----%@", [NSThread currentThread]);
//    });

    // 不会开启新的线程, 所有任务在主线程中执行
}

// 凡是函数名种带有create\copy\new\retain等字眼, 都需要在不需要使用这个数据的时候进行release
// GCD的数据类型在ARC环境下不需要再做release
// CF(Core Foundation)的数据类型在ARC环境下还是需要再做release

/**
 * 用dispatch_sync同步函数往串行列中添加任务
 */
- (void)syncSerialQueue
{
    // 1.创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("com.itheima.queue", NULL);

    // 2.添加任务到队列中 执行
    dispatch_sync(queue, ^{
        NSLog(@"----下载图片1-----%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"----下载图片2-----%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"----下载图片3-----%@", [NSThread currentThread]);
    });

    // 3.释放资源
//    dispatch_release(queue);   // MRC(非ARC)

    // 总结: 不会开启新的线程
}

/**
 * 用dispatch_sync同步函数往并发队列中添加任务
 */
- (void)syncGlobalQueue
{
    // 1.获得全局的并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    // 2.添加任务到队列中 执行
    dispatch_sync(queue, ^{
        NSLog(@"----下载图片1-----%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"----下载图片2-----%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"----下载图片3-----%@", [NSThread currentThread]);
    });

    // 总结: 不会开启新的线程, 并发队列失去了并发的功能
}

/**
 * 用dispatch_async异步函数往串行队列中添加任务
 */
- (void)asyncSerialQueue
{
    // 1.创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("com.baidu.queue", NULL);

    // 2.添加任务到队列中 执行
    dispatch_async(queue, ^{
        NSLog(@"----下载图片1-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----下载图片2-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----下载图片3-----%@", [NSThread currentThread]);
    });

    // 总结: 只开1个线程执行任务
}

/**
 * 用dispatch_async异步函数往并发队列中添加任务
 */
- (void)asyncGlobalQueue
{
    // 1.获得全局的并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    // 2.添加任务到队列中 执行
    dispatch_async(queue, ^{
        NSLog(@"----下载图片1-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----下载图片2-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----下载图片3-----%@", [NSThread currentThread]);
    });

    // 总结: 同时开启了3个线程
}

@end

9.6 线程间的通信
从子线程回到主线程
上代码

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        NSLog(@"--download--%@", [NSThread currentThread]);
        // 下载图片
        NSURL *url = [NSURL URLWithString:@"http://news.baidu.com/z/resource/r/image/2014-06-22/2a1009253cf9fc7c97893a4f0fe3a7b1.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url]; // 这行会比较耗时
        UIImage *image = [UIImage imageWithData:data];

        // 回到主线程显示图片
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"--imageView--%@", [NSThread currentThread]);
            self.imageView.image = image;
        });
    });
}

9.7 延时执行
2种方法

- (void)delay
{
    //    NSLog(@"----touchesBegan----%@", [NSThread currentThread]);

    //    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self performSelector:@selector(run) withObject:nil afterDelay:2.0];//第一种
    //    });
    // 1.全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    // 2.计算任务执行的时间
    dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));

    // 3.会在when这个时间点, 执行queue中的任务。第二种
    dispatch_after(when, queue, ^{
        NSLog(@"----run----%@", [NSThread currentThread]);
    });
}
//- (void)run
//{
//    NSLog(@"----run----%@", [NSThread currentThread]);
//}

9.8一次性代码
保证某段代码在程序运行的过程中只被执行1次

   static dispatch_once_t onceToken;
   dispatch_once(&onceToken, ^{
       NSLog(@"-------touchesBegan");
   });
}

9.9 队列组
首先有一个需求,分别异步执行2个耗时的操作,等2个异步操作都执行完毕后,再回到主线程执行操作。
dispatch_group_t group = dispatch_group_create();
上代码

 /**
     1.下载图片1和图片2

     2.将图片1和图片2合并成一张图片后显示到imageView上

     思考:
     * 下载图片 : 子线程
     * 等2张图片都下载完毕后, 才回到主线程
     */
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 创建一个组
    dispatch_group_t group = dispatch_group_create();

    // 开启一个任务下载图片1
    __block UIImage *image1 = nil;
    dispatch_group_async(group, global_queue, ^{
        image1 = [self imageWithURL:@"http://news.baidu.com/z/resource/r/image/2014-06-22/2a1009253cf9fc7c97893a4f0fe3a7b1.jpg"];
    });

    // 开启一个任务下载图片2
    __block UIImage *image2 = nil;
    dispatch_group_async(group, global_queue, ^{
        image2 = [self imageWithURL:@"http://news.baidu.com/z/resource/r/image/2014-06-22/b2a9cfc88b7a56cfa59b8d09208fa1fb.jpg"];
    });

    // 同时执行下载图片1\下载图片2操作

    // 等group中的所有任务都执行完毕, 再回到主线程执行其他操作
    dispatch_group_notify(group, main_queue, ^{
        self.imageView1.image = image1;
        self.imageView2.image = image2;

        // 合并
        UIGraphicsBeginImageContextWithOptions(CGSizeMake(200, 100), NO, 0.0);
        [image1 drawInRect:CGRectMake(0, 0, 100, 100)];
        [image2 drawInRect:CGRectMake(100, 0, 100, 100)];
        self.bigImageView.image = UIGraphicsGetImageFromCurrentImageContext();
        // 关闭上下文
        UIGraphicsEndImageContext();
    });

}
- (UIImage *)imageWithURL:(NSString *)urlStr
{
    NSURL *url = [NSURL URLWithString:urlStr];
    NSData *data = [NSData dataWithContentsOfURL:url]; // 这行会比较耗时
    return [UIImage imageWithData:data];
}

十、NSOperation

10.1 what
作用:配合使用NSOperation和NSOperationQueue也能实现多线程编程
实现步骤:

  • 先将需要执行的操作封装到一个NSOperation对象中

  • 然后将NSOperation对象添加到NSOperationQueue中

  • 系统会自动将NSOperation中封装的操作放到一条新线程中执行

10.2 how
NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类
使用NSOperationa子类有3种

  • NSInvocationOperation
  • NSBlockOperation
  • 自定义子类继承NSOperation,实现内部相应的方法

10.2.1 NSInvocationOperation

  • 创建SInvocationOperation对象
  • 调用start方法开始执行操作
    注意:
    默认情况下,调用了start方法后并不会开一条新线程去执行操作,而是在当前线程同步执行操作,只有将NSOperation放到NSOperationQueue中,才会异步执行操作。
    -(void)invocationOperation
    {
      // 1.创建操作对象, 封装要执行的任务
      NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download) object:nil];
      // 2.执行操作(默认情况下, 如果操作没有放到队列queue中, 都是同步执行)
      [operation start];
    }
    -(void)download
    {
      for (int i = 0; i<10; i++) {
          NSLog(@"------download---%@", [NSThread currentThread]);
          [NSThread sleepForTimeInterval:0.1];
      }
    }

10.2.2 NSBlockOperation
如下代码会开启3个线程,线程数取决于任务的个数

-(void)blockOperation
{
    // 1.封装操作
//    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
//        NSLog(@"NSBlockOperation------下载图片1---%@", [NSThread currentThread]);
//    }];
    NSBlockOperation *operation = [[NSBlockOperation alloc] init];

    [operation addExecutionBlock:^{
        NSLog(@"NSBlockOperation------下载图片1---%@", [NSThread currentThread]);
    }];

    [operation addExecutionBlock:^{
        NSLog(@"NSBlockOperation------下载图片2---%@", [NSThread currentThread]);
    }];

    [operation addExecutionBlock:^{
        NSLog(@"NSBlockOperation------下载图片3---%@", [NSThread currentThread]);
    }];

    // 2.执行操作
    [operation start];
}

添加到NSOperationQueue的操作

- (void)operationQueue
{
    // 1.封装操作
    NSInvocationOperation *operation1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download) object:nil];
//    operation1.queuePriority = NSOperationQueuePriorityHigh

    NSInvocationOperation *operation2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];

    NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i<10; i++) {
            NSLog(@"NSBlockOperation------下载图片---%@", [NSThread currentThread]);
            [NSThread sleepForTimeInterval:0.1];
        }
    }];
//    [operation3 addExecutionBlock:^{
//        for (int i = 0; i<10; i++) {
//            NSLog(@"NSBlockOperation------下载图片2---%@", [NSThread currentThread]);
//            [NSThread sleepForTimeInterval:0.1];
//        }
//    }];

    // 2.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//设置并发数
    queue.maxConcurrentOperationCount = 2; // 2 ~ 3为宜

    // 设置依赖
    [operation2 addDependency:operation3];
    [operation3 addDependency:operation1];

    // 3.添加操作到队列中(自动执行操作, 自动开启线程)
    [queue addOperation:operation1];
    [queue addOperation:operation2];
    [queue addOperation:operation3];

//    [queue setSuspended:YES];
}

10.3 对列的暂停、取消和恢复
[queue setSuspended:YES];//yes代表暂停,no代表恢复
[queue cancel]// 取消
10.4 操作依赖(代码在上边)
10.5 NSOperation下载图片的思路

《多线程在iOS 中的应用》

h.png

github上有一个比较好的下载图片框架,sdwebimage,可以去研究一下。

点赞