SDWebImage 源碼分析

SDWebImage 源碼分析

首先我 fork 了 SDWebImage 的源碼,見 conintet/SDWebImage珊燎,這樣在本文的鏈接中都是鏈到我的 fork 中,這么做的目的是防止將來 SDWebImage 代碼發(fā)生變化導(dǎo)致本文的鏈接不準(zhǔn)確。

有關(guān) SD (SDWebImage 簡稱為 SD) 的使用方式還是得參考其 README 或者 wiki许起。本文只是閱讀其源碼的筆記笋熬。

圖片下載

最先分析的就是圖片下載部分的代碼热某,因?yàn)檫@是最核心的功能。

因?yàn)?SD 在 UIImageView 上通過 Category 的方式增加了簡單易用的 API突诬,類似下面:

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;

于是通過幾步 Jump to Definition 就可以發(fā)現(xiàn)苫拍,SD 的圖片下載操作是由 SDWebImageDownloaderOperation 來完成的,于是看一下它的初始化方法:

- (id)initWithRequest:(NSURLRequest *)request
              options:(SDWebImageDownloaderOptions)options
             progress:(SDWebImageDownloaderProgressBlock)progressBlock
            completed:(SDWebImageDownloaderCompletedBlock)completedBlock
            cancelled:(SDWebImageNoParamsBlock)cancelBlock;

通過上面的方法簽名旺隙,可以大概反向的知道:

  1. 使用了 NSURLRequest绒极,那么很可能內(nèi)部就使用的 NSURLConnection 來完成的下載
  2. 既然提供了 progresscompleted 這兩個 callback蔬捷,那么內(nèi)部勢必需要知道下載的進(jìn)度
  3. 因?yàn)樘峁┝?cancelled 這個 callback垄提,那么內(nèi)部的下載操作還需要可以取消

再看一下 SDWebImageDownloaderOperation 是繼承于 NSOperation,因?yàn)橄螺d是一個可以獨(dú)立出來的計算單元周拐,所以作為 Opreation 是很好理解的铡俐。然后在實(shí)際的圖片下載中,為了下載的效率妥粟,下載的 Opreations 之間肯定是需要并發(fā)的审丘。Operation 默認(rèn)在其被調(diào)用的線程中是同步執(zhí)行的,不過由于 Operation Queue 的存在勾给,它可以將其中的 Operations 分別 attach 到由系統(tǒng)控制的線程中滩报,而這些由系統(tǒng)控制的線程之間是并發(fā)執(zhí)行的。

查看 SDWebImageDownloaderOperation 源碼發(fā)現(xiàn)內(nèi)部果然是使用的 NSURLConnection播急,那么由于需要提供 cancelled 的功能以及需要監(jiān)聽下載進(jìn)度脓钾,故必須將 NSURLConnection 的實(shí)例配置成異步的方式:

具體代碼在 L96

// 配置異步 NSURLConnection 的方式

// 實(shí)例化一個 NSURLConnection,并將自身(SDWebImageDownloaderOperation)設(shè)置為 NSURLConnection 實(shí)例的委托
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
// 因?yàn)樯弦徊降?startImmediately:NO桩警,所以這里手動的觸發(fā) start
// 這樣的效果和直接 startImmediately:YES 是一樣的
[self.connection start];
// 因?yàn)樯厦鎯刹浇Y(jié)合起來或者直接 startImmediately:YES 的結(jié)果就是下載例程將會在當(dāng)前 Run Loop 上以默認(rèn)的模式進(jìn)行調(diào)度可训,
// 而在 iOS 中除了主線程之外的線程都是默認(rèn)沒有運(yùn)行 Run Loop 的,所以需要手動的運(yùn)行一下
CFRunLoopRun();
// 之后的代碼將會被 CFRunLoopRun() 所阻塞,這樣 operation 所在的線程
// 就不會自動的退出握截,于是需要額外的代碼在下載完成之后手動的停止 RunLoop 使得
// operation 所在的線程可以退出

對于下載進(jìn)度的監(jiān)聽飞崖,SDWebImageDownloaderOperation 是通過將自身設(shè)置為 NSURLConnection 委托的形式完成的:

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response 在這一委托方法的實(shí)現(xiàn)中,SDWebImageDownloaderOperation 主要是獲取服務(wù)端響應(yīng)的 meta 信息川蒙,嘗試根據(jù)響應(yīng)的 statusCode 對下載過程進(jìn)行預(yù)判笔咽,比如如果是 304 狀態(tài)碼直接從本地緩存中返回圖片吼旧。但是這里的代碼寫的有些繁瑣了旋恼,并且性能上也是存在些問題泛鸟。首先可以看下這幅概覽圖:

URL Loading System
URL Loading System

上面就是 URL Loading System 的層次結(jié)構(gòu),可見 NSHTTPURLResponseNSURLResponse 唯一的子類康聂,并且含有其父類沒有的 statusCode 方法贰健。于是使用 isKindOfClass: 來判斷參數(shù)是否是 NSHTTPURLResponse 就可以了,使用 respondsToSelector: 沒有額外的好處而且丟失了性能恬汁,見 Performance penalty using respondsToSelector

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data 通過實(shí)現(xiàn)這個委托方法伶椿,就可以知道有 new response chunk 被接收,于是可以向外提供 progress氓侧,另外 SD 還實(shí)現(xiàn)了 display image progressively脊另,按照代碼中的描述,出自于這里 Progressive image download with ImageIO约巷,其中有一小段是說 iOS 的實(shí)現(xiàn)相對于 Mac 需要點(diǎn)額外的步驟偎痛,而我將其示例代碼下載了之后,在注釋掉其中關(guān)于 iOS 適配的部分代碼后運(yùn)行独郎,發(fā)現(xiàn)注釋掉也是可以的:

        /// Create the image
        CGImageRef image = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL);
        if (image) {
//#ifdef __IPHONE_4_0 // iOS
//            CGImageRef imgTmp = [self createTransitoryImage:image];
//            if (imgTmp) {
//                [_delegate downloadedImageUpdated:imgTmp];
//                CGImageRelease(imgTmp);
//            }
//#else // Mac OS
//            [_delegate downloadedImageUpdated:image];
//#endif
            [_delegate downloadedImageUpdated:image];
            CGImageRelease(image);
        }

也就是說這段 L290 代碼實(shí)際是有一點(diǎn)性能問題的踩麦,應(yīng)該找到一個臨界的版本號以此適配老版本,而不是直接 TARGET_OS_IPHONE氓癌。

還有一點(diǎn)在使用時需要注意的就是谓谦,如果需要獲得具體的 progress 百分比,那么在 new chunk 到達(dá)的時候贪婉,除了需要知道已經(jīng)下載了的 chunks 的 size 總和之外反粥,還需要知道 Content-Length,也就是在這里試圖通過響應(yīng)的 meta 信息(HTTP Headers)中獲取 expectedContentLength疲迂。

而根據(jù) HTTP 協(xié)議的描述 [1, 2]星压,如果服務(wù)端的響應(yīng)采用了 chunked 的方式,那么客戶端實(shí)現(xiàn)必須忽略服務(wù)端響應(yīng)中的 Content-Length(如果有的話鬼譬。按照標(biāo)準(zhǔn)定義,在使用 chunked 時逊脯,服務(wù)端也應(yīng)該不返回 Content-Length优质,當(dāng)然一般情況下也沒法返回),換句話說,如果服務(wù)端響應(yīng)的圖片信息使用 chunked transfer encoding 的話巩螃,那么客戶端在圖片沒有完全下載好之前就無法知道圖片的總大小演怎,于是試圖顯示一個下載百分比的進(jìn)度條就不行了。這段算是 tips 吧避乏。

- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection爷耀,需要知道下載完成的時間點(diǎn),故實(shí)現(xiàn)了這個委托方法

在另外的一些委托方法中拍皮,SD 完成了取消下載的相應(yīng)操作歹叮,以及當(dāng)請求的 HTTPS 證書不可信時的操作,以及當(dāng)服務(wù)端資源需要訪問授權(quán)時的操作铆帽。

小結(jié)

SD 通過 SDWebImageDownloaderOperation 將圖片的下載操作封裝成 NSOperation咆耿,在內(nèi)部通過設(shè)置 NSURLConnection 為異步的方式,并將自身設(shè)置為 NSURLConnection 委托爹橱,從而向外部提供下載進(jìn)度控制的功能萨螺。

圖片緩存

下一步需要分析的就是 SD 的緩存機(jī)制,首先從 SD 的 README 中得知 SD 提供了常見了 two-levels cache 機(jī)制愧驱,即 memory-disk 的方式慰技。在上一段分析下載的過程里,發(fā)現(xiàn) SD 下載圖片還是借由的 NSURLConnection组砚,從 Understanding Cache Access 得知吻商,iOS 中的 URL loading system 已經(jīng)自帶了 two-levels cache 的機(jī)制,那么為什么 SD 需要自己再實(shí)現(xiàn)一套呢惫确?SD 自己是這樣解釋的手报,完整的解釋見 How is SDWebImage better than X?,大概的意思就是:

