iOS开发-iOS多线程开发中踩过的坑-GCD的特性-NSOperation线程依赖-iOS多线程踩坑小结

本期内容:

  • iOS开发中从其他线程回到主线程的方法
  • dispatch_group_create 组的概念
  • dispatch_sync同步调度主线程会死锁的原因
  • 项目中什么时候选择GCD什么时候选择NSOperation
  • NSOperation 线程依赖的简单例子
  • GCD的计时器和延时执行
  • 简单说说线程死锁和线程安全⭐️

iOS开发中从其他线程回到主线程的方法

在开发中我们经常使用简单的多线程,用来让数据和视图更好分配来提高项目的性能。一般在调用其他线程和自己创建的线程完成数据的加载以及处理之后,我们需要回到主线程来更新我们的UI视图,该怎么做呢?其实很简单,以下三种方式都可以回到主线程来执行任务:

- (void)backToMainThread{
    // 方法1,可以直接使用NSThread
    [self performSelectorOnMainThread:@selector(dosomething:) withObject:nil waitUntilDone:NO];
    [self performSelectorOnMainThread:@selector(dosomething:) withObject:nil waitUntilDone:NO modes:nil];
    
    // 方法2,在使用GCD的时候,可以直接在线程中使用
    dispatch_async(dispatch_get_main_queue(), ^{
        // 使用异步线程获取主线程,如果在同步线程获取主线程,会阻塞线程
        // 关于为什么会阻塞线程,下面会仔细讲解
        [self dosomething:nil];
    });
    
    // 方法3,使用NSOperationQueue
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        // NSOperationQueue有类方法,直接获取就可以使用了
        [self dosomething:nil];
    }];
}

- (void)dosomething:(id)sender{
    NSLog(@"dosomething");
}

dispatch_group_create 组的概念

dispatch_group_create 有一个组的概念,可以把相关的任务归并到一个组内来执行
dispatch_group_async把异步任务提交到指定任务组和指定下拿出队列执行
参数有:
dispatch_group_enter
dispatch_group_leave
dispatch_group_wait
前两个要成对使用,wait会阻塞线程,等enter和leave执行完才会结束阻塞
dispatch_group_notify 待任务组执行完毕时调用,不会阻塞当前线程

因为下划线在MACDOWN语言中是斜体所以使用代码块来表述,以下来看看Group的使用:

- (void)downLoadImage{
    // 创建全局队列
    dispatch_queue_t imageDownLoadQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 把全局队列添加到异步线程里面
    dispatch_async(imageDownLoadQueue, ^{
        // 使用分组执行下载人物
        // 创建一个组
        dispatch_group_t imageGroup =dispatch_group_create();
        
        // 添加接收图片的数据对象
        //__block UIImage *image1 = nil;
        //__block UIImage *image2 = nil;
        //__block UIImage *image3 = nil;
        //__block UIImage *image4 = nil;
        
        // 在组里面创建异步任务
        dispatch_group_async(imageGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            // image1 = [self loadImage:imgUrl1];
        });
        dispatch_group_async(imageGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            // image2 = [self loadImage:imgUrl2];
        });
        dispatch_group_async(imageGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            // image3 = [self loadImage:imgUrl3];
        });
        dispatch_group_async(imageGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            // image4 = [self loadImage:imgUrl4];
        });
        
        // 等到所有任务都完成了,notify才会执行组的最后任务,在这里使用主线程刷新我们的UI
        dispatch_group_notify(imageGroup, dispatch_get_main_queue(), ^{
            //self.imageview1.image = image1;
            //self.imageView2.image = image2;
            //self.imageview3.image = image3;
            //self.imageView4.image = image4;
        });
        
    });
}

dispatch_sync同步调度主线程会死锁的原因

先看看以下代码:

- (void)GCDTest{
	// 以下代码没有输出,还会报错
	dispatch_queue_t mainThreadQueue = dispatch_get_main_queue();
	dispatch_sync(mainThreadQueue, ^{
        NSLog(@"看不到我");
    });
    
}

sync 用于将一个任务提交到我们的队列中同步调度执行,完成一个任务后才会返回。主线程队列mainThreadQueue是一个串行队列,在App启动的时候就有很多任务在执行,但是因为sync的特性是同步执行,所以在执行sync同步调度的时候,这些任务就会互相等待,就会造成阻塞。

上面的代码有点少,按照引述,我们的执行的过程好比以下代码:

- (void)GCDTest{
	dispatch_queue_t myQueue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
	dispatch_sync(myQueue, ^{
    	// 任务1
    	NSLog(@"1");
	});
	dispatch_sync(myQueue, ^{
    	// 任务2
    	NSLog(@"2");
	});
	dispatch_sync(myQueue, ^{
    	// 任务3
    	NSLog(@"3");
	});
	
	// 最后才轮到我们的主线程队列,输出:1,2,3,爱就像蓝天白云晴空万里,突然就暴风雨(崩溃)
	dispatch_sync(myQueue, ^{
    	dispatch_queue_t mainThreadQueue = dispatch_get_main_queue();
    	dispatch_sync(mainThreadQueue, ^{
        	NSLog(@"看不到我");
    	});
	});
	
	// 把上面这个代码替换为下面的代码,输出:1,2,3,看到我了
	
	// 因为同步调度是串行的,按照顺序执行,所以我们在同步调度的时候使用主线程也是要按规矩排队
	dispatch_sync(myQueue, ^{
    	dispatch_queue_t mainThreadQueue = dispatch_get_main_queue();
    	dispatch_sync(mainThreadQueue, ^{
        	NSLog(@"看到我了");
    	});
	});
}

sync同步调度主线程崩溃的坑估计90%的iOS开发者都踩过,哈哈哈?也是老生常谈的了


项目中什么时候选择GCD什么时候选择NSOperation

1.GCD内部实现,GCD是基于OSX内核实现的,最大的优点就是简单、易用,根据官方的说法就是更加安全高效。不用做繁杂的多线程操作,可以在一定量的节省代码量。【可用复杂项目的模块或者简单项目的】

2.NSOperationQueue是基于GCD的OC版本封装,剧本面向对象的特性(复用、封装),NSOperationQueue可以很方便地调整执行顺序、设置最大并发数量。 NSOperationQueue可以在轻松在Operation间设置依赖关系,而GCD需要写很多的代码才能实现,NSOperationQueue支持KVO,可以监测operation是否正在执行。【可用于复杂项目】


NSOperation 线程依赖的简单例子

先来简单说一下NSOperation,是执行操作的意思,是指在线程中执行的代码块。在NSOperation中,官方推荐我们使用NSOperation子类 NSInvocationOperation、NSBlockOperation,或者自定义子类来封装操作。NSOperationQueue指操作队列,用来存放操作的队列。NSOperationQueue对于添加到队列中的操作,先进入准备就绪的状态,然后进入就绪状态的操作的开始执行顺序。这里要说一点,就绪状态的操作,添加过依赖的就按照依赖的关系执行,再继续操作队列的后续执行。

NSOperation、NSOperationQueue创建步骤:

- (void)createNSOperation{
    // 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    // 2.创建操作
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        // 操作中使用你的代码
    }];

    // 3.添加操作
    [queue addOperation:operation];
}

NSOperation 线程依赖:

- (void)addDependency {
    // 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    // 2.创建操作
    NSBlockOperation *opertaion1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"1");
    }];
    NSBlockOperation *opertaion2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"2");
    }];
    NSBlockOperation *opertaion3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"3");
    }];

    // 3.添加依赖
    // opertaion3 依赖于 opertaion1和opertaion2
    [opertaion3 addDependency:opertaion1]; 
    [opertaion3 addDependency:opertaion2]; 
	
    // 4.添加操作到队列中
    [queue addOperation: opertaion1];
    [queue addOperation: opertaion2];
    [queue addOperation: opertaion3];
    
    // 输出为 1,2,3因为依赖关系需要等依赖的操作执行完才会继续,所以,1,2执行完才会执行3,这就是NSOperation的依赖
}

GCD的计时器和延时执行

使用GCD的source特性来做一个计时器,之前在做马甲包的时候简单的使用了这一特性来做短信获取的倒计时,代码如下:

