iOS面试题精选

一、SDWebImage原理

二、什么是Block?

三、RunLoop剖析


1、 SDWebImage原理


一个为UIImageView提供一个分类来支持远程服务器图片加载的库。前端

功能简介:

一、一个添加了web图片加载和缓存管理的UIImageView分类
      二、一个异步图片下载器
      三、一个异步的内存加磁盘综合存储图片而且自动处理过时图片
      四、支持动态gif图
      五、支持webP格式的图片
      六、后台图片解压处理
      七、确保一样的图片url不会下载屡次
      八、确保伪造的图片url不会重复尝试下载
      九、确保主线程不会阻塞
复制代码

工做流程

一、入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,而后 SDWebImageManager 根据 URL 开始处理图片。

二、进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:,交给 SDImageCache 从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:.

三、先从内存图片缓存查找是否有图片,若是内存中已经有图片缓存,SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。

四、SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展现图片。

五、若是内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。

六、根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操做,因此回主线程进行结果回调 notifyDelegate:。

七、若是上一操做从硬盘读取到了图片,将图片添加到内存缓存中(若是空闲内存太小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:。进而回调展现图片。

八、若是从硬盘缓存目录读取不到图片,说明全部缓存都不存在该图片,须要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。

九、共享或从新生成一个下载器 SDWebImageDownloader 开始下载图片。

十、图片下载由 NSURLConnection 来作,实现相关 delegate 来判断图片下载中、下载完成和下载失败。

十一、connection:didReceiveData: 中利用 ImageIO 作了按图片下载进度加载效果。connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 作图片解码处理。

十二、图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。若是有须要对下载的图片进行二次处理,最好也在这里完成,效率会好不少。

1三、在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。

1四、通知全部的 downloadDelegates 下载完成,回调给须要的地方展现图片。将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。

1五、SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过时图片。

1六、SDWI 也提供了 UIButton+WebCache 和 MKAnnotationView+WebCache,方便使用。

1七、SDWebImagePrefetcher 能够预先下载图片,方便后续使用。
复制代码

源码分析

主要用到的对象

1、图片下载

一、 SDWebImageDownloaderweb

  • 1.单例,图片下载器,负责图片异步下载,并对图片加载作了优化处理数组

  • 2.图片的下载操做放在一个NSOperationQueue并发操做队列中,队列默认最大并发数是6缓存

  • 3.每一个图片对应一些回调(下载进度,完成回调等),回调信息会存在downloader的URLCallbacks(一个字典,key是url地址,value是图片下载回调数组)中,URLCallbacks可能被多个线程访问,因此downloader把下载任务放在一个barrierQueue中,并设置屏障保证同一时间只有一个线程访问URLCallbacks。,在建立回调URLCallbacks的block中建立了一个NSOperation并添加到NSOperationQueue中。安全

  • 4.每一个图片下载都是一个operation类,建立后添加到一个队列中,SDWebimage定义了一个协议 SDWebImageOperation做为图片下载操做的基础协议,声明了一个cancel方法,用于取消操做。bash

@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

复制代码

二、SDWebImageDownloaderOption服务器

  • 1.继承自NSOperation类,没有简单实现main方法,而是采用更加灵活的start方法,以便本身管理下载的状态网络

  • 2.start方法中建立了下载使用的NSURLConnections对象,开启了图片的下载,并抛出一个下载开始的通知,数据结构

  • 3.小结:下载的核心是利用NSURLSession加载数据,每一个图片的下载都有一个operation操做来完成,并将这些操做放到一个操做队列中,这样能够实现图片的并发下载。并发

三、SDWebImageDecoder(异步对图片进行解码)

2、缓存

减小网络流量,下载完图片后存储到本地,下载再获取同一张图片时,直接从本地获取,提高用户体验,能快速从本地获取呈现给用户。 SDWebImage提供了对图片进行了缓存,主要由SDImageCache完成。该类负责处理内存缓存以及一个可选的磁盘缓存,其中磁盘缓存的写操做是异步的,不会对UI形成影响。

一、内存缓存及磁盘缓存

  • 1.内存缓存的处理由NSCache对象实现,NSCache相似一个集合的容器,它存储key-value对,相似于nsdictionary类,咱们一般使用缓存来临时存储短期使用但建立昂贵的对象,重用这些对象能够优化新能,同时这些对象对于程序来讲不是紧要的,若是内存紧张就会自动释放。

  • 2.磁盘缓存的处理使用NSFileManager对象实现,图片存储的位置位于cache文件夹,另外SDImageCache还定义了一个串行队列来异步存储图片。

  • 3.SDImageCache提供了大量方法来缓存、获取、移除及清空图片。对于图片的索引,咱们经过一个key来索引,在内存中,咱们将其做为NSCache的key值,而在磁盘中,咱们用这个key值做为图片的文件名,对于一个远程下载的图片其url实做为这个key的最佳选择。

二、存储图片 先在内存中放置一份缓存,若是须要缓存到磁盘,将磁盘缓存操做做为一个task放到串行队列中处理,会先检查图片格式是jpeg仍是png,将其转换为响应的图片数据,最后吧数据写入磁盘中(文件名是对key值作MD5后的串)

三、查询图片 内存和磁盘查询图片API:

- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;

复制代码

查看本地是否存在key指定的图片,使用一下API:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
复制代码

四、移除图片 移除图片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;

复制代码

五、清理图片(磁盘)

清空磁盘图片能够选择彻底清空和部分清空,彻底清空就是吧缓存文件夹删除。

- (void)clearDisk;
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
复制代码

部分清理 会根据设置的一些参数移除部分文件,主要有两个指标:文件的缓存有效期(maxCacheAge:默认是1周)和最大缓存空间大小(maxCacheSize:若是全部文件大小大于最大值,会按照文件最后修改时间的逆序,以每次一半的递归来移除哪些过早的文件,知道缓存文件总大小小于最大值),具体代码参考- (void)cleanDiskWithCompletionBlock;

六、小结 SDImageCache处理提供以上API,还提供了获取缓存大小,缓存中图片数量等API, 经常使用的接口和属性:

(1)-getSize  :得到硬盘缓存的大小

(2)-getDiskCount : 得到硬盘缓存的图片数量

(3)-clearMemory  : 清理全部内存图片

(4)- removeImageForKey:(NSString *)key  系列的方法 : 从内存、硬盘按要求指定清除图片

(5)maxMemoryCost  :  保存在存储器中像素的总和

(6)maxCacheSize  :  最大缓存大小 以字节为单位。默认没有设置,也就是为0,而清理磁盘缓存的先决条件为self.maxCacheSize > 0,因此0表示无限制。

(7)maxCacheAge : 在内存缓存保留的最长时间以秒为单位计算,默认是一周

复制代码

3、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的图片是否进行了磁盘缓存

复制代码

4、视图扩展

在使用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}

