iOS | 多线程(三) NSOperation

应用背景:

如果是简单的应用层开发,用GCD就足够了,NSThread,PThread,GCD,NSOperation,
但是如果是类似断点续传这样复杂业务的时候,就需要使用NSOperation,控制更加灵活

知识图谱:
image.png

我将从5个方面逐步的分析NSOperation:

Demo地址: github.com/tanghaitao/…

1.NSOperation初体验

Demo地址:
github.com/tanghaitao/…

image.png

从苹果官方文档可以看出,NSOperation需要注意以下三点:

  1. NSOperation本身是一个抽象类,不能实例化。
  2. 需要把任何添加到它的子类NSInvocationOperation或NSBlockOperation。
  3. 要执行任务,需要添加到队列NSOperationQueue才能执行任务。

总结: 不能直接用NSOperation --- 事务 () + queue = 把事务添加到队列 ---> 然后在新的线程上执行

NSInvationOperation

- (void)demo1{
    
    // 不能直接用NSOperation --- 事务 () + queue = 把事务添加到队列 ---> 然后去执行
//    NSOperation
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(xixihha) object:nil];
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
   // 由系统调控 开辟新线程执行 NSThread == <NSThread: 0x6000024bd300>{number = 4, name = (null)}
    [queue addOperation:op]; //这句和下面[op start] 不能同时调用
    
    // 手动吊起
//    [op start];//不用,立刻执行 NSThread == <NSThread: 0x60000382c1c0>{number = 1, name = main}
    
}

// 操作
- (void)xixihha{
    NSLog(@"NSThread == %@",[NSThread currentThread]);
    NSLog(@"123");
}
复制代码

NSBlockOperation

- (void)demo2{
       
        // 操作优先级
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
       
        NSLog(@"NSThread == %@",[NSThread currentThread]);
        // 并发
        [NSThread sleepForTimeInterval:1];//延迟1秒
        NSLog(@"执行123任务");
        
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
             NSLog(@"执行任务更新UI");
            [NSThread sleepForTimeInterval:10];
            NSLog(@"更新UI");
        }];
        // 不添加下面这两行代码,执行完op队列中的任务,mainQueue中的任务并没有执行完1+5<10 
        // 添加下面这两行代码的话,执行完所有op队列中的任务以前mainQueue中的任务也已经执行完了1+13>10
        // completionBlock是等任务执行完,大括号开始和结束'{}'
        //[NSThread sleepForTimeInterval:13];
        //NSLog(@"123");
    }];
    
    // CPU 调度的评率高
//    op.qualityOfService =
    
    [op addExecutionBlock:^{
        NSLog(@"NSThread == %@",[NSThread currentThread]);
        NSLog(@"执行456任务");
        [NSThread sleepForTimeInterval:5];
        NSLog(@"456");
    }];
    
    op.completionBlock = ^{
        NSLog(@"执行完成任务");
        [NSThread sleepForTimeInterval:1];
        NSLog(@"完成");
    };
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 由系统调控
    [queue addOperation:op];
//        [op start];
    // NSThread == <NSThread: 0x60000018c640>{number = 6, name = (null)}   
}
复制代码
//打印线程的执行任务顺序不固定  
 NSThread == <NSThread: 0x6000030fedc0>{number = 5, name = (null)}
 NSThread == <NSThread: 0x6000030fd900>{number = 4, name = (null)}//
 执行456任务  
 执行123任务
 执行任务更新UI//456在睡眠,所以先执行mainQueue
 456//mainQueue睡眠更久,5秒后执行456
 执行完成任务//op的任务都完成了,第5秒钟
 完成//完成,第6秒
 更新UI// 第10秒
复制代码

分析: 任务[op addExecutionBlock] 和任务blockOperationWithBlock:^{}是并发的,执行任务顺序不固定,然后执行打印执行456任务,再打印执行123任务,由于主队列的任务是在第一个block中的线程中的,堵塞,所以需要等待执行123任务执行完再打印,更新UI,其他的看上面的注释

