Objective-C內存管理:Block

總述

如下環境都在ARC環境下,常規設置,使用XCode10測試。html

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

  1. Block做爲屬性聲明時爲何都聲明爲Copy?git

  2. Block爲何能保存外部變量?程序員

  3. Block中__block關鍵字爲什麼能同步Block外部和內部的值?github

  4. Block有幾種類型?objective-c

  5. 何時棧上的Block會複製到堆?編程

  6. Block的循環引用應該如何處理?api

  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語言的擴充功能,是一個匿名函數而且能夠截獲(保存)局部變量。經過三個小節來解釋這個概念。app

其餘語言中的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高級編程》中的插圖:

__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

1、哪些狀況下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

相關文章
相關標籤/搜索