- (void)dealTimeBtnAciton{
    // 1.设定倒计时时间
    __block int timeout = 60; 
    // 2.获取全局队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 3.创建一个定时器,并将定时器交给全局队列执行,不会造成主线程阻塞
    dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,queue);
    // 4.设定每秒执行(1.0为参数)
    dispatch_source_set_timer(_timer,dispatch_walltime(NULL, 0),1.0*NSEC_PER_SEC, 0); 
    // 5.设置定时器的触发事件
    dispatch_source_set_event_handler(_timer, ^{
        if(timeout <= 0){
            // 操作:倒计时结束,关闭
            dispatch_source_cancel(_timer);
            // 操作:刷新UI
            dispatch_async(dispatch_get_main_queue(), ^{
                // 设置界面的显示 根据自己需求设置
                
                return;
            });
        }else{
        	  // 如果倒计时没结束,继续轮询以下方法:
            int seconds = timeout % 60;
            // 每秒刷新UI,更新秒数
            dispatch_async(dispatch_get_main_queue(), ^{
                // 设置界面的显示 根据自己需求设置
                if (seconds != 0) {
                		// 如果秒数不为0需要做的操作    
                }
            });
            // 更新倒计时的时间
            timeout--;
        }
    });
    // 启用定时器
    dispatch_resume(_timer);
    
}

延时执行,使用GCD的after特性,有时候在复杂UI赋值的时候用上,或者在dismiss一些提示页面的时候很受用,代码如下:

- (void)setDelayTime{
    // 1.设定延时时间
    double delayInSeconds = 2.0;
    // 2.创建延时时间
    dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
    dispatch_after(delayTime, dispatch_get_main_queue(), ^(void){
        NSLog(@"延迟了两秒执行");
    });
}

简单说说线程死锁和线程安全⭐️

线程死锁

线程中每一个资源每次只能被一个进程使用,一个线程因请求资源而阻塞时,对已获得的资源保持不放,但是线程已获得的资源,在末使用完之前,不能释放,若干进程之间都在互相循环等待资源这时候就造成了死锁。

通俗易懂的解释:在一条河上有一座桥,桥面较窄,只能容纳一辆汽车通过,无法让两辆汽车并行。如果有两辆汽车A和B分别由桥的两端驶上该桥,则对于A车来说,它走过桥面左面的一段路(即占有了桥的一部分资源),要想过桥还须等待B车让出所在的桥面,此时A车不能前进;对于B车来说,要想过桥还须等待A车让出的桥面,此时B车也不能前进。两边的车都不倒车,结果造成互相等待对方让出桥面,但是谁也不让路,就会无休止地等下去。这种现象就是死锁。

线程安全

线程安全,为了避免线程死锁做的操作。常用的方式就是对资源的获取操作加锁,以便保证资源被唯一访问。常见的锁:NSLock、@synchronized、NSConditionLock条件锁、NSRecursiveLock递归锁。

锁是最常用的同步工具。一段代码段在同一个时间只能允许被一个线程访问,比如一个线程A进入加锁代码之后由于已经加锁,另一个线程B就无法访问,只有等待前一个线程A执行完加锁代码后解锁,B线程才能访问加锁代码。使用线程锁就是做了一个简单的线程安全处理,防止多个线程去抢同一个资源,避免死锁或是阻塞。

这时候会有人说,原子性(atomic)可以自动加锁保证线程安全。来引申一下原子性:

原子性:指的是编译器会在property上自动添加原子锁,非原子性nonatomic,不考虑多线程情况时使用,提高效率。atomic本质上就是给get/set方法加锁,即原子锁,以避免线程A还没执行完setter,线程B又开始执行的,导致读取数据错误的问题。

atomic一定是线程安全的么??肯定不是啊!

首先atomic的释义是原子性,并不是线程安全。原子性这个概念表示一个操作序列就像一个操作一样不被打断,而不像一个操作序列一样中间容许被打断。所以nonatomic一定是线程不安全的,但是atomic却不一定是线程安全的。假设线程A执行在对某属性get之前线程B release了该属性,会导致程序崩溃。

atomic的作用只是给getter和setter加了个锁,atomic只能保证代码进入getter或者setter函数内部时是安全的,一旦出了getter和setter,多线程安全只能靠自己保障了。所以atomic属性和使用property的多线程安全并没什么直接的联系。另外,atomic由于加锁也会带来一些性能损耗,所以我们在编写iOS代码的时候,一般声明property为nonatomic,在需要做多线程安全的场景,自己去额外加锁做同步。

所以,我们如何做到多线程安全?没有绝对的安全,其实就是,在写代码的时候,能保证代码串行的执行,代码执行到一半的时候,不会有另一个线程介入。这就是我们所追求的线程安全。

所有笔记都出自日常踩坑小记,会持续更新

你可能感兴趣的:(iOS开发基础,iOS技术点开发)