[[NSOperationQueue mainQueue] addOperationWithBlock:^{} 必须等 任务1执行后'{'再执行,不一定执行完'}'。
op.completionBlock= ^{}必须等 op`添加到队列`的所有任务执行完,大括号开始和结束 ‘{}’,'{}'执行完毕,再执行
这里的队列是由系统调控
[queue addOperation:op];是不是主队列(mainQueue),不必等待mainQueue完成。
复制代码

2.NSOperation属性研究

Demo地址:
github.com/tanghaitao/…

2.1 maxConcurrentOperationCount

NSOperation控制并发数 非常简单,直接设置属性maxConcurrentOperationCount
GCD控制就相当复杂。

GCD控制最大并发数:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
//任务1
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@" --- %@ ---- 任务1",[NSThread currentThread]);
        NSLog(@"执行任务1");
        sleep(6);//【睡眠6秒】
        NSLog(@"任务1完成");
        dispatch_semaphore_signal(semaphore);
    });
    
    //任务2
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@" --- %@ ---- 任务2",[NSThread currentThread]);
        NSLog(@"执行任务2");
        sleep(5);// 【睡眠5秒】
        NSLog(@"任务2完成");
        dispatch_semaphore_signal(semaphore);
    });
    
    //任务3
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@" --- %@ ---- 任务3",[NSThread currentThread]);
        NSLog(@"执行任务3");
        NSLog(@"任务3完成");
        dispatch_semaphore_signal(semaphore);
    });
复制代码

NSOperation控制最大并发数:

/**
 关于operationQueue的挂起,继续,取消
 */
- (void)demo1{
    
    // GCD ---> 信号量  :  对于线程操作更自如  -- suspend  cancel finish
    // 多线程世界
    self.queue.name = @"com.haitao";
    self.queue.maxConcurrentOperationCount = 2;
    for (int i = 0; i<10; i++) {
        [self.queue addOperationWithBlock:^{
            [NSThread sleepForTimeInterval:2];
            NSLog(@"%@--%d",[NSThread currentThread],i);
        }];
    }
    
}
复制代码
begin <NSThread: 0x6000038e2fc0>{number = 6, name = (null)}--1
begin <NSThread: 0x6000038a8f00>{number = 3, name = (null)}--0
end <NSThread: 0x6000038e2fc0>{number = 6, name = (null)}--1
end <NSThread: 0x6000038a8f00>{number = 3, name = (null)}--0
begin <NSThread: 0x6000038a1040>{number = 4, name = (null)}--2
begin <NSThread: 0x6000038a8f00>{number = 3, name = (null)}--3
end <NSThread: 0x6000038a8f00>{number = 3, name = (null)}--3
end <NSThread: 0x6000038a1040>{number = 4, name = (null)}--2
begin <NSThread: 0x6000038a1040>{number = 4, name = (null)}--4
begin <NSThread: 0x6000038e2fc0>{number = 6, name = (null)}--5
end <NSThread: 0x6000038a1040>{number = 4, name = (null)}--4
end <NSThread: 0x6000038e2fc0>{number = 6, name = (null)}--5
begin <NSThread: 0x6000038a1040>{number = 4, name = (null)}--7
begin <NSThread: 0x6000038a8f00>{number = 3, name = (null)}--6
end <NSThread: 0x6000038a1040>{number = 4, name = (null)}--7
end <NSThread: 0x6000038a8f00>{number = 3, name = (null)}--6
begin <NSThread: 0x6000038a1040>{number = 4, name = (null)}--8
begin <NSThread: 0x6000038e2fc0>{number = 6, name = (null)}--9
end <NSThread: 0x6000038a1040>{number = 4, name = (null)}--8
end <NSThread: 0x6000038e2fc0>{number = 6, name = (null)}--9
复制代码

上图的输出日志: 每次执行2个任务,begin begin end end

