block
的本質是對象
、函數(shù)
徙鱼、結構體
。
一针姿、block 定義
- block:帶有自動變量的匿名函數(shù)袱吆。
-
匿名函數(shù):沒有函數(shù)名的函數(shù),一對
{}
包裹的內容是匿名函數(shù)的作用域距淫。 -
自動變量:棧上聲明一個變量既不是靜態(tài)變量绞绒,又不是全局變量,是不可以在棧內聲明的匿名函數(shù)中使用的溉愁。但是在block中卻可以处铛,雖然使用block不用聲明類,但是block提供了類似OC的類一樣拐揭,可以通過成員變量來保護作用域外變量值的方法撤蟆,那些再block的一對
{}
里面使用到,但卻是在{}
作用域外聲明的變量堂污,就是block接貨的自動變量家肯。
block表達式語法
^ 返回值類型(參數(shù)列表){表達式}
1、無參數(shù)盟猖,無返回值
// 1讨衣、無參數(shù)换棚,無返回值
void(^block)(void) = ^ {
NSLog(@"block");
};
block();
2、有參數(shù)反镇,無返回值
// 2固蚤、有參數(shù),無返回值
void(^block)(int, NSString*) = ^(int a, NSString *name){
NSLog(@"%@ -- %d", name, a);
};
block(10, @"block");
3歹茶、有參數(shù)夕玩,有返回值
// 3、有參數(shù)惊豺,有返回值
int(^block)(int, int) = ^(int a, int b){
NSLog(@"block -- %d", a+b);
return a+b;
};
block(10, 20);
4燎孟、無參數(shù),有返回值
// 4尸昧、無參數(shù)揩页,有返回值
int(^block)(void) = ^{
NSLog(@"block -- 40");
return 40;
};
block();
5、在開發(fā)中我們還可以用typedef
定義block
typedef int(^block)(int, NSString*);
可以這樣使用:
block Myblock = ^(int a, NSString *name){
NSLog(@"%@ -- %d", name, a);
return 40;
};
NSLog(@"%d", Myblock(10, @"block"));
也可以定義成屬性:
@property (nonatomic, copy) block Myblock;
二烹俗、block的分類
-
block主要分為三類:
① 全局block:_NSConcreteGlobalBlock
爆侣;存儲在全局內存中,相當于單例幢妄。
② 棧block:_NSConcreteStackBlock
累提;存儲在棧內存中,超出其作用域則馬上被銷毀磁浇。
③ 堆block:_NSConcreteMallocBlock
;存儲在堆內存中朽褪,是一個帶引用計數(shù)的對象置吓,需要自行管理其內存。
這三種block各自的存儲區(qū)域如下圖:
簡而言之缔赠,存儲在棧中的block
就是棧塊衍锚,存儲在堆區(qū)的就是堆塊,既不在棧區(qū)也不在堆區(qū)的就是全局塊 當我們遇到一個
block
嗤堰,怎么去判定這個block
的存儲位置呢戴质?
(1)block
不訪問外部變量(包括棧和堆中的變量)
此時block
既不在棧中,也不在堆中踢匣,在代碼段中告匠。ARC
和MRC
下都是如此。
此時為全局block
:_NSConcreteGlobalBlock
void(^block)(void) = ^{
};
NSLog(@"%@", block);
/*輸出結果為*/
<__NSGlobalBlock__: 0x100004030>
(2)block
訪問外部變量
MRC
環(huán)境下:訪問外部變量的block
默認是存儲在棧
中的离唬。
ARC
環(huán)境下:訪問外部變量的block
默認是存儲在堆
中的(實際是放在棧
區(qū)后专,然后ARC
情況下又自動拷貝到堆
區(qū)),自動釋放输莺。
- MRC 環(huán)境:
int a = 10;
void(^block)(void) = ^{
NSLog(@"%d", a);
};
NSLog(@"%@", block);
/*輸出結果為*/
<__NSStackBlock__: 0x7ffeefbff3e8>
- ARC 環(huán)境下
int a = 10;
void(^block)(void) = ^{
NSLog(@"%d", a);
};
NSLog(@"%@", block);
/*輸出結果為*/
<__NSMallocBlock__: 0x1040508b0>
- 在ARC環(huán)境下我們怎么獲取
棧block
呢戚哎?
我們可以這樣做:
int a = 10;
void(^ __weak block)(void) = ^{
NSLog(@"%d", a);
};
NSLog(@"%@", block);
/*輸出結果為*/
<__NSStackBlock__: 0x7ffeefbff3e8>
此時我們通過__weak
不進行強持有裸诽,block
就還是棧區(qū)的block。
- ARC環(huán)境下型凳,訪問外部變量的
block
為什么要自動從棧區(qū)拷貝到堆區(qū)呢丈冬?
因為:棧上的block
,如果其所屬的變量作用域結束甘畅,該block
就會被廢棄埂蕊,如同一般的自動變量。當然橄浓,block
中的__block
變量也同時會被廢棄粒梦。
為了解決棧塊在其變量作用域結束之后被廢棄(釋放)的問題,我們需要把block
復制到堆中荸实,延長其生命周期匀们。開啟ARC
時,大多數(shù)情況下編譯器會恰當?shù)倪M行判斷是否有必要將block
從棧復制到堆准给,如果有泄朴,自動生成將block
從棧復制到堆的代碼。block
的復制操作執(zhí)行的是Copy實例方法露氮。block
只要調用了Copy方法祖灰,棧塊就會變成堆塊。
eg:
typedef int(^myblock)(int);
myblock func(int a) {
return ^(int b) {
return a * b;
};
}
上面的代碼中畔规,函數(shù)返回的block
是配置在棧上的局扶,所以返回返回函數(shù)調用方法時,block
變量作用域就被釋放了叁扫,block
也會被釋放三妈。但是,在ARC
環(huán)境下是有效的莫绣,這種情況編譯器會自動完成復制畴蒲。
在非ARC
情況下則需要開發(fā)者調用Copy方法手動復制。
將block從棧區(qū)復制到堆區(qū)非常想好CPU对室,所以當block設置在棧上也能使用時色徘,就不要復制了代赁,因為此時的復制只是在浪費CPU資源辣辫。
block
的復制操作蝌箍,執(zhí)行的是Copy實例方法。不同類型的block
使用的Copy方法的效果如下:
block的類型 | 副本源的配置存儲區(qū)域 | 復制效果 |
---|---|---|
_NSConcreteGlobalBlock | 程序的數(shù)據(jù)區(qū)域 | 什么也不做 |
_NSConcreteStackBlock | 棧區(qū) | 從棧區(qū)復制到堆區(qū) |
_NSConcreteMallocBlock | 堆區(qū) | 引用計數(shù)增加 |
根據(jù)表格我們知道锭亏,block
在堆區(qū)Copy會造成引用計數(shù)增加纠吴,這與其它OC對象是一樣的。雖然block
在棧中也是以對象的身份存在慧瘤,但是棧區(qū)沒有引用計數(shù)戴已,因為不需要固该,我們都知道棧區(qū)的內存由編譯器自動分配釋放。
三糖儡、block 底層分析
int a = 10;
void(^ block)(void) = ^{
NSLog(@"%d", a);
};
NSLog(@"%@", block);
使用 clang
將OC代碼轉換成C++文件伐坏,查看block
的方法。
- 在命令行輸入下面的指令(XXX.m就是要編譯的文件握联,需在當前文件夾下面執(zhí)行)
clang -rewrite-objc XXX.m
執(zhí)行網上面的指令之后桦沉,當前文件夾中會多一個
XXX.cpp
的文件。此時在命令行輸入open XXX.cpp
或者 直接打開文件打開XXX.cpp文件金闽,在文件底部我們可以看到
main
函數(shù)被編譯之后的樣式:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int a = 10;
void(* block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_w__pbdfr2t16xx3pkwg63c2y5m80000gn_T_main_c75271_mi_1, block);
}
return 0;
}
我們從main
函數(shù)中提取一下block
void(* block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
/*簡化一下纯露,去除強制轉換*/
void(* block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a); ///構造函數(shù)
可以看到構造函數(shù)名為__main_block_impl_0
下面我們再尋找一下__main_block_impl_0
:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
- 可以看到
__main_block_impl_0
是一個結構體
。同時我們也可以說block
是一個__main_block_impl_0
類型的對象代芜,這也是為什么block
能夠%@
打印的原因
1埠褪、block自動捕獲外部變量
- block自動捕獲的外部變量,在block的函數(shù)體內是不允許被修改的挤庇。
① 通過上面的代碼我們可以看到__main_block_impl_0
函數(shù)的定義:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a; ///編譯器自動生成的名字相同的變量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy 值拷貝
NSLog((NSString *)&__NSConstantStringImpl__var_folders_w__pbdfr2t16xx3pkwg63c2y5m80000gn_T_main_ab7cb4_mi_0, a);
}
可以看到钞速,編譯器會自動生成一個同名的變量。
__main_block_func_0
中a
是值拷貝嫡秕。
因此渴语,在block
內存會生成一個,內容一樣的同名變量昆咽,此時如果在函數(shù)體內進行a++
的操作驾凶,則編譯器就不清楚該去修改哪個變量。所以block
自動捕獲的變量掷酗,在函數(shù)體內部是不允許修改的狭郑。
- 那么我們要修改外部變量要怎么辦呢?
1汇在、__block
修飾外部變量。
2脏答、將變量定義成全局變量
3糕殉、將變量用參數(shù)的形式,傳入block
里面殖告。
第2種和第3種方式阿蝶,想必大家都非常的熟悉,在這里就不再贅述黄绩。下面我們來看一下第1種方式羡洁,底層究竟做了些什么。
__block 原理
- 現(xiàn)在我們對
a
進行__block
編譯爽丹,之后我們就可以在block
內部對a
進行修改筑煮。
__block int a = 10;
void(^ block)(void) = ^{
a++;
NSLog(@"%d", a);
};
block();
下面我們再通過clang
來觀察一下辛蚊,底層代碼有了什么變化。
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref 指針拷貝
/// 等同于外界的 a++
(a->__forwarding->a)++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_w__pbdfr2t16xx3pkwg63c2y5m80000gn_T_main_b21337_mi_0, (a->__forwarding->a));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
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};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
void(* block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
- 首先我們看到真仲,在
main
函數(shù)里面袋马,此時a
變成了成了一個__Block_byref_a_0
類型的對象。 - 同時在
__main_block_func_0
函數(shù)中秸应,由之前的值拷貝(bound by copy
) 變成了現(xiàn)在的指針拷貝(bound by ref
)
*在main
函數(shù)中傳入的a
是一個對象虑凛,同時在__main_block_func_0
函數(shù)內部,對a
進行指針拷貝软啼;則此時創(chuàng)建的對象a
和傳入的對象a
指向同一片內存空間桑谍。
總結:
__block
修飾外界變量的時候:
1、外界變量
會生成__Block_byref_a_0
結構體
2祸挪、結構體用來保存原始變量的指針
和值
(可以在上面編譯后的代碼中找到)
3锣披、將變量生成的結構體對象的指針地址
傳遞給block
,然后在block
內部就可以對外界變量
進行修改了。
接下來匕积,在給大家看一個東西:
- 在上面的C++代碼中盈罐,
__main_block_func_0
函數(shù)中,大家會注意到執(zhí)行a++
的是這段代碼(a->__forwarding->a)++;
闪唆,那么這個__forwarding
又是什么呢盅粪?
接下來我們先看一下__Block_byref_a_0
結構體長什么樣子:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
可以看到,__forwarding
是一個指向自己本身的指針(自身為結構體)悄蕾。
那么票顾,在Copy操作之后,既然__block
變量也被Copy到堆區(qū)
上了帆调,那么訪問該變量是訪問棧
上的還是堆
上的呢奠骄?這個時候我們就要來看一下,在Copy過程中__forwarding
的變化了:
可以看到番刊,通過
__forwarding
含鳞,無論是在block
中,還是block
外訪問__block
變量芹务,也不管該變量是在棧
上或是在堆
上蝉绷,都能順利的訪問同一個__block
變量。注意:這里與上面的結論并不矛盾枣抱。大家要主要到
局部變量a
被__block
修飾之后熔吗,會變成__Block_byref_a_0
結構體對象。所以無論是在棧區(qū)
還是在堆區(qū)
佳晶,只要__forwarding
指向的地址一樣桅狠,那么就可以在block
內部修改外界變量。這里大家要仔細觀察一下__Block_byref_a_0
結構體
例題:
int a = 10;
int *p = &a;
NSLog(@"開始 a == %d, s == %p", a, &a);
NSLog(@"開始 p == %d, s == %p", *p, p);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
(*p) = 100;
NSLog(@"異步 a == %d, s == %p", a, &a);
NSLog(@"異步 p == %d, s == %p", *p, p);
});
///打印結果
開始 a == 10, s == 0x7ffeebb2a0ac
開始 p == 10, s == 0x7ffeebb2a0ac
異步 a == 10, s == 0x600003a26c68
異步 p == 0, s == 0x7ffeebb2a0ac
- 這里有一點要注意
我們上面用的是異步,所以最后的*p
打印的不是100
中跌,而是0
咨堤。
這就是因為異步,不用等待晒他,viewDidLoad
直接執(zhí)行完畢吱型,*p
對應的內存空間被釋放。
如果在dispatch_async
后面寫上其他的一些函數(shù)陨仅,輸出的*p
可能是任意值:
當然這也是一個釋放時機
的問題津滞,如果有很多業(yè)務要處理,可能打印*p
的時候灼伤,對應的內存地址還沒有釋放触徐。
-
a
的最后輸出值是10
這里就不多說,block
捕獲局部變量的拷貝上面有講狐赡。