内容简介:一个为UIImageView提供一个分类来支持远程服务器图片加载的库。减少网络流量,下载完图片后存储到本地,下载再获取同一张图片时,直接从本地获取,提升用户体验,能快速从本地获取呈现给用户。 SDWebImage提供了对图片进行了缓存,主要由SDImageCache完成。该类负责处理内存缓存以及一个可选的磁盘缓存,其中磁盘缓存的写操作是异步的,不会对UI造成影响。2、存储图片先在内存中放置一份缓存,如果需要缓存到磁盘,将磁盘缓存操作作为一个task放到串行队列中处理,会先检查图片格式是jpeg还是png,将
一个为UIImageView提供一个分类来支持远程服务器图片加载的库。
功能简介:
1、一个添加了web图片加载和缓存管理的UIImageView分类
2、一个异步图片下载器
3、一个异步的内存加磁盘综合存储图片并且自动处理过期图片
4、支持动态gif图
5、支持webP格式的图片
6、后台图片解压处理
7、确保同样的图片url不会下载多次
8、确保伪造的图片url不会重复尝试下载
9、确保主线程不会阻塞
复制代码
工作流程
1、入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。 2、进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:,交给 SDImageCache 从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:. 3、先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。 4、SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示图片。 5、如果内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。 6、根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。 7、如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。 8、如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。 9、共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。 10、图片下载由 NSURLConnection 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。 11、connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。 12、图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。 13、在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。 14、通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。 15、SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。 16、SDWI 也提供了 UIButton+WebCache 和 MKAnnotationView+WebCache,方便使用。 17、SDWebImagePrefetcher 可以预先下载图片,方便后续使用。 复制代码
源码分析
主要用到的对象
一、图片下载
1、 SDWebImageDownloader
-
1.单例,图片下载器,负责图片异步下载,并对图片加载做了优化处理
-
2.图片的下载操作放在一个NSOperationQueue并发操作队列中,队列默认最大并发数是6
-
3.每个图片对应一些回调(下载进度,完成回调等),回调信息会存在downloader的URLCallbacks(一个字典,key是url地址,value是图片下载回调数组)中,URLCallbacks可能被多个线程访问,所以downloader把下载任务放在一个barrierQueue中,并设置屏障保证同一时间只有一个线程访问URLCallbacks。,在创建回调URLCallbacks的block中创建了一个NSOperation并添加到NSOperationQueue中。
-
4.每个图片下载都是一个operation类,创建后添加到一个队列中,SDWebimage定义了一个协议 SDWebImageOperation作为图片下载操作的基础协议,声明了一个cancel方法,用于取消操作。
@protocol SDWebImageOperation <NSObject> -(void)cancel; @end 复制代码
- 5.对于图片的下载,SDWebImageDownloaderOperation完全依赖于NSURLConnection类,继承和实现了NSURLConnectionDataDelegate协议的方法
connection:didReceiveResponse: connection:didReceiveData: connectionDidFinishLoading: connection:didFailWithError: connection:willCacheResponse: connectionShouldUseCredentialStorage: -connection:willSendRequestForAuthenticationChalleng -connection:didReceiveData:方法,接受数据,创建一个CGImageSourceRef对象,在首次获取数据时(图片width,height),图片下载完成之前,使用CGImageSourceRef对象创建一个图片对象,经过缩放、解压操作生成一个UIImage对象供回调使用,同时还有下载进度处理。 注:缩放:SDWebImageCompat中SDScaledImageForKey函数 解压:SDWebImageDecoder文件中decodedImageWithImage 复制代码
2、SDWebImageDownloaderOption
-
1.继承自NSOperation类,没有简单实现main方法,而是采用更加灵活的start方法,以便自己管理下载的状态
-
2.start方法中创建了下载使用的NSURLConnections对象,开启了图片的下载,并抛出一个下载开始的通知,
-
3.小结:下载的核心是利用NSURLSession加载数据,每个图片的下载都有一个operation操作来完成,并将这些操作放到一个操作队列中,这样可以实现图片的并发下载。
3、SDWebImageDecoder(异步对图片进行解码)
二、缓存
减少网络流量,下载完图片后存储到本地,下载再获取同一张图片时,直接从本地获取,提升用户体验,能快速从本地获取呈现给用户。 SDWebImage提供了对图片进行了缓存,主要由SDImageCache完成。该类负责处理内存缓存以及一个可选的磁盘缓存,其中磁盘缓存的写操作是异步的,不会对UI造成影响。
1、内存缓存及磁盘缓存
-
1.内存缓存的处理由NSCache对象实现,NSCache类似一个集合的容器,它存储key-value对,类似于nsdictionary类,我们通常使用缓存来临时存储短时间使用但创建昂贵的对象,重用这些对象可以优化新能,同时这些对象对于程序来说不是紧要的,如果内存紧张就会自动释放。
-
2.磁盘缓存的处理使用NSFileManager对象实现,图片存储的位置位于cache文件夹,另外SDImageCache还定义了一个串行队列来异步存储图片。
-
3.SDImageCache提供了大量方法来缓存、获取、移除及清空图片。对于图片的索引,我们通过一个key来索引,在内存中,我们将其作为NSCache的key值,而在磁盘中,我们用这个key值作为图片的文件名,对于一个远程下载的图片其url实作为这个key的最佳选择。
2、存储图片先在内存中放置一份缓存,如果需要缓存到磁盘,将磁盘缓存操作作为一个task放到串行队列中处理,会先检查图片格式是jpeg还是png,将其转换为响应的图片数据,最后吧数据写入磁盘中(文件名是对key值做MD5后的串)
3、查询图片内存和磁盘查询图片API:
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key; - (UIImage *)imageFromDiskCacheForKey:(NSString *)key; 复制代码
查看本地是否存在key指定的图片,使用一下API:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock; 复制代码
4、移除图片移除图片API:
- (void)removeImageForKey:(NSString *)key; - (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion; - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk; - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion; 复制代码
5、清理图片(磁盘)
清空磁盘图片可以选择完全清空和部分清空,完全清空就是吧缓存文件夹删除。
- (void)clearDisk; - (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion; 复制代码
部分清理 会根据设置的一些参数移除部分文件,主要有两个指标:文件的缓存有效期(maxCacheAge:默认是1周)和最大缓存空间大小(maxCacheSize:如果所有文件大小大于最大值,会按照文件最后修改时间的逆序,以每次一半的递归来移除哪些过早的文件,知道缓存文件总大小小于最大值),具体代码参考- (void)cleanDiskWithCompletionBlock;
6、小结SDImageCache处理提供以上API,还提供了获取缓存大小,缓存中图片数量等API, 常用的接口和属性:
(1)-getSize :获得硬盘缓存的大小 (2)-getDiskCount : 获得硬盘缓存的图片数量 (3)-clearMemory : 清理所有内存图片 (4)- removeImageForKey:(NSString *)key 系列的方法 : 从内存、硬盘按要求指定清除图片 (5)maxMemoryCost : 保存在存储器中像素的总和 (6)maxCacheSize : 最大缓存大小 以字节为单位。默认没有设置,也就是为0,而清理磁盘缓存的先决条件为self.maxCacheSize > 0,所以0表示无限制。 (7)maxCacheAge : 在内存缓存保留的最长时间以秒为单位计算,默认是一周 复制代码
三、SDWebImageManager
实际使用中并不直接使用SDWebImageDownloader和SDImageCache类对图片进行下载和存储,而是使用SDWebImageManager来管理。包括平常使用UIImageView+WebCache等控件的分类,都是使用SDWebImageManager来处理,该对象内部定义了一个图片下载器(SDWebImageDownloader)和图片缓存(SDImageCache)
@interface SDWebImageManager : NSObject @property (weak, nonatomic) id <SDWebImageManagerDelegate> delegate; @property (strong, nonatomic, readonly) SDImageCache *imageCache; @property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader; ... @end 复制代码
SDWebImageManager声明了一个delegate属性,其实是一个id对象,代理声明了两个方法
// 控制当图片在缓存中没有找到时,应该下载哪个图片 - (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL; // 允许在图片已经被下载完成且被缓存到磁盘或内存前立即转换 - (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL; 复制代码
这两个方法会在SDWebImageManager的-downloadImageWithURL:options:progress:completed:方法中调用,而这个方法是SDWebImageManager类的核心所在(具体看源码)
SDWebImageManager的几个API:
(1)- (void)cancelAll : 取消runningOperations中所有的操作,并全部删除 (2)- (BOOL)isRunning :检查是否有操作在运行,这里的操作指的是下载和缓存组成的组合操作 (3) - downloadImageWithURL:options:progress:completed: 核心方法 (4)- (BOOL)diskImageExistsForURL:(NSURL *)url :指定url的图片是否进行了磁盘缓存 复制代码
四、视图扩展
在使用SDWebImage的时候,使用最多的是UIImageView+WebCache中的针对UIImageView的扩展,核心方法是sd_setImageWithURL:placeholderImage:options:progress:completed:, 其使用SDWebImageManager单例对象下载并缓存图片。
除了扩展UIImageView外,SDWebImage还扩展了UIView,UIButton,MKAnnotationView等视图类,具体可以参考源码,除了可以使用扩展的方法下载图片,同时也可以使用SDWebImageManager下载图片。
UIView+WebCacheOperation分类: 把当前view对应的图片操作对象存储起来(通过运行时设置属性),在基类中完成 存储的结构:一个loadOperationKey属性,value是一个字典(字典结构: key:UIImageViewAnimationImages或者UIImageViewImageLoad,value是 operation数组(动态图片)或者对象)
UIButton+WebCache分类 会根据不同的按钮状态,下载的图片根据不同的状态进行设置 imageURLStorageKey:{state:url}
五、技术点
- 1.dispatch_barrier_sync函数,用于对操作设置顺序,确保在执行完任务后再确保后续操作。常用于确保线程安全性操作
- 2.NSMutableURLRequest:用于创建一个网络请求对象,可以根据需要来配置请求报头等信息
- 3.NSOperation及NSOperationQueue:操作队列是OC中一种告诫的并发处理方法,基于GCD实现,相对于GCD来说,操作队列的优点是可以取消在任务处理队列中的任务,另外在管理操作间的依赖关系方面容易一些,对SDWebImage中我们看到如何使用依赖将下载顺序设置成后进先出的顺序
- 4.NSURLSession:用于网络请求及相应处理
- 5.开启后台任务
- 6.NSCache类:一个类似于集合的容器,存储key-value对,这一点类似于nsdictionary类,我们通常用使用缓存来临时存储短时间使用但创建昂贵的对象。重用这些对象可以优化性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,在内存紧张时会被丢弃
- 7.清理缓存图片的策略:特别是最大缓存空间大小的设置。如果所有缓存文件的总大小超过这一大小,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间。
- 8.图片解压操作:这一操作可以查看SDWebImageDecoder.m中+decodedImageWithImage方法的实现。
- 9.对GIF图片的处理
- 10.对WebP图片的处理。
二、什么是Block?
- Block是将函数及其执行上下文封装起来的对象。
比如:
NSInteger num = 3;
NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
return n*num;
};
block(2);
复制代码
通过clang -rewrite-objc WYTest.m命令编译该.m文件,发现该block被编译成这个形式:
NSInteger num = 3;
NSInteger(*block)(NSInteger) = ((NSInteger (*)(NSInteger))&__WYTest__blockTest_block_impl_0((void *)__WYTest__blockTest_block_func_0, &__WYTest__blockTest_block_desc_0_DATA, num));
((NSInteger (*)(__block_impl *, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 2);
复制代码
其中WYTest是文件名,blockTest是方法名,这些可以忽略。 其中__WYTest__blockTest_block_impl_0结构体为
struct __WYTest__blockTest_block_impl_0 {
struct __block_impl impl;
struct __WYTest__blockTest_block_desc_0* Desc;
NSInteger num;
__WYTest__blockTest_block_impl_0(void *fp, struct __WYTest__blockTest_block_desc_0 *desc, NSInteger _num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
复制代码
__block_impl结构体为
struct __block_impl {
void *isa;//isa指针,所以说Block是对象
int Flags;
int Reserved;
void *FuncPtr;//函数指针
};
复制代码
block内部有isa指针,所以说其本质也是OC对象 block内部则为:
static NSInteger __WYTest__blockTest_block_func_0(struct __WYTest__blockTest_block_impl_0 *__cself, NSInteger n) {
NSInteger num = __cself->num; // bound by copy
return n*num;
}
复制代码
所以说 Block是将函数及其执行上下文封装起来的对象 既然block内部封装了函数,那么它同样也有参数和返回值。
二、Block变量截获
1、局部变量截获 是值截获。 比如:
NSInteger num = 3;
NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
return n*num;
};
num = 1;
NSLog(@"%zd",block(2));
复制代码
这里的输出是6而不是2,原因就是对局部变量num的截获是值截获。 同样,在block里如果修改变量num,也是无效的,甚至编译器会报错。
2、局部静态变量截获 是指针截获。
static NSInteger num = 3;
NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
return n*num;
};
num = 1;
NSLog(@"%zd",block(2));
复制代码
输出为2,意味着num = 1这里的修改num值是有效的,即是指针截获。 同样,在block里去修改变量m,也是有效的。
3、全局变量,静态全局变量截获:不截获,直接取值。
我们同样用clang编译看下结果。
static NSInteger num3 = 300;
NSInteger num4 = 3000;
- (void)blockTest
{
NSInteger num = 30;
static NSInteger num2 = 3;
__block NSInteger num5 = 30000;
void(^block)(void) = ^{
NSLog(@"%zd",num);//局部变量
NSLog(@"%zd",num2);//静态变量
NSLog(@"%zd",num3);//全局变量
NSLog(@"%zd",num4);//全局静态变量
NSLog(@"%zd",num5);//__block修饰变量
};
block();
}
复制代码
编译后
struct __WYTest__blockTest_block_impl_0 {
struct __block_impl impl;
struct __WYTest__blockTest_block_desc_0* Desc;
NSInteger num;//局部变量
NSInteger *num2;//静态变量
__Block_byref_num5_0 *num5; // by ref//__block修饰变量
__WYTest__blockTest_block_impl_0(void *fp, struct __WYTest__blockTest_block_desc_0 *desc, NSInteger _num, NSInteger *_num2, __Block_byref_num5_0 *_num5, int flags=0) : num(_num), num2(_num2), num5(_num5->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
复制代码
( impl.isa = &_NSConcreteStackBlock;这里注意到这一句,即说明该block是栈block) 可以看到局部变量被编译成值形式,而静态变量被编成指针形式,全局变量并未截获。而__block修饰的变量也是以指针形式截获的,并且生成了一个新的结构体 对象 :
struct __Block_byref_num5_0 {
void *__isa;
__Block_byref_num5_0 *__forwarding;
int __flags;
int __size;
NSInteger num5;
};
复制代码
该对象有个属性:num5,即我们用__block修饰的变量。 这里__forwarding是指向自身的(栈block)。 一般情况下,如果我们要对block截获的局部变量进行赋值操作需添加__block 修饰符,而对全局变量,静态变量是不需要添加__block修饰符的。 另外,block里访问self或成员变量都会去截获self。
三、Block的几种形式
-
分为全局Block(_NSConcreteGlobalBlock)、栈Block(_NSConcreteStackBlock)、堆Block(_NSConcreteMallocBlock)三种形式
其中栈Block存储在栈(stack)区,堆Block存储在堆(heap)区,全局Block存储在已初始化数据(.data)区
1、不使用外部变量的block是全局block
比如:
NSLog(@"%@",[^{
NSLog(@"globalBlock");
} class]);
复制代码
输出:
__NSGlobalBlock__ 复制代码
2、使用外部变量并且未进行copy操作的block是栈block
比如:
NSInteger num = 10;
NSLog(@"%@",[^{
NSLog(@"stackBlock:%zd",num);
} class]);
复制代码
输出:
__NSStackBlock__ 复制代码
日常开发常用于这种情况:
[self testWithBlock:^{
NSLog(@"%@",self);
}];
- (void)testWithBlock:(dispatch_block_t)block {
block();
NSLog(@"%@",[block class]);
}
复制代码
3、对栈block进行copy操作,就是堆block,而对全局block进行copy,仍是全局block
- 比如堆1中的全局进行copy操作,即赋值:
void (^globalBlock)(void) = ^{
NSLog(@"globalBlock");
};
NSLog(@"%@",[globalBlock class]);
复制代码
输出:
__NSGlobalBlock__ 复制代码
仍是全局block
- 而对2中的栈block进行赋值操作:
NSInteger num = 10;
void (^mallocBlock)(void) = ^{
NSLog(@"stackBlock:%zd",num);
};
NSLog(@"%@",[mallocBlock class]);
复制代码
输出:
__NSMallocBlock__ 复制代码
对栈blockcopy之后,并不代表着栈block就消失了,左边的mallock是堆block,右边被copy的仍是栈block 比如:
[self testWithBlock:^{
NSLog(@"%@",self);
}];
- (void)testWithBlock:(dispatch_block_t)block
{
block();
dispatch_block_t tempBlock = block;
NSLog(@"%@,%@",[block class],[tempBlock class]);
}
复制代码
输出:
__NSStackBlock__,__NSMallocBlock__ 复制代码
- 即如果对栈Block进行copy,将会copy到堆区,对堆Block进行copy,将会增加引用计数,对全局Block进行copy,因为是已经初始化的,所以什么也不做。
另外,__block变量在copy时,由于__forwarding的存在,栈上的__forwarding指针会指向堆上的__forwarding变量,而堆上的__forwarding指针指向其自身,所以,如果对__block的修改,实际上是在修改堆上的__block变量。
即__forwarding指针存在的意义就是,无论在任何内存位置, 都可以顺利地访问同一个__block变量。
- 另外由于block捕获的__block修饰的变量会去持有变量,那么如果用__block修饰self,且self持有block,并且block内部使用到__block修饰的self时,就会造成多循环引用,即self持有block,block 持有__block变量,而__block变量持有self,造成内存泄漏。 比如:
__block typeof(self) weakSelf = self;
_testBlock = ^{
NSLog(@"%@",weakSelf);
};
_testBlock();
复制代码
如果要解决这种循环引用,可以主动断开__block变量对self的持有,即在block内部使用完weakself后,将其置为nil,但这种方式有个问题,如果block一直不被调用,那么循环引用将一直存在。 所以,我们最好还是用__weak来修饰self
三、RunLoop剖析
RunLoop是通过内部维护的 事件循环(Event Loop) 来对 事件/消息进行管理 的一个对象。
1、没有消息处理时,休眠已避免资源占用,由用户态切换到内核态(CPU-内核态和用户态) 2、有消息需要处理时,立刻被唤醒,由内核态切换到用户态
为什么main函数不会退出?
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
复制代码
UIApplicationMain内部默认开启了主线程的RunLoop,并执行了一段无限循环的代码(不是简单的for循环或while循环)
//无限循环代码模式(伪代码)
int main(int argc, char * argv[]) {
BOOL running = YES;
do {
// 执行各种任务,处理各种事件
// ......
} while (running);
return 0;
}
复制代码
UIApplicationMain函数一直没有返回,而是不断地接收处理消息以及等待休眠,所以运行程序之后会保持持续运行状态。
二、RunLoop的数据结构
NSRunLoop(Foundation) 是 CFRunLoop(CoreFoundation) 的封装,提供了面向对象的API RunLoop 相关的主要涉及五个类:
CFRunLoop :RunLoop对象 CFRunLoopMode :运行模式 CFRunLoopSource :输入源/事件源 CFRunLoopTimer :定时源 CFRunLoopObserver :观察者
1、CFRunLoop
由 pthread (线程对象,说明RunLoop和线程是一一对应的)、 currentMode (当前所处的运行模式)、 modes (多个运行模式的集合)、 commonModes (模式名称字符串集合)、 commonModelItems (Observer,Timer,Source集合)构成
2、CFRunLoopMode
由name、source0、source1、observers、timers构成
3、CFRunLoopSource
分为source0和source1两种
source0 source1
4、CFRunLoopTimer
基于时间的触发器,基本上说的就是NSTimer。在预设的时间点唤醒RunLoop执行回调。因为它是基于RunLoop的,因此它不是实时的(就是NSTimer 是不准确的。 因为RunLoop只负责分发源的消息。如果线程当前正在处理繁重的任务,就有可能导致Timer本次延时,或者少执行一次)。
5、CFRunLoopObserver
监听以下时间点: CFRunLoopActivity
kCFRunLoopEntry kCFRunLoopBeforeTimers kCFRunLoopBeforeSources kCFRunLoopBeforeWaiting kCFRunLoopAfterWaiting kCFRunLoopExit kCFRunLoopAllActivities
6、各数据结构之间的联系
线程和RunLoop一一对应, RunLoop和Mode是一对多的,Mode和source、timer、observer也是一对多的
三、RunLoop的Mode
关于Mode首先要知道一个RunLoop 对象中可能包含多个Mode,且每次调用 RunLoop 的主函数时,只能指定其中一个 Mode(CurrentMode)。切换 Mode,需要重新指定一个 Mode 。主要是为了分隔开不同的 Source、Timer、Observer,让它们之间互不影响。
当RunLoop运行在Mode1上时,是无法接受处理Mode2或Mode3上的Source、Timer、Observer事件的
总共是有五种 CFRunLoopMode :
-
kCFRunLoopDefaultMode:默认模式,主线程是在这个运行模式下运行 -
UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响) -
UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用 -
GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到 -
kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式,是同步Source/Timer/Observer到多个Mode中的一种解决方案
四、RunLoop的实现机制
这张图在网上流传比较广。 对于RunLoop而言最核心的事情就是保证线程在没有消息的时候休眠,在有消息时唤醒,以提高程序性能。RunLoop这个机制是依靠系统内核来完成的(苹果操作系统核心组件Darwin中的Mach)。
RunLoop通过 mach_msg() 函数接收、发送消息。它的本质是调用函数 mach_msg_trap() ,相当于是一个系统调用,会触发内核状态切换。在用户态调用 mach_msg_trap() 时会切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工作。 即基于port的source1,监听端口,端口有消息就会触发回调;而source0,要手动标记为待处理和手动唤醒RunLoop
Mach消息发送机制 大致逻辑为: 1、通知观察者 RunLoop 即将启动。 2、通知观察者即将要处理Timer事件。 3、通知观察者即将要处理source0事件。 4、处理source0事件。 5、如果基于端口的源(Source1)准备好并处于等待状态,进入步骤9。 6、通知观察者线程即将进入休眠状态。 7、将线程置于休眠状态,由用户态切换到内核态,直到下面的任一事件发生才唤醒线程。
- 一个基于 port 的Source1 的事件(图里应该是source0)。
- 一个 Timer 到时间了。
- RunLoop 自身的超时时间到了。
- 被其他调用者手动唤醒。
8、通知观察者线程将被唤醒。 9、处理唤醒时收到的事件。
- 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2。
- 如果输入源启动,传递相应的消息。
- 如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2
10、通知观察者RunLoop结束。
五、RunLoop与NSTimer
一个比较常见的问题:滑动tableView时,定时器还会生效吗? 默认情况下RunLoop运行在 kCFRunLoopDefaultMode 下,而当滑动tableView时,RunLoop切换到 UITrackingRunLoopMode ,而Timer是在 kCFRunLoopDefaultMode 下的,就无法接受处理Timer的事件。 怎么去解决这个问题呢?把Timer添加到 UITrackingRunLoopMode 上并不能解决问题,因为这样在默认情况下就无法接受定时器事件了。 所以我们需要把Timer同时添加到 UITrackingRunLoopMode 和 kCFRunLoopDefaultMode 上。 那么如何把timer同时添加到多个mode上呢?就要用到 NSRunLoopCommonModes 了
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 复制代码
Timer就被添加到多个mode上,这样即使RunLoop由 kCFRunLoopDefaultMode 切换到 UITrackingRunLoopMode 下,也不会影响接收Timer事件
六、RunLoop和线程
- 线程和RunLoop是一一对应的,其映射关系是保存在一个全局的 Dictionary 里
- 自己创建的线程默认是没有开启RunLoop的
1、怎么创建一个常驻线程?
1、为当前线程开启一个RunLoop(第一次调用 [NSRunLoop currentRunLoop]方法时实际是会先去创建一个RunLoop) 1、向当前RunLoop中添加一个Port/Source等维持RunLoop的事件循环(如果RunLoop的mode中一个item都没有,RunLoop会退出) 2、启动该RunLoop
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
复制代码
2、输出下边代码的执行顺序
NSLog(@"1");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2");
[self performSelector:@selector(test) withObject:nil afterDelay:10];
NSLog(@"3");
});
NSLog(@"4");
- (void)test
{
NSLog(@"5");
}
复制代码
答案是1423,test方法并不会执行。 原因是如果是带afterDelay的延时函数,会在内部创建一个 NSTimer,然后添加到当前线程的RunLoop中。也就是如果当前线程没有开启RunLoop,该方法会失效。 那么我们改成:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2");
[[NSRunLoop currentRunLoop] run];
[self performSelector:@selector(test) withObject:nil afterDelay:10];
NSLog(@"3");
});
复制代码
然而test方法依然不执行。 原因是如果RunLoop的mode中一个item都没有,RunLoop会退出。即在调用RunLoop的run方法后,由于其mode中没有添加任何item去维持RunLoop的时间循环,RunLoop随即还是会退出。 所以我们自己启动RunLoop,一定要在添加item后
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2");
[self performSelector:@selector(test) withObject:nil afterDelay:10];
[[NSRunLoop currentRunLoop] run];
NSLog(@"3");
});
复制代码
3、怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作?
当我们在子请求数据的同时滑动浏览当前页面,如果数据请求成功要切回主线程更新UI,那么就会影响当前正在滑动的体验。 我们就可以将更新UI事件放在主线程的 NSDefaultRunLoopMode 上执行即可,这样就会等用户不再滑动页面,主线程RunLoop由 UITrackingRunLoopMode 切换到 NSDefaultRunLoopMode 时再去更新UI
[self performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]]; 复制代码
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 26个精选的JavaScript面试问题
- 鹅厂,iOS高级开发精选面试题!
- 精选前端核心面试题及解答(基础篇)
- 27道Redis精选面试题,你会做几题?
- 面试还搞不懂Redis,快看看这40道面试题!| 博文精选
- 算法和编程面试题精选TOP50!(附代码+解题思路+答案)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
JavaScript & jQuery
David Sawyer McFarland / O Reilly / 2011-10-28 / USD 39.99
You don't need programming experience to add interactive and visual effects to your web pages with JavaScript. This Missing Manual shows you how the jQuery library makes JavaScript programming fun, ea......一起来看看 《JavaScript & jQuery》 这本书的介绍吧!