- (IBAction)pauseOrContinue:(id)sender {
    
    // 下载任务 ---> task ---> 挂起
    // 继续 ---->
    // 打断  --->  后台
    
    self.queue.suspended = !self.queue.isSuspended;
    [self.pauseOrContinueBtn setTitle:self.queue.suspended?@"继续":@"暂停" forState:UIControlStateNormal];
    
    if (self.queue.operationCount == 0) {
        NSLog(@"没有操作执行");
        return;
    }
    
    if (self.queue.suspended) {
        NSLog(@"当前挂起来了");
    }else{
        NSLog(@"执行....");
    }
//    self.queue        
}
复制代码
begin <NSThread: 0x600000888a00>{number = 8, name = (null)}--1
begin <NSThread: 0x600000880780>{number = 6, name = (null)}--0
end <NSThread: 0x600000888a00>{number = 8, name = (null)}--1
end <NSThread: 0x600000880780>{number = 6, name = (null)}--0
begin <NSThread: 0x60000089cd80>{number = 7, name = (null)}--2
begin <NSThread: 0x600000888a00>{number = 8, name = (null)}--3
前挂起来了
end <NSThread: 0x600000888a00>{number = 8, name = (null)}--3
end <NSThread: 0x60000089cd80>{number = 7, name = (null)}--2
复制代码

self.queue.suspended = !self.queue.isSuspended;
挂起后,正在执行的任务不会马上挂起或者取消,等待执行完再停止后续添加的新任务。

2.3 addDependency

控制任务的执行顺序‘{’, 会堵塞被依赖的线程,知道依赖的任务执行完成再会执行下一个,类似 op.completionBlock = ^{}

 - (void)demo2{

    self.queue.maxConcurrentOperationCount = 2;
    NSBlockOperation *bo1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"begin 请求token");
        [NSThread sleepForTimeInterval:0.5];
        NSLog(@"end 请求token");
    }];

    NSBlockOperation *bo2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"begin 拿着token,请求数据1");
        [NSThread sleepForTimeInterval:1.5];
        NSLog(@"end 拿着token,请求数据1");
    }];

    NSBlockOperation *bo3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"begin 拿着数据1,请求数据2");
        [NSThread sleepForTimeInterval:0.5];
        NSLog(@"end 拿着数据1,请求数据2");
    }];
    //[self.queue addOperation:bo1];
    //依赖
    [bo2 addDependency:bo1];
    [bo3 addDependency:bo2];

    [self.queue addOperations:@[bo1,bo2,bo3] waitUntilFinished:NO];

    NSLog(@"执行完了?我要干其他事");
}
复制代码
执行完了?我要干其他事//waitUntilFinished:NO,如YES,就堵塞,最后打印
begin 请求token
end 请求token
begin 拿着token,请求数据1
end 拿着token,请求数据1
begin 拿着数据1,请求数据2
end 拿着数据1,请求数据2
复制代码

3. NSOperation缓存方案和注意事项

Demo地址: github.com/tanghaitao/…

__weak typeof(self) weakSelf = self;
    self.viewModel = [[KCViewModel alloc] initWithBlock:^(id data) {
        [weakSelf.dataArray addObjectsFromArray:data];
        [weakSelf.collectionView reloadData];
    } fail:nil];
    //最好是dispatch_after子线程延迟操作,不要影响主线程,影响启动加载
复制代码
NSBlockOperation *op  = [NSBlockOperation blockOperationWithBlock:^{
        // 重复下载
        NSLog(@"下载图片: %@",model.title);
        NSURL   *url    = [NSURL URLWithString:model.imageUrl];
        NSData  *data   = [NSData dataWithContentsOfURL:url];
        UIImage *image  = [UIImage imageWithData:data];//data为nil不会奔溃
    
        // 更新UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            cell.imageView.image  = image;
        }];
    }];
复制代码

打印输出:

