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]];
複製代碼
相關文章
相關標籤/搜索