系列文章:
iOS Block概念、語法及基本使用
iOS Block實現(xiàn)原理
iOS Block __block說明符
本文將講解以下幾點:
- Block種類
- Block變量存儲域
- __block變量存儲域
- 截獲對象
- __block變量和對象
- Block循環(huán)引用
根據(jù)上幾篇文章Block語法編譯后的源代碼我們看到,__block_impl結(jié)構(gòu)體內(nèi)部有一個成員變量:isa指針兰迫,__main_block_impl_0結(jié)構(gòu)體初始化的時候沼填,isa指針初始化為 impl.isa = &_NSConcreteStackBlock吮播,因為Block也是OC對象挖藏,我們說該isa指針指向該Block實例所屬的Block類最楷。
Block種類
Block有以下幾種:
Block 類 | Block存儲域 |
---|---|
_NSConcreteStackBlock | 棧 |
_NSConcreteGlobalBlock | 程序的數(shù)據(jù)區(qū)域(.data 區(qū)) |
_NSConcreteMallocBlock | 堆 |
順便說一下程序的內(nèi)存分配情況:
區(qū)域 | 存放的東東 |
---|---|
棧區(qū)(stack) | 由編譯器自動分配釋放 循衰,存放函數(shù)的參數(shù)值铲敛,局部變量的值 |
堆區(qū)(heap) | 程序員分配(alloc/new/copy/mutableCopy) |
全局區(qū)(靜態(tài)區(qū))static | 全局變量和靜態(tài)變量 |
常量區(qū) | 常量字符串等 |
數(shù)據(jù)區(qū)(代碼區(qū)) | 存放函數(shù)體的二進(jìn)制代碼 |
到目前位置看到的Block全都是_NSConcreteStackBlock,其實不是這樣的会钝,在記述全局變量的地方使用Block語法時伐蒋,生成的Block為 _NSConcreteGlobalBlock,舉個例子看下:
@implementation ViewController
void (^block)(void) = ^{
NSLog(@"haha");
};
@end
編譯后__block_block_impl_0結(jié)構(gòu)體:
struct __block_block_impl_0 {
struct __block_impl impl;
struct __block_block_desc_0* Desc;
__block_block_impl_0(void *fp, struct __block_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteGlobalBlock;//global
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
該Block的類為_NSConcreteGlobalBlock類迁酸,即存放在數(shù)據(jù)區(qū)也就是代碼區(qū)先鱼,因為使用全局變量的地方不能使用自動變量,所以不存在對自動變量的截獲奸鬓。由此Block結(jié)構(gòu)體實例的內(nèi)容不依賴于執(zhí)行的狀態(tài)焙畔,所以整個程序中只需一個實例,因此把該結(jié)構(gòu)體實例放在數(shù)據(jù)區(qū)串远。
在以下情況下生成的Block結(jié)構(gòu)體實例屬于 _NSConcreteGlobalBlock類:
- 記述全局變量的地方有Block語法時宏多;
- Block語法的表達(dá)式中不使用截獲的自動變量時儿惫;
除以上兩種情況外,都會生成 _NSConcreteStackBlock類伸但,且保存在棧區(qū)域肾请。
一、Block變量存儲域
配置在全局變量上的Block更胖,從變量作用域外也可以通過指針安全地使用铛铁,但是設(shè)置在棧的Block,如果其所屬的變量作用域結(jié)束函喉,該block也就被廢棄避归。由于__block變量也配置在棧上,同樣其所屬的變量作用域結(jié)束管呵,則該__block變量也同樣被廢棄梳毙。
Block提供了將Block從棧區(qū)copy到堆區(qū)的方法。如下圖:
復(fù)制到堆上的Block將_NSConcreteMallocBlock類對象寫入Block結(jié)構(gòu)體實例的成員變量isa:
impl.isa = &_NSConcreteMallocBlock;
還記得上一節(jié)說到的__block變量結(jié)構(gòu)體實例的 __forwarding 指針指向__block變量結(jié)構(gòu)體自己吧捐下,也就是說無論Block結(jié)構(gòu)體實例配置在棧上還是堆上账锹,都能夠訪問__block變量。
那么什么時候Block從棧上復(fù)制到堆上呢坷襟,其實大多數(shù)情況下奸柬,編譯器會恰當(dāng)?shù)倪M(jìn)行判斷,自動生成將Block從棧上復(fù)制到堆上婴程。
以下情況需要程序員自己通過copy方法將Block從棧區(qū)復(fù)制到堆區(qū):
- 向方法或函數(shù)的參數(shù)中傳遞Block時廓奕;
不需要手動復(fù)制的情況:
- Cocoa框架的方法且方法名中含有usingBlock等時;
- GCD的API
下圖是按Block的存儲域档叔,使用copy后桌粉,Block有什么變化
Block的類 | Block原區(qū)域 | 復(fù)制后 |
---|---|---|
_NSConcreteStackBlock | 棧 | 從棧復(fù)制到堆 |
_NSConcreteGlobalBlock | 數(shù)據(jù)區(qū) | 什么也不做 |
_NSConcreteMallocBlock | 堆 | 引用計數(shù)增加 |
從從上邊可以看出,不管Block配置在何處衙四,用copy方法復(fù)制都不會出現(xiàn)任何問題铃肯。在不確定時調(diào)用copy方法即可。
此處有一個例子:
blk = [[[[blk copy] copy] copy] copy];
該代碼解釋如下:
{
//將配置在堆上的Block復(fù)制給變量tmp传蹈,變量tmp持有強(qiáng)引用的Block押逼;
blk_t tmp = [blk copy];
//將Block變量tmp賦值給blk變量,大括號走完后惦界,tmp釋放挑格,blk繼續(xù)持有Block;
blk = tmp;
}
//以此類推...
{
blk_t tmp = [blk copy];
blk = tmp;
}
{
blk_t tmp = [blk copy];
blk = tmp;
}
{
blk_t tmp = [blk copy];
blk = tmp;
}
由此可見沾歪,ARC下使用copy完全沒問題漂彤。
二、__block變量存儲域
Block從棧上復(fù)制到堆上,那么在Block中使用的__block變量是怎么處理的呢,看下表:
__block變量配置區(qū)域 | Block從棧復(fù)制到堆時的影響 |
---|---|
棧 | 從棧復(fù)制到堆并被Block持有 |
堆 | 被Block持有 |
說明:若一個Block中使用了__block變量显歧,當(dāng)Block變量從棧復(fù)制到堆上時仪或,那么__block變量也會被復(fù)制到堆上。
多個Block變量使用__block變量時,因為最先會將所有的Block配置在棧上士骤,所以__block變量也會配置在棧上范删。在任何一個Block變量被賦值到堆上時,__block變量一并被賦值到堆上拷肌,當(dāng)其他的Block變量復(fù)制到堆上時到旦,其使用的__block變量引用計數(shù)增加:
配置在堆上的Block被廢棄時,__block變量也被廢棄:
到這里我們看到巨缘,Block變量和OC對象的內(nèi)存管理機(jī)制是一樣的添忘,都是使用引用計數(shù),所以也驗證了那句話:Block是OC對象若锁。
三搁骑、截獲對象
先來看一個例子:
typedef void (^block)(id obj);
block blk;//全局變量Block
- (void)viewDidLoad {
[super viewDidLoad];
id array = [NSMutableArray array];
blk = [^(id obj){
[array addObject:obj];
NSLog(@"array count = %ld",[array count]);
} copy];
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
}
打印:
array count = 1
array count = 2
array count = 3
從源代碼可以看出又固,array變量是臨時變量仲器,viewDidLoad方法走完就被廢棄,但依然有打印仰冠,說明變量沒有釋放乏冀,從前幾篇文章可以想象,打印的array變量被Block結(jié)構(gòu)體實例持有了洋只,下面來驗證下辆沦,編譯后的代碼如下:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static struct __ViewController__viewDidLoad_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __ViewController__viewDidLoad_block_impl_0*, struct __ViewController__viewDidLoad_block_impl_0*);
void (*dispose)(struct __ViewController__viewDidLoad_block_impl_0*);
} __ViewController__viewDidLoad_block_desc_0_DATA = {
0,
sizeof(struct __ViewController__viewDidLoad_block_impl_0),
__ViewController__viewDidLoad_block_copy_0,
__ViewController__viewDidLoad_block_dispose_0
};
//函數(shù)指針調(diào)用的函數(shù)
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself, id obj) {
id array = __cself->array; // bound by copy
((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)array, sel_registerName("addObject:"), (id)obj);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_9k_z85dfkt91zd1j387gcxn8xkh0000gn_T_ViewController_503b9f_mi_0,((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));
}
//copy 和 dispose 函數(shù)
static void __ViewController__viewDidLoad_block_copy_0(struct __ViewController__viewDidLoad_block_impl_0*dst, struct __ViewController__viewDidLoad_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __ViewController__viewDidLoad_block_dispose_0(struct __ViewController__viewDidLoad_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
//Block結(jié)構(gòu)體
struct __ViewController__viewDidLoad_block_impl_0 {
struct __block_impl impl;
struct __ViewController__viewDidLoad_block_desc_0* Desc;
id array;//持有array變量
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//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"));
id array = ((NSMutableArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array"));
blk = (block)((id (*)(id, SEL))(void *)objc_msgSend)((id)((void (*)(id))&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, array, 570425344)), sel_registerName("copy"));
((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
}
請注意,可以看到识虚,id 類型的 array變量被Block結(jié)構(gòu)體持有了肢扯。
在這里說明一點,其實我們創(chuàng)建的對象舷礼,默認(rèn)會帶上__strong所有權(quán)修飾符鹃彻,比如:
id array = [NSMutableArray array];
上邊代碼等同于下邊代碼:
id __strong array = [NSMutableArray array];
在OC語言中郊闯,C語言的結(jié)構(gòu)體不能含有附有__strong修飾符的變量妻献,因為編譯器不知道應(yīng)何時進(jìn)行C語言結(jié)構(gòu)體的初始化和廢棄操作,不能很好的管理內(nèi)存团赁。
前兩節(jié)我們看到了copy dispose 函數(shù)育拨,沒有做詳細(xì)解釋,只是猜想了一下欢摄,接下來說說這兩個函數(shù)熬丧。
OC運(yùn)行時可以準(zhǔn)確的把握block從棧上復(fù)制到堆上和Block廢棄的時機(jī),因此Block結(jié)構(gòu)體內(nèi)部使用帶有__strong或__weak修飾符的變量怀挠,也可以在恰當(dāng)?shù)臅r刻初始化和廢棄析蝴,為此需要在 __ViewController__viewDidLoad_block_desc_0 結(jié)構(gòu)體內(nèi)部加上 copy 和 dispose 成員變量害捕,以及作為函數(shù)指針賦值給這兩個變量的 __ViewController__viewDidLoad_block_copy_0 和 __ViewController__viewDidLoad_block_dispose_0函數(shù)
copy函數(shù)內(nèi)部使用了_Block_object_assign函數(shù)將對象類型對象賦值給Block結(jié)構(gòu)體內(nèi)的成員變量并持有該對象。_Block_object_assign函數(shù)調(diào)用相當(dāng)于retain實例方法的函數(shù)闷畸。
dispose函數(shù)內(nèi)部使用_Block_object_dispose函數(shù)釋放Block結(jié)構(gòu)體內(nèi)部的對象類型的成員變量尝盼。_Block_object_dispose函數(shù)調(diào)用相當(dāng)于release實例方法的函數(shù)。
我們只看到了生成的copy和dispose函數(shù)佑菩,但是沒看到調(diào)用啊盾沫,那到底啥時候調(diào)用這兩個函數(shù)呢,這是系統(tǒng)自動發(fā)生的動作:
函數(shù) | 調(diào)用時機(jī) |
---|---|
copy | 棧上Block被復(fù)制到堆上時 |
dispose | 堆上Block被廢棄時 |
當(dāng)Block從棧上復(fù)制到堆上時殿漠,會調(diào)用copy函數(shù)赴精;當(dāng)堆上的Block被廢棄時,會調(diào)用dispose函數(shù)绞幌。
上一節(jié)提到了兩點蕾哟,什么時候block會從棧上復(fù)制到堆上,現(xiàn)在總結(jié)如下:
- Block調(diào)用copy方法時
- Block作為函數(shù)返回值返回時
- 將Block賦值給賦有__strong修飾符id類型的類或Block類型成員變量時
- 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中傳遞Block時
有了這種構(gòu)造莲蜘,通過使用__strong修飾符的變量渐苏,Block中截獲的對象就能超出其變量作用域存在。
上一節(jié)我們研究__block變量的時候菇夸,看到過copy 和 dispose函數(shù)琼富,現(xiàn)在Block截獲對象的也出現(xiàn)了,而且轉(zhuǎn)換后的代碼基本相同庄新,后邊的注釋不同:
類型 | _Block_object_assign/dispose函數(shù) |
---|---|
Block截獲對象 | BLOCK_FIELD_IS_OBJECT |
__block變量 | BLOCK_FIELD_IS_BYREF |
通過這兩個OBJECT鞠眉、BYREF來區(qū)分copy/dispose函數(shù)的對象類型是對象還是__block變量。與copy函數(shù)持有截獲的對象择诈,dispose釋放持有的對象相同械蹋,copy函數(shù)持有Block所使用的__block變量,dispose函數(shù)釋放__block變量羞芍。
有一點需要說明哗戈,這本書上的截獲對象的例子,Block不調(diào)用copy方法荷科,我本地測試的不會強(qiáng)制結(jié)束唯咬。可以解釋為:blk變量為全局變量畏浆,生成的Block結(jié)構(gòu)體實例也是全局變量胆胰,全局變量持有array變量,所以程序不會強(qiáng)制結(jié)束刻获。如果這個解釋有誤的話蜀涨,還請讀者指正,謝!
四厚柳、__block變量和對象
__block說明符可指定任意類型的變量氧枣。下面看下__block修飾OC對象。
__block id obj = [[NSObject alloc] init];
clang轉(zhuǎn)換如下:
__block結(jié)構(gòu)體
struct __Block_byref_obj_0 {
void *__isa;
__Block_byref_obj_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
id obj;
};
//聲明部分
__attribute__((__blocks__(byref))) __Block_byref_obj_0 obj = {
(void*)0,
(__Block_byref_obj_0 *)&obj,
33554432,
sizeof(__Block_byref_obj_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
[[NSObject alloc] init]
}
Block截獲對象這一小節(jié)中别垮,當(dāng)Block從棧復(fù)制到堆上時挑胸,使用copy函數(shù)持有截獲的對象,當(dāng)Block被廢棄時宰闰,使用dispose釋放截獲的對象茬贵。
在__block說明符修飾對象時,在__block變量結(jié)構(gòu)體中看到了copy和dispose函數(shù)移袍,那說明當(dāng)__block變量從棧上復(fù)制到堆上時解藻,使用copy函數(shù)持有賦值給__block變量的對象,當(dāng)堆上的__block變量被廢棄時葡盗,使用dispose函數(shù)釋放賦值給__block變量的對象螟左。
由此可知,只要堆上的__block結(jié)構(gòu)體實例變量沒有被釋放觅够,那么__block變量就不會被釋放胶背。
五、Block循環(huán)引用
原因:在Block內(nèi)部使用對象類型的變量喘先,該變量持有Block钳吟,當(dāng)Block從棧上復(fù)制到堆上時,Block同時持有了對象類型變量窘拯,那么當(dāng)對象類型釋放時红且,由于變量和Block互相引用導(dǎo)致內(nèi)存泄漏,舉個例子:
typedef void (^block)(id obj);
@property (nonatomic, copy) block blk;
- (void)viewDidLoad {
[super viewDidLoad];
self.array = [NSMutableArray array];
self.blk = ^(id obj){
[self.array addObject:obj];
NSLog(@"array count = %ld",[self.array count]);
};
}
這樣寫如果這個VC被pop涤姊,那么這個VC是釋放不了的暇番,VC持有Block,Block內(nèi)部持有VC思喊。
修改一下:
- (void)viewDidLoad {
[super viewDidLoad];
self.array = [NSMutableArray array];
ViewController * __weak temp = self;
self.blk = ^(id obj){
[temp.array addObject:obj];
NSLog(@"array count = %ld",[temp.array count]);
};
}
循環(huán)引用消失:
在此根據(jù)自己的項目中使用到的Block場景壁酬,來總結(jié)下Block使用時的注意事項,說不定項目中真的有內(nèi)存泄漏呢
1恨课、UIView 的 animation動畫塊使用了Block舆乔,內(nèi)部使用self不會循環(huán)引用,為什么呢
答:UIView 動畫塊是類方法庄呈,不被self持有蜕煌,所以不會循環(huán)引用派阱。
2诬留、Monsary也使用了Block來設(shè)置控件的布局,Block內(nèi)部使用self,為什么不會循環(huán)引用呢
答:看源碼可以看出文兑,Monsary使用的Block是當(dāng)做參數(shù)傳遞的盒刚,即便block內(nèi)部持有self,設(shè)置布局的view持有block绿贞,但是block不持有view因块,當(dāng)block執(zhí)行完后就釋放了,self的引用計數(shù)-1籍铁,所以block也不會持有self涡上,所以不會導(dǎo)致循環(huán)引用。
3拒名、reactiveCocoa如果不使用@weakify @strongify吩愧,會循環(huán)引用,兩個宏就等于下邊代碼:
__weak typeof(self) weakSelf = self;
__strong typeof(weakSelf) strongSelf = weakSelf;
六增显、總結(jié)
以上幾篇文章基本就把Block(以及__block變量)的定義雁佳、語法、應(yīng)用同云、原理介紹完了糖权,主要的目的還是能更靈活的應(yīng)用于項目。
歡迎提出寶貴意見炸站,喜歡贊一下吧星澳。
圖有點low,莫見怪旱易,哈哈哈...