2021-05-26 10:59:05.004338+0800 003---自定义NSOperation[14013:11296141] 下载图片: 典雅的教堂
2021-05-26 10:59:05.005366+0800 003---自定义NSOperation[14013:11296143] 下载图片: 西湖美女
2021-05-26 10:59:05.005908+0800 003---自定义NSOperation[14013:11296146] 下载图片: 优美海景
2021-05-26 10:59:05.006446+0800 003---自定义NSOperation[14013:11296144] 下载图片: 微信壁纸
2021-05-26 10:59:05.006990+0800 003---自定义NSOperation[14013:11296149] 下载图片: 高清无码美女
2021-05-26 10:59:05.007565+0800 003---自定义NSOperation[14013:11296147] 下载图片: 深沉匹若曹
2021-05-26 10:59:05.008052+0800 003---自定义NSOperation[14013:11296142] 下载图片: 毛笔执念
2021-05-26 10:59:05.008553+0800 003---自定义NSOperation[14013:11296151] 下载图片: 简约蒲苇
2021-05-26 10:59:05.150593+0800 003---自定义NSOperation[14013:11296154] [] nw_protocol_get_quic_image_block_invoke dlopen libquic failed
2021-05-26 10:59:07.123287+0800 003---自定义NSOperation[14013:11296151] 下载图片: 笑脸路飞
2021-05-26 10:59:07.141981+0800 003---自定义NSOperation[14013:11296142] 下载图片: 高清玉米棒子
2021-05-26 10:59:07.158524+0800 003---自定义NSOperation[14013:11296154] 下载图片: 五彩星星
2021-05-26 10:59:07.175219+0800 003---自定义NSOperation[14013:11296150] 下载图片: 欧豪篮球
2021-05-26 10:59:07.220150+0800 003---自定义NSOperation[14013:11296168] 下载图片: 渣男
2021-05-26 10:59:07.237240+0800 003---自定义NSOperation[14013:11296155] 下载图片: 姑娘走了
2021-05-26 10:59:07.253587+0800 003---自定义NSOperation[14013:11296166] 下载图片: 简约dog
2021-05-26 10:59:07.269462+0800 003---自定义NSOperation[14013:11296143] 下载图片: 简约dog
2021-05-26 10:59:07.302763+0800 003---自定义NSOperation[14013:11296170] 下载图片: 简约dog
2021-05-26 10:59:07.319611+0800 003---自定义NSOperation[14013:11296147] 下载图片: 简约dog
2021-05-26 10:59:07.352682+0800 003---自定义NSOperation[14013:11296171] 下载图片: 可爱超人
2021-05-26 10:59:07.371843+0800 003---自定义NSOperation[14013:11296141] 下载图片: 可爱超人
2021-05-26 10:59:07.789925+0800 003---自定义NSOperation[14013:11296143] 下载图片: 优美海景
2021-05-26 10:59:07.807064+0800 003---自定义NSOperation[14013:11296166] 下载图片: 微信壁纸
2021-05-26 10:59:07.827490+0800 003---自定义NSOperation[14013:11296170] 下载图片: 典雅的教堂
2021-05-26 10:59:07.840837+0800 003---自定义NSOperation[14013:11296172] 下载图片: 西湖美女
2021-05-26 10:59:08.311868+0800 003---自定义NSOperation[14013:11296154] 下载图片: 简约dog
2021-05-26 10:59:08.328546+0800 003---自定义NSOperation[14013:11296172] 下载图片: 简约dog
2021-05-26 10:59:08.345750+0800 003---自定义NSOperation[14013:11296144] 下载图片: 可爱超人
2021-05-26 10:59:08.362655+0800 003---自定义NSOperation[14013:11296167] 下载图片: 可爱超人
复制代码

重复下载,譬如:下载图片: 典雅的教堂执行了多次,浪费内存。

解决方案

3.1 从模型加载数据

// 从模型加载数据
if (model.image) {
    NSLog(@"从模型加载数据");
    cell.imageView.image  = model.image;
    return cell;
}

 [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            cell.imageView.image  = image;
            model.image           = image;
 }];

 2021-05-26 11:01:30.524417+0800 003---自定义NSOperation[14094:11306727] 下载图片: 典雅的教堂
