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也是一對多的
三、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]];
複製程式碼