Objective-C中block實現和技巧學習

LynetteFure 8年前發布 | 5K 次閱讀 iOS開發 移動開發 Objective-C

什么是block?

首先,看一個極簡的block:

int main(int argc, const char * argv[]) {
 @autoreleasepool {

 ^{ };
 }
 return 0;
}

如何聲明一個block在 Objective-C ?

  • As a local variable:

    returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...};
    
  • As a property:

    @property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes);
    
  • As a method parameter:

    - (void)someMethodThatTakesABlock:(returnType (^nullability)(parameterTypes))blockName;
    
  • As an argument to a method call:

    [someObject someMethodThatTakesABlock:^returnType (parameters) {...}];
    
  • As a typedef:

     typedef returnType (^TypeName)(parameterTypes);
    TypeName blockName = ^returnType(parameters) {...};
    

block編譯轉換結構

int myMain()
{
 ^{ } ();

 ^{ } ();

 return 0;
}

對其執行 clang -rewrite-objc 編譯轉換成C++實現,得到以下代碼:

block的實際結構

關于block的數據結構和runtime是開源的,可以在llvm項目看到,或者下載蘋果的 libclosure 庫的源碼來看。蘋果也提供了 在線的代碼查看方式 ,其中包含了很多示例和文檔說明。

接下來觀察下Block_private.h文件中對block的相關結構體的真實定義:

  • invoke,同上文的FuncPtr,block執行時調用的函數指針,block定義時內部的執行代碼都在這個函數中
  • Block_descriptor,block的詳細描述

總體來說,block就是一個里面存儲了指向 函數體中包含定義block時的代碼塊 的函數指針,以及 block外部上下文 變量等信息的結構體。

block的類型

在block runtime中,定義了6種類:

  • _NSConcreteStackBlock 棧上創建的block
  • _NSConcreteMallocBlock 堆上創建的block
  • _NSConcreteGlobalBlock 作為全局變量的block
  • _NSConcreteWeakBlockVariable
  • _NSConcreteAutoBlock
  • _NSConcreteFinalizingBlock

其中我們能接觸到的主要是前3種,后三種用于GC,咱們就先不看了。

block的常見類型有3種:

  • _NSConcreteGlobalBlock(全局)
  • _NSConcreteStackBlock(棧)
  • _NSConcreteMallocBlock(堆)

APUE(Unix環境高級編程)的進程虛擬內存段分布圖:

其中前2種在Block.h種聲明,后1種在Block_private.h中聲明。

NSConcreteGlobalBlock和NSConcreteStackBlock

首先,根據前面兩種類型,編寫以下代碼:

void (^globalBlock)() = ^{

};


int block_type_Main()
{
 void (^stackBlock1)() = ^{

 };

 stackBlock1();
 globalBlock();

 return 0;
}

對其進行編譯轉換后得到以下代碼:

可以看出globalBlock的isa指向了_NSConcreteGlobalBlock,即在全局區域創建,編譯時就已經確定了,位于上圖中的代碼段;stackBlock的isa指向了_NSConcreteStackBlock,即在棧區創建。

NSConcreteMallocBlock

堆中的block無法直接創建,其需要由_NSConcreteStackBlock類型的block拷貝而來(也就是說block需要執行copy之后才能存放到堆中)。由于block的拷貝最終都會調用_Block_copy_internal函數,所以觀察這個函數就可以知道堆中block是如何被創建的了:

三種類型block測試(MRC)

#import "block_in_MRC.h"

typedef long (^BlkSum)(int, int);


@implementation block_in_MRC
+ (void)main
{
 BlkSum blk1 = ^ long (int a, int b) {
 return a + b;
 };
 NSLog(@"blk1 = %@", blk1);// blk1 = <__NSGlobalBlock__: 0x47d0>


 int base = 100;
 BlkSum blk2 = ^ long (int a, int b) {
 return base + a + b;
 };
 NSLog(@"blk2 = %@", blk2); // blk2 = <__NSStackBlock__: 0xbfffddf8>

 BlkSum blk3 = [[blk2 copy] autorelease];
 NSLog(@"blk3 = %@", blk3); // blk3 = <__NSMallocBlock__: 0x902fda0>
}
@end

捕捉變量對block結構的影響

編譯轉換捕捉不同變量類型的block,以對比它們的區別。

局部變量

代碼

// 局部變量

int capture_var_effect_block_Main()
{

 int a;
 ^{a;};

 // 報錯 var is not assignable(missing __block type specifier)
// ^{a = 10;};

 return 0;
}

對其進行編譯轉換后得到以下代碼(注釋不會被編譯):

我們通過指針傳遞

