前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >iOS多线程——你要知道的NSOperation都在这里你要知道的iOS多线程NSThread、GCD、NSOperation、RunLoop都在这里

iOS多线程——你要知道的NSOperation都在这里你要知道的iOS多线程NSThread、GCD、NSOperation、RunLoop都在这里

作者头像
WWWWDotPNG
发布于 2018-04-10 03:49:40
发布于 2018-04-10 03:49:40
1.7K00
代码可运行
举报
文章被收录于专栏:iOS技术杂谈iOS技术杂谈
运行总次数:0
代码可运行

你要知道的iOS多线程NSThread、GCD、NSOperation、RunLoop都在这里

转载请注明出处 https://cloud.tencent.com/developer/user/1605429

本系列文章主要讲解iOS中多线程的使用,包括:NSThread、GCD、NSOperation以及RunLoop的使用方法详解,本系列文章不涉及基础的线程/进程、同步/异步、阻塞/非阻塞、串行/并行,这些基础概念,有不明白的读者还请自行查阅。本系列文章将分以下几篇文章进行讲解,读者可按需查阅。

  • iOS多线程——你要知道的NSThread都在这里
  • iOS多线程——你要知道的GCD都在这里
  • iOS多线程——你要知道的NSOperation都在这里
  • iOS多线程——你要知道的RunLoop都在这里
  • iOS多线程——RunLoop与GCD、AutoreleasePool

NSOperation&&NSOperationQueue的使用姿势全解

经过前面的学习,讲解了最基础的NSThread使用方法,封装更完善的GCDGCD提供了极其便捷的方法来编写多线程程序,可以自动实现多核的真正并行计算,自动管理线程的生命周期,好处不言而喻,但可定制性就有点不足了,Foundation框架提供了NSOperationNSOperationQueue这一面向对象的多线程类,这两个类与GCD提供的功能类似,NSOperation提供任务的封装,NSOperationQueue顾名思义,提供执行队列,可以自动实现多核并行计算,自动管理线程的生命周期,如果是并发的情况,其底层也使用线程池模型来管理,基本上可以说这两个类提供的功能覆盖了GCD,并且提供了更多可定制的开发方式,开发者可以按需选择。

使用NSOperationNSOperationQueue来编写多线程程序非常简单,只需要创建一个任务对象,创建一个执行队列或者和获取主线程一样获取一个主任务队列,然后将任务提交给队列即可实现并发,如过你想要串行只需要将队列的并发数设置为一即可。接下来将分别介绍两个类的使用。

NSOperation “任务的封装”

GCD类似,GCD向队列提交任务,NSOperation就是对任务进行的封装,封装好的任务交给不同的NSOperationQueue即可进行串行队列的执行或并发队列的执行。这里的任务就是NSOperation类的一个方法,main方法或start方法(两个方法有区别,后文会讲),但NSOperation类的这两个方法是空方法,没有干任何事情,因此,我们需要自定义继承NSOperation类并重写相关方法,OC也提供了两个子类供我们使用NSBlockOperationNSInvocationOperation

接下来看一下NSOperation类中比较重要的属性和方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/*
对于并发Operation需要重写该方法
也可以不把operation加入到队列中,手动触发执行,与调用普通方法一样
*/
- (void)start;

/*
非并发Operation需要重写该方法
*/
- (void)main;

//只读属性任务是否取消,如果自定义子类,需要重写该属性
@property (readonly, getter=isCancelled) BOOL cancelled;

/*
设置cancelled属性为YES
仅仅标记cancelled属性,不退出任务,和NSThread的cancel一个机制
自定义子类时需要使用该属性判断是否在外部触发了取消任务的操作,手动退出任务
*/
- (void)cancel;

//只读属性,任务是否正在执行,如果自定义子类,需要重写该属性
@property (readonly, getter=isExecuting) BOOL executing;

/*
只读属性,任务是否结束,如果自定义子类,需要重写该方法
对于加入到队列的任务来说,当finished设置为YES后,队列会将任务移除出队列
*/
@property (readonly, getter=isFinished) BOOL finished;

