Objective-C記憶體管理:Block

Shanesun發表於2018-10-29

總述

以下環境都在ARC環境下,常規設定,使用XCode10測試。

這篇文章會解決以下幾個問題:

  1. Block作為屬性宣告時為什麼都宣告為Copy?

  2. Block為什麼能儲存外部變數?

  3. Block中__block關鍵字為何能同步Block外部和內部的值?

  4. Block有幾種型別?

  5. 什麼時候棧上的Block會複製到堆?

  6. Block的迴圈引用應該如何處理?

  7. Block外部__weak typeof(self) weakSelf = self; Block 內部 typeof(weakSelf) strongSelf = weakSelf;,為什麼需要這樣操作?

Block測試:

以下Block在ARC環境下能正常執行嗎?若能分別列印什麼值?

void exampleA_addBlockToArray(NSMutableArray*array) {
    char b = 'A';
 
    [array addObject:^{
        printf("%c\n", b);
    }];
}

void exampleA() {
    NSLog(@"---------- exampleA ---------- \n");
    
    NSMutableArray *array = [NSMutableArray array];
    exampleA_addBlockToArray(array);
    void(^block)(void) = [array objectAtIndex:0];
    block(); 
}
複製程式碼
void exampleB_addBlockToArray(NSMutableArray *array) {
    [array addObject:^{
        printf("B\n");
    }];
}

void exampleB() {
    NSLog(@"---------- exampleB ---------- \n");
    
    NSMutableArray *array = [NSMutableArray array];
    exampleB_addBlockToArray(array);
    void(^block)(void) = [array objectAtIndex:0];
    block(); 
}
複製程式碼
typedef void(^cBlock)(void);
cBlock exampleC_getBlock() {
    char d = 'C';
    return^{
        printf("%c\n", d);
    };
}

void exampleC() {
    NSLog(@"---------- exampleC ---------- \n");
    
    cBlock blk_c = exampleC_getBlock();
    blk_c();  
}
複製程式碼
NSArray* exampleD_getBlockArray() {
    int val = 10;
    
    return [[NSArray alloc] initWithObjects:^{NSLog(@"blk1:%d",val);}, ^{NSLog(@"blk0:%d",val);}, ^{NSLog(@"blk0:%d",val);}, nil];
}

void exampleD() {
    NSLog(@"---------- exampleD ---------- \n");
    
    typedef void (^blk_t)(void);
    NSArray *array = exampleD_getBlockArray();
    
    NSLog(@"array count = %ld", [array count]);
    blk_t blk = (blk_t)[array objectAtIndex:1];
    
    blk();  
}
複製程式碼
NSArray* exampleE_getBlockArray() {
    int val = 10;
 
    NSMutableArray *mutableArray = [NSMutableArray new];
    [mutableArray addObject:^{NSLog(@"blk0:%d",val);}];
    [mutableArray addObject:^{NSLog(@"blk1:%d",val);}];
    [mutableArray addObject:^{NSLog(@"blk2:%d",val);}];
    
    return mutableArray;
}

