在Objective-C:探索block(一)中簡單地說了下block的基本上面貌百框,包括它的語法和底層定義骤宣,了解block的底層定義對在項(xiàng)目開發(fā)中正確使用block極為重要诞帐。
本篇文章要探索block使用中不可避免的幾個(gè)方面
- 基本類型變量與對象的截取
- __block修飾符
- block存儲(chǔ)域
- copy的使用
- 相互引用問題
一. 基本數(shù)據(jù)類型變量與對象的截取
1.block截獲基本數(shù)據(jù)類型變量
在這小節(jié)會(huì)解答:為什么給定義在代碼體外面的局部變量重新賦值會(huì)引起編譯錯(cuò)誤
下面是block截取基本數(shù)據(jù)類型的示例代碼:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
int aInteger = 3;
char *str = "hello world";
void (^myprint)(void) = ^void(void){
printf("%s-%d\n",str,aInteger);
};
myprint();
}
return 0;
}
注意:由于控制臺標(biāo)準(zhǔn)輸出有緩存淘正,當(dāng)有行刷新標(biāo)志或者行緩存已滿時(shí)百侧,系統(tǒng)才會(huì)把緩存數(shù)據(jù)輸出到控制臺蜻势,Xcode8編譯printf打印函數(shù)結(jié)束加上\n換行符勺三,后臺才有打印輸出
示例代碼中,代碼塊myprint的代碼體中使用了變量aInteger代态,str
,使用終端命令反編譯:clang -rewrite-objc main.m
, 可以在main.cpp文件的底部看到代碼塊myprint的底層定義疹吃,主要有:
//1.存儲(chǔ)與block實(shí)現(xiàn)相關(guān)的信息
struct __block_impl {
void *isa; //指向block結(jié)構(gòu)體實(shí)例所在的內(nèi)存區(qū)域
int Flags; //系統(tǒng)默認(rèn)值為0
int Reserved; //構(gòu)造方法里沒看到賦值蹦疑,用來存儲(chǔ)block保留內(nèi)存空間大小
void *FuncPtr; //指向block代碼體實(shí)現(xiàn)的函數(shù)指針,block的調(diào)用關(guān)鍵就它來尋址了
};
//2.此結(jié)構(gòu)體記錄block的描述信息萨驶,它在定義時(shí)順便初始化了個(gè)實(shí)例__main_block_desc_0_DATA
static struct __main_block_desc_0 {
size_t reserved; //指明block在內(nèi)存中要保留一塊內(nèi)存空間的大小歉摧,這塊內(nèi)存區(qū)暫沒用途。
size_t Block_size;//指明block的結(jié)構(gòu)體實(shí)例的大写勰臁:sizeof(struct __main_block_impl_0)
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
//3.代碼塊myprint的真身判莉,就是一個(gè)結(jié)構(gòu)體
struct __main_block_impl_0 {
struct __block_impl impl; //存儲(chǔ)與block實(shí)現(xiàn)(代碼體)相關(guān)的信息
struct __main_block_desc_0* Desc;//存儲(chǔ)block的描述信息
char *str; //截獲到的str變量
int aInteger; //截獲到的aInteger變量
//結(jié)構(gòu)體的構(gòu)造方法
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, char *_str, int _aInteger, int flags=0) : str(_str), aInteger(_aInteger) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//4.block代碼體的實(shí)現(xiàn)函數(shù),就是一個(gè)C函數(shù)育谬,可以通過函數(shù)指針來調(diào)用,傳入的參數(shù)為block的結(jié)構(gòu)體實(shí)例本身
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//編譯器在編譯過程中會(huì)在block代碼體的實(shí)現(xiàn)函數(shù)中券盅,聲明與截獲的外部局部變量名稱相同的局部變量,并進(jìn)行賦值
char *str = __cself->str; // cself為block的結(jié)構(gòu)體實(shí)例本身
int aInteger = __cself->aInteger; //
printf("%s%d\n",str,aInteger);
}
//程序入口膛檀,主函數(shù)
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
//這兩個(gè)變量雖然與block的結(jié)構(gòu)體實(shí)例的成員變量名稱相同锰镀,但根本是不同,所以在block代碼體的實(shí)現(xiàn)函數(shù)里給它們賦值咖刃,會(huì)編譯報(bào)錯(cuò):Variable is not assignable,因?yàn)槌隽俗兞康淖饔糜? int aInteger = 3;
char *str = "hello world";
/*-----------重點(diǎn)-----------*/
//1.void (*myprint)(void):聲明一個(gè)返回類型為空泳炉,參數(shù)為空,名稱叫myprint的函數(shù)指針,它指向了block的構(gòu)造方法,構(gòu)造方法將創(chuàng)建一個(gè)block結(jié)構(gòu)體實(shí)例
void (*myprint)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, str, aInteger));
//2.(void (*)(__block_impl *))這是一個(gè)返回值為void嚎杨,參數(shù)類型為__block_impl *的指針類型花鹅,用來修飾實(shí)例myprint的成員變量FuncPtr(即myprint->FuncPtr), ((__block_impl *)myprint)為FuncPtr的參數(shù)
((void (*)(__block_impl *))((__block_impl *)myprint)->FuncPtr)((__block_impl *)myprint);
}
//類型轉(zhuǎn)換簡化后枫浙,1和2相當(dāng)于下面:
struct __main_block_impl_0 tmp = __main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA,str,aInteger);
struct __main_block_impl_0 *myprint = &tmp;
(*myprint->impl.FuncPtr)(myprint);
/*-----------重點(diǎn)-----------*/
return 0;
}
顯然刨肃,代碼塊myprint經(jīng)過編譯后,代碼塊myprint底層結(jié)構(gòu)體會(huì)新增成員變量aInteger,str
并在構(gòu)造函數(shù)中進(jìn)行初始化箩帚。同時(shí)可以看到代碼塊myprint的實(shí)現(xiàn)函數(shù)static void __main_block_func_0(struct __main_block_impl_0 *__cself)
中重新聲明了相同名稱的變量int aInteger和char *str
,此時(shí)的aInteget真友,str與聲明在主函數(shù)的局部變量是兩個(gè)完全沒關(guān)系的不同變量,它們不在同一作用域(同一函數(shù))并且編譯器有它自己的一套編譯規(guī)則(1.支持截獲變量的瞬間值紧帕,2.不支持在代碼體內(nèi)給外部的局部變量賦值盔然,因?yàn)椴辉试S在block的實(shí)現(xiàn)函數(shù)里給main函數(shù)的局部變量賦值,盡管兩個(gè)變量名稱相同),這也解釋為什么給定義在代碼體外面的局部變量重新賦值會(huì)引起編譯錯(cuò)誤愈案。想要給aInteger,str
重新賦值挺尾,可以把它們聲明為全局變量或靜態(tài)變量來解決作用域問題,但一般不選擇這樣做刻帚,而是在它們前面加上修飾符__block潦嘶,這樣就可以通過編譯(將在第二小節(jié)探索)。
2.block截獲對象
下面是block截取對象的oc源代碼:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
{
NSMutableArray *array = [[NSMutableArray alloc] init];
void(^myprint)(void)=^{
printf("數(shù)組Count=%ld\n",array.count);
};
myprint();
}
}
return 0;
}
示例中崇众,myprint截獲了可變數(shù)組array掂僵,以下是轉(zhuǎn)換成C++的代碼:
//代碼塊myprint的結(jié)構(gòu)體
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSMutableArray *array; //截獲的array
//代碼塊myprint結(jié)構(gòu)體的構(gòu)造方法
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock; //isa指向結(jié)構(gòu)體實(shí)例本身,實(shí)例分配在棧區(qū)
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//myprint代碼體對應(yīng)的實(shí)現(xiàn)函數(shù)
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSMutableArray *array = __cself->array;
//array.count反編譯成runtime的消息發(fā)送objc_msgSend(arry,@selector(count));
printf("數(shù)組Count=%ld\n",((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));
}
//拷貝操作函數(shù)顷歌,
//1.這個(gè)函數(shù)用來拷貝myprint結(jié)構(gòu)體實(shí)例的array對象
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
//廢棄操作函數(shù)锰蓬,廢棄myprint結(jié)構(gòu)體實(shí)例的array對象,相當(dāng)對象realse
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
//存儲(chǔ)代碼塊myprint的結(jié)構(gòu)體的描述信息并實(shí)例化一個(gè)實(shí)例__main_block_desc_0_DATA
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
//函數(shù)指針指向拷貝操作函數(shù)
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
//函數(shù)指針指向廢棄操作函數(shù)
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
//主函數(shù)
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
{
//(NSMutableArray *(*)(id, SEL))把objc_msgSend(id,SEL)轉(zhuǎn)成返回值為NSMutableArray *,參數(shù)為id,SEL的函數(shù)指針。
//簡化如下:
NSMutableArray *aZone = objc_msgSend(objc_getClass("NSMutableArray"),sel_registerName("alloc"));
NSMutableArray *array = objc_msgSend(aZone,sel_registerName("alloc"));
//
NSMutableArray *array = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("alloc")), sel_registerName("init"));
void(*myprint)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344));
((void (*)(__block_impl *))((__block_impl *)myprint)->FuncPtr)((__block_impl *)myprint);
}
}
return 0;
}
由此可知眯漩,編譯器對block截獲對象與截獲變量的處理基本一樣:在block的結(jié)構(gòu)體中生成一個(gè)與截獲的變量名稱相同的成員變量芹扭。與截獲基本數(shù)據(jù)類型變量不同的是,截獲對象時(shí)赦抖,編譯器會(huì)生成block_copy
和block_dispose
函數(shù)用于對象的內(nèi)存管理舱卡。
二. __block修飾符
1.編譯器對__block的處理
我們知道在block代碼體中不能給截獲的變量重新賦值,但可以用__block來修飾被截獲的變量队萤,再進(jìn)行重新賦值轮锥,示例如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int aInteger = 1;
void(^myprint)(void)=^{
aInteger = 3;
printf("aInteger=%d\n",aInteger);
};
myprint();
}
return 0;
}
為了給block截獲的變量重新賦值,可使用__block修飾符要尔,下面是編譯器對加了__block修飾符的變量的處理舍杜,與上面反編譯后的代碼相差不大:
//編譯器會(huì)為被__block修飾的變量生成一個(gè)結(jié)構(gòu)體類型,
struct __Block_byref_aInteger_0 {
void *__isa;
__Block_byref_aInteger_0 *__forwarding; //__forwarding指向__block變量結(jié)構(gòu)體實(shí)例本身
int __flags;
int __size;
int aInteger; //與外部的局部變量名稱相同的成員變量
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_aInteger_0 *aInteger; // 截獲的__block變量aInteger
//block結(jié)構(gòu)體的構(gòu)造函數(shù)
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_aInteger_0 *_aInteger, int flags=0) : aInteger(_aInteger->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp; //函數(shù)指針指向block代碼體的實(shí)現(xiàn)函數(shù)
Desc = desc;
}
};
//block代碼體的實(shí)現(xiàn)函數(shù)赵辕,以block的結(jié)構(gòu)實(shí)例本身作為參數(shù)傳遞
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//結(jié)構(gòu)體aInteger的__forwarding指向自身既绩,繞了一步再取成員變量int aInteger
__Block_byref_aInteger_0 *aInteger = __cself->aInteger;
(aInteger->__forwarding->aInteger) = 3;
printf("aInteger=%d\n",(aInteger->__forwarding->aInteger));
}
//block變量的copy函數(shù)用于對象的內(nèi)存管理 (拷貝對象,引用計(jì)數(shù)+1)
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->aInteger, (void*)src->aInteger, 8/*BLOCK_FIELD_IS_BYREF*/);
}
//block變量的dispose函數(shù)用于對象的內(nèi)存管理 (廢棄對象)
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->aInteger, 8/*BLOCK_FIELD_IS_BYREF*/);}
//存儲(chǔ)block的描述信息还惠,在聲明同時(shí)實(shí)例化一個(gè)__main_block_desc_0_DATA實(shí)例
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*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
//主函數(shù)
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
{
//實(shí)例化aInteger結(jié)構(gòu)體
__attribute__((__blocks__(byref))) __Block_byref_aInteger_0 aInteger = {
(void*)0 //void *isa
(__Block_byref_aInteger_0 *)&aInteger, //__forwardind
0,
sizeof(__Block_byref_aInteger_0),
1
};
//函數(shù)指針myprint指向myprint代碼體的構(gòu)造函數(shù)即指向了結(jié)構(gòu)體實(shí)例
void(*myprint)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_aInteger_0 *)&aInteger, 570425344));
//下面去掉類型轉(zhuǎn)換:(myprint->FuncPtr)(myprint),即結(jié)構(gòu)體實(shí)例myprint引用成員變量FuncPtr指針饲握,來調(diào)用block代碼體的實(shí)現(xiàn)函數(shù)__main_block_func_0
((void (*)(__block_impl *))((__block_impl *)myprint)->FuncPtr)((__block_impl *)myprint);
}
}
return 0;
}
2.對象的內(nèi)容可以進(jìn)行操作
不能對沒有__block修飾符修飾的變量進(jìn)行重新賦值,但截獲的變量如果是對象類型的話蚕键,是可以對其內(nèi)容進(jìn)行操作的互拾,例如:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:2];
void(^myprint)(void)=^{
[mArray addObject:@"Hello"];
printf("mArray.count=%ld\n",mArray.count);
};
myprint();
}
return 0;
}
三. block存儲(chǔ)域
下面是內(nèi)存分配的一些說明:
一:聲明在函數(shù)內(nèi)部的局部變量,系統(tǒng)自動(dòng)分配到棧上嚎幸,且在函數(shù)執(zhí)行結(jié)束后,被釋放寄猩。
二:由程序員調(diào)用alloc,malloc,new嫉晶,copy等方法初始化的變量,會(huì)分配到堆上,程序員可手動(dòng)調(diào)用free來釋放替废。否則箍铭,等程序結(jié)束后,由系統(tǒng)釋放回收
三:一些靜態(tài)變量椎镣,全局變量诈火,常量會(huì)在編譯階段分配在數(shù)據(jù) 區(qū)(data區(qū))
四:操作指令會(huì)分配到文本區(qū)(text區(qū))
以下是一個(gè)應(yīng)用在內(nèi)存中進(jìn)程空間的布局(線程thread共享進(jìn)程的內(nèi)存空間)
1.分配在棧上的block
聲明在函數(shù)內(nèi)部的block代碼體會(huì)分配在棧上。
在上面的c++代碼中状答,block的結(jié)構(gòu)體實(shí)例構(gòu)造方法會(huì)給impl.isa賦值為 &_NSConcreteStackBlock冷守,我們知道isa指針是指向了block結(jié)構(gòu)體實(shí)例自身,即&_NSConcreteStackBlock指向了block結(jié)構(gòu)體實(shí)例在內(nèi)存中的起址地址惊科,NSConcreteStackBlock拍摇,見文知義,Stack是棧的意思馆截,可以推測系統(tǒng)將block的結(jié)構(gòu)體實(shí)例分配在棧上充活。
2.分配在堆上的block
當(dāng)block代碼體賦值給擁有strong,__strong蜡娶,copy修飾的變量時(shí)混卵,block會(huì)在堆上生成一份副本,并將副本賦值給變量窖张,block結(jié)構(gòu)體實(shí)例的impl.isa=&_NSConcreteMallocBlock幕随,isa指針指向堆上的實(shí)例
3.分配在數(shù)據(jù)區(qū)(全局)的block
在全局地方聲明的block代碼體(在@interface外面聲明),會(huì)分配在數(shù)據(jù)區(qū)荤堪,impl.isa=&_NSConcreteGlobalBlock 指向數(shù)據(jù)區(qū)合陵。
四.copy的使用
1.為什么要用copy呢?
一般情況下澄阳,block的代碼體會(huì)聲明在某個(gè)方法內(nèi)而極少聲明在@interface外面(可理解為全局的地方拥知,因數(shù)在全局地方block截獲不了有用的變量),我們知道碎赢,在方法內(nèi)聲明的(不是由all,new初始化的)會(huì)分配到棧上低剔,并且會(huì)在方法執(zhí)行完結(jié)時(shí)被釋放,這樣就極容易引用了被釋放的對象肮塞,從而拋出內(nèi)存讀取錯(cuò)誤的異常襟齿,所以會(huì)為block聲明一個(gè)copy屬性,使它指向堆上的對象以確保其生命周期枕赵。
在項(xiàng)目開發(fā)猜欺,為某個(gè)類聲明一個(gè)block屬性時(shí),通常會(huì)這樣寫:
typdef void (^Myblock)(void)
@interface MyClass
@property (copy,nonatomic) Myblock oneBlock;
...
{
self.oneBlock=^{ NSLog(@"Hello world!"); };
}
當(dāng)為屬性指明copy屬性時(shí)拷窜,^{ NSLog(@"Hello world!"); }
的結(jié)構(gòu)體實(shí)例將被從棧上copy一份副本到堆上开皿,且_oneBlock指向了堆上的副本涧黄。其實(shí),聲明onBlock屬性也可以這樣寫 @property (strong,nonatomic) Myblock oneBlock;
或者@property (nonatomic) Myblock oneBlock;
因?yàn)閷傩詓trong作用相當(dāng)于修飾符__strong(__strong是對象的隱式說明)赋荆,編譯器會(huì)為__strong的對象分配到堆上作內(nèi)存管理笋妥。但如果非要作死,寫成@property (weak,nonatomic) Myblock oneBlock;
窄潭,就會(huì)報(bào)錯(cuò)了bad_address_access春宣。
2.不用手動(dòng)copy的情況
在一些情況下,出于內(nèi)存管理的原因嫉你,編譯器會(huì)自動(dòng)幫我們copy, 不我們自己去copy,情況如下:
一:block作為函數(shù)的返回值被傳遞時(shí)月帝,如
-(OneBlock)someMethod{
OneBlock aBlock = ^{...};
return aBlock; //不需要寫成return [aBlock copy];
}
二:Cocoa框架方法或GCD的API方法中含有usingBlock來傳遞block時(shí)
三:將block賦值給帶有__strong修飾符的id類型的類或__block說明符的變量。
五.相互引用問題
由于block會(huì)截獲出現(xiàn)在代碼體的外部對象(定義在代碼體{}外部的對象)均抽,并在block的結(jié)構(gòu)體內(nèi)聲明一個(gè)與之名稱相同的成員變量并在實(shí)例化時(shí)指向并截獲的外部對象嫁赏,如果被截獲的對象又擁有或間接擁有(持有)該block,就會(huì)形成閉環(huán)油挥,引起相互引用的問題潦蝇,不能被有效釋放。
1.相互引用
引起循環(huán)引用的示例如下:
#import "MyClass.h"
typedef void(^Myprint)(void);
@interface MyClass ()
{
Myprint _myprint;
}
@end
@implementation MyClass
-(void)myPrintMethod{
_myprint = ^{
NSLog(@"%@",self); //編譯器警告:Capturing 'self'strongly in this block is likely to lead to a retain cycle
};
}
@end
2.間接相互引用
#import "MyClass.h"
typedef void(^Myprint)(void);
@interface MyClass ()
{
Myprint _myprint;
}
@property(strong,nonatomic) NSString *str;
@end
@implementation MyClass
-(void)myPrintMethod{
_myprint = ^{
NSLog(@"%@",_str);//編譯器警告:Capturing 'self'strongly in this block is likely to lead to a retain cycle
};
}
@end
3.__weak解決相互引用
形成相互引用的兩個(gè)對象深寥,不能被有效釋放攘乒,dealloc方法也不會(huì)執(zhí)行。
A對象要釋放惋鹅,首先要釋放B则酝,B要釋放,那么A對象就要先釋放闰集,這樣會(huì)形成死鎖沽讹,導(dǎo)致A,B對象都不能釋放。__weak修飾符或__unsafe_unretained修飾符可解決循環(huán)引用問題武鲁,示例如下:
#import "MyClass.h"
typedef void(^Myprint)(void);
@interface MyClass ()
{
Myprint _myprint;
}
@end
@implementation MyClass
-(void)myPrintMethod{
__weak typeof(self) weakSelf = self; //__weak修飾符讓weak可引用self,但并不持有self
_myprint = ^{
NSLog(@"%@",weakSelf);
};
}
@end