iOS面試題精選

iOS_時光荏苒發表於2019-05-24

1、SDWebImage原理

2、什麼是Block?

3、RunLoop剖析


一、 SDWebImage原理


一個為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: 即非基於port的,也就是使用者觸發的事件。需要手動喚醒執行緒,將當前執行緒從核心態切換到使用者態
  • source1: 基於port的,包含一個 mach_port 和一個回撥,可監聽系統埠和通過核心和其他執行緒傳送的訊息,能主動喚醒RunLoop,接收分發系統事件。 具備喚醒執行緒的能力

4、CFRunLoopTimer

基於時間的觸發器,基本上說的就是NSTimer。在預設的時間點喚醒RunLoop執行回撥。因為它是基於RunLoop的,因此它不是實時的(就是NSTimer 是不準確的。 因為RunLoop只負責分發源的訊息。如果執行緒當前正在處理繁重的任務,就有可能導致Timer本次延時,或者少執行一次)。

5、CFRunLoopObserver

監聽以下時間點:CFRunLoopActivity

  • kCFRunLoopEntry RunLoop準備啟動
  • kCFRunLoopBeforeTimers RunLoop將要處理一些Timer相關事件
  • kCFRunLoopBeforeSources RunLoop將要處理一些Source事件
  • kCFRunLoopBeforeWaiting RunLoop將要進行休眠狀態,即將由使用者態切換到核心態
  • kCFRunLoopAfterWaiting RunLoop被喚醒,即從核心態切換到使用者態後
  • kCFRunLoopExit RunLoop退出
  • kCFRunLoopAllActivities 監聽所有狀態

6、各資料結構之間的聯絡

執行緒和RunLoop一一對應, RunLoop和Mode是一對多的,Mode和source、timer、observer也是一對多的

iOS面試題精選

三、RunLoop的Mode

關於Mode首先要知道一個RunLoop 物件中可能包含多個Mode,且每次呼叫 RunLoop 的主函式時,只能指定其中一個 Mode(CurrentMode)。切換 Mode,需要重新指定一個 Mode 。主要是為了分隔開不同的 Source、Timer、Observer,讓它們之間互不影響。

iOS面試題精選

當RunLoop執行在Mode1上時,是無法接受處理Mode2或Mode3上的Source、Timer、Observer事件的

總共是有五種CFRunLoopMode:

  • kCFRunLoopDefaultMode:預設模式,主執行緒是在這個執行模式下執行

  • UITrackingRunLoopMode:跟蹤使用者互動事件(用於 ScrollView 追蹤觸控滑動,保證介面滑動時不受其他Mode影響)

  • UIInitializationRunLoopMode:在剛啟動App時第進入的第一個 Mode,啟動完成後就不再使用

  • GSEventReceiveRunLoopMode:接受系統內部事件,通常用不到

  • kCFRunLoopCommonModes:偽模式,不是一種真正的執行模式,是同步Source/Timer/Observer到多個Mode中的一種解決方案

四、RunLoop的實現機制

iOS面試題精選

這張圖在網上流傳比較廣。 對於RunLoop而言最核心的事情就是保證執行緒在沒有訊息的時候休眠,在有訊息時喚醒,以提高程式效能。RunLoop這個機制是依靠系統核心來完成的(蘋果作業系統核心元件Darwin中的Mach)。

iOS面試題精選

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同時新增到UITrackingRunLoopModekCFRunLoopDefaultMode上。 那麼如何把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]];
複製程式碼

相關文章