//是否为并发任务,该属性已经被标识即将弃用,应该使用下面的asynchronous属性
@property (readonly, getter=isConcurrent) BOOL concurrent; // To be deprecated; use and override 'asynchronous' below

/*
只读属性,判断任务是否为并发任务,默认返回NO
如果需要自定义并发任务子类,需要重写getter方法返回YES
*/
@property (readonly, getter=isAsynchronous) BOOL asynchronous;

/*
只读属性,任务是否准备就绪
对于加入队列的任务,当ready为YES,标识该任务即将开始执行
如果任务有依赖的任务没有执行完成ready为NO
*/
@property (readonly, getter=isReady) BOOL ready;

/*
添加一个NSOperation为当前任务的依赖
如果一个任务有依赖,需要等待依赖的任务执行完成才能开始执行
*/
- (void)addDependency:(NSOperation *)op;

//删除一个依赖
- (void)removeDependency:(NSOperation *)op;

//任务在队列里的优先级
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

//任务在队列里的优先级
@property NSOperationQueuePriority queuePriority;

/*
任务完成后的回调方法
当finished属性设置为YES时才会执行该回调
*/
@property (nullable, copy) void (^completionBlock)(void);

上述内容中有一些属性和方法是在自定义NSOperation子类中必须要重写的,自定义子类能够提供更高的可定制性,因此,编写自定义子类更复杂,自定义子类在后面会讲,如果我们只需要实现GCD那样的功能,提交一个并发的任务,OC为我们提供了两个子类NSBlockOperationNSInvocationOperation,这两个子类已经帮我们完成了各种属性的设置操作,我们只需要编写一个任务的block或者一个方法即可像使用GCD一样方便的编写多线程程序。

接下来举两个创建任务的栗子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//创建一个NSBlockOperation对象,传入一个block
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    for (int i = 0; i < 100; i++)
    {
        NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
    }
}];

/*
创建一个NSInvocationOperation对象,指定执行的对象和方法
该方法可以接收一个参数即object
*/
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task:) object:@"Hello, World!"];

可以发现,创建任务真的很简单,就像GCD中创建任务一样简洁,任务创建完成就可以创建队列了。

NSOperationQueue

NSOperationQueue就是任务的执行队列,看一下该类中有哪些比较重要的属性和方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//向队列中添加一个任务
- (void)addOperation:(NSOperation *)op;

/*
向队列中添加一组任务
是否等待任务完成,如果YES,则阻塞当前线程直到所有任务完成
如果为False,不阻塞当前线程
*/
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait;

//向队列中添加一个任务,任务以block的形式传入,使用更方便
- (void)addOperationWithBlock:(void (^)(void))block;

//获取队列中的所有任务
@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;

//获取队列中的任务数量
@property (readonly) NSUInteger operationCount;

/*
队列支持的最大任务并发数
如果为1,则只支持同时处理一个任务,即串行队列,主队列就是串行队列使用主线程执行任务
如果为大于1的数,则支持同时处理多个任务,即并发队列,底层使用线程池管理多个线程来执行任务
*/
@property NSInteger maxConcurrentOperationCount;

//队列是否挂起
@property (getter=isSuspended) BOOL suspended;

//队列的名称
@property (nullable, copy) NSString *name;

/*
取消队列中的所有任务
即所有任务都执行cancel方法,所有任务的cancelled属性都置为YES
*/
- (void)cancelAllOperations;

//阻塞当前线程直到所有任务完成
- (void)waitUntilAllOperationsAreFinished;

//类属性,获取当前队列
@property (class, readonly, strong, nullable) NSOperationQueue *currentQueue;

//类属性,获取主队列,任务添加到主队列就会使用主线程执行,主队列的任务并发数为1,即串行队列
@property (class, readonly, strong) NSOperationQueue *mainQueue;

上述属性中比较重要的就是maxConcurrentOperationCount,该属性直接决定了队列是串行的还是并发的,接下来看一个栗子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
- (void)task:(id)obj
{
    for (int i = 0; i < 100; i++)
    {
        NSLog(@"Task2 %@ %d %@", [NSThread currentThread], i, obj);
    }
}