void exampleE() {
    NSLog(@"---------- exampleE ---------- \n");
    
    typedef void (^blk_t)(void);
    NSArray *array = exampleE_getBlockArray();
    NSLog(@"array count = %ld", [array count]);
    
    blk_t blk = (blk_t)[array objectAtIndex:1];
    blk(); 
}
複製程式碼
void exampleF() {
    NSLog(@"---------- exampleF ---------- \n");
    
    typedef void (^blk_f)(id obj);
   
    __unsafe_unretained blk_f blk;
    {
        id array = [[NSMutableArray alloc] init];
        
        blk = ^(id obj) {
            [array addObject:obj];
            NSLog(@"array count = %ld", [array count]);
        };
    }
    
    blk([[NSObject alloc] init]);   
    blk([[NSObject alloc] init]);   
}
複製程式碼
void exampleG() {
    NSLog(@"---------- exampleG ---------- \n");
    
    typedef void (^blk_f)(id obj);
    blk_f blk;
    {
        id array = [[NSMutableArray alloc] init];
        
        blk = ^(id obj) {
            [array addObject:obj];
            NSLog(@"array count = %ld", [array count]);
        };
    }
    
    blk([[NSObject alloc] init]);   
    blk([[NSObject alloc] init]);   
}
複製程式碼
void exampleH() {
    NSLog(@"---------- exampleH ---------- \n");
    
    typedef void (^blk_f)(id obj);
    blk_f blk;
    {
        id array = [[NSMutableArray alloc] init];
        id __weak weakArray = array;
        
        blk = ^(id obj) {
            [weakArray addObject:obj];
            NSLog(@"array count = %ld", [weakArray count]);
        };
    }
    
    blk([[NSObject alloc] init]);   
    blk([[NSObject alloc] init]);  
}
複製程式碼
void exampleI() {
    NSLog(@"---------- exampleI ---------- \n");
    
    typedef void (^blk_g)(id obj);
    blk_g blk;
    {
        id array = [[NSMutableArray alloc] init];
        __block id __weak blockWeakArray = array;
        
        blk = [^(id obj) {
            [blockWeakArray addObject:obj];
            NSLog(@"array count = %ld", [blockWeakArray count]);
        } copy];
    }
    
    blk([[NSObject alloc] init]);  
    blk([[NSObject alloc] init]); 
}
複製程式碼

什麼是Block

Objective-C中的Block中文名閉包,是C語言的擴充功能,是一個匿名函式並且可以截獲(儲存)區域性變數。通過三個小節來解釋這個概念。

其他語言中的Block概念

程式語言 Block的名稱
Swift Closures
Smalltalk Block
Ruby Block
LISP Lambda
Python Lambda
Javascript Anonymous function

為什麼Block的寫法很彆扭?

因為Block是在模仿C語言函式指標的寫法:

int func(int count) {
    return count + 1;
}
// int (^tmpBlock)(int i) = ...
int (*funcptr)(int) = &func;
複製程式碼

但是Block的寫法依舊非常難記,國外的朋友更是專門寫了一個叫fuckingblock網頁提供Block的各種寫法。

截獲區域性變數(或叫自動變數)

    // 演示擷取區域性變數
    int tmpVal = 10;
    void (^blk)(void) = ^{
        printf("val = %d", tmpVal); // val = 10
    };
    
    tmpVal = 2;
    blk();
複製程式碼

這裡依舊顯示val = 10,Block會擷取當前狀態下val的值。至於為什麼能截獲區域性變數的值,我們下一節中討論。

Block實現原理

Block結構

通過clang -rewrite-objc main.m將上面的示例程式碼翻譯成C,關鍵程式碼如下:

// Block基礎結構
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
複製程式碼

Block如何擷取區域性變數

// 根據示例中blk的實現,生成不同的 __main_block_impl_0 結構體。
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int tmpVal;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _tmpVal, int flags=0) : tmpVal(_tmpVal) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製程式碼

根據上面的程式碼能解決我們3個疑惑:

  1. __block_impl中有isa指標,那麼Block也是一個物件。
  2. 生成不同的__main_block_impl_0,這裡結構裡面包含int tmpVal就是我們區域性變數,而__main_block_impl_0的建構函式中是值傳遞。所以block內部截獲的變數不受外部影響。
  3. __main_block_impl_0建構函式中有個void *fp函式指標指向的就是block實現。

我們向上面示例程式碼再新增多一些變數型別:

static int outTmpVal = 30; // 靜態全域性變數

int main(int argc, char * argv[]) {
    int tmpVal = 10;				// 區域性變數		
    static int localTmpVal = 20;	// 區域性靜態變數
    NSMutableArray *localMutArray = [NSMutableArray new];	// 區域性OC物件
    
    void (^blk)(void) = ^{
        printf("val = %d\n", tmpVal); // val = 10
        printf("localTmpVal = %d\n", localTmpVal); // localTmpVal = 21
        printf("outTmpVal = %d\n", outTmpVal); // outTmpVal = 31
        
        [localMutArray addObject:@"newObj"];
        printf("localMutArray.count = %d\n", (int)localMutArray.count); // localMutArray.count = 2
    };
    
    tmpVal = 2;
    localTmpVal = 21;
    outTmpVal = 31;
    [localMutArray addObject:@"startObj"];
    blk();
}

