ios开发进阶之多线程02 NSOperation

一 单例模式

  • 单例模式的作用

    • 可以保证在程序运行过程,一个类只有一个实例,而且该实例易于供外界访问
    • 从而方便地控制了实例个数,并节约系统资源
  • ARC中,单例模式的实现

    • 在.m中保留一个全局的static的实例
      static id _instance;

    • 重写allocWithZone:方法,在这里创建唯一的实例(注意线程安全)

  + (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [super allocWithZone:zone];
    });
    return _instance;
}
  • 提供1个类方法让外界访问唯一的实例
+ (instancetype)sharedInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[self alloc] init];
    });
    return _instance;
}
  • 实现copyWithZone:方法
- (id)copyWithZone:(struct _NSZone *)zone
{
    return _instance;
}
  • 注意点
    • 不要继承单例类
      • 先创建子类永远是子类对象
      • 线创建父类永远是父类对象
  • 如何判断是否是ARC
#if __has_feature(objc_arc)
//ARC
#else
//MRC
#endif

二 NSOperation基本使用

  • NSOperation的作用

    • 配合使用NSOperation和NSOperationQueue也能实现多线程编程
  • NSOperation和NSOperationQueue实现多线程的具体步骤

    • 先将需要执行的操作封装到一个NSOperation对象中
    • 然后将NSOperation对象添加到NSOperationQueue中
    • 系统会自动将NSOperationQueue中的NSOperation取出来
    • 将取出的NSOperation封装的操作放到一条新线程中执行
  • NSOperation的子类

    • NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类

    • 使用NSOperation子类的方式有3种

      • NSInvocationOperation
      • NSBlockOperation
      • 自定义子类继承NSOperation,实现内部相应的方法
  • NSInvocationOperation

// 1.将操作封装到Operation中
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(demo) object:nil];
// 2.执行封装的操作
// 如果直接执行NSInvocationOperation中的操作, 那么默认会在主线程中执行
    [op1 start];
  • NSBlockOperation
   // 1.封装操作
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"1- %@", [NSThread currentThread]);
    }];

    // 2.添加操作
    [op1 addExecutionBlock:^{
        NSLog(@"2- %@", [NSThread currentThread]);
    }];
    [op1 addExecutionBlock:^{
        NSLog(@"3- %@", [NSThread currentThread]);
    }];

    // 2.执行操作
    // 如果只封装了一个操作, 那么默认会在主线程中执行
    // 如果封装了多个操作, 那么除了第一个操作以外, 其它的操作会在子线程中执行
    [op1 start];
  • 自定义子类继承NSOperation
    自定义类继承于NSOperation, 那么需要将操作写到自定义类的main方法中
@implementation XMOperation

- (void)main
{
    NSLog(@"%s, %@", __func__,[NSThread currentThread]);
}

三 NSOperationQueue

  • NSOperationQueue的作用

    • NSOperation可以调用start方法来执行任务,但默认是同步执行的
    • 如果将NSOperation添加到NSOperationQueue(操作队列)中,系统会自动异步执行NSOperation中的操作

    • 添加操作到NSOperationQueue中

- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;
  • NSInvocationOperation
// 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.封装任务
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(demo) object:nil];
// 3.将任务添加到队列中
  // 只要将一个任务添加到alloc/init的队列中, 那么队列内部会自动调用start
  // 只要将一个任务添加到alloc/init的队列中, 就会开启一个新的线程执行队列
    [queue addOperation:op1];
  • NSBlockOperation
// 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.封装任务
     NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
     NSLog(@"1 = %@", [NSThread currentThread]);
     }];
// 3.将任务添加到队列中
     [queue addOperation:op1];

另一种简洁写法:

// 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.将任务添加到队列中
    // addOperationWithBlock方法会做两件事情
    // 1.根据传入的block, 创建一个NSBlockOperation对象
    // 2.将内部创建好的NSBlockOperation对象, 添加到队列中
    [queue addOperationWithBlock:^{
        NSLog(@"1 = %@", [NSThread currentThread]);
    }];
  • 自定义子类继承NSOperation
 // 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
 // 2.封装任务
    XMGOperation *op1 = [[XMGOperation alloc] init];
 // 3.将任务添加到队列中
    [queue addOperation:op1];

四 最大并发数

  • 什么是并发数:同时执行的任务数

    • maxConcurrentOperationCount 默认等于 -1, 代表不限制, 可以创建N多线程
    • 默认就是并发
    • 如果想实现串行, 那么就设置maxConcurrentOperationCount = 1
    • 注意: 最大并发数, 不能设置为0, 否则任务不会被执行

五 队列的暂停、取消、恢复

  • 暂停-恢复
    • 不会暂停当前正在执行的任务,会暂停队列中的下一个任务
    • 恢复任务,会从第一个未执行的任务恢复执行