2021-05-26 11:01:30.525197+0800 003---自定义NSOperation[14094:11306723] 下载图片: 西湖美女
2021-05-26 11:01:30.525595+0800 003---自定义NSOperation[14094:11306728] 下载图片: 优美海景
2021-05-26 11:01:30.526080+0800 003---自定义NSOperation[14094:11306725] 下载图片: 微信壁纸
2021-05-26 11:01:30.526448+0800 003---自定义NSOperation[14094:11306722] 下载图片: 高清无码美女
2021-05-26 11:01:30.526841+0800 003---自定义NSOperation[14094:11306724] 下载图片: 深沉匹若曹
2021-05-26 11:01:30.527250+0800 003---自定义NSOperation[14094:11306731] 下载图片: 毛笔执念
2021-05-26 11:01:30.527632+0800 003---自定义NSOperation[14094:11306733] 下载图片: 简约蒲苇
2021-05-26 11:01:30.669017+0800 003---自定义NSOperation[14094:11306738] [] nw_protocol_get_quic_image_block_invoke dlopen libquic failed
2021-05-26 11:03:40.483158+0800 003---自定义NSOperation[14094:11306728] 下载图片: 笑脸路飞
2021-05-26 11:03:40.498987+0800 003---自定义NSOperation[14094:11315435] 下载图片: 高清玉米棒子
2021-05-26 11:03:40.515254+0800 003---自定义NSOperation[14094:11315433] 下载图片: 五彩星星
2021-05-26 11:03:40.531969+0800 003---自定义NSOperation[14094:11315441] 下载图片: 欧豪篮球
2021-05-26 11:03:40.561970+0800 003---自定义NSOperation[14094:11315434] 下载图片: 渣男
2021-05-26 11:03:40.578452+0800 003---自定义NSOperation[14094:11315445] 下载图片: 姑娘走了
2021-05-26 11:03:40.612491+0800 003---自定义NSOperation[14094:11315440] 下载图片: 简约dog
2021-05-26 11:03:40.629275+0800 003---自定义NSOperation[14094:11315447] 下载图片: 简约dog
2021-05-26 11:03:40.661915+0800 003---自定义NSOperation[14094:11315446] 下载图片: 简约dog
2021-05-26 11:03:40.678988+0800 003---自定义NSOperation[14094:11315444] 下载图片: 简约dog
2021-05-26 11:03:40.728374+0800 003---自定义NSOperation[14094:11315450] 下载图片: 可爱超人
2021-05-26 11:03:40.744980+0800 003---自定义NSOperation[14094:11315449] 下载图片: 可爱超人
2021-05-26 11:03:41.299598+0800 003---自定义NSOperation[14094:11306166] 从模型加载数据
2021-05-26 11:03:41.315792+0800 003---自定义NSOperation[14094:11306166] 从模型加载数据
2021-05-26 11:03:41.331618+0800 003---自定义NSOperation[14094:11306166] 从模型加载数据
2021-05-26 11:03:41.348666+0800 003---自定义NSOperation[14094:11306166] 从模型加载数据
2021-05-26 11:03:42.868586+0800 003---自定义NSOperation[14094:11306166] 从模型加载数据
2021-05-26 11:03:42.885300+0800 003---自定义NSOperation[14094:11306166] 从模型加载数据
2021-05-26 11:03:42.901959+0800 003---自定义NSOperation[14094:11315445] 下载图片: 笑脸路飞
2021-05-26 11:03:42.918589+0800 003---自定义NSOperation[14094:11306728] 下载图片: 高清玉米棒子
2021-05-26 11:03:42.951874+0800 003---自定义NSOperation[14094:11315444] 下载图片: 五彩星星
2021-05-26 11:03:42.968557+0800 003---自定义NSOperation[14094:11306166] 从模型加载数据
复制代码

