NSThread线程间的通信

来源:互联网 发布:jquery 表单数据 编辑:程序博客网 时间:2024/06/07 06:56

  在iOS开发中,一个进程中通常会有多个线程,而且这些线程往往不是孤立的,多个线程之间通常需要进行通信。线程通信具体表现为一个线程向另一个线程传递数据,或者某个线程在执行完特定的任务以后,需要转到另一个线程中继续执行任务。下面,我们就通过一个实例来简单演示一下线程间的通信。

一、简单实现一个图片下载实例

  
  新建一个工程,程序运行以后,当我们点击屏幕,让它下载网络上指定的图片。在项目目录中选中Main.storyboard文件,往里面拖一个UIImageView控件,然后给它拖线。在ViewController中实现- touchesBegan: withEvent:方法:

// MARK:- 点击屏幕下载图片- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {    // 点击下载图片    [self downloadImage];}// MARK:- 下载网络上指定的图片- (void)downloadImage {    // 将URL地址转换为字符串    NSURL *url = [NSURL URLWithString:@"http://img1.gtimg.com/ent/pics/hv1/224/60/2171/141184799.jpg"];    // 根据URL地址将图片资源的二进制文件下载到本地    NSData *imageData = [NSData dataWithContentsOfURL:url];    // 将二进制数据转换为图片    UIImage *image = [UIImage imageWithData:imageData];    // 将图片设置到UIImageView控件上去    self.imageView.image = image;    // 打印当前线程    NSLog(@"%@", [NSThread currentThread]);}

  因为我们的图片来自网络,所以肯定需要图片的URL地址。有了地址以后,需要先将它转换为NSURL对象,然后再根据这个NSURL对象将图片的二进制数据下载到本地,接着再将图片的二进制数据转换为图片,最后再将它设置到UIImageView控件上去。

  程序运行以后,当我们点击屏幕,并不会马上看到下载下来的图片。此时,控制台会打印一条消息:


控制台打印出来的App Transport Security Settings消息.png

  产生这个问题的原因是,从Xcode 7.0以后,苹果就不再允许我们直接在代码中发送HTTP请求,转而要求我们发送HTTPS请求。但是,网络上有很多资源依然还是HTTP,为了能够继续使用它,我们必须更改info.plist文件中的设置。下面,我们就演示一下如何进行相应的设置。

  在项目目录中点击info.plist文件,将光标放在最后一个key上,点击加号,在弹出的对话框中选择App Transport Security Settings,然后展开App Transport Security Settings前面的小三角,再点击它后面的加号,在弹出的对话框中选中Allow Arbitrary Loads。最后,再将它的Value值修改为YES。具体操作如下图所示:


Allow Arbitrary Loads.png

  运行程序,然后再点击屏幕,看看图片有没有下载下来:


点击屏幕下载网络图片.gif

  从上面的运行结果来看,当我们点击屏幕时,网络上的图片很快就下载到模拟器上了。但是,这里还存在一个很严重的问题。从控制台打印出来的消息看,我们是在主线程中完成网络图片的下载的。在实际开发中,可能需要下载很多张图片,而且每张图片也可能会比较大,再加上受网络环境的影响,下载过程可能会比较漫长。此时,如果我们还将网络请求部分的代码放在主线程中执行,可能在很长一段时间内导致UI界面无法操作,这样会给用户一个非常糟糕的体验。为此,我们需要将网络请求部分的代码放在子线程中执行。即在子线程中发起网络请求,完了之后在主线程中刷新UI,由此又涉及线程间通信的问题。接下来,我们会改进代码,并演示如何在线程中进行通信。

二、在NSThread线程中进行通信

  
  通常情况下,网络请求部分都是比较耗时的操作,为了提升用户体验,需要将它们放在子线程中去执行。为此,我们先开启一个子线程:

// MARK:- 点击屏幕下载图片- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {    // 创建一个子线程    [NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil];}// MARK:- 下载网络上指定的图片- (void)downloadImage {    // 将URL地址转换为字符串    NSURL *url = [NSURL URLWithString:@"http://img1.gtimg.com/ent/pics/hv1/224/60/2171/141184799.jpg"];    // 根据URL地址将图片资源的二进制文件下载到本地    NSData *imageData = [NSData dataWithContentsOfURL:url];    // 将二进制数据转换为图片    UIImage *image = [UIImage imageWithData:imageData];    // 回到主线程中刷新UI    [self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:NO];    /**     *  performSelectorOnMainThread:<#(nonnull SEL)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>     *  第一个参数 : 表示回到主线程时需要调用的方法;     *  第二个参数 : 表示表示前面方法选择其中的方法需要接收的参数;     *  第三个参数 : 表示是否需要等待前面方法选择器中的方法执行完毕以后,再接着执行后面的代码。     */    // 打印当前线程    NSLog(@"下载图片数据:%@", [NSThread currentThread]);}// MARK:- 回到主线程刷新UI- (void)showImage:(UIImage *)image {    // 将下载下来的图片设置到UIImageView控件上去    self.imageView.image = image;    // 打印当前线程    NSLog(@"刷新UI界面:%@", [NSThread currentThread]);}

  我们在- touchesBegan: withEvent:中使用+ detachNewThreadSelector: toTarget: withObject:方法创建了一条子线程,然后在downloadImage方法中使用- performSelectorOnMainThread: withObject: waitUntilDone:回到主线程刷新UI。

  - performSelectorOnMainThread: withObject: waitUntilDone:方法可以让我们在子线程和主线程中进行通信。这个方法有3个参数,第一个参数表示我们要回到主线程时需要调用的方法;第二个参数表示前面方法选择器中需要接收的参数,因为我们是在- showImage:方法中刷新UI界面的,所以必须将- downloadImage方法中下载下来的图片数据传递给- showImage:方法;第三个参数表示是否需要等待- showImage:方法执行完成以后,再接着执行- downloadImage方法后面的代码,传NO或者YES在我们这里没有太大影响。

  先运行程序,再看一下控制台打印情况:


在子线程中下载网络图片,在主线程中刷新UI界面.gif

  从控制台打印出来的消息看,我们现在做到了在子线程中下载网络图片,在主线程中刷新UI界面。线程间的通信,除了上面这个方法之外,还有一个- performSelector: onThread: withObject: waitUntilDone:方法,它也可以让我们回到主线程:

// MARK:- 下载网络上指定的图片- (void)downloadImage {    // 将URL地址转换为字符串    NSURL *url = [NSURL URLWithString:@"http://img1.gtimg.com/ent/pics/hv1/224/60/2171/141184799.jpg"];    // 根据URL地址将图片资源的二进制文件下载到本地    NSData *imageData = [NSData dataWithContentsOfURL:url];    // 将二进制数据转换为图片    UIImage *image = [UIImage imageWithData:imageData];    // 回到主线程中刷新UI    [self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:NO];    /**     *  performSelector:<#(nonnull SEL)#> onThread:<#(nonnull NSThread *)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>     *  第一个参数 : 表示回到主线程时需要调用的方法;     *  第二个参数 : 表示需要回到哪个线程。它既可以回到主线程,也可以转到其它子线程;     *  第三个参数 : 表示表示前面方法选择其中的方法需要接收的参数;     *  第四个参数 : 表示是否需要等待前面方法选择器中的方法执行完毕以后,再接着执行后面的代码。     */    // 打印当前线程    NSLog(@"下载图片数据:%@", [NSThread currentThread]);}

  这个方法和前面那个方法的使用方式差不多,程序运行的结果也是一样的,只不过,相比起来多了一个选择线程的参数,使用更为灵活。

  关于上面这两个方法的使用,其实还有一个更简便的方式,不需要额外再写一个方法。按住command键,点击进入这两个方法的头文件,可以看到它们是声明在NSObject的分类中的,这就意味着,只要是任意一个继承自NSObject的类都可以调用它:


NSObject的分类.png

  我们使用self.imageView调用这个方法,而image属性本身就有一个setImage:方法,只需要把这个方法传进去就可以了:

// MARK:- 下载网络上指定的图片- (void)downloadImage {    // 将URL地址转换为字符串    NSURL *url = [NSURL URLWithString:@"http://img1.gtimg.com/ent/pics/hv1/224/60/2171/141184799.jpg"];    // 根据URL地址将图片资源的二进制文件下载到本地    NSData *imageData = [NSData dataWithContentsOfURL:url];    // 将二进制数据转换为图片    UIImage *image = [UIImage imageWithData:imageData];    // 回到主线程中刷新UI    [self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];    // 打印当前线程    NSLog(@"下载图片数据:%@", [NSThread currentThread]);}

  它的运行效果和上面这两个方法一样。关于NSThread线程间通信的基本知识,暂时先简单的讲到这里,详细代码参见InterThreadsCommunication。

原创粉丝点击