// 如果是YES, 代表需要暂停
// 如果是NO ,代表恢复执行
self.queue.suspended = YES;
self.queue.suspended = !self.queue.suspended;
  • 取消
    • 不会取消当前正在执行的任务
    • 取消后任务不能恢复
    • 耗时操作应该每执行一段判断一次
// 内部会调用所有任务的cancel方法
[self.queue cancelAllOperations];

六 NSOperation线程间通信

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 开启子线程下载图片
[queue addOperationWithBlock:^{
NSString *urlStr = @"http://...";
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlStr];
NSData *data = [NSData dataWithContentsOfURL:url];

//生成下载好的图片
UIImage *image = [UIIMage imageWithData:data]
        // 回到主线程更新UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        self.imageView.image = image;
        }];

}];

七 NSOperation依赖关系

  • 依赖和监听
    • 只有被依赖的任务完成, 才会执行当前任务
    • 可以跨队列依赖
    [operationB addDependency:operationA]; // 操作B依赖于操作A
    op1.completionBlock = ^{
        NSLog(@"第一张图片下载完毕");
    };
    op2.completionBlock = ^{
        NSLog(@"第二张图片下载完毕");
    };

图片合成案例代码

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
//    queue.maxConcurrentOperationCount = 1;

    __block UIImage *image1 = nil;
    __block UIImage *image2 = nil;
    // 1.开启一个线程下载第一张图片
    NSOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSURL *url = [NSURL URLWithString:@"http://www.ibayue.com/images/fileup/201411/20141103150824.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        // 2.生成下载好的图片
        UIImage *image = [UIImage imageWithData:data];
        image1 = image;
    }];

    // 2.开启一个线程下载第二长图片
    NSOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        NSURL *url = [NSURL URLWithString:@"http://i0.letvimg.com/lc02_yunzhuanma/201503/07/00/48/170acdff2830753f89957742b0e8f5b8_25431356/thumb/2_400_225.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        // 2.生成下载好的图片
        UIImage *image = [UIImage imageWithData:data];
        image2 = image;

    }];
    // 3.开启一个线程合成图片
    NSOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
        UIGraphicsBeginImageContext(CGSizeMake(200, 200));
        [image1 drawInRect:CGRectMake(0, 0, 100, 200)];
        [image2 drawInRect:CGRectMake(100, 0, 100, 200)];
        UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();

        // 4.回到主线程更新UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            NSLog(@"回到主线程更新UI");
            self.imageView.image = newImage;
        }];
    }];


    // 监听任务是否执行完毕
    op1.completionBlock = ^{
        NSLog(@"第一张图片下载完毕");
    };
    op2.completionBlock = ^{
        NSLog(@"第二张图片下载完毕");
    };

    // 添加依赖
    // 只要添加了依赖, 那么就会等依赖的任务执行完毕, 才会执行当前任务
    // 注意:
    // 1.添加依赖, 不能添加循环依赖
    // 2.NSOperation可以跨队列添加依赖
    [op3 addDependency:op1];
    [op3 addDependency:op2];

    // 将任务添加到队列中
    [queue addOperation:op1];
    [queue addOperation:op2];
    [queue2 addOperation:op3];
}

八 多图片下载

  • 字典转模型
#import "XMGApp.h"

@implementation XMGApp

- (instancetype)initWithDict:(NSDictionary *)dict{
    if (self = [super init]) {
        [self setValuesForKeysWithDictionary:dict];
    }
    return self;
}

+ (instancetype)appWithDict:(NSDictionary *)dict
{
    return [[self alloc] initWithDict:dict];
}
@end
  • 懒加载