int test()
{
 int a = 0;
 // 利用指針p存儲a的地址
 int *p = &a;

 ^{
 // 通過a的地址設置a的值
 *p = 10;
 }();

 return 0;
}

變量a的生命周期是和方法test的棧相關聯的,當test運行結束,棧隨之銷毀,那么變量a就會被銷毀,p也就成為了野指針。如果block是作為參數或者返回值,這些類型都是跨棧的,也就是說再次調用會造成野指針錯誤。

全局變量

代碼

// 全局靜態
static int a;
// 全局
int b;

int capture_global_var_effect_block()
{
 ^{
 a = 10;
 b = 10;
 }();

 return 0;
}

編譯轉換后

直接使用了a,b變量;

局部靜態變量

代碼

int capture_local_var_effect_block()
{
 static int a;
 // 靜態局部變量是存儲在靜態數據存儲區域的,也就是和程序擁有一樣的生命周期,也就是說在程序運行時,都能夠保證block訪問到一個有效的變量。但是其作用范圍還是局限于定義它的函數中,所以只能在block通過靜態局部變量的地址來進行訪問。
 ^{
 a = 10;
 }();

 return 0;
}

編譯轉換后

__block修飾的變量

代碼

int __block_modify_var()
{
 __block int a;

 ^{
 a = 10;
 }();

 return 0;
}

編譯轉換后

runtime.c _Block_byref_assign_copy 方法

self隱式循環引用

代碼

@implementation self_hidden_retain_cycle
{
 int _a;
 void (^_block)();
}

- (void)test
{
 void (^_block)() = ^{
 _a = 10;
 };
}
@end

編譯轉換后

ObjC對象(MRC)

代碼

@interface MyClass : NSObject {
 NSObject* _instanceObj;
}
@end

@implementation MyClass

NSObject* __globalObj = nil;

- (id) init {
 if (self = [super init]) {
 _instanceObj = [[NSObject alloc] init];
 }
 return self;
}

- (void) test {
 static NSObject* __staticObj = nil;
 __globalObj = [[NSObject alloc] init];
 __staticObj = [[NSObject alloc] init];

 NSObject* localObj = [[NSObject alloc] init];
 __block NSObject* blockObj = [[NSObject alloc] init];

 typedef void (^MyBlock)(void) ;
 MyBlock aBlock = ^{
 NSLog(@"%@", __globalObj);
 NSLog(@"%@", __staticObj);
 NSLog(@"%@", _instanceObj);
 NSLog(@"%@", localObj);
 NSLog(@"%@", blockObj);
 };
 aBlock = [[aBlock copy] autorelease];
 aBlock();

 NSLog(@"%d", [__globalObj retainCount]);
 NSLog(@"%d", [__staticObj retainCount]);
 NSLog(@"%d", [_instanceObj retainCount]);
 NSLog(@"%d", [localObj retainCount]);
 NSLog(@"%d", [blockObj retainCount]);
}
@end

執行結果為1 1 1 2 1。

globalObj和staticObj在內存中的位置是確定的,所以Block copy時不會retain對象。

_instanceObj在Block copy時也沒有直接retain _instanceObj對象本身,但會retain self。所以在Block中可以直接讀寫_instanceObj變量。

localObj在Block copy時,系統自動retain對象,增加其引用計數。

blockObj在Block copy時也不會retain。

非ObjC對象,如GCD隊列dispatch_queue_t。Block copy時并不會自動增加他的引用計數。

Block中使用的ObjC對象的行為

@property (nonatomic, copy) void(^myBlock)(void);

block_in_MRC* obj = [[[block_in_MRC alloc] init] autorelease];
 self.myBlock = ^ {
 // obj doSomething
 };

對象obj在Block被copy到堆上的時候自動retain了一次。因為Block不知道obj什么時候被釋放,為了不在Block使用obj前被釋放,Block retain了obj一次,在Block被釋放的時候,obj被release一次。

不同類型block的復制

block的復制代碼在_Block_copy_internal函數中。

Block的copy、retain、release操作(MRC)

+ (void)test
{
 int base = 100;
 BlkSum blk2 = ^ long (int a, int b) {
 return base + a + b;
 };
 NSLog(@"blk2 = %@", blk2); // blk2 = <__NSStackBlock__: 0xbfffddf8>

 BlkSum blk3 = [[[[[blk2 copy] copy] copy] copy] copy];
 NSLog(@"blk3 = %@", blk3); // blk3 = <__NSMallocBlock__: 0x902fda0>
 NSLog(@"blk3 retainCount = %@", @([blk3 retainCount]));// blk3 retainCount = 1


 BlkSum blk4 = [blk2 copy];
 [blk4 retain];
 NSLog(@"blk4 retainCount = %@", @([blk4 retainCount]));// blk4 retainCount = 1
 [blk4 release];
 NSLog(@"blk4 retainCount = %@", @([blk4 retainCount]));// blk4 retainCount = 1
}