複製程式碼

對應輸出結果為:
val = 10
localTmpVal = 21
outTmpVal = 31
localMutArray.count = 2
clang -rewrite-objc main.m後關鍵程式碼如下:

static int outTmpVal = 30;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int tmpVal;
  int *localTmpVal;
  NSMutableArray *localMutArray;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _tmpVal, int *_localTmpVal, NSMutableArray *_localMutArray, int flags=0) : tmpVal(_tmpVal), localTmpVal(_localTmpVal), localMutArray(_localMutArray) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製程式碼

因為涉及到OC物件,這裡還會有2個新的方法,這2個方法會放到後面講:

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->localMutArray, (void*)src->localMutArray, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src){
    _Block_object_dispose((void*)src->localMutArray, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
複製程式碼
  1. static int outTmpVal = 30;儲存在記憶體中的.data段,static限制了作用域,該檔案作用域內可修改。
  2. static int localTmpVal = 20;int main(int argc, char * argv[]) { }作用域可修改,注意__main_block_impl_0建構函式中是傳遞的*_localTmpVal指標,所以外部修改Block內部同樣有效,因為是static所以,Block內部也可以修改localTmpVal的值。
  3. NSMutableArray *localMutArray__main_block_impl_0傳遞的是指向的地址,所以localMutArray內部操作對於block內同樣有效。
  1. 靜態變數的這種方式同樣也可以作用到區域性變數上,傳遞一個指標到block內,通過指標來讀取指向的值,通知也可以修改。但是這種方式在block離開區域性變數所在作用域後再呼叫就會出現問題,因為區域性變數已經被釋放。
  2. static int localTmpVal = 20;能通過指標的方式修改值,NSMutableArray *localMutArray修改指向的值為什麼不可以? 這是clang對於Block內修改指標的一個保護措施。

總結下:

  1. 靜態變數靜態全域性變數全域性變數都可以訪問,修改,保持同一份值。
  2. OC物件,可以進行內部操作。但不能修改OC物件的值(指向的記憶體地址)。

__block關鍵字如何實現?

同樣的方式,我們先看__block用C是怎麼實現的,下面是一段使用__block的程式碼:

int main(int argc, char * argv[]) {
    __block int val = 10;
    void (^blk)(void) = ^{
        val = 1;
        printf("val = %d", val);
    };
    
    blk();
}
複製程式碼

翻譯成C,只保留關鍵程式碼:

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};
複製程式碼

這就是__block對應C中的新結構體:

  1. *__forwarding是一個與自己同型別的指標。
  2. int val;這個變數就是為了儲存原本__block int val = 10;的值。
  3. 並且__block int val = 10;對應的結構體__Block_byref_val_0也是和之前一樣建立在棧上的。

接下來繼續看,blk的結構:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
    
  __main_block_impl_0.... // 和之前的__block_impl構造方式一致
};
複製程式碼

blk結構內部新增了__Block_byref_val_0 *val作為成員變數,和之前原理一致。

blk的實現val = 1;

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref

        (val->__forwarding->val) = 1;
        printf("val = %d", (val->__forwarding->val));
    }
複製程式碼

(val->__forwarding->val) = 1;這句非常重要,不是直接通過val->val進行賦值操作,而是經過__forwarding指標進行賦值,這帶來非常大的靈活性,現在是blk__block int val都是在棧上,__forwarding也都指向了棧上的__Block_byref_val_0 。以上程式碼解決了在Block內修改外部區域性變數的值。