- (void)viewWillAppear:(BOOL)animated
{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue setMaxConcurrentOperationCount:2];
    
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 100; i++)
        {
            NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
        }
    }];
    
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task:) object:@"Hello, World!"];

    [queue addOperation:operation];
    [queue addOperation:invocationOperation];
}

上面这个栗子就很简单了,首先创建了一个队列,最大任务并发数设置为2,接下来创建了两个任务并添加进了队列,摘取几个输出如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Task2 <NSThread: 0x1c427e440>{number = 3, name = (null)} 0 Hello, World!
Task1 <NSThread: 0x1c0462fc0>{number = 4, name = (null)} 0

从输出中可以发现,两个任务使用了两个不同的线程来执行,如果将最大任务并发数量设置为1这里就会使用同一个线程串行执行,任务2必须得等任务1执行完成才能开始执行,就不再做实验了。使用Foundation提供的NSBlockOperationNSInvocationOperation很方便,这两个子类已经帮我们完成了各个重要属性的设置操作,当block或传入的方法任务在执行时会设置executing属性值为YES,执行完成后将executing设置为NO并将finished设置为YES,但是,如果在block中使用另一个线程或是GCD异步执行任务,block或方法会立即返回,此时就会将finished设置为YES,但是其实任务并没有完成,所以这种情况下不能使用该属性,当需要更高定制性时需要使用自定义NSOperation子类。

这个栗子很简单,效果就和我们使用GCD编写的多线程程序一样,接下来再举个添加依赖的栗子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
- (void)task:(id)obj
{
    for (int i = 0; i < 100; i++)
    {
        NSLog(@"Task5 %@ %d %@", [NSThread currentThread], i, obj);
    }
}

- (void)viewWillAppear:(BOOL)animated
{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue setMaxConcurrentOperationCount:4];
    
    NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 100; i++)
        {
            NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
        }
    }];
    
    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 100; i++)
        {
            NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
        }
    }];
    
    NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 100; i++)
        {
            NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
        }
    }];
    
    NSBlockOperation *operation4 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 100; i++)
        {
            NSLog(@"Task4 %@ %d", [NSThread currentThread], i);
        }
    }];
    
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task:) object:@"Hello, World!"];

    [operation2 addDependency:operation1];
    [operation3 addDependency:operation1];
    [operation4 addDependency:operation3];
    
    [queue addOperation:operation1];
    [queue addOperation:operation2];
    [queue addOperation:operation3];
    [queue addOperation:operation4];
    [queue addOperation:invocationOperation];
}

上述栗子添加了五个任务,任务依赖关系如下图所示:

任务依赖关系图

如图所示,任务2依赖任务1,任务3依赖任务1,任务4依赖任务3,而任务5是独立的,所以任务2需要等待任务1完成后才可以开始执行,任务3也是同样,而任务4需要等待任务3完成后才可以开始执行,所以任务34是串行执行的,任务5是独立的没有任何依赖,所以任务5与其他任务并行执行,输出结果就不给出了,我们还可以根据业务的不同设置不同的更复杂的依赖。

自定义NSOperation子类

经过前文的讲解,关于NSOperationNSOperationQueue的基础使用已经有了一个初步的掌握,如果我们去阅读AFNetworkingSDWebImage的源码时可以发现,这些库中大量用了NSOperationNSOperationQueue,当然也用了GCD,比如SDWebImage下载图片的任务是自定义的NSOperation子类SDWebImageDownloaderOperation,之所以选择使用自定义子类,正是因为自定义子类可以提供更多定制化的方法,而不仅仅局限于一个block或一个方法,接下来将讲解具体的自定义实现方法。

在官方文档中指出经自定义NSOperation子类有两种形式,并发和非并发,非并发形式只需要继承NSOperation类后实现main方法即可,而并发形式就比较复杂了,接下来会分别介绍两种形式。

非并发的NSOperation自定义子类

官方文档中有说明,非并发的自定义子类只需要实现main方法即可,栗子如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@interface TestOperation: NSOperation

@property (nonatomic, copy) id obj;

- (instancetype)initWithObject:(id)obj;

@end

@implementation TestOperation

