美文网首页
iOS 多线程之 NSThread

iOS 多线程之 NSThread

作者: 大成小栈 | 来源:发表于2018-12-18 17:16 被阅读11次

    1. 线程的概念

    首先简单叙述一下这两个概念,我们在电脑上单独运行的每个程序就是一个独立的进程,通常进程之间是相互独立存在的,进程是系统分配资源的最小单元。进程中的最小执行单位就是线程,并且一个进程中至少有一个线程,进程中的所有线程共用这个进程的资源,线程也是系统进行调度的最小单元。

    1.1 多线程在多核CPU中处理任务
    多线程在多核CPU中处理任务的过程
    1.2 线程状态的切换
    线程状态切换
    1.3 线程安全问题

    线程安全问题是在多个线程处理任务的情况下产生。例如多个线程在同事执行下面的任务时,如果多个线程可以同时执行这段代码(任务),当多个线程同时在getTicket方法中执行count--时,count的结果就会出现不准确的情况,这个处理过程就不是线程安全的。

    NSInteger ticketCount = 100;
    - (void)getTicket() {
    
           count--;
           NSLog(@"剩余票数 = %d", ticketCount);
       }
    
    1.4 iOS 中的多线程

    在iOS中每个app启动后都会建立一个主线程,也称UI线程。由于除了主线程的其他子线程都是独立于Cocoa Touch的,所以一般只使用主线程来更新UI界面。iOS中的多线程有三种方式: NSThread、NSOperation、GCD,其中GCD是目前苹果比较推荐的方式。对于这篇文章,我们主要了解一下NSThread。

    2. NSThread简介

    NSThread是轻量级的多线程开发,优点是我们可以直接实例化一个NSThread对象并直接操作这个线程对象,但是使用NSThread需要自己管理线程生命周期。iOS开发过程中,NSThread最常用到的方法就是 [NSThread currentThread]获取当前线程,其他常用属性及方法如下:

    // 线程字典
    @property (readonly, retain) NSMutableDictionary *threadDictionary;
    // 线程名称
    @property (nullable, copy) NSString *name;
    // 优先级
    @property double threadPriority ; 
    // 是否为主线程
    @property (readonly) BOOL isMainThread
    // 读取线程状态
    @property (readonly, getter=isExecuting) BOOL executing;
    @property (readonly, getter=isFinished) BOOL finished;
    @property (readonly, getter=isCancelled) BOOL cancelled;
    
    
    // 直接将操作添加到线程中并启动
    + (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument
    
    // 创建一个线程对象
    - (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 
    
    // 启动
    - (void)start;
    
    // 撤销
    - (void)cancel;
    
    // 退出
    + (void)exit;
    
    // 休眠
    + (void)sleepForTimeInterval:(NSTimeInterval)ti;
    

    在NSObject(NSThreadPerformAdditions)类中的几个常用方法,实现了在特定线程上执行任务的功能,该分类也定义在NSThread.h中:

    // 在主线程上执行一个方法
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
    
    // 在指定的线程上执行一个方法,需要用户创建一个线程对象
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
    
    // 在后台执行一个操作,本质就是重新创建一个线程执行当前方法
    - (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
    
    2.1 NSThread的使用过程

    例如在app从网络下载图片时,由于网络原因可能需要较长时间,这时如果只在主线程中进行下载,则这个过程中用户将无法进行其他操作,直到网络图片下载完成之前界面都处于卡死状态(线程阻塞)。我们在主线程中另起一个新线程来单独下载即可解决这一问题,不管资源是否下载完成都可以继续操作界面,不会造成阻塞。示例代码如下:

    @interface MultiThread_NSThread ()
    
    // 显示图片
    @property (nonatomic, strong) UIImageView *imgView;
    
    @end
    
    @implementation MultiThread_NSThread
    
    - (void)viewDidLoad {
        
        [super viewDidLoad];
        [self setTitle:@"NSThread"];
        [self.view setBackgroundColor:[UIColor whiteColor]];
        self.edgesForExtendedLayout = UIRectEdgeNone;
        
        [self layoutViews];
    }
    
    - (void)layoutViews {
        
        CGSize size = self.view.frame.size;
        
        _imgView =[[UIImageView alloc] initWithFrame:CGRectMake(0, 0, size.width, 300)];
        [self.view addSubview:_imgView];
        
        UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
        button.frame = CGRectMake(15, CGRectGetMaxY(_imgView.frame) + 30, size.width - 15 * 2, 45);
        [button setTitle:@"点击加载" forState:UIControlStateNormal];
        [button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:button];
    }
    
    
    #pragma mark - 多线程下载图片
    
    - (void)loadImageWithMultiThread {
        
        ////1. 对象方法
        //NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(loadImage) object:nil];
        //[thread start];
        
        //2. 类方法
        [NSThread detachNewThreadSelector:@selector(downloadImg) toTarget:self withObject:nil];
    }
    
    
    #pragma mark - 加载图片
    
    - (void)downloadImg {
        
        // 请求数据
        NSData *data = [self requestData];
        // 回到主线程更新UI
        [self performSelectorOnMainThread:@selector(updateImg:) withObject:data waitUntilDone:YES];
    }
    
    
    #pragma mark - 请求图片数据
    
    - (NSData *)requestData {
        
        NSURL *url = [NSURL URLWithString:@"https://store.storeimages.cdn-apple.com/8756/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/apple/products/apple-products-section1-one-holiday-201811?wid=2560&hei=1046&fmt=jpeg&qlt=95&op_usm=0.5,0.5&.v=1540576114151"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        return data;
    }
    
    
    #pragma mark - 将图片显示到界面
    
    - (void)updateImg:(NSData *)imageData {
        
        UIImage *image = [UIImage imageWithData:imageData];
        _imgView.image = image;
    }
    
    @end
    

    在请求数据的代码上打一个断点,可以看出NSThread是对pthread的封装:


    NSThread是对pthread的封装
    2.2 使用NSThread实现多线程并发

    下面我们使用NSThread实现多线程加载多张网络图片,来了解NSThread多线程处理任务的过程。示例代码如下:

    @implementation NSThreadImage
    
    @end
    
    #define ColumnCount    4
    #define RowCount       5
    #define Margin         10
    
    @interface MultiThread_NSThread1 ()
    
    // imgView数组
    @property (nonatomic, strong) NSMutableArray *imgViewArr;
    // thread数组
    @property (nonatomic, strong) NSMutableArray *threadArr;
    
    @end
    
    @implementation MultiThread_NSThread1
    
    - (void)viewDidLoad {
        
        [super viewDidLoad];
        [self setTitle:@"NSThread1"];
        [self.view setBackgroundColor:[UIColor whiteColor]];
        self.edgesForExtendedLayout = UIRectEdgeNone;
        
        [self layoutViews];
    }
    
    - (void)layoutViews {
        
        CGSize size = self.view.frame.size;
        CGFloat imgWidth = (size.width - Margin * (ColumnCount + 1)) / ColumnCount;
        
        _imgViewArr = [NSMutableArray array];
        for (int row=0; row<RowCount; row++) {
            for (int colomn=0; colomn<ColumnCount; colomn++) {
                UIImageView *imageView=[[UIImageView alloc] initWithFrame:CGRectMake(Margin + colomn * (imgWidth + Margin), Margin + row * (imgWidth + Margin), imgWidth, imgWidth)];
                imageView.backgroundColor=[UIColor cyanColor];
                [self.view addSubview:imageView];
                [_imgViewArr addObject:imageView];
            }
        }
        
        UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
        button.frame = CGRectMake(15, (imgWidth + Margin) * RowCount + Margin, size.width - 15 * 2, 45);
        [button addTarget:self action:@selector(loadImgWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
        [button setTitle:@"点击加载" forState:UIControlStateNormal];
        [self.view addSubview:button];
    }
    
    
    #pragma mark - 多线程下载图片
    
    - (void)loadImgWithMultiThread {
        
        _threadArr = [NSMutableArray array];
        for (int i=0; i<RowCount*ColumnCount; ++i) {
            NSThreadImage *threadImg = [[NSThreadImage alloc] init];
            threadImg.index = i;
            NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(loadImg:) object:threadImg];
            thread.name = [NSString stringWithFormat:@"myThread%i",i];
            //// 优先级
            //thread.threadPriority = 1.0;
            [thread start];
            [_threadArr addObject:thread];
        }
    }
    
    
    #pragma mark - 加载图片
    
    - (void)loadImg:(NSThreadImage *)threadImg {
        
        //// 休眠
        //[NSThread sleepForTimeInterval:2.0];
        //// 撤销(停止加载图片)
        //[[NSThread currentThread] cancel];
        //// 退出当前线程
        //[NSThread exit];
        
        // 请求数据
        threadImg.imgData =  [self requestData];
        // 回到主线程更新UI
        [self performSelectorOnMainThread:@selector(updateImg:) withObject:threadImg waitUntilDone:YES];
        
        // 打印当前线程
        NSLog(@"current thread: %@", [NSThread currentThread]);
    }
    
    
    #pragma mark - 请求图片数据
    
    - (NSData *)requestData{
        
        NSURL *url = [NSURL URLWithString:@"https://store.storeimages.cdn-apple.com/8756/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/apple/products/apple-products-section1-one-holiday-201811?wid=2560&hei=1046&fmt=jpeg&qlt=95&op_usm=0.5,0.5&.v=1540576114151"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        return data;
    }
    
    
    #pragma mark - 将图片显示到界面
    
    - (void)updateImg:(NSThreadImage *)threadImg {
        
        UIImage *image = [UIImage imageWithData:threadImg.imgData];
        UIImageView *imageView = _imgViewArr[threadImg.index];
        imageView.image = image;
    }
    
    
    //#pragma mark 停止加载网络图片
    //
    //- (void)stopLoadingImgs {
    //
    //    for (int i=0; i<RowCount*ColumnCount; ++i) {
    //
    //        NSThread *thread = _threadArr[i];
    //        if (!thread.isFinished) {
    //            [thread cancel];
    //        }
    //    }
    //}
    
    @end
    

    3. 关于NSThread线程状态的说明

    NSThread类型的对象可以获取到线程的三种状态属性isExecuting(正在执行)、isFinished(已经完成)、isCancellled(已经撤销),其中撤销状态是可以在代码中调用线程的cancel方法手动设置的(在主线程中并不能真正停止当前线程)。isFinished属性标志着当前线程上的任务是否执行完成,cancel一个线程只是撤销当前线程上任务的执行,监测到isFinished = YES或调用cancel方法都不能代表立即退出了这个线程,而调用类方法exit方法才可立即退出当前线程。

    例如在加载多张网络图片时,中途停止加载动作的执行:

    #pragma mark 停止加载网络图片
    
    - (void)stopLoadingImgs {
        
        for (int i=0; i<RowCount*ColumnCount; ++i) {
            
            NSThread *thread = _threadArr[i];
            if (!thread.isFinished) {
                [thread cancel];
            }
        }
    }
    

    PS:

    1. 更新UI需回到主线程中操作;
    2. 线程处于就绪状态时会处于等待状态,不一定立即执行;
    3. 区分线程三种状态的不同,尤其是撤销和退出两种状态的不同;
    4. 在线程死亡之后,再次点击屏幕尝试重新开启线程,则程序会挂;
    5. NSThread可以设置对象的优先级thread.threadPriority,threadPriority取值范围是0到1;
    6. NSThread并没有提供设置线程间的依赖关系的方法,也就不能单纯通过NSThread来设置任务处理的先后顺序,但是我们可以通过设置NSThread的休眠或优先级来尽量优化任务处理的先后顺序;
    7. 在自己试验的工程中,虽然NSThread实例的数量理论上不受限制,但是正常的处理过程中需要控制线程的数量。

    工程源码GitHub地址

    相关文章

      网友评论

          本文标题:iOS 多线程之 NSThread

          本文链接:https://www.haomeiwen.com/subject/ysydhqtx.html