__block新增了2個方法:__main_block_copy_0__main_block_dispose_0

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign(&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
    
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
複製程式碼

通過方法命名和引數,可以大致猜出是對Block的拷貝和釋放。

Block和__block的儲存區域

通過以上clange的編譯,Block和__block都是有isa指標的,兩者都應該是Objective-C的物件。isa指向的就是它的類物件。在ARC下大致有以下幾種,根據名字可以知道對應儲存空間:

  • _NSConcreteStackBlock 棧上
  • _NSConcreteGlobalBlock 全域性 對應的是.data段
  • _NSConcreteMallocBlock 堆上

clang轉出的結果和執行程式碼時 Block 實際顯示的isa型別是不一樣的,在實際的編譯過程中已經不會經過clang翻譯成C再編譯。

_NSConcreteGloalBlock有兩種情況下可以生成:

  • 宣告的是全域性變數Block。
  • 在作用域內但是不截獲外部變數。

_NSConcreteStackBlock因為在棧上,在函式作用域內宣告的Block。

_NSConcreteMallocBlock正因為_NSConcreteStackBlock的作用域在棧上,超出作用域後想要繼續使用Block,這就得複製到堆上。那些情況會觸發這種複製:

  • ARC下大多數情況會自動複製。比如,棧上block賦值給Strong修飾的屬性時。Block作為一個返回值時(超出作用域還能使用,autorelease處理物件生命週期)。
  • 需要手動copy。向方法或函式的引數中傳遞Block時,編譯器無法判斷是什麼樣的情況,因為從Block從棧上覆制到堆上很消耗cpu。所以編譯器並沒有幫忙copy
  • Cocoa框架的方法且方法名中含有usingBlock等時,不用外部copy。內部已經進行copy。
  • GCD的Api,也不用外部copy

這裡有個比較經典的例子(摘自《Objective-C高階程式設計》):

- (id)getBlockArray {
	int val = 10;
	return [[NSArray alloc] initWithObjects:^{NSLog(@"blk0:%d",val);},
            ^{NSLog(@"blk1:%d",val);}, nil];
}

{
	id obj = [self getBlockArray];
	typedef void (^blk_t)(void);
	blk_t blk = (blk_t)[obj objectAtIndex:0]; 

	blk(); 
}
// crash

複製程式碼

在ARC情況下,NSArray 陣列類會有2個元素,第一個在堆上,第二個棧上。在超出getBlockArray作用域後,第二棧上的block會變成野指標。在所有作用域結束時,Array會釋放陣列內所有元素。野指標物件執行銷燬時會觸發崩潰。 正常情況下NSArray應該持有陣列內所有元素。但使用initWithObjects:方法時,發現只有第一個元素進行了持有操作,第二個Block依舊在棧上。當我使用NSMutableArrayaddObject:方法時,每個Block都會進行持有賦值到堆上。我懷疑應該是initWithObjects:方法中多參形式比較特殊。

反覆提到Block就是OC的物件,對於物件Copy會帶來哪些變化:

Block類 原來儲存域 複製產生的影響
_NSConcreteStackBlock 從棧複製到堆
_NSConcreteGlobalBlock .data 無變化
_NSConcreteMallocBlock 引用計數增加

__block的儲存區域

Block是一個OC物件,所以涉及到從棧到堆,引用計數的變更等,常見OC物件記憶體管理的問題。同時Block在堆上時又會對__block進行持有,那麼對於 __block同樣也是OC物件,記憶體管理有什麼區別呢?

Block從棧複製到堆時對__block變數產生的影響:

__block 儲存域 Block 從棧複製到堆時對__block的影響
從棧複製到堆並被Block持有
被Block持有
  • __block從棧上覆制到堆上後,原本棧上的__block依舊會存在,被複制到堆上的__block會被Block持有__block的引用計數會增加,棧上的__block會因為作用域結束而釋放,堆上的__block會在引用計數歸零後釋放。
  • 堆上的__block的記憶體管理就是OC物件的引用計數管理方式,沒有被其他Block持有時引用計數歸0後釋放。

上面提到當__block從棧上覆制到堆上,會有兩個__block產生,一個棧上的一個堆上的。這兩個不同儲存區域的__block是如何實現資料同步的?

這就利用__block關鍵字如何實現?中提到的指向自己的*__forwarding,當持有__block的Block沒有從棧上拷貝到堆上時:*__forwarding指向棧上的__block, 當持有__block的Block拷貝到堆上時後,棧上的__block->__forwarding->堆上的__block,堆上的__block->__forwarding->堆上的__block。讀起來有點繞,借用《Objective-C高階程式設計》中的插圖:

Objective-C記憶體管理:Block

__block 和 OC物件從棧上覆制到堆上?

上面講了Block__block在從棧上覆制到堆上時的一些變化。為了解決__blockOC物件Block結構體內的生命週期問題,新增了一下幾個方法:

  1. __main_block_desc_0中新加2個成員方法:copydispose,這是兩個函式指標,指向的分別就是__main_block_copy_0__main_block_dispose_0
  2. Block中使用OC物件__block關鍵字時新增的2個方法:__main_block_copy_0__main_block_dispose_0 ,這兩個方法用於在Blockcopy到堆上時,管理__blockOC物件的生命週期。

Block

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
}
複製程式碼

