1 多图下载综合案例
包含数据展示、缓存处理、子线程下载图片以及内存警告处理
#import <UIKit/UIKit.h>
@interface ViewController : UITableViewController
@end
#import "ViewController.h"
#import "AppModel.h"
@interface ViewController ()
@property (strong, nonatomic) NSArray *app;
@property (strong, nonatomic) NSMutableDictionary *images;
@property (strong, nonatomic) NSOperationQueue *queue;
@property (strong, nonatomic) NSMutableDictionary *operations;
@end
@implementation ViewController
#pragma mark - UITableView delegate
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.app.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"app"];
AppModel *app = self.app[indexPath.row];
cell.textLabel.text = app.name;
cell.detailTextLabel.text = app.download;
// 判断图片是否存在于内存缓存中,如果存在,则从内存缓存中取出并使用
// 如果内存缓存中不存在,则判断是否存在于磁盘缓存中,如果存在,则从磁盘缓存中取出使用
// 沙盒目录
// Document:会备份,苹果不允许将备份数据保存在这里
// Library:包含下面两种
// 1>Preference:偏好设置 保存账号
// 2>Cache:缓存文件,会备份
// tmp:会随时被删除
UIImage *image = [self.images objectForKey:app.icon];
if (image) {
cell.imageView.image = image;
NSLog(@"%zd处使用了内存缓存中的图片",indexPath.row);
} else {
// 判断图片是否存在于磁盘缓存中
// 1> 获取沙盒路径
NSString *path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
// 2> 获取文件名称
NSString *fileName = [app.icon lastPathComponent];
// 3> 获取路径全称
NSString *fullName = [path stringByAppendingPathComponent:fileName];
NSData *data = [NSData dataWithContentsOfFile:fullName];
if (data) {
UIImage *image = [UIImage imageWithData:data];
cell.imageView.image = image;
// 将图片保存到内存缓存中
[self.images setObject:image forKey:app.icon];
NSLog(@"%zd处使用了磁盘缓存中的图片",indexPath.row);
} else {
// 判断是否正在下载,如果没有添加到队列,则才添加
NSBlockOperation *download = [self.operations objectForKey:app.icon];
if (!download) {
// 清空复用cell中的图片
cell.imageView.image = nil;
download = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:app.icon];
NSData *data = [[NSData alloc] initWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
// 回到主线程刷新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
//cell.imageView.image = image;
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}];
// 容错处理
if (!image) {
[self.operations removeObjectForKey:app.icon];
return;
}
// 保存到内存缓存中
[self.images setObject:image forKey:app.icon];
// 保存图片到沙盒缓存
[data writeToFile:fullName atomically:YES];
NSLog(@"%zd处下载图片",indexPath.row);
// 移除图片的下载操作
[self.operations removeObjectForKey:app.icon];
}];
[self.queue addOperation:download];
// 将已创建的操作添加到内存中保存,避免重复添加任务
[self.operations setObject:download forKey:app.icon];
}
}
}
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 60;
}
#pragma mark - getter
- (NSArray *)app {
if (!_app) {
NSArray *arr = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil]];
NSMutableArray *arrM = [NSMutableArray array];
for (NSDictionary *dic in arr) {
AppModel *appM = [AppModel appWithDictionary:dic];
[arrM addObject:appM];
}
_app = arrM;
}
return _app;
}
- (NSMutableDictionary *)images {
if (!_images) {
_images = [NSMutableDictionary dictionary];
}
return _images;
}
- (NSOperationQueue *)queue {
if (!_queue) {
_queue = [[NSOperationQueue alloc] init];
_queue.maxConcurrentOperationCount = 10;
}
return _queue;
}
- (NSMutableDictionary *)operations {
if (!_operations) {
_operations = [NSMutableDictionary dictionary];
}
return _operations;
}
#pragma mark - life cycle
- (void)didReceiveMemoryWarning {
// 移除图片缓存
[self.images removeAllObjects];
// 取消下载任务
[self.queue cancelAllOperations];
}
@end
2 SDWebImage的使用和说明
// 下载图片且需要获取下载进度
// 内存缓存&磁盘缓存
- (void)download1 {
NSString *imageStr = @"http://dmimg.5054399.com/allimg/pkm/pk/22.jpg";
NSURL *url = [NSURL URLWithString:imageStr];
[self.imageView sd_setImageWithURL:url placeholderImage:[UIImage imageNamed:@""] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
self.imageView.image = image;
NSLog(@"cachetype:%zd",cacheType);
}];
}
// 下载图片,不需要设置
// 内存缓存&磁盘缓存
- (void)download2 {
NSString *imageStr = @"http://dmimg.5054399.com/allimg/pkm/pk/22.jpg";
NSURL *url = [NSURL URLWithString:imageStr];
[[SDWebImageManager sharedManager] downloadImageWithURL:url options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
self.imageView.image = image;
NSLog(@"cachetype:%zd",cacheType);
}];
}
// 下载图片,不缓存
// 没有任何缓存处理
- (void)download3 {
NSString *imageStr = @"http://dmimg.5054399.com/allimg/pkm/pk/22.jpg";
NSURL *url = [NSURL URLWithString:imageStr];
[[SDWebImageDownloader sharedDownloader] downloadImageWithURL:url options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {
} completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
// 该处是子线程,需要回到主线程更新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.imageView.image = image;
}];
}];
}
- (void)gif {
UIImage *image = [UIImage sd_animatedGIFNamed:@"0011"];
self.imageView.image = image;
}
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
// 清空缓存
// clearDisk:直接删除,然后重新创建
// cleanDisk:清除过期缓存,计算当前缓存的大小,和当前设置的最大缓存大小比较,如果超过则会继续删除(按照文件创建的先后顺序删除)
// 过期时间:7天
[[SDWebImageManager sharedManager].imageCache cleanDisk];
[[SDWebImageManager sharedManager].imageCache clearDisk];
// 取消当前所有的操作
[[SDWebImageManager sharedManager] cancelAll];
}
SDWebImage的框架内部细节
1 最大并发数量 == 6
2 缓存文件保存名称如何处理?拿到图片的URL路径,对路径进行MD5加密
3 该框架内部对内存警告的处理方式?内部通过监听通知的方式清除缓存
4 该框架进行缓存处理的方式:NSCache
5 如何判断图片的类型?判断图片类型的时候,匹配data的一个字节
6 队列中的任务处理方式:FIFO
7 如何下载图片?发送网络请求下载图片,NSURLConnection
8 请求超时的时间:15s
3 NSCache知识点补充
01.NSCache是专门用来进行缓存处理的,
02.NSCache简单介绍:
2-1 NSCache是苹果官方提供的缓存类,具体使用和NSDictionary类似,在AFN和SDWebImage框架中被使用来管理缓存
2-2 苹果官方解释NSCache在系统内存很低时,会自动释放对象(但模拟器演示不会释放)
建议:接收到内存警告时主动调用removeAllObject方法释放对象
2-3 NSCache是线程安全的,在多线程操作中,不需要对NSCache加锁
2-4 NSCache的Key只是对对象进行Strong引用,不是拷贝
03 属性介绍:
name:名称
delegete:设置代理
totalCostLimit:缓存空间的最大总成本,超出上限会自动回收对象。默认值为0,表示没有限制
countLimit:能够缓存的对象的最大数量。默认值为0,表示没有限制
evictsObjectsWithDiscardedContent:标识缓存是否回收废弃的内容
04 方法介绍
- (void)setObject:(ObjectType)obj forKey:(KeyType)key;//在缓存中设置指定键名对应的值,0成本
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;//在缓存中设置指定键名对应的值,并且指定该键值对的成本,用于计算记录在缓存中的所有对象的总成本,出现内存警告或者超出缓存总成本上限的时候,缓存会开启一个回收过程,删除部分元素
- (void)removeObjectForKey:(KeyType)key;//删除缓存中指定键名的对象
- (void)removeAllObjects;//删除缓存中所有的对象
#import "ViewController.h"
@interface ViewController ()<NSCacheDelegate>
@property (strong, nonatomic) NSCache *cache;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.cache.totalCostLimit = 5; // 设置最大成本,如果缓存的成本超过最大成本数量,则会自动收回之前的缓存对象
}
- (IBAction)saveData:(UIButton *)sender {
NSData *data = [NSData dataWithContentsOfFile:@"/Users/apple/Pictures/Snip20190131_1.png"];
for (int i = 0; i < 10; i++) {
//NSData *data = [NSData dataWithContentsOfFile:@"/Users/apple/Pictures/Snip20190131_1.png"];
//NSCache的Key只是对对象进行Strong引用,不是拷贝
[self.cache setObject:data forKey:@(i) cost:1];
NSLog(@"save data %d",i);
}
}
- (IBAction)findData:(UIButton *)sender {
NSLog(@"++++++++++++++++++++++++++++++++");
for (int i = 0; i < 10; i++) {
NSData *data = [self.cache objectForKey:@(i)];
if (data) {
NSLog(@"find data %d",i);
}
}
}
- (IBAction)deleteData:(UIButton *)sender {
[self.cache removeAllObjects];
}
- (void)cache:(NSCache *)cache willEvictObject:(id)obj {
NSLog(@"回收%zd",[obj length]);
}
- (NSCache *)cache {
if (!_cache) {
_cache = [[NSCache alloc] init];
_cache.delegate = self;
}
return _cache;
}
4 位移枚举简单介绍
#import "ViewController.h"
// 枚举第一种写法
typedef enum {
ZYXTypeOne1,
ZYXTypeOne2,
}ZYXTypeOne;
// 枚举第二种写法
typedef NS_ENUM(NSInteger, ZYXTypeTwo){
ZYXTypeTwo1,
ZYXTypeTwo2,
};
// 枚举第三种写法,位移枚举
// 一个参数可以传递多个值
// 按位与 & 1&1=1 1&0=0 1&0=0 只要有0 则为0
// 按位或 | 1|1= 1 1|0=1 0|0=0 只要有1 则为1
// 如果是位移枚举,观察第一个参数,如果值 != 0 ,那么可以默认传0作为参数,如果传0作为参数,那么效率最高,因为什么也不会做
typedef NS_OPTIONS(NSInteger, ZYXTypeThree) {
ZYXTypeThree1 = 1 << 0, //1 * 2的0次方 1
ZYXTypeThree2 = 1 << 1, //1 * 2的1次方 2
ZYXTypeThree3 = 1 << 2, //1 * 2的2次方 4
ZYXTypeThree4 = 1 << 3, //1 * 2的3次方 8
};
@interface ViewController ()
@end
@implementation ViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self demo:ZYXTypeThree1 | ZYXTypeThree2 | ZYXTypeThree3 | ZYXTypeThree4];
}
- (void)demo:(ZYXTypeThree)type {
if (type & ZYXTypeThree1) {
NSLog(@"%zd",type & ZYXTypeThree1);
}
if (type & ZYXTypeThree2) {
NSLog(@"%zd",type & ZYXTypeThree2);
}
if (type & ZYXTypeThree3) {
NSLog(@"%zd",type & ZYXTypeThree3);
}
if (type & ZYXTypeThree4) {
NSLog(@"%zd",type & ZYXTypeThree4);
}
}
5 Runloop简单介绍
5.1 什么是Ruploop
- 从字面意思看,就是运行循环,跑圈
- 基本作用
1)保持程序的持续运行
2)处理App中的各种事件(比如触摸事件、定时器事件、Selector事件)
3)节省CPU资源,提高程序性能:该做事时做事,该休息时休息
5.2 main函数中的Runloop
- 第14行代码中的UIApplicationMain函数的内部启动了一个Runloop
- 所以UIApplicationMain函数一直没有返回,保持了程序的持续运行
- 这个默认启动的Runloop是跟主线程相关联的
5.3 Runloop对象
iOS中有2套API来访问和使用Runloop
- Foundation:NSRunLoop
- Core Foundation:CFRunLoopRef
- NSRunLoop和CFRunLoopRef都代表着RunLoop对象
- NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多了解CFRunLoopRef层面的API(Core Foundation层面)
6 Runloop资料
CFRunLoopRef开源代码下载地址:
http://opensource.apple.com/source/CF/CF-1151.16/
7 Runloop对象和线程
7.1 RunLoop与线程
- 每条线程都有唯一的一个与之对应的Runloop对象
- 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建
- RunLoop在第一次获取时创建,在线程结束是销毁
7.2 获得RunLoop对象
- Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
- Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSRunLoop *mainRunloop = [NSRunLoop mainRunLoop];
NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop];
NSLog(@"%p---%p",mainRunloop,currentRunloop);
NSLog(@"%p---%p",CFRunLoopGetMain(),CFRunLoopGetCurrent());
NSLog(@"%p",mainRunloop.getCFRunLoop);
// RunLoop与线程的关系:一一对应,主线程的已经创建,子线程的需要手动创建
[[[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil] start];
}
- (void)run {
NSLog(@"%p--%@",[NSRunLoop currentRunLoop],[NSThread currentThread]);
}
8 RunLoop相关类
8.1 RunLoop相关类
- Core Foundation中关于RunLoop的5个类
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopOberverRef
- RunLoop中有多个运行模式,但是RunLoop只能选择一种模式运行
- Model里面至少要有一个Timer或者是Source
8.2 CFRunLoopModeRef
- CFRunLoopModeRef代表RunLoop的运行模式
- 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer
- 每次RunLoop启动时,只能指定其中的一个Mode,这个Mode被称作CurrentMode
- 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入
- 这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响
8.3 CFRunLoopModeRef的5种Mode
- 系统默认注册了5个Mode
- KCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
- UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响
- UIInitializationRunLoopMode:在刚启动App时进入的第一个Mode,启动后就不再使用
- GSEventReceiveRunLoopMode:接收系统事件内部Mode,通常用不到
- KCFRunLoopCommonModes:这是一个占位用的Mode,不是一种真正的Mode
8.4 简单应用
注:界面滚动视图滑动的过程中,会自动切换到“界面追踪模式”,停止滑动后会自动切换会“默认”模式。
- (void)timer1 {
// 创建定时器
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 将定时器添加到runloop中,指定runloop的运行模式为NSDefaultRunLoopMode
/*
参数1:定时器
参数2:runloop运行模式
*/
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//UITrackingRunLoopMode 界面追踪模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
//NSRunLoopCommonModes = NSDefaultRunLoopMode + UITrackingRunLoopMode
//占用,标签,凡事添加到UITrackingRunLoopMode中的h事件都会被同时添加到打上common标签的运行模式上
//0 : <CFString 0x7fff86743f40 [0x7fff80615350]>{contents = "UITrackingRunLoopMode"}
//2 : <CFString 0x7fff80628740 [0x7fff80615350]>{contents = "kCFRunLoopDefaultMode"}
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)timer2 {
// 该方法会默认将timer添加到当前的runloop中,并且模式设置为默认
// 但是如果是在子线程,那么需要创建runloop,并且启动runloop
// NSRunLoop *currentLoop = [NSRunLoop currentRunLoop];
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 开启runloop
//[currentLoop run];
}