#pragma mark - lazy
- (NSArray *)apps
{
    if (!_apps) {
        // 1.从plist中加载数组
        NSString *path = [[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil];
        NSArray *arr = [NSArray arrayWithContentsOfFile:path];

        // 2.定义数组保存转换好的模型
        NSMutableArray *models = [NSMutableArray arrayWithCapacity:arr.count];

        // 3.遍历数组中所有的字典, 将字典转换为模型
        for (NSDictionary *dict in arr) {
            XMGApp *app = [XMGApp appWithDict:dict];
            [models addObject:app];
        }
        _apps = [models copy];
    }
    return _apps;
}


- (NSMutableDictionary *)imageCaches
{
    if (!_imageCaches) {
        _imageCaches = [NSMutableDictionary dictionary];
    }
    return _imageCaches;
}
#pragma mark - UITableViewDatasource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.apps.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//    NSLog(@"%s", __func__);
    // 1.获取cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"app"];

    // 2.设置数据
    XMGApp *app = self.apps[indexPath.row];

    cell.textLabel.text = app.name;
    cell.detailTextLabel.text = [NSString stringWithFormat:@"下载:%@", app.download];

    // 设置图片
    /*
     存在的问题:
     1.在主线程中下载图片, 可能会阻塞主线程
     2.重复下载
     */
    // 1.先从内存缓存中获取, 如果没有才去下载
    UIImage *image = self.imageCaches[app.icon];
    if (image == nil) {

        // 2.再从磁盘缓存中获取, 如果没有才去下载
        NSString *filePath = [app.icon cacheDir];
        NSData *data = [NSData dataWithContentsOfFile:filePath];

        if (data == nil) {
            NSLog(@"下载图片");
            // 内存缓存中没有值, 需要下载
            NSURL *url = [NSURL URLWithString:app.icon];
            data = [NSData dataWithContentsOfURL:url];
            UIImage *image = [UIImage imageWithData:data];

            // 将下载好的图片缓存到内存缓存中
            self.imageCaches[app.icon] = image;

            // 将下载好的图片写入到磁盘
            [data writeToFile:filePath atomically:YES];

            // 更新UI
            cell.imageView.image = image;
        }else
        {
            NSLog(@"使用磁盘缓存");
            NSData *data = [NSData dataWithContentsOfFile:filePath];
            UIImage *image = [UIImage imageWithData:data];

            // 将下载好的图片缓存到内存缓存中
            self.imageCaches[app.icon] = image;

            // 更新UI
            cell.imageView.image = image;
        }
    }else
    {
        NSLog(@"使用内存缓存");
        // 更新UI
        cell.imageView.image = image;
    }
    // 3.返回cell
    return cell;
}
  • 目录结构
  • Documents

    • 需要保存由”应用程序本身”产生的文件或者数据,例如:游戏进度、涂鸦软件的绘图
    • 目录中的文件会被自动保存在 iCloud
    • 注意:不要保存从网络上下载的文件,否则会无法上架!
  • Caches

    • 保存临时文件,”后续需要使用”,例如:缓存图片,离线数据(地图数据)
    • 系统不会清理 cache 目录中的文件
    • 就要求程序开发时,”必须提供 cache 目录的清理解决方案”
  • Preferences

    • 用户偏好,使用 NSUserDefault 直接读写!
    • 如果要想数据及时写入磁盘,还需要调用一个同步方法
  • tmp

    • 保存临时文件,”后续不需要使用”
    • tmp 目录中的文件,系统会自动清理
    • 重新启动手机,tmp 目录会被清空
    • 系统磁盘空间不足时,系统也会自动清理
  • 封装获取文件路径方法

- (NSString *)cacheDir
{
    // 1.获取cache目录
    NSString *dir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    // 2.生成绝对路径
    return [dir stringByAppendingPathComponent:[self lastPathComponent]];
}
- (NSString *)documentDir {
    NSString *dir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    return [dir stringByAppendingPathComponent:[self lastPathComponent]];
}

- (NSString *)tmpDir {
    NSString *dir = NSTemporaryDirectory();
    return [dir stringByAppendingPathComponent:[self lastPathComponent]];
}

优化版本:

#pragma mark - UITableViewDatasource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.apps.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//    NSLog(@"%s", __func__);
    // 1.获取cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"app"];

    // 2.设置数据
    XMGApp *app = self.apps[indexPath.row];

    cell.textLabel.text = app.name;
    cell.detailTextLabel.text = [NSString stringWithFormat:@"下载:%@", app.download];
    cell.imageView.image = [UIImage imageNamed:@"abc"];  //开始没有图片,搞占位图片

    // 设置图片
    /*
     存在的问题:
     1.在主线程中下载图片, 可能会阻塞主线程
     2.重复下载
     */
    // 1.先从内存缓存中获取, 如果没有才去下载
    UIImage *image = self.imageCaches[app.icon];
    if (image == nil) {

        // 2.再从磁盘缓存中获取, 如果没有才去下载
        NSString *filePath = [app.icon cacheDir];
        __block NSData *data = [NSData dataWithContentsOfFile:filePath];

        if (data == nil) {
            NSLog(@"下载图片");
            /*
             存在的问题:
             1.重复设置
             2.重复下载
             */
            NSOperationQueue *queue = [[NSOperationQueue alloc] init];

            // 3.判断当前图片是否有任务正在下载
            NSBlockOperation *op = self.operations[app.icon];
            if (op == nil) {
                // 没有对应的下载任务
                op = [NSBlockOperation blockOperationWithBlock:^{
                    // 开启子线程下载
                    // 内存缓存中没有值, 需要下载
                    NSURL *url = [NSURL URLWithString:app.icon];
                    data = [NSData dataWithContentsOfURL:url];
                    if (data == nil) {
                        // 如果下载失败, 应该将当前图片对应的下载任务从缓存中移除 \以便于下次可以再次尝试下载
                      [self.operations removeObjectForKey:app.icon];
                        return;
                    }
               UIImage *image = [UIImage imageWithData:data];
                    // 将下载好的图片缓存到内存缓存中
                    self.imageCaches[app.icon] = image;

                    // 将下载好的图片写入到磁盘
                 [data writeToFile:filePath atomically:YES];

                    // 回到主线程更新UI
               [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                        NSLog(@"更新UI");

                        // 刷新指定的行,网速慢的情况下防止图片跳来跳去
               [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];

                        // 从缓存中将当前图片对应的下载任务移除
                 [self.operations removeObjectForKey:app.icon];
                    }];
                }];

                // 先将下载任务保存到缓存中
                self.operations[app.icon] = op;

                // 将任务添加到队列中
                [queue addOperation:op];
            }

        }else
        {
            NSLog(@"使用磁盘缓存");
            NSData *data = [NSData dataWithContentsOfFile:filePath];
            UIImage *image = [UIImage imageWithData:data];

            // 将下载好的图片缓存到内存缓存中
            self.imageCaches[app.icon] = image;

            // 更新UI
            cell.imageView.image = image;
        }
    }else
    {
        NSLog(@"使用内存缓存");
        // 更新UI
        cell.imageView.image = image;
    }

    // 3.返回cell
    return cell;
}
  • 图片下载注意点(面试4点)
    • 重复下载问题
      • 定义字典保存下载好的图片
    • 磁盘缓存问题
      • 内存没有尝试从磁盘获取
    • 阻塞主线程问题
      • 新建NSOperationQueue下载图片
    • 重复设置问题
      • reloadRowsAtIndexPaths