OC物件

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->localMutArray, (void*)src->localMutArray, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->localMutArray, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
複製程式碼

__block

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign(&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
複製程式碼

捕獲OC物件和使用__block變數時在引數上會不同:

OC物件 BLOCK_FIELD_IS_OBJECT
__block BLOCK_FIELD_IS_BYREF

_Block_object_assign就相當於retain方法,_Block_object_dispose就相當於release方法,但是我們在clang翻譯的C語言中並沒有發現 __main_block_copy_0__main_block_dispose_0的呼叫。只有在以下時機copydispose方法才會呼叫:

copy函式 棧上的Block複製到堆時
dispose函式 堆上的Block被廢棄時(引用計數為0)

什麼時候棧上的Block會複製到堆?

  1. 呼叫Blockcopy例項方法。
  2. Block作為函式返回值返回時。(autorelease 物件延長生命週期)
  3. Block賦值給附有__strong修飾符的id型別的類或Block型別成員變數(賦值給Strong修飾的Block型別屬性時,編譯器會幫忙複製到堆)。
  4. 在方法名中含有usingBlockCocoa框架方法GCD的api中傳遞Block時。

Block tips

一、哪些情況下Block內self為nil時會引起崩潰?這個時候需要使用Weak-Strong-Dance。

  1. 使用self.blockxxx()時,使用clang轉換成C時,可以看到Bblock的呼叫實際是呼叫Block`內的函式指標與OC物件呼叫發訊息的形式不一樣。

  2. 其他業務場景,比如使用self的成員變數做NSAarryNSDictionary 做增加操作時。

    不要無腦使用,更加清晰的理解Weak-Strong-DanceBlock內部strong selfBlock會繼續持有self,有些場景並不需要。

解答

  1. 宣告成StrongCopy效果都一樣。在ARC環境下編譯會自動將作為屬性的Block從棧Copy到堆,這裡Apple建議繼續使用Copy防止程式設計師忘記編譯器有Copy動作。
  2. Block內部能截獲外部變數。Block結構體中會有建立一個成員變數與截獲的變數型別一直,這個值與截獲時的值一致,這是一個值傳遞,儲存的是一個瞬時值。
  3. __block關鍵字的實現是一個結構體,結構體中有個自己同型別的*_farwarding指標,當Block在棧上,__block也是在棧上時:*_farwarding指向棧上的自己。當Block拷貝到堆,堆中建立的__block*_farwarding指向自己,同時將棧上的*_farwarding指向堆中__block
  4. 三種。棧上,堆上,全域性。
  5. 1 手動copy。2 作為返回值返回。3 將Block賦值給__strong修飾的id型別Block型別成員變數。4 方面名中含有usingBlockcocoa框架方法GCD
  6. 使用__weak弱引用,或者手動斷開強引用。
  7. Block內的weakSelf可能會出現nil的情況,nil可能會造成奔潰或是其他意外結果。所以在Block內作用域內宣告一個Strong型別的區域性變數,在作用域結束後會自動釋放不會造成迴圈引用。

程式設計題目答案,請參考Github上的repo:TestBlock

歷史文章

Objective-記憶體管理:物件

參考

  1. 《Objective-C高階程式設計》
  2. 淺談 block - 截獲變數方式
  3. Blocks Programming Topics
  4. Working with Blocks
  5. fuckingblock

Objective-C記憶體管理:Block

相關文章