Block_release in runtime.c

  • 對Block不管是retain、copy、release都不會改變引用計數retainCount,retainCount始終是1;
  • NSGlobalBlock:retain、copy、release操作都無效;
  • NSStackBlock:retain、release操作無效,必須注意的是,NSStackBlock在函數返回后,Block內存將被回收。即使retain也沒用。容易犯的錯誤是[[mutableAarry addObject:stackBlock],在函數出棧后,從mutableAarry中取到的stackBlock已經被回收,變成了野指針。正確的做法是先將stackBlock copy到堆上,然后加入數組:[mutableAarry addObject:[[stackBlock copy] autorelease]]。支持copy,copy之后生成新的NSMallocBlock類型對象。
  • NSMallocBlock支持retain、release,雖然retainCount始終是1,但內存管理器中仍然會增加、減少計數。copy之后不會生成新的對象,只是增加了一次引用,類似retain;

ARC中的block

蘋果文檔 提及,在ARC模式下,在棧間傳遞block時,不需要手動copy棧中的block,即可讓block正常工作。主要原因是ARC對棧中的block自動執行了copy,將_NSConcreteStackBlock類型的block轉換成了_NSConcreteMallocBlock的block。

block 實驗

+ (void)main
{
 int i = 10;
 void (^block)() = ^{i;};

 __weak void (^weakBlock)() = ^{i;};

 void (^stackBlock)() = ^{};

 // ARC情況下

 // 創建時,都會在棧中
 // <__NSStackBlock__: 0x7fff5fbff730>
 NSLog(@"%@", ^{i;});

 // 因為stackBlock為strong類型,且捕獲了外部變量,所以賦值時,自動進行了copy
 // <__NSMallocBlock__: 0x100206920>
 NSLog(@"%@", block);

 // 如果是weak類型的block,依然不會自動進行copy
 // <__NSStackBlock__: 0x7fff5fbff728>
 NSLog(@"%@", weakBlock);

 // 如果block是strong類型,并且沒有捕獲外部變量,那么就會轉換成__NSGlobalBlock__
 // <__NSGlobalBlock__: 0x100001110>
 NSLog(@"%@", stackBlock);

 // 在非ARC情況下,產生以下輸出
 // <__NSStackBlock__: 0x7fff5fbff6d0>
 // <__NSStackBlock__: 0x7fff5fbff730>
 // <__NSStackBlock__: 0x7fff5fbff700>
 // <__NSGlobalBlock__: 0x1000010d0>
}

可以看出,ARC對類型為strong且捕獲了外部變量的block進行了copy。并且當block類型為strong,但是創建時沒有捕獲外部變量,block最終會變成 NSGlobalBlock 類型(這里可能因為block中的代碼沒有捕獲外部變量,所以不需要在棧中開辟變量,也就是說,在編譯時,這個block的所有內容已經在代碼段中生成了,所以就把block的類型轉換為全局類型)

block作為參數傳遞

在棧中的block需要注意的情況:

NSMutableArray *arrayM;

void myBlock()
{
 int a = 5;
 Block block = ^ {
 NSLog(@"%d", a);
 };

 [arrayM addObject:block];
 NSLog(@"%@", block);
}

+ (void)test
{
 arrayM = @[].mutableCopy;

 myBlock();

 Block block = [arrayM firstObject];
 // 非ARC這里崩潰
 block();
}

可以看到,ARC情況下因為自動執行了copy,所以返回類型為 NSMallocBlock ,在函數結束后依然可以訪問;而非ARC情況下,需要我們手動調用[block copy]來將block拷貝到堆中,否則因為棧中的block生命周期和函數中的棧生命周期關聯,當函數退出后,相應的堆被銷毀,block也就不存在了。

如果把block的以下代碼刪除:

NSLog(@"%d", a);

那么block就會變成全局類型,在test中訪問也不會出崩潰。

block作為返回值

在非ARC情況下,如果返回值是block,則一般這樣操作:

return [[block copy] autorelease];

對于外部要使用的block,更趨向于把它拷貝到堆中,使其脫離棧生命周期的約束。

block屬性

這里還有一點關于block類型的ARC屬性。上文也說明了,ARC會自動幫strong類型且捕獲外部變量的block進行copy,所以在定義block類型的屬性時也可以使用strong,不一定使用copy。也就是以下代碼:

/** 假如有棧block賦給以下兩個屬性 **/

// 這里因為ARC,當棧block中會捕獲外部變量時,這個block會被copy進堆中
// 如果沒有捕獲外部變量,這個block會變為全局類型
// 不管怎么樣,它都脫離了棧生命周期的約束

@property (strong, nonatomic) Block *strongBlock;

// 這里都會被copy進堆中
@property (copy, nonatomic) Block *copyBlock;

ARC與非ARC(MRC)下的Weak-Strong Dance

ARC

在使用block過程中,經常會遇到 retain cycle 的問題,例如:

- (void)dealloc 
{ 
 [[NSNotificationCenter defaultCenter] removeObserver:_observer]; 
} 

- (void)loadView 
{ 

 [super loadView]; 

 _observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey" 
 object:nil 
 queue:nil 
 usingBlock:^(NSNotification *note) { 
 [self dismissModalViewControllerAnimated:YES]; 
 }]; 
}