雖然 NSURLCache 提供了 two-levels cache改化,但是它緩存的內(nèi)容是 raw bytes掩蛤,也就是說從 NSURLCache 中取出的是圖片的 raw bytes,如果需要使用圖片還需要進(jìn)行進(jìn)一步的操作陈肛,比如解析圖片的信息揍鸟,使其成為在 iOS 中可以使用的形式。而 SD 的緩存的則是將解析后的可以在 iOS 中直接使用的圖片句旱,這樣從緩存中取回的內(nèi)容就不需要在解析一遍了阳藻,從而進(jìn)一步節(jié)約了系統(tǒng)資源。

進(jìn)一步了解 two-levels cache 或者 N-levels cache谈撒,其核心思想就是將需要緩存的內(nèi)容放到多個 cache storages 中腥泥,然后在取出緩存內(nèi)容時,盡量的從響應(yīng)速度較快的 storage 中取回啃匿。那么很明顯蛔外,對于 memory-disk 這樣的 two-levels cache蛆楞,無非就是將需要緩存的內(nèi)容同時放到 memory 和 disk 中,然后取回的時候先嘗試較快的 storage夹厌,那么勢必先檢索 memory cache storage豹爹,如果 memory cache 沒有命中的話,則嘗試 disk cache storage矛纹。下一步就是分析 SD 中具體是如何完成這些工作的臂聋。

首先 SD 中使用 SDWebImageManager 去集中管理圖片的下載操作,并且 SDWebImageManager 使用了單例的模式或南,在其初始化操作是這樣的:

- (id)init {
    if ((self = [super init])) {
         // 初始化 two-levels cache孩等,它以 SDImageCache 的單例去操作
        _imageCache = [self createCache];
        // 以單例的形式初始化 SDWebImageDownloader
        _imageDownloader = [SDWebImageDownloader sharedDownloader];
        // 存放失敗的 URLs,為了 re-try 的判斷
        _failedURLs = [NSMutableSet new];
        // 正在運(yùn)行的 operations迎献,方便統(tǒng)一的管理
        _runningOperations = [NSMutableArray new];
    }
    return self;
}

執(zhí)行下載操作的是 SDWebImageManager 中的這個方法(具體的實(shí)現(xiàn)在 L110):

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;

downloadImageWithURL 的具體實(shí)現(xiàn)中瞎访,使用了 SDWebImageCombinedOperation 來統(tǒng)一管理兩個操作(主要是取消的功能),一個操作就是先嘗試從緩存中取回圖片吁恍,另一個操作就是如果緩存沒有命中扒秸,嘗試從源地址下載的操作。這樣只要取消 SDWebImageCombinedOperation 就會同時取消那兩個操作冀瓦。

在下載的 subOperation 中伴奥,使用了 weakOperationL183

這是因?yàn)?這里,如果在 subOperation 中沒有使用 weakOperation 的話翼闽,那么就會發(fā)生 retain cycle

                                     retain                            retain
+---------------------------------+           +---------------------+           +----------------------+
|   SDWebImageCombinedOperation   +----------->     cancelBlock     +----------->     subOperation     |
+----------------^----------------+           +---------------------+           +-----------+----------+
                 |                                                                          |
                 |                                                                          |
                 |                                                                          |
                 |                                    retain                                |
                 +--------------------------------------------------------------------------+

另外由于需要在 self.runningOperationadd/remove SDWebImageCombinedOperation 的實(shí)例拾徙,所以加上了 __block 修飾

由于 SDWebImageManager 是單例的形式感局,而其可能在多線程的情況下被調(diào)用尼啡,所以對于其非線程安全的屬性,在操作時使用了 @synchronized 來確保數(shù)據(jù)的完整性询微。

具體的業(yè)務(wù)邏輯是這樣的:

  1. 首先從 SD 自己的緩存存儲中嘗試取回圖片 L149
  2. 如果在 SD 自己的緩存存儲中沒有取到圖片崖瞭,或者選項(xiàng)中標(biāo)記需要刷新緩存,那么此時就需要從源地址下載圖片撑毛,但是之前還需要判斷下源地址是否允許被下載 L158
  3. L159 的意思是书聚,如果選項(xiàng)標(biāo)記需要刷新緩存,但是在本地緩存中找到了相關(guān)圖片藻雌,那么就先使用這個緩存的圖片調(diào)用下 completedBlock雌续,然后再繼續(xù)進(jìn)行下載操作。