5、技术点

  • 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图片的处理。

2、什么是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内部封装了函数,那么它一样也有参数和返回值。

2、Block变量截获

一、局部变量截获 是值截获。 好比:

NSInteger num = 3;

    NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){

        return n*num;
    };

    num = 1;

    NSLog(@"%zd",block(2));

复制代码

这里的输出是6而不是2,缘由就是对局部变量num的截获是值截获。 一样,在block里若是修改变量num,也是无效的,甚至编译器会报错。

二、局部静态变量截获 是指针截获。

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,也是有效的。

三、全局变量,静态全局变量截获:不截获,直接取值。

咱们一样用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。

3、Block的几种形式

  • 分为全局Block(_NSConcreteGlobalBlock)、栈Block(_NSConcreteStackBlock)、堆Block(_NSConcreteMallocBlock)三种形式

    其中栈Block存储在栈(stack)区,堆Block存储在堆(heap)区,全局Block存储在已初始化数据(.data)区

一、不使用外部变量的block是全局block

好比:

NSLog(@"%@",[^{
        NSLog(@"globalBlock");
    } class]);

复制代码

输出:

__NSGlobalBlock__

复制代码

二、使用外部变量而且未进行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]);
}

复制代码

三、对栈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


3、RunLoop剖析


RunLoop是经过内部维护的事件循环(Event Loop)来对事件/消息进行管理的一个对象。

一、没有消息处理时,休眠已避免资源占用,由用户态切换到内核态(CPU-内核态和用户态) 二、有消息须要处理时,马上被唤醒,由内核态切换到用户态

为何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函数一直没有返回,而是不断地接收处理消息以及等待休眠,因此运行程序以后会保持持续运行状态。

2、RunLoop的数据结构

NSRunLoop(Foundation)CFRunLoop(CoreFoundation)的封装,提供了面向对象的API RunLoop 相关的主要涉及五个类:

CFRunLoop:RunLoop对象 CFRunLoopMode:运行模式 CFRunLoopSource:输入源/事件源 CFRunLoopTimer:定时源 CFRunLoopObserver:观察者

一、CFRunLoop

pthread(线程对象,说明RunLoop和线程是一一对应的)、currentMode(当前所处的运行模式)、modes(多个运行模式的集合)、commonModes(模式名称字符串集合)、commonModelItems(Observer,Timer,Source集合)构成

二、CFRunLoopMode

由name、source0、source一、observers、timers构成

三、CFRunLoopSource

分为source0和source1两种

  • source0: 即非基于port的,也就是用户触发的事件。须要手动唤醒线程,将当前线程从内核态切换到用户态
  • source1: 基于port的,包含一个 mach_port 和一个回调,可监听系统端口和经过内核和其余线程发送的消息,能主动唤醒RunLoop,接收分发系统事件。 具有唤醒线程的能力

四、CFRunLoopTimer