从模型加载数据,有个小bug: 在页面加载后滑动页面,从模型加载数据,接着点击模拟器在菜单栏上选择Debug Simulate Memory Warning后,会调用内存警告函数didReceiveMemoryWarning
image.png

 - (void)didReceiveMemoryWarning{
        NSLog(@"收到内存警告,你要清理内存了!!!");
}
复制代码
2021-05-26 11:12:10.189135+0800 003---自定义NSOperation[14535:11382577] 从模型加载数据
2021-05-26 11:12:10.204499+0800 003---自定义NSOperation[14535:11382577] 从模型加载数据
2021-05-26 11:12:10.466714+0800 003---自定义NSOperation[14535:11383187] 下载图片: 笑脸路飞
2021-05-26 11:12:10.483011+0800 003---自定义NSOperation[14535:11383199] 下载图片: 高清玉米棒子
2021-05-26 11:12:13.340177+0800 003---自定义NSOperation[14535:11382577] 收到内存警告,你要清理内存了!!!
复制代码

此时如果将模型中的数据删除的话,这样是不可取的,因为每次删除内存,耗费内存,对手机电量造成极大影响

正确的处理方法就是下面的:

3.2 从内存,磁盘加载数据

关于内存和磁盘的问题:
 内存就是存储到字典,数组等oc或c对象,链表中,页面退出或者程序退出后相应内存就会被删除,`一般通过key读取`
 磁盘就是沙盒中保存的plist,file等文件,读取速度比内存慢很多,`通过路径读取`
// 内存 , 磁盘  首先加载内存 (快)  ---> 磁盘 (保存一份到内存) ---> 下载 保存内存和磁盘
复制代码
 // 从内存加载数据
    UIImage *cacheImage = self.imageCacheDict[model.imageUrl];
    if (cacheImage) {
        NSLog(@"从内存加载数据");
        cell.imageView.image  = cacheImage;
        return cell;
    }
    
    // 磁盘加载
    cacheImage = [UIImage imageWithContentsOfFile:[model.imageUrl getDowloadImagePath]];
    if (cacheImage) {
        NSLog(@"从磁盘加载数据");
        cell.imageView.image  = cacheImage;
        //磁盘找到后保存到内存,方便下次直接从内存中读取
        [self.imageCacheDict setObject:cacheImage forKey:model.imageUrl];
        return cell;
    }
    
   /**
 下载图片的路径

 @return MD5加密的图片下载地址
 */
- (NSString *)getDowloadImagePath{
    
    // url ---> 唯一性,url相同,根据path后不唯一,md5可以保证唯一
    // url 很长 ;md5可以保证长度 固定
    NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    NSString *md5Str = [self kc_md5String];
    return [cachePath stringByAppendingPathComponent:md5Str];
}

/**
 MD5 加密

 @return 返回MD5加密数据
 */
- (NSString *)kc_md5String{
//    const char *cStr = [self UTF8String];
//    unsigned char result[16];
//    CC_MD5(cStr, (CC_LONG)strlen(cStr), result);
//    return [NSString stringWithFormat:
//            @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
//            result[0], result[1], result[2], result[3],
//            result[4], result[5], result[6], result[7],
//            result[8], result[9], result[10], result[11],
//            result[12], result[13], result[14], result[15]
//            ];
    
    const char *str = self.UTF8String;
    if (str == NULL) {
        str = "";
    }
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSURL *keyURL = [NSURL URLWithString:self];
    NSString *ext = keyURL ? keyURL.pathExtension : self.pathExtension;
    return [NSString stringWithFormat:
            @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
            r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7],
            r[8], r[9], r[10],r[11], r[12], r[13], r[14], r[15],
            ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]];

}
复制代码

存储数据的时候,一定要保证当前地址是唯一的。

/Users/hai/Library/Developer/CoreSimulator/Devices/DFFAFFED-D649-4A68-9DC5-5CC46BEFBB37/data/Containers/Data/Application/ED0A679B-1B7B-4E5C-B733-55795509F611
复制代码

image.png

http://e.hiphotos.baidu.com/image/h%3D300/sign=0708e32319ce36d3bd0485300af23a24/fcfaaf51f3deb48fd0e9be27fc1f3a292cf57842.jpg 
 md5Str=== e69dc1165d81d02165c1c19efe6102f8.jpg
复制代码
http://e.hiphotos.baidu.com/image/h%3D300/sign=0708e32319ce36d3bd0485300af23a24/fcfaaf51f3deb48fd0e9be27fc1f3a292cf57842.jpg 
 md5Str=== e69dc1165d81d02165c1c19efe6102f8.jpg
