iOS探索:Block解析淺談

NO IMAGE

什麼是Block

  • Block是將函數及其執行上下文封裝起來的對象

接下來讓我們通過源碼來看一看Block的本質

iOS探索:Block解析淺談

  • 我們在一個方法中寫了三行代碼,第一行是定義了一個局部變量,第二行是一個Block,第三行是這個Block的調用

這裡我們通過一個clang的編譯命令clang -rewrite-objc xxx.m來看一下源碼的實現

iOS探索:Block解析淺談

  • 我們的那段代碼通過編譯器編寫後,首先第一行I代表的是一個實例方法後面的是對象和方法名,傳了兩個參數一個是self,一個是選擇器因子

  • 然後我們方法中的第一行代碼在編譯後沒有發生改變,我們著重看一下Block方法編譯後的改變

  • 首先我們可以看到__BlockOneObj__testMethod_block_impl_0這樣一個結構體,在這個結構體中傳遞了幾個參數,第一個參數(void*)__BlockOneObj__testMethod_block_func_0我們通過名字可以知道這是一個無類型的函數指針,第二個參數&__BlockOneObj__testMethod_block_desc_0_DATA是一個Block相關描述的結構體然後取地址符,第三個參數muIntNum就是我們定義的局部變量。最後取這個結構體地址強制轉換賦值給我們定義的這個Block

然後我們來看看__BlockOneObj__testMethod_block_impl_0這個結構體中有什麼具體操作,如下圖

iOS探索:Block解析淺談

其中第一個結構體裡面又是什麼數據結構呢,請看下圖

iOS探索:Block解析淺談

在我們上面介紹的結構體下面還有一個函數,具體解釋請看下圖

iOS探索:Block解析淺談

那麼什麼是Block的調用呢

iOS探索:Block解析淺談

Block的調用其實就是函數的調用,從源碼中我們可以看出來

  • 首先先對這個Block進行一個強制類型轉換(__block_impl *)Block

  • 之後又取出它之中的成員變量FuncPtr(函數指針),找到我們上面解析的結構體和函數,在其中拿到對應的函數調用,然後把其中的參數傳遞進去,一個參數是我們這個Block本身,一個是我們傳遞的2,然後就回去調用__BlockOneObj__testMethod_block_func_0函數,最終進行調用

Block截獲變量

首先我們先來看一段代碼

- (void)testMethod {
int muIntNum = 6;
int(^Block)(int) = ^int(int num){
return num *muIntNum;
};
muIntNum = 4;
Block(2);
}

這段代碼執行完Block(2)返回的值是多少呢?——-答案是12
接下來我們看一下為什麼是12以及Block截獲變量的本質是什麼

  • 對於基本數據類型的局部變量截獲其值

  • 對於對象類型的局部變量連同其所有權修飾符一起截獲

  • 對於局部靜態變量是以指針形式去截獲

  • 對於全局變量和靜態全局變量不截獲

下面直接上代碼

#import "BlockTwoObj.h"
//全局變量
int global_var = 4;
//全局靜態變量
static int static_global_var = 5;
@implementation BlockTwoObj
- (void)testMethodTwo {
//基本數據類型的局部變量
int var = 1;
//對象類型的的局部變量
__unsafe_unretained id unsafe_obj = nil;
__strong id strong_obj = nil;
//局部靜態變量
static int static_var = 3;
void(^Block)(void) = ^{
NSLog(@"基本數據類型局部變量:%d", var);
NSLog(@"對象類型局部變量(__unsafe_unretained修飾):%@", unsafe_obj);
NSLog(@"對象類型局部變量(__strong修飾):%@", strong_obj);
NSLog(@"局部靜態變量:%d", static_var);
NSLog(@"全局變量:%d", global_var);
NSLog(@"全局靜態變量:%d", static_global_var);
};
Block();
}
@end

接下來我們通過clang命令clang -rewrite-objc -fobjc-arc xxx.m來看一下源碼