基于时间的触发器,基本上说的就是NSTimer。在预设的时间点唤醒RunLoop执行回调。由于它是基于RunLoop的,所以它不是实时的(就是NSTimer 是不许确的。 由于RunLoop只负责分发源的消息。若是线程当前正在处理繁重的任务,就有可能致使Timer本次延时,或者少执行一次)。

五、CFRunLoopObserver

监听如下时间点:CFRunLoopActivity

  • kCFRunLoopEntry RunLoop准备启动
  • kCFRunLoopBeforeTimers RunLoop将要处理一些Timer相关事件
  • kCFRunLoopBeforeSources RunLoop将要处理一些Source事件
  • kCFRunLoopBeforeWaiting RunLoop将要进行休眠状态,即将由用户态切换到内核态
  • kCFRunLoopAfterWaiting RunLoop被唤醒,即从内核态切换到用户态后
  • kCFRunLoopExit RunLoop退出
  • kCFRunLoopAllActivities 监听全部状态

六、各数据结构之间的联系

线程和RunLoop一一对应, RunLoop和Mode是一对多的,Mode和source、timer、observer也是一对多的

3、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中的一种解决方案

4、RunLoop的实现机制

这张图在网上流传比较广。 对于RunLoop而言最核心的事情就是保证线程在没有消息的时候休眠,在有消息时唤醒,以提升程序性能。RunLoop这个机制是依靠系统内核来完成的(苹果操做系统核心组件Darwin中的Mach)。

RunLoop经过mach_msg()函数接收、发送消息。它的本质是调用函数mach_msg_trap(),至关因而一个系统调用,会触发内核状态切换。在用户态调用 mach_msg_trap()时会切换到内核态;内核态中内核实现的mach_msg()函数会完成实际的工做。 即基于port的source1,监听端口,端口有消息就会触发回调;而source0,要手动标记为待处理和手动唤醒RunLoop

Mach消息发送机制 大体逻辑为: 一、通知观察者 RunLoop 即将启动。 二、通知观察者即将要处理Timer事件。 三、通知观察者即将要处理source0事件。 四、处理source0事件。 五、若是基于端口的源(Source1)准备好并处于等待状态,进入步骤9。 六、通知观察者线程即将进入休眠状态。 七、将线程置于休眠状态,由用户态切换到内核态,直到下面的任一事件发生才唤醒线程。

  • 一个基于 port 的Source1 的事件(图里应该是source0)。
  • 一个 Timer 到时间了。
  • RunLoop 自身的超时时间到了。
  • 被其余调用者手动唤醒。

八、通知观察者线程将被唤醒。 九、处理唤醒时收到的事件。

  • 若是用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2。
  • 若是输入源启动,传递相应的消息。
  • 若是RunLoop被显示唤醒并且时间还没超时,重启RunLoop。进入步骤2

十、通知观察者RunLoop结束。

5、RunLoop与NSTimer

一个比较常见的问题:滑动tableView时,定时器还会生效吗? 默认状况下RunLoop运行在kCFRunLoopDefaultMode下,而当滑动tableView时,RunLoop切换到UITrackingRunLoopMode,而Timer是在kCFRunLoopDefaultMode下的,就没法接受处理Timer的事件。 怎么去解决这个问题呢?把Timer添加到UITrackingRunLoopMode上并不能解决问题,由于这样在默认状况下就没法接受定时器事件了。 因此咱们须要把Timer同时添加到UITrackingRunLoopModekCFRunLoopDefaultMode上。 那么如何把timer同时添加到多个mode上呢?就要用到NSRunLoopCommonModes

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
复制代码

Timer就被添加到多个mode上,这样即便RunLoop由kCFRunLoopDefaultMode切换到UITrackingRunLoopMode下,也不会影响接收Timer事件

6、RunLoop和线程

  • 线程和RunLoop是一一对应的,其映射关系是保存在一个全局的 Dictionary 里
  • 本身建立的线程默认是没有开启RunLoop的

一、怎么建立一个常驻线程?

一、为当前线程开启一个RunLoop(第一次调用 [NSRunLoop currentRunLoop]方法时实际是会先去建立一个RunLoop) 一、向当前RunLoop中添加一个Port/Source等维持RunLoop的事件循环(若是RunLoop的mode中一个item都没有,RunLoop会退出) 二、启动该RunLoop

@autoreleasepool {
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
复制代码

二、输出下边代码的执行顺序

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");
    });
复制代码

三、怎样保证子线程数据回来更新UI的时候不打断用户的滑动操做?

当咱们在子请求数据的同时滑动浏览当前页面,若是数据请求成功要切回主线程更新UI,那么就会影响当前正在滑动的体验。 咱们就能够将更新UI事件放在主线程的NSDefaultRunLoopMode上执行便可,这样就会等用户再也不滑动页面,主线程RunLoop由UITrackingRunLoopMode切换到NSDefaultRunLoopMode时再去更新UI

[self performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
复制代码
相关文章
相关标签/搜索