其實(shí)這一步放得有些散了胯杭,它是和 L180 以及 L216 搭配起來的驯杜。通過 L180,當(dāng)發(fā)現(xiàn) Response 是被 NSURLCache 緩存的做个,那么 L216 的條件就會滿足艇肴,為什么會滿足呢腔呜?因?yàn)?這里,于是 downloadedImagenil再悼。

滿足條件了于是就什么也沒做(要做的在 L159 已經(jīng)被做了)。也就是說一旦設(shè)置了 SDWebImageRefreshCached 選項(xiàng)膝但,那么在使用 NSURLConnection 下載的時候冲九,發(fā)現(xiàn) Response 是此前緩存的,那么就直接從 SD 的緩存中返回處理好的圖片跟束,這么做的原因上文已經(jīng)說過了 NSURLCache 的緩存是數(shù)據(jù)的 raw bytes莺奸,而 SD 中緩存的圖片數(shù)據(jù)是 out of the box。

  1. 如果新下載了圖片冀宴,那么肯定是要先將其存儲在 SD 緩存中灭贷,SD 提供了緩存選項(xiàng)可以讓調(diào)用者決定是單存 memory 或 disk 或 both,見 L237略贮。

上面主要是分析了 SDWebImageManager 在下載圖片時的操作甚疟,即先檢索本地 SD 緩存,然后再根據(jù)下載選項(xiàng)決定是否從源地址進(jìn)行下載逃延,以及下載好圖片之后將其存放到 SD 緩存中览妖。

并發(fā)下載

在第一節(jié)中介紹了 SD 將下載操作封裝為了 SDWebImageDownloaderOperation。SD 內(nèi)部在使用時揽祥,并不是直接操作 SDWebImageDownloaderOperation 的讽膏,而是使用的 SDWebImageDownloader 單例,在 SDWebImageDownloader 單例初始化的時候拄丰,產(chǎn)生了一個 NSOperationQueue府树,見 L67,并且設(shè)置了對了的并發(fā)數(shù)為 6料按,見 L68奄侠。然后在需要下載的時候,將 SDWebImageDownloaderOperation 實(shí)例添加到了其內(nèi)部的下載隊(duì)列中站绪,這要就完成了并發(fā)下載的功能遭铺。

緩存的細(xì)節(jié)

現(xiàn)在開始分析下 SD 中的一些關(guān)于緩存操作的細(xì)節(jié)。檢索本地 SD 緩存分為兩步恢准,當(dāng)檢索 memory cache storage 時魂挂,采用的是同步的方式,這是因?yàn)閮?nèi)存緩存的操作速度是很快的馁筐,當(dāng)檢索 disk cache storage 時涂召,SD 使用的是異步的方式,見 L372敏沉。SD 將緩存存儲以及其相關(guān)的操作封裝為 SDImageCache 并且以單例的模式進(jìn)行操作果正,SDImageCache 的初始化在 SDWebImageManager 的初始化中進(jìn)行調(diào)用炎码。

有一點(diǎn)需要注意的就是,SD 中實(shí)現(xiàn)的 sharedXXX 方法并不能表示一個確切的單例模式秋泳,具體的描述見 Create singleton using GCD's dispatch_once in Objective C潦闲,如果用其他面向?qū)ο笳Z言描述的話就是,必須將構(gòu)造函數(shù)隱藏起來不要讓外部調(diào)用到迫皱,比如設(shè)置成 private歉闰,然后提供一個類似 getSingleton 的靜態(tài)方法。不過就像上面的鏈接中描述的一樣卓起,如果口頭約定總是使用 sharedXXX 方法來獲取實(shí)例對象的話那也沒有太大的問題和敬。

對于異步的檢索磁盤的方式,SD 采用的是 GCD戏阅,首先在 SDImageCache 初始化時創(chuàng)建了一個 ioQueue昼弟,注意 SD 中采用的是一個 serial queue,見 L99奕筐。使用 serial queue 的目的就是省得使用鎖去管理磁盤數(shù)據(jù)的讀寫了舱痘。