- (instancetype)initWithObject:(id)obj
{
    if (self = [super init])
    {
        self.obj = obj;
    }
    return self;
}

- (void)main
{
    for (int i = 0; i < 100; i++)
    {
        NSLog(@"Task %@ %d %@", self.obj, i, [NSThread currentThread]);
    }
    NSLog(@"Task Complete!");
}

@end

- (void)viewWillAppear:(BOOL)animated
{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue setMaxConcurrentOperationCount:4];
    
    TestOperation *operation = [[TestOperation alloc] initWithObject:@"Hello, World!"];
    [operation main];
    //[operation start];
    //[queue addOperation:operation];
    
}

上述栗子也很简单,就是自定义子类继承了NSOperation并且实现了main方法,在官方文档中指出,非并发任务,直接调用main方法即可,调用之后就和调用普通对象的方法一样,使用当前线程来执行main方法,在本栗中即主线程,这个栗子没有什么特别奇特的地方,但其实也可以将其加入到队列中,但这样存在一个问题,由于我们没有实现finished属性,所以获取finished属性时只会返回NO,任务加入到队列后不会被队列删除,一直会保存,而且任务执行完成后的回调块也不会执行,所以最好不要只实现一个main方法就交给队列去执行,即使我们没有实现start方法,这里调用start方法以后依旧会执行main方法。这个非并发版本不建议写,好像也没有什么场景需要这样写,反而更加复杂,如果不小心加入到队列中还会产生未知的错误。

并发的NSOperation自定义子类

关于并发的NSOperation自定义子类就比较复杂了,但可以提供更高的可定制性,这也是为什么SDWebImage使用自定义子类来实现下载任务。

按照官方文档的要求,实现并发的自定义子类需要重写以下几个方法或属性:

  • start方法: 任务加入到队列后,队列会管理任务并在线程被调度后适时调用start方法,start方法就是我们编写的任务,需要注意的是,不论怎样都不允许调用父类的start方法
  • isExecuting: 任务是否正在执行,需要手动调用KVO方法来进行通知,这样,其他类如果监听了任务的该属性就可以获取到通知
  • isFinished: 任务是否结束,需要手动调用KVO方法来进行通知,队列也需要监听该属性的值,用于判断任务是否结束,由于我们编写的任务很可能是异步的,所以start方法返回也不一定代表任务就结束了,任务结束需要开发者手动修改该属性的值,队列就可以正常的移除任务
  • isAsynchronous: 是否并发执行,之前需要使用isConcurrent,但isConcurrent被废弃了,该属性标识是否并发

直接看栗子吧:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@interface MyOperation: NSOperation

@property (nonatomic, assign, getter=isExecuting) BOOL executing;
@property (nonatomic, assign, getter=isFinished) BOOL finished;

@end

@implementation MyOperation

@synthesize executing = _executing;
@synthesize finished = _finished;

- (void)start
{
    //在任务开始前设置executing为YES,在此之前可能会进行一些初始化操作
    self.executing = YES;
    for (int i = 0; i < 500; i++)
    {
        /*
        需要在适当的位置判断外部是否调用了cancel方法
        如果被cancel了需要正确的结束任务
        */
        if (self.isCancelled)
        {
            //任务被取消正确结束前手动设置状态
            self.executing = NO;
            self.finished = YES;
            return;
        }
        //输出任务的各个状态以及队列的任务数
        NSLog(@"Task %d %@ Cancel:%d Executing:%d Finished:%d QueueOperationCount:%ld", i, [NSThread currentThread], self.cancelled, self.executing, self.finished, [[NSOperationQueue currentQueue] operationCount]);
        [NSThread sleepForTimeInterval:0.1];
    }
    NSLog(@"Task Complete.");
    //任务执行完成后手动设置状态
    self.executing = NO;
    self.finished = YES;
}

- (void)setExecuting:(BOOL)executing
{
    //调用KVO通知
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    //调用KVO通知
    [self didChangeValueForKey:@"isExecuting"];
}

- (BOOL)isExecuting
{
    return _executing;
}

