iOS 如果你使用過block,最好看一下這篇文章

Perfect_Dream發表於2017-12-20

首先,本文並不會涉及太多block的原始碼,更多的是block使用方面的一些東西。 新人寫文,如有錯誤,敬請斧正!

原理

使用也好認識也好,首要任務就是搞懂block到底是個什麼東西,其中的內容我們使用到的都有哪些,所以先介紹一下block的本體。block的本體是一個結構體,長這樣:

struct __main_block_impl_0 {  
  struct __block_impl impl;  
  struct __main_block_desc_0* Desc;  
  __main_block_impl_0(voidvoid *fp, struct __main_block_desc_0 *desc, int flags=0) {  
    impl.isa = &_NSConcreteStackBlock;  
    impl.Flags = flags;  
    impl.FuncPtr = fp;  
    Desc = desc;  
  }  
};  
複製程式碼

1.第一個成員變數:impl也是一個結構體,長這樣:

struct __block_impl {  
    voidvoid *isa;  
    int Flags;  
    int Reserved;  
    voidvoid *FuncPtr;  
};  
複製程式碼

impl結構體第一個成員變數是isa指標,與NSObject及其派生類物件的isa不同,impl指向的是block三種型別的其中一種。 impl第二第三個成員變數可以忽略過去。 第四個成員變數是一個函式指標,也就是block所執行的程式碼段的真正地址。

block的三種型別(重點)

1._NSConcreteGlobalBlock,儲存在資料區域 2._NSConcreteMallocBlock,儲存在堆控制元件 3._NSConcreteStackBlock,儲存在棧控制元件

2.第二個成員變數:Desc直接跳過,用不到 3.第三個成員變數:第三個成員變數是一個結構體函式,如果細說也會有些篇幅,所以就不多做解釋了,我們需要知道的是:block宣告的時候會呼叫這個結構體函式進行賦值,記憶體地址會指向impl結構體的isa指標,需要執行程式碼塊地址會指向impl結構體的函式指標FuncPtr,其他的忽略過去就好了。

宣告

1.block被宣告的時候會例項化型別為__NSGlobalBlock的block,根據後續使用會轉換成不同型別的block。 2.block被宣告的時候block塊內程式碼會轉換成__main_block_func_0函式,引數就是__main_block_impl_0結構體。 3.block被宣告實際上就是將__main_blcok_impl_0結構體例項化。 4.block被宣告之後的呼叫是通過impl結構體的FuncPtr指標呼叫,引數就是結構體本身。

以上就是block宣告會幹的事情

使用

屬性的宣告有三種方式,所以block的宣告也有三種方式

1.私有區域性block 2.私有全域性block 3.公有全域性block

然後來看一段程式碼和其執行結過:

定義.png
結果.png

上邊程式碼定義了一個私有區域性block,在宣告之初這個block是__NSGlobalBlock__型別的。以上述三種方式宣告的block,在最初都是__NSGlobalBlock__型別的

然後看另一段程式碼:

使用外部區域性私有屬性.png
結果.png

列印結果顯示,在block內部輸出block本身為空,因為block是區域性變數,在block內部輸出block本身的時候區域性block已經被釋放,所以在block內部輸出block本身為空。block外部輸出block本身的時候,型別已經變為__NSMallocBlock__。表明block的儲存地址已經被修改了,並且存放空間已經由資料區域(全域性區域,後不做表述)轉移到堆區域,然後解釋一下為什麼會這樣。

無論是哪種方式宣告的block在使用外部變數或屬性的時候,block預設會將其複製一份加入到自己的結構體中。所以block內部使用的變數越多其體積就會越大。並且block只能訪問這個區域性變數,不能對其作出任何修改

如果想對這個變數做出修改需要在區域性變數宣告的時候加上__block修飾符,__block的原理可以看我這篇文章,解釋同樣簡單粗暴。這裡只做簡單解釋,被__block修飾後的變數在block內部會變成名為Block_byref_(變數名)_0的結構體例項,該結構體會包含一個對該結構體例項本身的引用,此時對變數做修改的時候會通過Block_byref_(變數名)_0結構體中名為forwarding的成員變數間接訪問這個變數。

block的記憶體地址

block的記憶體地址位置需要仔細說一下,這會涉及到後邊迴圈引用的問題

1.初始化剛剛結束的時候,block會被放到資料區。 2.在block內部訪問外界變數,block會被轉移到棧空間儲存。 3.在ARC環境下,訪問外界變數的block會先被轉移到棧空間,然後copy到堆空間。

在MRC環境下,開發者可以手動管理block的引用計數,可以避免棧空間的block被提前釋放影響後續使用。但是在ARC環境下開發者不能管理其引用計數,所以block預設會被copy到堆空間。

會引起迴圈引用的block使用方式

區域性block

