iOS

iOS下的多线程

苹果为iOS的多线程提供了很多种方法,我主要是用GCD,总结一下其他的方法。

Posted by yasin on August 8, 2017

基本方法

获取主线程

[NSThread mainThread]

获取当前线程

[NSThread currentThread]

NSThread的使用

基础用法,需要手动启动线程,可以对线程进行一些设置(线程名,优先级)

    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run:) object:@"test1"];
    thread.name = @"mythread1";
    [thread start];

类方法快速启动线程,不需要手动开启,不能对线程进行设置

    [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"test2"];

开启一个后台线程,不能对线程进行设置

    [self performSelectorInBackground:@selector(run:) withObject:@"test3"];

###线程状态

执行的步骤 线程状态
alloc 创建线程对象
start 放入线程池(就绪,等待CPU调度)
CPU调度 运行
CPU调度其它线程 回到就绪状态
CPU调度 运行
执行完毕 线程退出

线程阻塞(休眠)

sleep/等待同步锁会线程阻塞,线程从线程池中移除,当阻塞结束重新进入线程池等待CPU调度

    [NSThread sleepForTimeInterval:2];//休眠2秒
    
    [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];//休眠到指定的时间
    //这里的NSDate可以设置[NSDate distantFuture]时间为极大值,这样能实现只执行一次

线程死亡

1.线程任务执行完成 2.异常/强制退出 一旦线程死亡,就不能再开启任务。

    [NSThread exit]

互斥锁(同步锁)

优点:能有效防止多线程抢夺资源造成数据错乱。 缺点:会产生线程等待,消耗大量的CPU资源。

线程同步是多条线程在同一条线上按顺序执行。 通过@synchronized可以使代码获得原子性,同时只能在一条线程上执行,保证了多线程的安全。这里有比较详细的介绍。

    //token是锁对象,唯一的,会生成同步锁,当一条线程执行完后,才执行下一条线程
    @synchronized (token) {数据操作代码}

    //这里传入一个唯一的对象,用来做锁对象,多个线程同时调用这段代码,会一个线程一个线程的执行,所以count的值不会混乱
    @synchronized (self) {
        self.count++;
    }

回主线程

    [self performSelectorOnMainThread:@selector(changeUI) withObject:nil waitUntilDone:YES];
    //UntilDone表示执行完changeUI方法在执行下面的语句,即同步还是异步

GCD

获得并行队列concurrent

    //获取全局的的并发队列 参数1是队列的优先级,参数2没用传0
    dispatch_get_global_queue(long identifier, unsigned long flags)
    
    //创建队列 参数1是队列名,参数2是队列属性,判断是并行还是串行队列,DISPATCH_QUEUE_CONCURRENT表示并行队列
    dispatch_queue_create(const char * _Nullable label, dispatch_queue_attr_t  _Nullable attr)

获得串行队列serial

    dispatch_get_main_queue()//获得主队列
    
    //参数2传入DISPATCH_QUEUE_SERIAL或NULL表示串行队列
    dispatch_queue_create(const char * _Nullable label, dispatch_queue_attr_t  _Nullable attr)

同步任务sync

    dispatch_sync(queue, ^{
        for(int i= 0;i<5;i++){
            NSLog(@"--2--%@",[NSThread currentThread]);
        }
    });

异步任务async

    dispatch_async(queue, ^{
        for(int i= 0;i<5;i++){
            NSLog(@"--1--%@",[NSThread currentThread]);
        }
    });

队列类型:决定了任务执行是顺序还是并发 任务类型:决定了是否开启新线程(主队列异步任务不会开启新线程)

子线程回主线程

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //耗时操作
        dispatch_async(dispatch_get_main_queue(), ^{
            //主线程ui操作
        });
    });

延时

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        //2秒后异步执行这里的代码
    });

一次性代码

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //只执行1次的代码
    });

遍历

    //参数1表示遍历次数。遍历是并发的。
    dispatch_apply(10, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t index) {
        //index当前遍历的下标
        NSLog(@"%d",index);
    });

队列组

    //1.创建队列组
    dispatch_group_t group = dispatch_group_create();
    
    //2.添加任务
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        //任务1
    });
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        //任务2
    });
    
    //3.设置任务完后回调
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        //任务1和任务2执行完成后,回到执行的线程执行这里的代码
    });

NSOperation和NSOperationQueue

NSOperation是基于GCD实现的。

实现多线程的步骤
1.先将任务封装到NSOperation对象中。
2.把NSOperation添加到NSOperationQueue中。
3.系统自动从NSOperationQueue中提取NSOperation。
4.将提取出的NSOperation封装的任务放到一条线程中去执行。

NSOperation

NSOperation是一个抽象类,不具备封装任务的能力,必须使用它的子类。NSInvocationOperationNSBlockOperation或者自定义子类继承NSOperation

NSInvocationOperation的使用

    NSInvocationOperation *ip = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(run) object:nil];
    
    //开启任务
    [ip start];