- (void)setFinished:(BOOL)finished
{
    //调用KVO通知
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    //调用KVO通知
    [self didChangeValueForKey:@"isFinished"];
}

- (BOOL)isFinished
{
    return _finished;
}

- (BOOL)isAsynchronous
{
    return YES;
}

@end

- (void)cancelButtonClicked
{
    [self.myOperation cancel];
}

- (void)btnClicked
{
    NSLog(@"MyOperation Cancel:%d Executing:%d Finished:%d QueueOperationCount:%ld", self.myOperation.isCancelled, self.myOperation.isExecuting, self.myOperation.isFinished, self.queue.operationCount);    
}

- (void)viewWillAppear:(BOOL)animated
{
    self.queue = [[NSOperationQueue alloc] init];
    [self.queue setMaxConcurrentOperationCount:1];
    
    self.myOperation = [[MyOperation alloc] init];
    [self.queue addOperation:self.myOperation];
    
}

上面的栗子也比较简单,各个状态需要根据业务逻辑来设置,需要注意的是,一定要正确的设置各个状态,并且在设置状态时需要手动触发KVO进行通知,因为可能有其他对象在监听任务的某一个状态,比如finished属性,队列就会监听任务的属性,start方法内部很可能会有异步方法的执行,所以start方法返回并不代表任务结束,队列不能根据start方法是否返回来判断任务是否结束,所以需要开发者手动修改相关属性并触发KVO通知。

上述栗子的输出如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//任务的输出内容
Task 95 <NSThread: 0x1c027c780>{number = 3, name = (null)} Cancel:0 Executing:1 Finished:0 QueueOperationCount:1

//任务正在执行的时候,点击按钮的输出
MyOperation Cancel:0 Executing:1 Finished:0 QueueOperationCount:1

//当任务执行完成后,点击按钮的输出
MyOperation Cancel:0 Executing:0 Finished:1 QueueOperationCount:0

从输出中可以看到任务和执行队列的相关属性的变化。

接下来举一个下载文件的栗子,使用自定义的NSOperation子类,提供了取消下载的功能,具体代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//FileDownloadOperation.h文件代码
#ifndef FileDownloadOperation_h
#define FileDownloadOperation_h

#import <Foundation/Foundation.h>

@class FileDownloadOperation;

//定义一个协议,用于反馈下载状态
@protocol FileDownloadDelegate <NSObject>

@optional
- (void)fileDownloadOperation:(FileDownloadOperation *)downloadOperation downloadProgress:(double)progress;
- (void)fileDownloadOperation:(FileDownloadOperation *)downloadOperation didFinishWithData:(NSData *)data;
- (void)fileDownloadOperation:(FileDownloadOperation *)downloadOperation didFailWithError:(NSError *)error;

@end

@interface FileDownloadOperation: NSOperation
//定义代理对象
@property (nonatomic, weak) id<FileDownloadDelegate> delegate;
//初始化构造函数,文件URL
- (instancetype)initWithURL:(NSURL*)url;

@end

#endif /* FileDownloadOperation_h */

FileDownloadOperation.m文件源码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#import "FileDownloadOperation.h"

@interface FileDownloadOperation() <NSURLConnectionDelegate>

//定义executing属性
@property (nonatomic, assign, getter=isExecuting) BOOL executing;
//定义finished属性
@property (nonatomic, assign, getter=isFinished) BOOL finished;

//要下载的文件的URL
@property (nonatomic, strong) NSURL *fileURL;
//使用NSURLConnection进行网络数据的获取
@property (nonatomic, strong) NSURLConnection *connection;
//定义一个可变的NSMutableData对象,用于添加获取的数据
@property (nonatomic, strong) NSMutableData *fileMutableData;
//记录要下载文件的总长度
@property (nonatomic, assign) NSUInteger fileTotalLength;
//记录已经下载了的文件的长度
@property (nonatomic, assign) NSUInteger downloadedLength;

@end

@implementation FileDownloadOperation

@synthesize delegate = _delegate;

@synthesize executing = _executing;
@synthesize finished = _finished;

@synthesize fileURL = _fileURL;
@synthesize connection = _connection;
@synthesize fileMutableData = _fileMutableData;
@synthesize fileTotalLength = _fileTotalLength;
@synthesize downloadedLength = _downloadedLength;