复制代码

上述说法有误。
一般第三方网络框架,网络请求的话,会执行NSString *URLIdentifier = request.URL.absoluteString,即忽略掉服务器的域名,只会执行图片下载地址的url相对地址,如果更换服务器域名,相对地址没变,就会导致不能下载服务器最新域名对应的图片,直接使用md5的话,是绝对地址的md5,这样就保证了地址唯一性.

https://www.baidu.com/photos/1.jpg
https://www.sina.com/photos/1.jpg
request.URL.absoluteString = photos/1.jpg
request.URL.absoluteString = photos/1.jpg
//使用md5后
absoluteString([URL+md5(URL)]) = photos/1.jpg + md5(https://www.baidu.com/photos/1.jpg)
absoluteString([URL+md5(URL)]) = photos/2.jpg + md5(https://www.sina.com/photos/1.jpg)`
复制代码

具体情况根据缓存cache.db文件存储的key来决定。,上面的讲解可能跟具体情况不一致,但是原理都是类似的。这也是使用第三方的好处,做了很多额外的工作。

3.3 缓存下载任务

3.2 从内存和磁盘中读取的话,有个小bug。

下面是演示过程:
打开模拟器,打开Charles,记住要勾选MacOS Proxy,设置带宽,如下图
image.png
在网速很差或者低端机, 图片下载一直没回来或者写入磁盘非常慢,一直在队列上添加任务,导致内存剧增,达到一定值,程序就会闪退。网络延迟默认是60s,60s内没有请求完,页面一直滑动就会一直添加任务。

image.png

解决办法是 :相同的操作只执行一次,不要重新执行下载任务。

 if (self.operationDict[model.imageUrl]) {
        NSLog(@"兄弟,稍微一等< %@已经提交下载了",model.title);
        return cell;
    }
复制代码
 NSBlockOperation *op  = [NSBlockOperation blockOperationWithBlock:^{
     //反复执行的任务
 }];
 // 将下载事务添加到队列
[self.queue addOperation:op];
// 将下载事务做记录
[self.operationDict setObject:op forKey:model.imageUrl];
复制代码
// 下载完成操作 从记录清除,否则如果下载完成但是只下载一半,就不会重新下载了
[self.operationDict removeObjectForKey:model.imageUrl];
复制代码

image.png

4. 自定义NSOperation

3.中的流程非常繁琐,每次执行一个图片下载的页面,需要写那么多代码,这里可以封装自定义的NSOperation.
参考SdWebImage

[cell.imageView sd_setImageWithURL:[NSURL URLWithString:model.imageUrl]];
复制代码

Demo地址:
github.com/tanghaitao/…

思路:
封装遵循一个原则: 单一原则:
ImageView分类, 调度ImageView是Manager,下载是NSOperation

1. UIImageView+KCWebCache 分类
[cell.imageView kc_setImageWithUrlString:model.imageUrl title:model.title indexPath:indexPath];
判断URL,当前正在执行的事务记录等
2.KCWebImageManager 单例
管理所有缓存策略,最大并发数,内存警告
 // 只要调用单利,就会来到这里 那么我就可以在这里做一系列的初始化
- (instancetype)init{
    if (self=[super init]) {
        
        self.queue.maxConcurrentOperationCount = 2;
        // 注册内存警告通知
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(memoryWarning) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
    }
    return self;
}
3. KCWebImageDownloadOperation 
自定义Operation执行图片下载任务,回调执行结果

https://developer.apple.com/library/archive/navigation/ 搜索NSOperation找到Sample

For non-concurrent operations, you typically override only one method
- main
If you are creating a concurrent operation, you need to override the following methods and properties at a minimum:
- start
- asynchronous
- executing
- finished
非并发的重新main就好了
并发的需要执行start等函数做处理

// 因为父类的属性是Readonly的,重载时如果需要setter的话则需要手动合成。
复制代码
- (void)start{
    // 延长生命周期
    // 大型循环
    // 自己创建管理线程
    
    @autoreleasepool{
         [_lock lock];//线程安全
         [_lock unlock];
    }
 }
复制代码

5. NSOperation自定义总结

//对当前下载图片判断,是否需要创建操作,相同url,有一个执行完成就不需要再去下载了。
if (self.operationDict[urlString]) {
    NSLog(@"正在下载,让子弹飞会 %@",title);
    NSLog(@"正在下载的回调Block %@的%@",title,completeHandle);
    NSMutableArray *mArray = self.handleDict[urlString];
    if (mArray == nil) {
        mArray = [NSMutableArray arrayWithCapacity:1];
    }
    [mArray addObject:completeHandle];

    //相同url的第1个任务,一开始数组个数为1,completeHandle1
    //相同url的第2个任务,     数组个数为2,completeHandle1,completeHandle2


    [self.handleDict setObject:mArray forKey:urlString];
    return;
}

 // 下面就是创建操作 下载 --- 自定义
KCWebImageDownloadOperation *downOp = [[KCWebImageDownloadOperation alloc] initWithDownloadImageUrl:urlString completeHandle:^(NSData *imageData,NSString *kc_urlString) {

    UIImage *downloadImage = [UIImage imageWithData:imageData];
    if (downloadImage) {

        [self.imageCacheDict setObject:downloadImage forKey:urlString];
        [self.operationDict removeObjectForKey:urlString];

        [[NSOperationQueue mainQueue] addOperationWithBlock:^{

            completeHandle(downloadImage,kc_urlString);
            //去取回调
            //回调相同url的所有任务结果
            if (self.handleDict[kc_urlString]) {
                NSMutableArray *mArray = self.handleDict[kc_urlString];
                for (KCCompleteHandleBlock handleBlock in mArray) {
                    handleBlock(downloadImage,kc_urlString);
                }
                [self.handleDict removeObjectForKey:urlString];
            }
        }];
    }
} title:title];
复制代码
[mArray addObject:completeHandle];
[self.handleDict setObject:mArray forKey:urlString];
复制代码

如果界面上有多个相同的url任务,相同url,有一个执行完成就不需要再创建新的下载任务了。

  if (self.kc_urlString && self.kc_urlString.length>0 && ![self.kc_urlString isEqualToString:urlString]) {
        // 下载ing 没有回来(图片太大 网速太慢)  取消(已死), 之前线程中的任务回调
//        NSLog(@"取消之前的下载操作 %@",title);
        NSLog(@"取消%@之前的下载操作:%@---%@ \n%@---%@",indexPath,self.kc_title,title,self.kc_urlString,urlString);
        [[KCWebImageManager sharedManager] cancelDownloadImageWithUrlString:self.kc_urlString];
    }
    //新操作要开始下载 就要记录
    self.kc_urlString = urlString;
    self.kc_title = title;
    self.image = nil;
    
    [[KCWebImageManager sharedManager] downloadImageWithUrlString:urlString completeHandle:^(UIImage *downloadImage,NSString *urlString) {
        //下载完成 要制空
        if ([urlString isEqualToString:self.kc_urlString]) {
            self.kc_urlString = nil;
            self.kc_title = nil;
            self.image = downloadImage;
        }
    } title:title];
    
    - (void)setKc_urlString:(NSString *)kc_urlString{
        /**
         1: 绑定的对象
         2: 关联键,通过这个键去找
         3: 值
         4: 关联策略
         */
        objc_setAssociatedObject(self, kcAssociatedKey_imageUrlString, kc_urlString, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }

- (NSString *)kc_urlString{
        return objc_getAssociatedObject(self, kcAssociatedKey_imageUrlString);
   }
复制代码

通过runtime objc_setAssociatedObjectobjc_getAssociatedObject 去监听当前任务属性 kc_urlString 的变化,如果没有请求完成,则 kc_urlString一直不为nil
表示下载ing 没有回来(图片太大 网速太慢),需要 取消该任务 (已死) cancelDownloadImageWithUrlString:self.kc_urlString

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享