NSBlockOperation的使用

    NSBlockOperation *bp = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"--2--%@",[NSThread currentThread]);
    }];
    
    //可以添加额外任务,在子线程中完成
    [bp addExecutionBlock:^{
        NSLog(@"other--%@",[NSThread currentThread]);
    }];
    
    [bp start];

自定义Operation的使用

1.创建CustomOperation继承于NSOperation。
2.添加一个属性存放要执行的代码,可以用block,用于外部传入代码。
3.重写main方法,在main方法里面执行步骤2传入的代码。
4.调用CustomOperation的start方法会自动调用main方法。

NSOperationQueue的基本使用

    //创建队列(默认是并行队列)
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    
    //创建任务
    NSInvocationOperation *op = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(run) object:nil];
    
    //添加任务
    [queue addOperation:op];

NSOperation线程通信

    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    
    [queue addOperationWithBlock:^{
        //耗时操作
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            //回到主线程刷新UI
        }];
    }];

Dependency任务依赖

NSOperationaddDependency方法添加依赖,如果A依赖B,等B执行完后,再执行A

    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    
    NSBlockOperation *bp = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"--1--%@",[NSThread currentThread]);
    }];
    
    NSBlockOperation *bp1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"--2--%@",[NSThread currentThread]);
    }];
    
    //后面的先执行 不能相互依赖
    [bp1 addDependency:bp];

maxConcurrentOperationCount最大并发数

通过NSOperationQueuemaxConcurrentOperationCount属性可以设置最大并发数。默认为-1,不限制并发的数量。设置为1是,队列为串行队列。

suspended队列的挂起和取消

  • 将队列挂起可以暂停队列里面任务的执行。
  • 通过NSOperationQueuesuspended属性控制是否挂起,为YES是挂起。
  • 当任务处于执行状态,设置挂起还是会继续执行,受影响的是那些还没有执行的任务。
  • 当队列设置为挂起状态后,可以通过修改其状态再次恢复任务。

cancelAllOperations取消

  • 通过NSOperationQueuecancelAllOperations方法来取消任务。
  • 取消任务后不可恢复。
  • 若任务是自定义NSOperation类型的话,执行完一个耗时操作后,需加一个是否取消任务的判断,再去执行另外一个耗时操作。

RunLoop

内部由do-while循环实现的

作用:

1.保证程序的持续运行。

2.处理APP的各种事件。

3.节省CPU资源,提高程序性能。

    int main(int argc,char * argv[]){
    	do{
        	NSLog(@"haha");//程序开始
        }while(1);
        return 0;//程序结束
    }

runloop其实就是一个死循环,保持程序运行

    int main(int argc, char * argv[]) {
        @autoreleasepool {
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        }
    }

UIApplicationMain函数内部就启动了一个RunLoop,一直不返回程序就持续运行。 程序启动会默认启动一个RunLoop,跟主线程相关联,主要处理与主线程相关的事件。

RunLoop对象

  • NSRunLoop
  • CFRunLoopRef

RunLoop与线程

  • 每条线程都有唯一的一个与之对应的RunLoop对象。
  • 子线程的RunLoop需要手动创建
  • 获得主线程RunLoop的方法[NSRunLoop mainRunLoop];
  • 创建子线程RunLoop的方法是[NSRunLoop currentRunLoop];

RunLoop相关类

  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

CFRunLoopModeRef

1.CFRunLoopModeRef是RunLoop的运行模式。

2.一个Runloop可以包含若干个Mode,每个Mode包含若干个Source/Timer/Observer。

3.每次RunLoop启动时,只能指定其中的一个Mode,这个Mode被称为currentMode。

4.如果需要切换Mode,需要退出RunLoop,在重新指定一个Mode进入。这样是为了分离不同组的Source/Timer/Observer,让它们互不影响。

系统默认注册了5个Mode

1. NSDefaultRunLoopMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。

2. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。

3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。

4: GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。

5: NSRunLoopCommonModes: 这是一个占位的 Mode,代表了NSDefaultRunLoopMode和UITrackingRunLoopMode。

NSRunLoopCommonModes只是一个标记,NSDefaultRunLoopMode和UITrackingRunLoopMode都拥有这个标记。 设置为NSRunLoopCommonModes可以在NSDefaultRunLoopMode和UITrackingRunLoopMode模式下都执行。

CFRunLoopTimerRef

这个类是基于时间的触发器

CFRunLoopSourceRef

这个类是一个事件源,也可以称为输入源。 官方文档分为3类

1.Port-Based Sources从其他线程或内核发出的
2.Custom Input Sources自定义的
3.CoCoa Perform Selector Sources

函数调用栈分为2类

1.Sources0:非基于Port的(按钮点击)
2.Sources1:基于Port的,通过其他线程或内核通信、接受、分发系统事件。

CFRunLoopObserverRef

CFRunLoopObserverRef是观察者,能够监听RunLoop的状态的改变。状态是CFRunLoopActivity这个枚举。

    //创建观察者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"%lu",activity);//打印RunLoop的状态
    });
    //添加观察者
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);