在block中用到了self,self會被block retain,而_observer會copy一份該block,就是說_observer間接持有self,同時當前的self也會retain _observer,最終導致self持有_observer,_observer持有self,形成 retain cycle 。

對于在block中的 retain cycle ,在2011 WWDC Session #322 (Objective-C Advancements in Depth)有一個解決方案 weak-strong dance ,很漂亮的名字。其實現如下:

- (void)dealloc 
{ 
 [[NSNotificationCenter defaultCenter] removeObserver:_observer]; 
} 

- (void)loadView 
{ 
 [super loadView]; 
 __weak TestViewController *wself = self; 
 _observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey" 
 object:nil 
 queue:nil 
 usingBlock:^(NSNotification *note) { 
 __strong TestViewController *sself = wself; 
 [sself dismissModalViewControllerAnimated:YES]; 
 }]; 
}

在block中使用self之前先用一個 __weak 變量引用self,導致block不會retain self,打破retain cycle,然后在block中使用wself之前先用 __strong 類型變量引用wself,以確保使用過程中不會dealloc。簡而言之就是推遲對self的retain,在使用時才進行retain。這有點像lazy loading的意思。

注:iOS5以下沒有 __weak ,則需使用 __unsafe_unretained 。

非ARC(MRC)

在非ARC環境中,顯然之前的使用的 __weak 或 __unsafe_unretained 將會是無效的,那么我們需使用另外一種方法來代替,這里就需要用到 __block 。

__block 在ARC和非ARC中有點細微的差別( Automatic Reference Counting : Blocks ):

  • 在ARC中, __block 會自動進行retain

    // ARC 中 `__block`會自動進行retain 實驗
    + (void)test__Block
    {
     // You can use CFGetRetainCount with Objective-C objects, even under ARC:
     NSObject *objc = [[NSObject alloc] init];
     NSLog(@"test__Block-- objc Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)objc));
     __block NSObject *objcNew = objc;
     NSLog(@"test__Block-- objc Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)objc));
    }
    // 輸出
    // test__Block-- objc Retain count is 1
    // test__Block-- objc Retain count is 2
    
  • 在非ARC中, __block 不會自動進行retain

    // 在MRC中 __block不會自動進行retain
    + (void)test__Block
    {
     // You can use CFGetRetainCount with Objective-C objects, even under ARC:
     NSObject *objc = [[NSObject alloc] init];
     NSLog(@"test__Block-- objc Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)objc));
     __block NSObject *objcNew = objc;
     NSLog(@"test__Block-- objc Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)objc));
    }
    // 輸出
    // test__Block-- objc Retain count is 1
    // test__Block-- objc Retain count is 1
    

因此首先要注意的一點就是用 __block 打破 retain cycle 的方法僅在非ARC下有效,下面是非ARC的 weak-strong dance :

- (void)dealloc 
{ 
 [[NSNotificationCenter defaultCenter] removeObserver:_observer]; 
 [_observer release]; 
 [super dealloc]; 
} 

- (void)loadView 
{ 
 [super loadView]; 
 __block TestViewController *bself = self; 
 _observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey" 
 object:nil 
 queue:nil 
 ngBlock:^(NSNotification *note) { 

 [bself retain]; 
 [bself dismissModalViewControllerAnimated:YES]; 
 [bself release]; 

 }]; 
}

將self賦值為 __block 類型變量,在非ARC中 __block 類型變量不會進行retain,從而打破retain cycle,然后在使用bself前進行retain,以確保在使用過程中不會dealloc 。

總結

打破循環引用:

  • ARC下: __week
  • 非ARC(MRC)下:__block

__block的作用:

非ARC(MRC)下

  1. 說明變量可改
  2. 說明指針指向的對象不做隱式retain操作。

ARC下只有1。

 

來自:http://www.lijianfei.cn/2016/07/21/objective-block-learning/

 

 本文由用戶 LynetteFure 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!