//executing属性的setter
- (void)setExecuting:(BOOL)executing
{
    //设置executing属性需要手动触发KVO方法进行通知
    [self willChangeValueForKey:@"executing"];
    _executing = executing;
    [self didChangeValueForKey:@"executing"];
}
//executing属性的getter
- (BOOL)isExecuting
{
    return _executing;
}
//finished属性的setter
- (void)setFinished:(BOOL)finished
{
    //同上,需要手动触发KVO方法进行通知
    [self willChangeValueForKey:@"finished"];
    _finished = finished;
    [self didChangeValueForKey:@"finished"];
}
//finished属性的getter
- (BOOL)isFinished
{
    return _finished;
}
//返回YES标识为并发Operation
- (BOOL)isAsynchronous
{
    return YES;
}
//内部函数,用于结束任务
- (void)finishTask
{
    //中断网络连接
    [self.connection cancel];
    //设置finished属性为YES,将任务从队列中移除
    //会调用setter方法,并触发KVO方法进行通知
    self.finished = YES;
    //设置executing属性为NO
    self.executing = NO;
}
//初始化构造函数
- (instancetype)initWithURL:(NSURL *)url
{
    if (self = [super init])
    {
        self.fileURL = url;
        
        self.fileMutableData = [[NSMutableData alloc] init];
        self.fileTotalLength = 0;
        self.downloadedLength = 0;
    }
    return self;
}
//重写start方法
- (void)start
{
    //任务开始执行前检查是否被取消,取消就结束任务
    if (self.isCancelled)
    {
        [self finishTask];
        return;
    }
    //构造NSURLConnection对象,并设置不立即开始,手动开始
    self.connection = [[NSURLConnection alloc] initWithRequest:[[NSURLRequest alloc] initWithURL:self.fileURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:25] delegate:self startImmediately:NO];
    //判断是否连接,没有连接就结束任务
    if (self.connection == nil)
    {
        [self finishTask];
        return;
    }
    //成功连接到服务器后检查是否取消任务,取消任务就结束
    if (self.isCancelled)
    {
        [self finishTask];
        return;
    }
    //设置任务开始执行
    self.executing = YES;
    //获取当前RunLoop
    NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
    //将任务交由RunLoop规划
    [self.connection scheduleInRunLoop:currentRunLoop forMode:NSRunLoopCommonModes];
    //开始从服务端获取数据
    [self.connection start];
    //判断执行任务的是否为主线程
    if (currentRunLoop != [NSRunLoop mainRunLoop])
    {
        //不为主线程启动RunLoop
        CFRunLoopRun();
    }
}

//MARK - NSURLConnectionDelegate 方法

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    //获取并设置将要下载文件的长度大小
    self.fileTotalLength = response.expectedContentLength;
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    //网络获取失败,调用代理方法
    if ([self.delegate respondsToSelector:@selector(fileDownloadOperation:didFailWithError:)])
    {
        //需要将代理方法放到主线程中执行,防止代理方法需要修改UI
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.delegate fileDownloadOperation:self didFailWithError:error];
        });
    }
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    //收到数据包后判断任务是否取消,取消则结束任务
    if (self.isCancelled)
    {
        [self finishTask];
        return;
    }
    //添加获取的数据
    [self.fileMutableData appendData:data];
    //修改已下载文件长度
    self.downloadedLength += [data length];
    //调用回调函数
    if ([self.delegate respondsToSelector:@selector(fileDownloadOperation:downloadProgress:)])
    {
        //计算下载比例
        double progress = self.downloadedLength * 1.0 / self.fileTotalLength;
        //同上,放在主线程中调用,防止主线程有修改UI的操作
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.delegate fileDownloadOperation:self downloadProgress:progress];
        });
    }
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    //网络下载完成前检查是否取消任务,取消就结束任务
    if (self.isCancelled)
    {
        [self finishTask];
        return;
    }
    //调用回调函数
    if ([self.delegate respondsToSelector:@selector(fileDownloadOperation:didFinishWithData:)])
    {
        //同理,放在主线程中调用
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.delegate fileDownloadOperation:self didFinishWithData:self.fileMutableData];
        });
    }
    //下载完成,任务结束
    [self finishTask];
}