對于內(nèi)存緩存,SD 實(shí)現(xiàn)了一個內(nèi)部類 AutoPurgeCache救欧,它繼承自 NSCache衰粹,功能就是在通過 Notification 來接受內(nèi)存不足的通知,然后清除自身存儲緩存所占用的內(nèi)存空間笆怠。但是注意到一個細(xì)節(jié)铝耻,比如在 L106,看到下面的代碼:

dispatch_async(dispatch_get_main_queue(), ^{
    [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});

為什么需要在主線程上 postNotificationName:(注:如遇到方法的簽名我沒有寫全的情況請不必在意) 呢蹬刷?

具體的內(nèi)容在 Notification Programming Topics瓢捉,大概的意思就是:

Regular notification centers deliver notifications on the thread in which the notification was posted. Distributed notification centers deliver notifications on the main thread. At times, you may require notifications to be delivered on a particular thread that is determined by you instead of the notification center. For example, if an object running in a background thread is listening for notifications from the user interface, such as a window closing, you would like to receive the notifications in the background thread instead of the main thread. In these cases, you must capture the notifications as they are delivered on the default thread and redirect them to the appropriate thread.

上面的一段引用其實(shí)說了幾點(diǎn)內(nèi)容,不過當(dāng)前只需要知道第一句的意思:通常情況下 notification center 會把 posted notifications 派送給與 post 動作所在的同一線程中的 observers办成。而上面的 L106 中的代碼可以看出泡态,它期望的 observers 是在主線程的,那么 observers 就可以在主線程中更新 UI 來給用戶相關(guān)的進(jìn)度提示迂卢。

那為什么需要 dispatch_async 呢某弦?這是因?yàn)?Notification Centers 中描述的:

A notification center delivers notifications to observers synchronously. In other words, when posting a notification, control does not return to the poster until all observers have received and processed the notification. To send notifications asynchronously use a notification queue, which is described in Notification Queues

再看 AutoPurgeCache 中注冊的 observer L24,observer 注冊在 AutoPurgeCache 運(yùn)行時所在的線程而克,根據(jù)上面的第一段引用中的描述靶壮,對于 local notification 而言,postor 和 receiver 需要在同一線程员萍,于是就猜測是不是對于系統(tǒng)通知而言腾降,會在所有的線程上進(jìn)行 notify。但是沒有在 Apple Doc 中找到明確的相關(guān)文字描述碎绎,不過進(jìn)過測試確實(shí)對于系統(tǒng)通知而言螃壤,notifition center 會對進(jìn)程中的所有線程進(jìn)行 notify抗果。下面是測試的代碼:

@interface Worker : NSThread
@end

@implementation Worker

- (void)main
{
    NSLog(@"Worker is running...");
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(testNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];

    [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    NSLog(@"Worker is exiting...");
}

- (void)testNotification
{
    NSLog(@"testNotification");
}

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    Worker* worker = [[Worker alloc] init];
    [worker start];
}

可以運(yùn)行模擬器然后 Hardware -> Simulate Memory Warning 就可以看到子線程是可以接收到通知的。

以上就是我閱讀源碼后的分析奸晴,雖然沒有面面俱到冤馏,也還是希望能有所幫助。

[2015-11-24 修正]

上面有一段這樣說到:

另外由于需要在 self.runningOperation
中 add/remove
SDWebImageCombinedOperation
的實(shí)例寄啼,所以加上了 __block
修飾

我今天回頭看了一下宿接,發(fā)現(xiàn)我之前那樣的描述是不對的。

首先可以看下這里的描述辕录,大概意思就是說如果需要讓那些被 block 所 captured 變量是 mutable 的,那么就需要使用 __block 前綴去修飾梢卸。

那么看看上面提到的 SD 中的代碼走诞,簡化后就是這樣:

// 這里的 __block 不需要
 __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];

operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }

    @synchronized (self.runningOperations) {
        [self.runningOperations removeObject:operation];
    }
}];

return operation;

注意到在 cacheOperation 那一行產(chǎn)生的 block,它對 operation 進(jìn)行了 capture蛤高,但是在 block 內(nèi)部并沒有改變 operation 的指向蚣旱。所以這里的 __block 是不需要的。Obj 對象在 block 是以引用去操作的戴陡,可以想象是對象的內(nèi)存地址被捕獲塞绿,如果是這樣就需要加上 __block

 __block SDWebImageCombinedOperation *operation = nil;

operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
    // 捕獲這 operation,然而我們需要改變它的內(nèi)容
    // 把它的內(nèi)容變成新對象的地址
    // 所以上面使用了 __block 前綴修飾
    operation = [SDWebImageCombinedOperation new]
  
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }

    @synchronized (self.runningOperations) {
        [self.runningOperations removeObject:operation];
    }
}];

return operation;

我看可以使用下面的代碼來驗(yàn)證下上面的說法:

//
//  main.m
//  __block
//
//  Created by mconintet on 11/24/15.
//  Copyright ? 2015 mconintet. All rights reserved.
//

#import <Foundation/Foundation.h>

int main(int argc, const char* argv[])
{
    @autoreleasepool
    {
        static NSMutableArray* arr;
        static dispatch_once_t once;
        dispatch_once(&once, ^{
            arr = [[NSMutableArray alloc] init];
        });

        dispatch_semaphore_t sema = dispatch_semaphore_create(0);
        dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);

        NSInteger opCount = 3;

        for (NSInteger i = opCount; i > 0; i--) {
            NSOperation* op = [[NSOperation alloc] init];

            dispatch_async(queue, ^{
                [arr addObject:op];
            });

            dispatch_async(queue, ^{
                [arr removeObject:op];
                if (![arr count]) {
                    dispatch_semaphore_signal(sema);
                }
            });
        }

        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
        NSLog(@"arr count: %ld", [arr count]);
    }
    return 0;
}

對比下這段代碼:

//
//  main.m
//  __block
//
//  Created by mconintet on 11/24/15.
//  Copyright ? 2015 mconintet. All rights reserved.
//

#import <Foundation/Foundation.h>

int main(int argc, const char* argv[])
{
    @autoreleasepool
    {
        static NSMutableArray* arr;
        static dispatch_once_t once;
        dispatch_once(&once, ^{
            arr = [[NSMutableArray alloc] init];
        });

        dispatch_semaphore_t sema = dispatch_semaphore_create(0);
        dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);

        NSInteger opCount = 3;

        for (NSInteger i = opCount; i > 0; i--) {
            NSOperation* op;

            dispatch_async(queue, ^{
                op = [[NSOperation alloc] init]
                [arr addObject:op];
            });

            dispatch_async(queue, ^{
                [arr removeObject:op];
                if (![arr count]) {
                    dispatch_semaphore_signal(sema);
                }
            });
        }

        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
        NSLog(@"arr count: %ld", [arr count]);
    }
    return 0;
}

你會發(fā)現(xiàn)后一段代碼會被 IDE 提示:

為什么不能賦值恤批?因?yàn)橹羔樀牟东@也是作為了 const异吻,和基本類型一樣。

總結(jié)起來說就是喜庞,objc 對象在 block 中捕獲的是指向其真實(shí)地址的指針诀浪,指針以 const 的形式被捕獲,不使用 __block 修飾就無法改變指針的內(nèi)容延都,但是對于指針指向的對象雷猪,它們的內(nèi)容還是可以改變的。

[2015-11-26 修正]
上面的關(guān)于 NSNotification 的說明有些紕漏晰房,修正見 NSNotificationCenter

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末求摇,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子殊者,更是在濱河造成了極大的恐慌与境,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件幽污,死亡現(xiàn)場離奇詭異嚷辅,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)距误,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門簸搞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來扁位,“玉大人,你說我怎么就攤上這事趁俊∮虺穑” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵寺擂,是天一觀的道長暇务。 經(jīng)常有香客問我,道長怔软,這世上最難降的妖魔是什么垦细? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮挡逼,結(jié)果婚禮上括改,老公的妹妹穿的比我還像新娘。我一直安慰自己家坎,他們只是感情好嘱能,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著虱疏,像睡著了一般惹骂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上做瞪,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天对粪,我揣著相機(jī)與錄音,去河邊找鬼穿扳。 笑死衩侥,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的矛物。 我是一名探鬼主播茫死,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼履羞!你這毒婦竟也來了峦萎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤忆首,失蹤者是張志新(化名)和其女友劉穎爱榔,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體糙及,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡详幽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片唇聘。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡版姑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出迟郎,到底是詐尸還是另有隱情剥险,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布宪肖,位于F島的核電站表制,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏控乾。R本人自食惡果不足惜么介,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蜕衡。 院中可真熱鬧夭拌,春花似錦、人聲如沸衷咽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽镶骗。三九已至,卻和暖如春躲雅,著一層夾襖步出監(jiān)牢的瞬間鼎姊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工相赁, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留相寇,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓钮科,卻偏偏與公主長得像唤衫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子绵脯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容