1.什么是block
block是將函數(shù)及其執(zhí)行上下文封裝起來(lái)的對(duì)象,是一段代碼塊,是一個(gè)結(jié)構(gòu)體送爸,里面有isa指針指向自己的類(global malloc stack),有desc結(jié)構(gòu)體描述block的信息占遥,__forwarding指向自己或堆上自己的地址幼东,如果block對(duì)象截獲變量颖系,這些變量也會(huì)出現(xiàn)在block結(jié)構(gòu)體中。最重要的block結(jié)構(gòu)體有一個(gè)函數(shù)指針忆蚀,指向block代碼塊矾利。block結(jié)構(gòu)體的構(gòu)造函數(shù)的參數(shù)懊悯,包括函數(shù)指針,描述block的結(jié)構(gòu)體梦皮,自動(dòng)截獲的變量(全局變量不用截獲)炭分,引用到的__block變量。(__block對(duì)象也會(huì)轉(zhuǎn)變成結(jié)構(gòu)體)
block代碼塊在編譯的時(shí)候會(huì)生成一個(gè)函數(shù)剑肯,函數(shù)第一個(gè)參數(shù)是前面說(shuō)到的block對(duì)象結(jié)構(gòu)體指針捧毛。執(zhí)行block,相當(dāng)于執(zhí)行block里面__forwarding里面的函數(shù)指針。
2.什么是block調(diào)用
block調(diào)用即是函數(shù)的調(diào)用
3.__block修飾符
一般情況下,對(duì)被截獲變量進(jìn)行賦值操作需添加__block修飾符
__block不能修飾全局變量、靜態(tài)變量(static)
{
NSMutableArray *array = nil;
void(^Block)(void) = ^{
array = [NSMutableArray array];
}
Block();
}
是否存在問(wèn)題?
需要在array聲明處添加__block修飾符
__block int multiplier = 6;
int(^Block)(int) = ^int(int num)
{
return num * multiplier;
};
multiplier = 4;
NSLog(@"result is %d", Block(2));
結(jié)果為8
__block修飾的變量變成了對(duì)象
- _ _block 這個(gè)修飾符做了什么操作呢眷蜈?就是讓block內(nèi)部可以訪問(wèn)自動(dòng)變量
__weak將int類型的數(shù)據(jù)轉(zhuǎn)換成了一個(gè)__Block_byref_i_0的結(jié)構(gòu)體類型
struct __Block_byref_i_0 {
void *__isa;
__Block_byref_i_0 *__forwarding;
int __flags;
int __size;
int i;
};
從賦值上看萨脑,isa為0竿秆,既然有isa指針,那么說(shuō)明這個(gè)結(jié)構(gòu)體也是一個(gè)對(duì)象,__forwarding存儲(chǔ)的是__Block_byref_i_0的地址值,flags為0泞辐,size為Block_byref_i_0的內(nèi)存大小,i是真正存儲(chǔ)變量值的地方,是通過(guò)__Block_byref_i_0結(jié)構(gòu)體的指針__forwarding讀取和修改的變量i.
-
為什么要通過(guò)__forwarding轉(zhuǎn)一下呢竞滓,而不是直接讀取i
這是因?yàn)楫?dāng)我們調(diào)用block的時(shí)候咐吼,block可能存在于棧中可能存在于堆中
__block修飾后的底層實(shí)現(xiàn):
1.__block將int i進(jìn)行包裝,包裝成一個(gè)__Block_byref_i_0結(jié)構(gòu)體對(duì)象商佑,結(jié)構(gòu)體中的i是存儲(chǔ)i的int值的锯茄;
2.當(dāng)我們?cè)赽lock內(nèi)修改或訪問(wèn)該對(duì)象時(shí),是通過(guò)該對(duì)象的__forwarding去找對(duì)應(yīng)的結(jié)構(gòu)體再找對(duì)應(yīng)的屬性值茶没,這是因?yàn)開_forwarding在不同情況下指向不同的地址肌幽,防止只根據(jù)單一的一個(gè)內(nèi)存地址出現(xiàn)變量提前釋放無(wú)法訪問(wèn)的情況。
那么我們就明白為什么可以修改__block修飾的自動(dòng)變量了抓半,__block修飾下的i不再是int類型而變成一個(gè)對(duì)象(對(duì)象p)喂急,我們block內(nèi)部訪問(wèn)和修改的是這個(gè)對(duì)象內(nèi)部的一個(gè)屬性,并不是這個(gè)對(duì)象琅关,所以是可以修改訪問(wèn)的煮岁。只不過(guò)這個(gè)轉(zhuǎn)化為對(duì)象的內(nèi)部過(guò)程封裝起來(lái)不讓開發(fā)者看到,所以就給人的感覺是可以修改auto變量也就是修改時(shí)是int i涣易。
4.block的內(nèi)存管理
//全局block
_NSConcreteGlobalBlock
//棧block
_NSConcreteStackBlock
//堆block
_NSConcreteMallocBlock
為什么捕獲局部變量而不捕獲全局變量?
全局變量:整個(gè)項(xiàng)目都可以訪問(wèn)冶伞,block調(diào)用的時(shí)候可以直接拿到訪問(wèn)新症,不用擔(dān)心變量被釋放的情況;
局部變量:則不同响禽,局部變量是有作用域的徒爹,如果blcok調(diào)用的時(shí)候blcok已經(jīng)被釋放了荚醒,就會(huì)出現(xiàn)嚴(yán)重的問(wèn)題,所以為了避免這個(gè)問(wèn)題block需要捕獲需要的局部變量隆嗅。(比如我們局部變量和block都卸載了viewDidLoad方法界阁,但是我在touchesBegan方法中調(diào)用block,這個(gè)時(shí)候局部變量早就釋放了胖喳,所以block要捕獲局部變量)為什么auto變量是捕獲的值泡躯,而靜態(tài)變量是捕獲的地址呢?
自動(dòng)變量和靜態(tài)變量存儲(chǔ)的區(qū)域不同丽焊,兩者釋放時(shí)間也不同较剃。
自動(dòng)變量:存放在棧中的,創(chuàng)建與釋放是由系統(tǒng)設(shè)置的技健,隨時(shí)可能釋放掉写穴。
靜態(tài)變量:存儲(chǔ)在全局存儲(chǔ)區(qū)的,生命周期和app是一樣的雌贱,不會(huì)被銷毀啊送。
所以對(duì)于隨時(shí)銷毀的自動(dòng)變量肯定是把值拿進(jìn)來(lái)保存了,如果保存自動(dòng)變量的地址欣孤,那么等自動(dòng)變量釋放后我們根據(jù)地址去尋值肯定會(huì)發(fā)生懷內(nèi)存訪問(wèn)的情況删掀,而靜態(tài)變量因?yàn)轫?xiàng)目運(yùn)行中永遠(yuǎn)不會(huì)被釋放,所以保存它的地址值就完全可以了导街,等需要用的時(shí)候直接根據(jù)地址去尋值披泪,就能找到。為什么靜態(tài)變量和全局變量同樣不會(huì)被銷毀搬瑰,為什么一個(gè)被捕獲地址一個(gè)則不會(huì)被捕獲呢款票?
靜態(tài)變量和全局變量因?yàn)閮烧咴L問(wèn)方式不同造成的
全局變量:整個(gè)項(xiàng)目都可以拿來(lái)訪問(wèn),所以某個(gè)全局變量在全局而言是唯一的(也就是全局變量不能出現(xiàn)同名的情況泽论,即使類型不同也不行艾少,否則系統(tǒng)不知道你具體訪問(wèn)的是哪一個(gè))
靜態(tài)變量:則不是,全局存儲(chǔ)區(qū)可能存儲(chǔ)著若干個(gè)名為type的靜態(tài)變量翼悴。
所以這就導(dǎo)致了訪問(wèn)方式的不同缚够,比如說(shuō)有個(gè)block,內(nèi)部有一個(gè)靜態(tài)變量和一個(gè)全局變量鹦赎,那么在調(diào)用的時(shí)候系統(tǒng)可以直接根據(jù)全局變量名去全局存儲(chǔ)區(qū)查找就可以找到谍椅,名稱是惟一的,所以不用捕獲任何信息即可訪問(wèn)古话。而靜態(tài)變量而不行雏吭,全局存儲(chǔ)區(qū)可能存儲(chǔ)著若干個(gè)名為type的靜態(tài)變量,所以blcok只能根據(jù)內(nèi)存地址去區(qū)分調(diào)用自己需要的那個(gè)-
block的copy操作
棧上的block copy之后,MRC環(huán)境下是否會(huì)引起內(nèi)存泄漏?
是的,copy操作之后,堆上的block沒(méi)有額外的成員變量指向它,正如我們和alloc對(duì)象后,沒(méi)有進(jìn)行relese,造成內(nèi)存泄漏
5.block的底層結(jié)構(gòu)
通過(guò)clang命令將oc代碼轉(zhuǎn)換成c++代碼(如果遇到_weak的報(bào)錯(cuò)是因?yàn)開weak是個(gè)運(yùn)行時(shí)函數(shù)陪踩,所以我們需要在clang命令中指定運(yùn)行時(shí)系統(tǒng)版本才能編譯):
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main.cpp
-(void)viewDidLoad{
[super viewDidLoad];
int i = 1;
void(^block)(void) = ^{
NSLog(@"%d",i);
};
block();
}
轉(zhuǎn)換成c++代碼如下:
//block的真實(shí)結(jié)構(gòu)體
struct __ViewController__viewDidLoad_block_impl_0 {
struct __block_impl impl;
struct __ViewController__viewDidLoad_block_desc_0* Desc;
int i;
//構(gòu)造函數(shù)(相當(dāng)于OC中的init方法 進(jìn)行初始化操作) i(_i):將_i的值賦給i flags有默認(rèn)值杖们,可忽略
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int _i, int flags=0) : i(_i) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//封存block代碼的函數(shù)
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
int i = __cself->i; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3g_7t9fzjm91xxgdq_ysxxghy_80000gn_T_ViewController_c252e7_mi_0,i);
}
//計(jì)算block需要多大的內(nèi)存
static struct __ViewController__viewDidLoad_block_desc_0 {
size_t reserved;
size_t Block_size;
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)};
//viewDidLoad方法
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
//定義的局部變量i
int i = 1;
//定義的blcok底部實(shí)現(xiàn)
void(*block)(void) = &__ViewController__viewDidLoad_block_impl_0(
__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, i));
//block的調(diào)用
bloc->FuncPtr(block);
}
可看出悉抵,定義的block實(shí)際上就是一直指向結(jié)構(gòu)體_ViewController_viewDidLoad_block_impl_0的指針(將一個(gè)_ViewController_viewDidLoad_block_impl_0結(jié)構(gòu)體的地址賦值給了block變量)。
_ViewController_viewDidLoad_block_impl_0包含以下幾個(gè)部分:
- impl
- Desc : 存儲(chǔ)兩個(gè)參數(shù)reserved和Block_size摘完,并且reserved賦值為0而Block_size則存儲(chǔ)著__ViewController__viewDidLoad_block_impl_0的占用空間大小姥饰。最終將desc結(jié)構(gòu)體的地址傳入__ViewController__viewDidLoad_block_impl_0中賦值給Desc。所以Desc的作用是記錄Block結(jié)構(gòu)體的內(nèi)存大小孝治。
- 引用的局部變量
- 構(gòu)造方法
其中impl包含:
- isa指針列粪,存放結(jié)構(gòu)體的內(nèi)存地址,存儲(chǔ)著&_NSConcreteStackBlock地址荆秦,可以暫時(shí)理解為其類對(duì)象地址篱竭,block就是_NSConcreteStackBlock類型的
- Flags:這個(gè)用不到 有默認(rèn)值
-
FuncPtr:block代碼塊地址,存儲(chǔ)著viewDidLoad_block_func_0函數(shù)的地址步绸,也就是block代碼塊的地址掺逼。所以當(dāng)調(diào)用block的時(shí)候,bloc->FuncPtr(block);是直接調(diào)用的FuncPtr方法瓤介。
簡(jiǎn)化圖.png
6.循環(huán)引用問(wèn)題
循環(huán)引用也是block中一個(gè)常見的問(wèn)題吕喘,什么是循環(huán)引用呢?
從block捕獲對(duì)象變量的過(guò)程中可看出刑桑,block在堆中的時(shí)候會(huì)根據(jù)變量自己的修飾符來(lái)進(jìn)行強(qiáng)引用或者弱引用氯质,假設(shè)block對(duì)person對(duì)象進(jìn)行強(qiáng)引用,而person如果對(duì)block也進(jìn)行強(qiáng)引用的話祠斧,那就形成了循環(huán)引用闻察,person對(duì)象和block都有強(qiáng)指針指引著,使它們得不到釋放琢锋。
解決方法:
__weak和__unsafe_unretained
相同點(diǎn):表示的是對(duì)象的一種弱引用關(guān)系
不同點(diǎn):__weak修飾的對(duì)象被釋放后辕漂,指向?qū)ο蟮闹羔槙?huì)置空,也就是指向nil,不會(huì)產(chǎn)生野指針
__unsafe_unretained修飾的對(duì)象被釋放后吴超,指針不會(huì)置空钉嘹,而是變成一個(gè)野指針,那么此時(shí)如果訪問(wèn)這個(gè)對(duì)象的話鲸阻,程序就會(huì)Crash跋涣,拋出BAD_ACCESS的異常。
block可以給NSMutableArray中添加元素嗎鸟悴,需不需要添加__block陈辱?
不需要,因?yàn)樵赽lock塊中僅僅是使用了array的內(nèi)存地址遣臼,往內(nèi)存地址中添加內(nèi)容性置,并沒(méi)有修改arry的內(nèi)存地址,因此array不需要使用__block修飾也可以正確編譯揍堰。
blcok為什么能回調(diào)聲明時(shí)的代碼塊呢鹏浅?
因?yàn)閛c調(diào)用”block()” 實(shí)際就是這句block->FuncPtr(block);,因?yàn)閎lcok->FuncPtr保存的就是__main_block_func_0函數(shù)屏歹。
總結(jié)block聲明的時(shí)候保存了__main_block_impl_0地址隐砸,而__main_block_impl_0則保存了函數(shù)體,block的類型蝙眶,和blcok的結(jié)構(gòu)體大小季希,最后block回調(diào)的時(shí)候block->FuncPtr(block)就是調(diào)用了__main_block_impl_0中保存的函數(shù)__main_block_func_0.