@end

上述代码的注释很详尽,就不再赘述了,只提供了取消下载的功能,还可以添加暂停和断点下载的功能,读者可自行实现。具体效果如下图,点击取消后就会结束任务:

下载的效果

备注

由于作者水平有限,难免出现纰漏,如有问题还请不吝赐教。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017.10.23 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
浅谈web自适应
随着移动设备的普及,移动web在前端工程师们的工作中占有越来越重要的位置。移动设备更新速度频繁,手机厂商繁多,导致的问题是每一台机器的屏幕宽度和分辨率不一样。这给我们在编写前端界面时增加了困难,适配问题在当下显得越来越突出。记得刚刚开始开发移动端产品的时候向设计MM要了不同屏幕的设计图,结果可想而知。本篇博文分享一些卤煮处理多屏幕自适应的经验,希望有益于诸君。
疯狂的技术宅
2019/03/28
1.4K0
浅谈web自适应
–我对移动端适配的了解
[总结]我对移动端适配的了解 不知不觉做前端已经两年了,从PC端,移动端,微信小程序一路走来到今天刚刚开放注册的快应用(手机厂商对抗小程序的新技能,所以在注册时用的是qq邮箱的话要去垃圾箱里才能找到注册邮件),对于前端圈日新月异的磅礴发展对于大前端发展是喜闻乐见的,这次的快应用的手机厂商们为其开放了应用入口和系统推广引流入口。这些新能力为前端开发者们带来更强的作战能力。 我们在开发PC站时经常在浏览器兼容问题上耗费巨大的时间,到了移动端,面对webkit内核的Safari与Chrome会舒心很多。b
大象无痕
2018/06/27
2.1K2
可爱的rem
嗯,这种的样式的工作量超级大,因为要对图片和文字等资源针对不同的尺寸进行设置。一种凉凉的感觉从心底飘过~~~
Jimmy_is_jimmy
2019/07/31
9550
第119天:移动端:CSS像素、屏幕像素和视口的关系
移动前端中常说的 viewport (视口)就是浏览器显示页面内容的屏幕区域。其中涉及几个重要概念是 dip ( device-independent pixel 设备逻辑像素 )和 CSS 像素之间的关系。这里首先了解以下几个概念。
半指温柔乐
2018/09/11
1.8K0
第119天:移动端:CSS像素、屏幕像素和视口的关系
移动 web 开发最佳实践
刘春鹏
2017/06/14
3.2K0
移动 web 开发最佳实践
细说移动端 经典的REM布局 与 新秀VW布局
说到前端页面的布局方案,可以从远古时代的Table布局说起,然后来到 DIV+CSS布局,之后有了Float布局,Flex布局,Column布局,Grid布局等等。
书童小二
2018/10/09
12.1K0
细说移动端 经典的REM布局 与 新秀VW布局
css移动端适配最佳实践
移动端适配,在移动端里经常有遇到,在不同分辨率移动端设备精确还原UI设计稿,这是一个令人抓狂的问题,好在有flex,box布局解决了自适应很大一部分问题。
Maic
2022/12/21
1K0
css移动端适配最佳实践
浅谈Web自适应
前言 随着移动设备的普及,移动web在前端工程师们的工作中占有越来越重要的位置。移动设备更新速度频繁,手机厂商繁多,导致的问题是每一台机器的屏幕宽度和分辨率不一样。这给我们在编写前端界面时增加了困难,适配问题在当下显得越来越突出。记得刚刚开始开发移动端产品的时候向设计MM要了不同屏幕的设计图,结果可想而知。本篇博文分享一些卤煮处理多屏幕自适应的经验,希望有益于诸君。 特别说明:在开始这一切之前,请开发移动界面的工程师们在头部加上下面这条meta: 简单事情简单做-宽度自适应 所谓宽度自适应严格来说是一种
企鹅号小编
2018/02/08
1.6K0
浅谈Web自适应
浅谈-web屏幕适配的解决方案
就目前看来,web的屏幕适配是贯穿整个前端行业的,如常见的PC端,移动端,响应式,小程序等。
万少
2025/02/11
1340
浅谈-web屏幕适配的解决方案
从零开始学 Web 之 CSS3(八)CSS3三个案例
而分辨率则一般用像素来度量 px,表示屏幕水平和垂直方向的像素数,例如 1920*1080 指的是屏幕垂直方向和水平方向分别有1920和1080个像素点而构成。
Daotin
2018/08/31
1.4K0
从零开始学 Web 之 CSS3(八)CSS3三个案例
关于移动端适配,你必须要知道的
上面这些问题可能我们在开发中已经知道如何解决,但是问题产生的原理,以及解决方案的原理可能会模糊不清。在解决这些问题的过程中,我们往往会遇到非常多的概念:像素、分辨率、 PPI、 DPI、 DP、 DIP、 DPR、视口等等,你真的能分清这些概念的意义吗?
ConardLi
2019/05/23
2.1K0
移动端常用适配方案四
在 JS 中我们可以通过 window.devicePixelRatio 获取当前的设备像素。
程序员NEO
2023/09/28
2860
移动端常用适配方案四
web移动端适配方案实践
移动端web页面的开发适配一直是前端开发津津乐道的话题,在实际开发过程中,移动端和PC端web页面的差异不仅仅体现在设备宽度的不同。由于项目历史背景的原因,下文的方案是团队选择的能较好满足当前项目需求的方案,已经经过线上用户的考验,但不一定是当下最完美的移动端适配解决方案。下文来详细介绍该方案选型。
CS逍遥剑仙
2019/09/02
3.1K0
响应式或自适应布局的流派
(此图有可能名称反了,但不重要,我个人更偏向于 bootstrap 被叫作响应式的)
一起重学前端
2024/09/30
1790
前端学习笔记—移动端web开发
响应式网站:即pc和移动端共用一套网站,只不过在不同宽度的屏幕下,样式会自动适配。配合媒体查询监听,通过判断屏幕宽度来改变样式,以适应不同终端。例如:三星电子官网: www.samsung.com/cn/ 缺点:制作麻烦,需要花很大精力去调兼容性问题
木溪bo
2024/03/23
2830
前端学习笔记—移动端web开发
移动端Web App 的屏幕适配方法(总结)
移动端web页面的开发,由于手机屏幕尺寸、分辨率不同,或者需要考虑横竖屏问题,为了使得web页面在不同移动设备上具有相适应的展示效果,需要在开发过程中使用合理的适配方案来解决这个问题。
用户9914333
2022/07/21
1.6K0
Css-移动端适配总结 前言PC端Mobile总结参考&引用
工作以后,大部分的业务工作都是基于移动端H5的,开发过程中学习了很多东西,遇到过许多问题,诸如rem\em\css px\device px等,本文纯属个人的归纳总结,如有问题,请指出亲喷~
菜的黑人牙膏
2019/04/18
2.5K0
Css-移动端适配总结
		前言PC端Mobile总结参考&引用
移动端适配必须掌握的基本概念和适配方案
随着技术的发展,移动设备越来越流行,并且不同设备间屏幕尺寸和屏幕像素的差异,移动端开发面临着多分辨率适配的问题。
用户6167509
2020/07/23
1.1K0
移动端页面的自适应rem
具体来说,有的屏幕320px宽,有的屏幕640px宽,有的更宽,如果你写固定px,那么要么小的放不下,要么大的有大片空白。
javascript艺术
2021/05/28
2.5K0
移动端页面的自适应rem
web移动端适配方案实践
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
csxiaoyao
2019/09/18
1.7K0
web移动端适配方案实践
相关推荐
浅谈web自适应
更多 >
LV.1
这个人很懒,什么都没有留下~
目录
  • 你要知道的iOS多线程NSThread、GCD、NSOperation、RunLoop都在这里
    • NSOperation&&NSOperationQueue的使用姿势全解
      • NSOperation “任务的封装”
      • NSOperationQueue
      • 自定义NSOperation子类
    • 备注
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档