逻辑1 - 从来没下载过
 1.查看内存缓存是否有图片
 2.查看磁盘缓存是否有图片
 3.查看时候有任务正在下载当前图片
 4.开启任务下载图片
 5.写入磁盘
 6.缓存到内存
 7.移除下载操作
 8.显示图片

 逻辑2 - 已经下载过
 1.查看内存缓存是否有图片
 2.查看磁盘缓存是否有图片
 3.使用磁盘缓存
 4.将图片缓存到内存中
 5.更新UI

 逻辑3 - 已经下载过, 并且不是重新启动
  1.查看内存缓存是否有图片
  2.更新UI

九 SDWebImage架构

  • SDWebImageManager

    • SDImageCache
    • SDWebImageDownloader
      • SDWebImageDownloaderOperation
  • SDWebImage常见面试题

  • 默认缓存时间多少

    • 一周
  • 缓存的地址

    • NSString *fullNamespace = [@”com.hackemist.SDWebImageCache.” stringByAppendingString:ns];
  • cleanDisk如何清理过期图片

    • 删除早于过期日期的文件
    • 保存文件属性以计算磁盘缓存占用空间
    • 如果剩余磁盘缓存空间超出最大限额,再次执行清理操作,删除最早的文件
  • clearDisk如何清理磁盘

    • 删除缓存目录
    • 新建缓存目录
  • SDWebImage如何播放图片

    • 取出gif中每一帧, 生成一张可动画图片
  • SDWebImage如何判断图片类型

    • 判断图片二进制前8个字节
    • kPNGSignatureBytes[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
    • 多图片下载案例
- (void)viewDidLoad
{
    [super viewDidLoad];
    self.tableView.rowHeight = 150;
    // 直接下载一张图片
    /*
     第1个参数: 需要下载图片的URL
     第2个参数: 下载的配置信息(例如是否需要缓存等等)
     第3个参数: 下载过程中的回调
     第4个参数: 下载完成后的回调
     */
    NSURL *url = [NSURL URLWithString:@"http://ia.topit.me/a/f9/0a/1101078939e960af9ao.jpg"];
    [[SDWebImageManager sharedManager] downloadImageWithURL:url options:kNilOptions progress:^(NSInteger receivedSize, NSInteger expectedSize) {
        // receivedSize : 已经接受到的数据大小
        // expectedSize : 需要下载的图片的总大小
        NSLog(@"正在下载 %zd %zd", receivedSize, expectedSize);
    } completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
        // image : 下载好的图片
        // error: 错误信息
        // cacheType: 缓存的类型
        // finished: 是否下载完成
        // imageURL: 被下载的图片的地址
        NSLog(@"下载成功 %@", image);
    }];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 1.获取cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"app"];

    // 2.设置数据
    XMGApp *app = self.apps[indexPath.row];

    cell.textLabel.text = app.name;
    cell.detailTextLabel.text = [NSString stringWithFormat:@"下载:%@", app.download];

    // 在老版本的SDWebImage中, 以下方法是没有sd_前缀的
    [cell.imageView sd_setImageWithURL:[NSURL URLWithString:app.icon] placeholderImage:[UIImage imageNamed:@"abc"]];

    // 3.返回cell
    return cell;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值