我在區域性block做出了以下嘗試,都沒有引起迴圈引用。

1.在區域性block內--使用外部臨時變數 2.在區域性block內--使用外部私有全域性變數 3.在區域性block內--使用外部全域性私有變數 4.在區域性block內--修改其他類屬性 5.在區域性block內--呼叫全域性方法 3.在區域性block內--使用全域性block回撥到來源類執行耗時操作 3.在區域性block內--誇三介面使用block執行耗時操作

根據上述內容得知,在block內部使用的屬性會被copy一份加入到自己的結構體中,而區域性block早在其所在方法體結束的時候就被釋放了。所以在此大膽猜測,區域性block無論任何情況都不會引起迴圈引用。如果其結構體成員變數指向物件有延時(或耗時)操作,會在操作結束後做釋放操作。(敬請斧正!)

全域性block

全域性block分兩種,一種是私有全域性block,另一種是公有全域性block,兩種block的情況基本相同,本不應該分開敘述,但是公有全域性block會涉及到其他情況,所以還是分開描述一下。接下來就全域性block會引起迴圈引用的情況做一下說明。

1.私有全域性block 只要是全域性block,無論哪種全域性block都會有很多種情況造成迴圈引用。 比如:

  1. 在全部block內--使用外部全域性屬性
  2. 在全部block內--呼叫外部方法
  3. 在全部block內--修改其他類屬性(或呼叫其他類方法、例項方法)

2.公有全域性block 公有全域性block的迴圈引用情況和私有的基本差不多,唯一的區別就是公有block會被其他類呼叫,如果其他類裡邊有迴圈引用,就會導致當前本來沒有迴圈引用的類也不被釋放。 比如: A類有迴圈引用,呼叫了B類,會導致B類也無法釋放。

其實解決block迴圈引用的方法特別多,眾所周知的是這個方法:

__weak __typeof(self) weakSelf = self;

為自己做一個弱引用指標確實可以解決當前類的迴圈引用,注意:是解決當前類的迴圈引用。就像上述所說的問題,如果你有一個公有全域性block,你的類本身沒有迴圈引用問題,但是如果其他類實現了你的block,如果它有迴圈引用的問題,就會導致你的類也無法釋放,所以還有另外一種解決迴圈引用的方法。

引用一下SDWebImage的程式碼:

- (void)startPrefetchingAtIndex:(NSUInteger)index {
    if (index >= self.prefetchURLs.count) return;
    self.requestedCount++;
    [self.manager downloadImageWithURL:self.prefetchURLs[index] options:self.options progress:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
        if (!finished) return;
        self.finishedCount++;

        if (image) {
            if (self.progressBlock) {
                self.progressBlock(self.finishedCount,[self.prefetchURLs count]);
            }
        }
        else {
            if (self.progressBlock) {
                self.progressBlock(self.finishedCount,[self.prefetchURLs count]);
            }
            // Add last failed
            self.skippedCount++;
        }
        if ([self.delegate respondsToSelector:@selector(imagePrefetcher:didPrefetchURL:finishedCount:totalCount:)]) {
            [self.delegate imagePrefetcher:self
                            didPrefetchURL:self.prefetchURLs[index]
                             finishedCount:self.finishedCount
                                totalCount:self.prefetchURLs.count
             ];
        }
        if (self.prefetchURLs.count > self.requestedCount) {
            dispatch_async(self.prefetcherQueue, ^{
                [self startPrefetchingAtIndex:self.requestedCount];
            });
        } else if (self.finishedCount == self.requestedCount) {
            [self reportStatus];
            if (self.completionBlock) {
                self.completionBlock(self.finishedCount, self.skippedCount);
                self.completionBlock = nil;
            }
            self.progressBlock = nil;
        }
    }];
}
複製程式碼

看這一段程式碼:

            if (self.completionBlock) {
                self.completionBlock(self.finishedCount, self.skippedCount);
                self.completionBlock = nil;
            }
複製程式碼

在block被執行之後將block釋放掉,當然有幾種情況這樣也不能解決你的類的迴圈引用。

1.你的類被其他類宣告成全域性變數。 2.你的類被宣告為臨時變數,但是在block中被呼叫了。

所以,如果你在寫功能就儘量注意不要讓自己的類迴圈引用了。如果你是準備封裝一個功能,儘量不要暴露初始化方法給外邊。

還有一種情況:

畫蛇添足.png
請不要畫蛇添足,本來沒有問題的程式碼,加上這句話就會有問題了。
image.png
這樣子就可以了。時間關係沒有用MRC做過測試,我覺著如果你是MRC,可以自己控制,如果你ARC,就不要這樣寫了。



有志者、事竟成,破釜沉舟,百二秦關終屬楚;

苦心人、天不負,臥薪嚐膽,三千越甲可吞吳.

相關文章