iOS探索:Block解析淺談

  • 在這張圖中可以很清晰的看到Block中的變量截獲,其中需要注意的是對於局部的靜態變量截獲的是指針,也就是說如果後面這個局部靜態變量發生了修改,那麼Block中使用的是最新的值

__block修飾符

我們在什麼情況下使用__block修飾符呢?
一般情況下,對被截獲變量進行賦值操作需要添加__block修飾符,這裡需要注意的是賦值不等於是使用,切記!!!

例如在下面的代碼中是否需要__block修飾符來修飾

NSMutableArray *muArr = [[NSMutableArray alloc] init];
void(^Block)(void) = ^{
//這裡只是做了添加操作,並非賦值,所以不需要用__block進行修飾
[muArr addObject:@"111"];
};
Block();

那麼在下面的代碼段當中呢?

__block NSMutableArray *muArrOther = nil;
void(^BlockOther)(void) = ^{
//這裡做了賦值操作,所以需要用__block進行修飾,否則會出現編譯報錯
muArrOther = [NSMutableArray array];
};
BlockOther();

對變量進行賦值時

  • 需要__block修飾符修飾的是局部變量(包括基本數據類型和對象類型)

  • 不需要__block修飾符修飾的是靜態局部變量、全局變量和靜態全局變量,因為對於全局變量和靜態全局變量不涉及到變量的截獲,而對於靜態局部變量呢,是通過使用指針來操作對應的變量的,所以也不需要修飾

下面請看一段代碼,還是我們上面的那個例子

- (void)testMethod {
__block int muIntNum = 6;
int(^Block)(int) = ^int(int num){
return num *muIntNum;
};
muIntNum = 4;
Block(2);
}

此時Block返回的是8,這裡是為什麼呢,我們只是用了__block來修飾

  • 因為在這裡會發生一個非常奇妙的變化,__block修飾的變量變成了對象

請看下面的流程圖

iOS探索:Block解析淺談

  • 首先__block int muIntNum會被轉化成第一個這樣一個結構體,其中具有isa指針,我們也可以理解成一個對象

  • 從這個角度來看muIntNum經過編譯後就會變成一個對象,通過__forwarding指針去找到對應的對象,然後進行賦值

  • 剛才我們看到的代碼段是在棧上,在__block變量中有一個__forwarding指針,而這個指針指向的是自己,這裡要注意的是前提是在棧上,如果在堆上,這個__forwarding指針指向的就不是自己了,在下面會講到

  • 所以在棧上我們修改這個變量的值,就會通過__forwarding指針找到自己本省去修改這個變量的值

那麼這裡有一個問題就是我們在棧上這個__forwarding指向的是自己到底有什麼用呢?我們完全可以通過訪問成員變量來修改,為什麼還需要這個指針呢,請繼續往下看

Block的內存管理

Block有三種類型

  • _NSConcreteGlobalBlock 全局Block

  • _NSConcreteStackBlock 棧Block

  • _NSConcreteMallocBlock 堆Block

Block的Copy操作

iOS探索:Block解析淺談

  • 當我們棧上的Block通過copy在堆上產生一個一樣的Block,有相同的Block和__block變量,當變量作用於結束後,棧上的Block對象就會被銷燬,而堆上的block依舊存在,所有如果棧上Block不用copy拷貝到堆上,在作用於銷燬後會因為找不到Block對象而崩潰

  • 當然我們在這裡有一個問題,假如說在MRC環境下,如果在棧上進行了copy操作,會不會產生內存洩漏,答案是肯定的,相當於一個對象alloc出來,但是並沒有對應的relese操作一樣

iOS探索:Block解析淺談

  • 當我們棧上的Block經過copy操作後,在堆上會產生一個一樣的Block,在棧中的Block中的__forwarding指針指向的事堆上Block的__block變量,並且在堆上Block的__forwarding指針也是指向的它自己的__block變量

參考書籍

Objective – C 高級編程:iOS與OS X多線程和內存管理

Github

Demo

相關文章

iOSCoreData(二)版本升級和數據庫遷移

iOSCoreData(一)增刪改查

iOS探索:網絡相關

iOS探索:RunLoop本質、數據結構以及常駐線程實現