探究系列已發(fā)布文章列表,有興趣的同學(xué)可以翻閱一下:
第一篇 | iOS 屬性 @property 詳細(xì)探究
第三篇 | iOS 類別 Category 和擴(kuò)展 Extension 及關(guān)聯(lián)對(duì)象詳解
第四篇 | iOS 常用鎖 NSLock ,@synchronized 等的底層實(shí)現(xiàn)詳解
------- 正文開始 -------
引言
在 iOS 日常開發(fā)中,Block 的使用頻率是比較多的,我們不會(huì)每天都做啟動(dòng)優(yōu)化,也不會(huì)每天都做性能優(yōu)化,但有可能每天都會(huì)用到 Block 。本文就著重介紹一下 Block 在日常開發(fā)中值得我們關(guān)注的技術(shù)點(diǎn),大家一起學(xué)習(xí)。
代碼規(guī)范
// 定義一個(gè) Block
typedef returnType (^BlockName)(parameterA, parameterB, ...);
eg: typedef void (^RequestResult)(BOOL result);
// 實(shí)例
^{
NSLog(@"This is a block");
}
本質(zhì)
Block 本質(zhì)上是一個(gè) Objective-C 的對(duì)象,它內(nèi)部也有一個(gè) isa
指針,它是一個(gè)封裝了函數(shù)及函數(shù)調(diào)用環(huán)境的 Objective-C 對(duì)象,可以添加到 NSArray
及 NSDictionary
等集合中,它是基于 C
語言及運(yùn)行時(shí)特性,有點(diǎn)類似標(biāo)準(zhǔn)的 C
函數(shù)。但除了可執(zhí)行代碼以外,另外包含了變量同堆或棧的自動(dòng)綁定。
常用介紹
- Block 的類型:
- NSGlobalBlock
void (^exampleBlock)(void) = ^{
// block
};
NSLog(@"exampleBlock is: %@",[exampleBlock class]);
打印日志:exampleBlock is: __NSGlobalBlock__
如果一個(gè) block
沒有訪問外部局部變量,或者訪問的是全局變量,或者靜態(tài)局部變量,此時(shí)的 block
就是一個(gè)全局 block
,并且數(shù)據(jù)存儲(chǔ)在全局區(qū)。
- NSStackBlock
int temp = 100;
void (^exampleBlock)(void) = ^{
// block
NSLog(@"exampleBlock is: %d", temp);
};
NSLog(@"exampleBlock is: %@",[exampleBlock class]);
打印日志:exampleBlock is: __NSMallocBlock__
???
不是說好的 __NSStackBlock__
的嗎?為什么打印的是__NSMallocBlock__
呢?這里是因?yàn)槲覀兪褂昧?ARC ,Xcode 默認(rèn)幫我們做了很多事情。
我們可以去 Build Settings
里面,找到 Objective-C Automatic Reference Counting
,并將其設(shè)置為 No
,然后再 Run 一次代碼。你會(huì)看到打印日志是:exampleBlock is: __NSStackBlock__
如果 block
訪問了外部局部變量,此時(shí)的 block
就是一個(gè)棧 block
,并且存儲(chǔ)在棧區(qū)。由于棧區(qū)的釋放是由系統(tǒng)控制,因此棧中的代碼在作用域結(jié)束之后內(nèi)存就會(huì)銷毀,如果此時(shí)再調(diào)用 block
就會(huì)發(fā)生問題,( 注: 此代碼運(yùn)行在 MRC 下)如:
void (^simpleBlock)(void);
void callFunc() {
int age = 10;
simpleBlock = ^{
NSLog(@"simpleBlock-----%d", age);
};
}
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
callFunc();
simpleBlock();
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return 0;
}
打印日志:simpleBlock--------41044160
- NSMallocBlock
當(dāng)一個(gè) __NSStackBlock__
類型 Block 做 copy
操作后就會(huì)將這個(gè) Block 從棧上復(fù)制到堆上,而堆上的這個(gè) Block 類型就是 __NSMallocBlock__
類型。在 ARC 環(huán)境下,編譯器會(huì)根據(jù)情況,自動(dòng)將 Block 從棧上 copy
到堆上。具體會(huì)進(jìn)行 copy
的情況有如下 4 種:
- block 作為函數(shù)的返回值時(shí);
- block 賦值給 __strong 指針,或者賦值給 block 類型的成員變量時(shí);
- block 作為 Cocoa API 中方法名含有 usingBlock 的方法參數(shù)時(shí);
- block 作為 GCD API 的方法參數(shù)時(shí);
- __block 的作用
簡(jiǎn)單來說,__block
作用是允許 block
內(nèi)部訪問和修改外部變量,在 ARC 環(huán)境下還可以用來防止循環(huán)引用;
__block int age = 10;
void (^exampleBlock)(void) = ^{
// block
NSLog(@"1.age is: %d", age);
age = 16;
NSLog(@"2.age is: %d", age);
};
exampleBlock();
NSLog(@"3.age is: %d", age);
__block
主要用來解決 block
內(nèi)部無法修改 auto
變量值的問題,為什么加上 __block
修飾之后,auto
變量值就能修改了呢?
這是因?yàn)椋由?__block
修飾之后,編譯器會(huì)將 __block
變量包裝成一個(gè)結(jié)構(gòu)體 __Block_byref_age_0
,結(jié)構(gòu)體內(nèi)部 *__forwarding
是指向自身的指針,并且結(jié)構(gòu)體內(nèi)部還存儲(chǔ)著外部 auto
變量。
struct __Block_byref_val_0 {
void *__isa; // isa指針
__Block_byref_val_0 *__forwarding;
int __flags;
int __size; // Block結(jié)構(gòu)體大小
int age; // 捕獲到的變量
}
從上圖可以看到,如果 block
是在棧上,那么這個(gè) __forwarding
指針就是指向它自己,當(dāng)這個(gè) block
從棧上復(fù)制到堆上后,棧上的 __forwarding
指針指向的是復(fù)制到堆上的 __block
結(jié)構(gòu)體。堆上的 __block
結(jié)構(gòu)體中的 __forwarding
指向的還是它自己,即 age->__forwarding
獲取到堆上的 __block
結(jié)構(gòu)體,age->__forwarding->age
會(huì)把堆上的 age
賦值為 16 。因此不管是棧上還是堆上的 __block
結(jié)構(gòu)體,最終使用到的都是堆上的 __block
結(jié)構(gòu)體里面的數(shù)據(jù)。
- __weak 的作用
簡(jiǎn)單來說是為了防止循環(huán)引用。
self
本身會(huì)對(duì) block
進(jìn)行強(qiáng)引用,block
也會(huì)對(duì) self
形成強(qiáng)引用,這樣就會(huì)造成循環(huán)引用的問題。我們可以通過使用 __weak
打破循環(huán),使 block
對(duì)象對(duì) self
弱引用。
此時(shí)我們注意,由于 block
對(duì) self
的引用為 weak
引用,因此有可能在執(zhí)行 block
時(shí),self
對(duì)象本身已經(jīng)釋放,那么我們?nèi)绾伪WC self
對(duì)象不在 block
內(nèi)部釋放呢?這就引出了下面__strong
的作用。
-
__strong 的作用
簡(jiǎn)單來說,是防止 Block 內(nèi)部引用的外部weak
變量被提前釋放,進(jìn)而在 Block 內(nèi)部無法獲取weak
變量以繼續(xù)使用的情況;
__weak __typeof(self) weakSelf = self;
void (^exampleBlock)(void) = ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
[strongSelf exampleFunc];
};
這樣就保證了在 block
作用域結(jié)束之前,block
內(nèi)部都持有一個(gè) strongSelf
對(duì)象可供使用。
但是,即便如此,依然有一個(gè)場(chǎng)景,就是執(zhí)行 __strong __typeof(weakSelf) strongSelf = weakSelf;
之前,weakSelf
對(duì)象已經(jīng)釋放,這時(shí)如果給 self
對(duì)象發(fā)送消息,這沒有問題,Objective-C 的消息發(fā)送機(jī)制允許我們給一個(gè) nil
對(duì)象發(fā)送消息,這不會(huì)出現(xiàn)問題。
但如果有額外的一些操作,比如說將 self
添加到數(shù)組,這時(shí)因?yàn)?self
為 nil
,程序就會(huì) Crash。
我們可以增加一層安全保護(hù)來解決這個(gè)問題,如:
__weak __typeof(self) weakSelf = self;
void (^exampleBlock)(void) = ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
// Add operation here
}
};
拓展知識(shí)
- 思考題
Block
內(nèi)修改外部 NSMutableString
、NSMutableArray
、NSMutableDictionary
對(duì)象,是否需要添加 __block
修飾?
NSMutableArray *mutableArray = [[NSMutableArray alloc] init];
[mutableArray addObject:@"1"];
void (^exampleBlock)(void) = ^{
// block
[mutableArray addObject:@"2"];
};
exampleBlock();
NSLog(@"mutableArray: %@", mutableArray);
打印日志:
mutableArray: (
1,
2
)
答案是:不需要。因?yàn)樵?block
內(nèi)部,我們只是使用了對(duì)象 mutableArray
的內(nèi)存地址,往其中添加內(nèi)容。并沒有修改其內(nèi)存地址,因此不需要使用 __block
也可以正確執(zhí)行。當(dāng)我們只是使用局部變量的內(nèi)存地址,而不是對(duì)其內(nèi)存地址進(jìn)行修改時(shí),我們無需對(duì)其添加 __block
,如果添加了 __block
系統(tǒng)會(huì)自動(dòng)創(chuàng)建相應(yīng)的結(jié)構(gòu)體,這種情況冗余且低效。
- Block 數(shù)據(jù)結(jié)構(gòu)
Block 內(nèi)部數(shù)據(jù)結(jié)構(gòu)圖如下:
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
Block_layout
結(jié)構(gòu)體成員含義如下:
isa:
指向所屬類的指針,也就是 block 的類型
flags:
按 bit 位表示一些 block 的附加信息,比如判斷 block 類型、判斷 block 引用計(jì)數(shù)、判斷 block 是否需要執(zhí)行輔助函數(shù)等;
reserved:
保留變量;
invoke:
block 函數(shù)指針,指向具體的 block 實(shí)現(xiàn)的函數(shù)調(diào)用地址,block 內(nèi)部的執(zhí)行代碼都在這個(gè)函數(shù)中;
descriptor:
結(jié)構(gòu)體 Block_descriptor,block 的附加描述信息,包含 copy/dispose 函數(shù),block 的大小,保留變量;
variables:
因?yàn)?block 有閉包性,所以可以訪問 block 外部的局部變量。這些 variables 就是復(fù)制到結(jié)構(gòu)體中的外部局部變量或變量的地址;
Block_descriptor
結(jié)構(gòu)體成員含義如下:
reserved:
保留變量;
size:
block 的大小;
copy:
函數(shù)用于捕獲變量并持有引用;
dispose:
析構(gòu)函數(shù),用來釋放捕獲的資源;
總結(jié)
使用 Block 過程中需要我們關(guān)注的重點(diǎn)有 4 個(gè):
- block 的三種類型;
- block 避免引起循環(huán)引用;
- block 對(duì) auto 變量的 copy 操作;
- __block、__weak、__strong 的作用;
以上就是本文對(duì) Block 的相關(guān)知識(shí)點(diǎn)的介紹,感謝閱讀。
參考資料:
關(guān)于技術(shù)組
iOS 技術(shù)組主要用來學(xué)習(xí)、分享日常開發(fā)中使用到的技術(shù),一起保持學(xué)習(xí),保持進(jìn)步。文章倉庫在這里:https://github.com/minhechen/iOSTechTeam
微信公眾號(hào):iOS技術(shù)組,歡迎聯(lián)系交流,感謝閱讀。