什麼是Block
Block其實就是一個程式碼塊,通常被稱為“閉包”,它封裝了函式呼叫以及函式呼叫環境,以便在合適的時機進行呼叫,在OC中,Block其實就是一個OC物件,它可以當做引數傳遞。
Block的結構如下:
Block的本質
無外部變數訪問時Block的底層結構
- 首先,建立一個Demo,在main.m中加入如下程式碼:
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^test)(void) = ^{
NSLog(@"Block");
};
test();
}
return 0;
}
複製程式碼
- 然後通過xcrun指令將main.m檔案轉換成C++程式碼
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
複製程式碼
- 檢視生成的main.cpp檔案,首先看main函式,轉換成C++之後,結構如下,此處去除了多餘的強轉操作,方便閱讀
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
//Block的定義
void(*test)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA
);
//block的呼叫
test->FuncPtr(test);
}
return 0;
}
複製程式碼
- Block在編譯完成之後,轉換成了__main_block_impl_0型別的結構體,它的內部結構如下
struct __main_block_impl_0 {
//存放了block的一些基本資訊,包括isa,函式地址等等
struct __block_impl impl;
//存放block的一些描述資訊
struct __main_block_desc_0* Desc;
//建構函式
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
複製程式碼
由於結構體__block_impl是直接存放在__main_block_impl_0結構體的內部,所以__main_block_impl_0結構體也可以轉換成如下形式
struct __block_impl {
void *isa; //isa指標,可以看出Block其實就是一個OC物件
int Flags; //標識,預設為0
int Reserved; //保留欄位
void *FuncPtr;//函式記憶體地址
};
struct __main_block_impl_0 {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0* Desc;
};
複製程式碼
block將我們所要呼叫的程式碼封裝成了函式__main_block_func_0,並且將函式__main_block_func_0的記憶體地址儲存在到void *FuncPtr中,具體函式如下
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//此處就是呼叫的NSLog
NSLog((NSString *)&__NSConstantStringImpl__var_folders_f3_lg91hwts5rjdlzjph0sn82m80000gp_T_main_4f0065_mi_0);
}
複製程式碼
結構體__main_block_desc_0中則儲存了block所佔用記憶體大小等描述資訊
static struct __main_block_desc_0 {
size_t reserved; //保留欄位
size_t Block_size; //__main_block_impl_0結構體所佔記憶體大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
複製程式碼
- 由此可以看出block底層其實就是一個OC物件,因為它內部擁有isa指標。同時block將內部所要執行的程式碼封裝成了函式,並且將函式記憶體地址儲存到結構體當中,以便在合適的時機進行呼叫
訪問外部變數時Block的底層結構
我們在使用Block的過程中,可以在Block內部訪問外部的變數,包含區域性變數、靜態變數(相當於私有的全域性變數)、全域性變數等等。現在就通過一個Demo來看一下block底層是如何訪問外部變數的。
- 首先建立Demo,在main.m檔案中新增如下程式碼
//定義全域性變數c
int c = 30;
int main(int argc, const char * argv[]) {
@autoreleasepool {
//區域性變數a
int a = 10;
//靜態變數b
static int b = 20;
void(^test)(void) = ^{
NSLog(@"Block - %d, %d, %d", a, b, c);
};
test();
}
return 0;
}
複製程式碼
- 將main.m轉換成C++程式碼後,再次檢視main函式,結果如下
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int a = 10;
static int b = 20;
void(*test)(void) = (&__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
a,
&b));
test->FuncPtr(test);
}
return 0;
}
複製程式碼
可以看出,此時__main_block_impl_0結構體中多了兩個引數,分別是區域性變數a的值,靜態變數b的指標,也就是它的記憶體地址。
- 檢視__main_block_impl_0結構體的記憶體結構
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int *b;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
複製程式碼
發現,在__main_block_impl_0結構體中多了兩個成員變數,一個是int a,一個是int *b
- 當通過test->FuncPtr(test)執行block時,會通過結構體中的FuncPtr找到函式__main_block_func_0的地址進行呼叫,檢視__main_block_func_0函式如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
int *b = __cself->b; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_f3_lg91hwts5rjdlzjph0sn82m80000gp_T_main_064cd6_mi_0, a, (*b), c);
}
複製程式碼
在__main_block_func_0函式中,訪問區域性變數a和靜態變數b時都是通過傳遞過來的__main_block_impl_0結構體拿到對應的成員變數進行訪問,但是全域性變數c並沒有存放在結構體中,而是直接進行訪問。
- 由此我們就可以得出結論,block中有變數捕獲的機制
- 當訪問區域性變數的時候,會將區域性變數的值捕獲到block中,存放在一個同名的成員變數中。
- 當訪問靜態變數時,會將靜態變數的地址捕獲到block中,存放在一個同名的成員變數中。
- 當訪問全域性變數時,因為全域性變數是一直存在,不會銷燬,所以在block中直接訪問全域性變數,不需要進行捕獲
此處需要注意的是,其實在OC中有個預設的關鍵字auto,在我們建立區域性變數的時候,會預設在區域性變數前加上auto關鍵字進行修飾,例如上文中的int a,其實就相當於auto int a。auto關鍵字的含義就是它所修飾的變數會自動釋放,也表示著它所修飾的變數會存放到棧空間,系統會自動對其進行釋放。
block總結
block底層結構總結
block在編譯完成之後會轉換成結構體進行儲存,結構體中的成員變數如下,其中在成員變數descriptor指向的結構體中,多了兩個函式指標分別為copy和dispose,這兩個函式和block內部物件的記憶體管理有關,後面會具體說明。
block變數捕獲總結
block使用變數捕獲機制來保證在block內部能夠正常的訪問外部變數。
- 當block訪問的是auto型別的區域性變數時,會將區域性變數捕獲到block內部的結構體中,並且是直接捕獲變數的值。
- 當block訪問的是static型別的靜態變數時,會將靜態變數捕獲到block內部的結構體中,並且捕獲的是靜態變數的地址。
- 當block訪問的是全域性變數時,不會進行捕獲,直接進行訪問。
block的型別
block的三種型別
在OC當中block其實擁有三種型別,可以通過class或者isa指標來檢視block具體的型別
- 首先在main.m中新增以下示例程式碼
int main(int argc, const char * argv[]) {
@autoreleasepool {
//第一種型別NSGlobalBlock
NSLog(@"%@",[^{
NSLog(@"NSGlobalBlock");
} class]);
//第二種型別NSStackBlock
int a = 10;
NSLog(@"%@",[^{
NSLog(@"%d", a);
} class]);
//第三種型別NSMallocBlock - 1
void(^test2)(void) = ^{
NSLog(@"NSMallocBlock - %d", a);
};
NSLog(@"%@",[test2 class]);
//第三種型別NSMallocBlock - 2
NSLog(@"%@",[[^{
NSLog(@"%d", a);
} copy] class]);
}
return 0;
}
複製程式碼
執行結果如下:
- 執行後可以發現,block可以有三種型別,分別是NSGlobalBlock、NSStackBlock和NSMallocBlock,這三種型別分別存放在.data區、棧區和堆區。對應的結構圖如下
圖中的block型別和上文中列印出來的block型別對應關係如下
class方法返回型別 | isa指向型別 |
---|---|
NSGlobalBlock | _NSConcreteGlobalBlock |
NSStackBlock | _NSConcreteStackBlock |
NSMallocBlock | _NSConcreteMallocBlock |
但是不管它是哪種block型別,最終都是繼承自NSBlock型別,而NSBlock繼承自NSObject,所以這也說明了block本身就是一個物件。
如何區分block型別
在上述示例中,提到了四種生成不同型別的block的方法,分別如下:
- 沒有訪問區域性變數的block,並且沒有強指標指向block,則此block為NSGlobalBlock
- 訪問了區域性變數的block,但是沒有強指標指向block,則此block為NSStackBlock
- 訪問了區域性變數的block,並且有強指標指向block,則此block為NSMallocBlock
- NSStackBlock型別的block,執行了copy操作之後,生成的block為NSMallocBlock
其實第三點和第四點生成的都是NSMallocBlock,由此我們就可以得到下面的結論
block的型別 | block執行的操作 |
---|---|
NSGlobalBlock | 沒有訪問auto型別的變數 |
NSStackBlock | 訪問了auto型別的變數 |
NSMallocBlock | __NSStackBlock__型別的block執行了copy操作 |
block的copy操作
block執行copy操作後的記憶體變化
NSGlobalBlock、NSStackBlock和NSMallocBlock三種型別的block分別存放在了資料區、棧區和堆區。將三種型別的block分別進行copy操作之後,產生的結果如下:
- 對NSGlobalBlock的block進行copy操作,什麼也不會發生,生成的還是NSGlobalBlock型別的block
- 對NSStackBlock型別的block進行操作,會將block從棧上覆制一份到堆中,生成NSMallocBlock型別的block
- 對NSMallocBlock型別的block進行copy操作,此block的引用計數會加1
結構圖如下
ARC環境下哪些操作會自動進行copy操作?
在上述示例中,NSStackBlock型別的block,執行了copy操作之後,生成的block為NSMallocBlock,其實不止這一種方式生成NSMallocBlock,以下是OC中在ARC環境下自動觸發copy操作的幾種情況:
- block作為返回值時,會自動進行copy
typedef void(^block)(void);
block test(){
return ^{
NSLog(@"NSMallocBlock");
};
}
複製程式碼
- 使用__strong型別的指標指向block時,會執行copy操作
void(^test2)(void) = ^{
NSLog(@"NSMallocBlock - %d", a);
};
複製程式碼
- block作為Cocoa API中含有usingBlock的方法的引數時,會執行copy操作
NSArray *arr = @[@"1",@"2"];
[arr enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"NSMallocBlock");
}];
複製程式碼
- block作為GCD方法的引數時會執行copy操作
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"NSMallocBlock");
});
複製程式碼
我們平常在使用block作為屬性的時候,都會使用copy修飾符來修飾,其實內部就是對block進行了一次copy操作,將block拷貝到堆上,以便我們手動管理block的記憶體
block訪問物件型別
訪問物件型別的auto變數時,block的底層結構
上文中Block訪問的外部變數都是基本資料型別,所以不涉及到記憶體管理,如果在block中訪問外部物件時,block內部又是什麼樣的結構呢?
- 首先在main.m中加入以下示例程式碼
int main(int argc, const char * argv[]) {
@autoreleasepool {
//預設物件
NSObject *obj1 = [[NSObject alloc] init];
void(^test1)(void) = ^{
NSLog(@"NSMallocBlock - %@", obj1);
};
test1();
//使用__weak指標修飾物件
NSObject *obj2 = [[NSObject alloc] init];
__weak typeof(obj2) weakObj = obj2;
void(^test2)(void) = ^{
NSLog(@"NSMallocBlock - %@", weakObj);
};
test2();
}
return 0;
}
複製程式碼
- 使用如下指令將main.m檔案轉換成C++程式碼
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
複製程式碼
此處由於使用了__weak關鍵字來修飾物件,涉及到runtime,所有需要指定runtime的版本。
- 轉換成main.cpp檔案後,檢視block的底層結構為
//直接訪問外部物件的block內部結構
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
//生成strong型別的指標
NSObject *__strong obj;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _obj, int flags=0) : obj(_obj) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//訪問__weak修飾符修飾的外部物件的block內部結構
struct __main_block_impl_1 {
struct __block_impl impl;
struct __main_block_desc_1* Desc;
//自動生成weak型別的指標
NSObject *__weak weakObj;
__main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, NSObject *__weak _weakObj, int flags=0) : weakObj(_weakObj) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
複製程式碼
這時發現,如果直接在block中訪問外部的auto型別的物件,預設是在block結構體中生成一個strong型別的指標指向外部物件,如結構體__main_block_impl_0。如果在block中訪問了__weak修飾符修飾的外部物件,那麼在它的內部會生成一個weak型別的指標指向外部物件,如結構體__main_block_impl_1。
在__main_block_impl_0的建構函式中,obj(_obj)就代表著,以後建構函式傳過來的_obj引數會自動賦值給結構體中的成員變數obj。
- 由於__main_block_desc_0和__main_block_desc_1結構相同,所以以下只以__main_block_desc_0為例,檢視__main_block_desc_0結構體,會發現它內部新增加了兩個函式指標,如下
static struct __main_block_desc_0 {
size_t reserved; //保留欄位
size_t Block_size; //整個block所佔記憶體空間
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); //copy函式
void (*dispose)(struct __main_block_impl_0*); //dispose函式
} __main_block_desc_0_DATA = { 0,
sizeof(struct __main_block_impl_0),
__main_block_copy_0,
__main_block_dispose_0};
複製程式碼
新增加了copy和dispose兩個函式指標,對應著函式__main_block_copy_0和__main_block_dispose_0,如下
//copy指標指向的函式
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
//dispose指標指向的函式
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
複製程式碼
之前說過,block封裝了函式呼叫和函式呼叫環境,這也就意味這如果它引用了外部的物件,就需要對外部物件進行記憶體管理操作。__main_block_copy_0函式內部會呼叫_Block_object_assign函式,它的主要作用是根據外部引用的物件的修飾符來進行相應的操作,如果外部物件是使用__strong來修飾,那麼_Block_object_assign函式會對此物件進行一次類似retain的操作,使得外部物件的引用計數+1。
__main_block_dispose_0函式內部會呼叫_Block_object_dispose函式,它的作用就是在block內部函式執行完成之後對block內部引用的外部物件進行一次release操作。
總結
Block在棧上
如果block在棧上,那麼在block中訪問物件型別的auto變數時,是不會對auto變數產生強引用的。這個需要在MRC情況下進行測試,將Xcode中Build Settings下的Automatic Reference Counting設定成NO,表明當前使用MRC環境。
- 首先建立XLPerson類,重寫dealloc方法,方便測試
@implementation XLPerson
- (void)dealloc{
[super dealloc];
NSLog(@"%s", __func__);
}
@end
複製程式碼
- 在main.m中增加如下測試程式碼
typedef void(^TestBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
//建立block
TestBlock block;
{
XLPerson *person = [[XLPerson alloc] init];
block = ^{
NSLog(@"block --- %p", &person);
};
NSLog(@"%@", [block class]);
[person release];
}
NSLog(@"block執行前");
block();
[block release];
NSLog(@"block執行後");
}
return 0;
}
複製程式碼
- 執行程式,得到如下的列印資訊
可以發現,在MRC環境下,即使是有強指標指向block,系統也不會對block進行預設的copy操作,所以當前的block型別依舊為NSStackBlock型別。而且,在block執行之前,XLPerson就已經釋放了,說明在棧上的block並沒有對person物件進行強引用。
block被copy堆上
- 首先,如果block被copy到了堆上,在訪問auto修飾的物件變數時,內部會自動呼叫copy函式,它內部會呼叫_Block_object_assign函式,_Block_object_assign函式會根據auto變數的修飾符(__strong、__weak、__unsafe_unretained)做出相應的處理,如果是__strong修飾,則內部對外部的物件形成強引用,如果是__weak或者__unsafe_unretained,則會形成弱引用
- 如果block執行完成,被系統從堆中移除時,會呼叫dispose函式,它內部呼叫_Block_object_dispose函式,_Block_object_dispose函式會自動釋放引用的auto變數,也就是對引用的auto變數進行一次release操作。
- copy和dispose函式呼叫時機如下
函式 | 呼叫時機 |
---|---|
copy | 棧上的block被複制到堆上 |
dispose | 堆上的block被釋放時 |
__block
__block的作用
使用block時,如果block中訪問到了外部被auto修飾的變數,我們經常使用到__block來修飾外部變數,它的主要作用就是能夠讓我們在block內部來修改外部變數的值,當然,block只能用來修飾auto變數,不能用來修飾全域性變數和靜態變數。
- 首先來建立Demo,檢視原始碼
typedef void(^TestBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block XLPerson *person = [[XLPerson alloc] init];
__block int a = 10;
TestBlock block = ^{
person = nil;
a = 20;
NSLog(@"block -- a:%d, person:%@",a,person);
};
block();
NSLog(@"block呼叫後,a:%d, person:%@",a,person);
}
return 0;
}
複製程式碼
- 通過以下指令轉換成C++程式碼
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
複製程式碼
- 首先檢視block結構體的原始碼,發現block內部多了兩個指標,__Block_byref_person_0型別的指標person和__Block_byref_a_1型別的指標a
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_person_0 *person; // by ref
__Block_byref_a_1 *a; // by ref
};
複製程式碼
- 再檢視main函式中,區域性變數和block的建立方式
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
//封裝person物件
__Block_byref_person_0 person = {
0, //
&person,
33554432,
sizeof(__Block_byref_person_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
objc_msgSend(objc_msgSend(objc_getClass("XLPerson"), sel_registerName("alloc")), sel_registerName("init"))};
//封裝變數a
__Block_byref_a_1 a = {
0,
(__Block_byref_a_1 *)&a,
0,
sizeof(__Block_byref_a_1),
10
};
//建立block
TestBlock block = (&__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
&person,
&a,
570425344));
block->FuncPtr(block);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_f3_lg91hwts5rjdlzjph0sn82m80000gp_T_main_115560_mi_1,(a.__forwarding->a),(person.__forwarding->person));
}
return 0;
}
複製程式碼
__block修飾物件型別auto變數
通過__block修飾的person物件在編譯後被封裝成了__Block_byref_person_0型別的結構體,內部有多個成員變數,如下
#將person物件封裝成結構體__Block_byref_person_0
struct __Block_byref_person_0 {
void *__isa; //isa指標
__Block_byref_person_0 *__forwarding; //forwarding指標
int __flags; //標識位
int __size; //結構體所佔記憶體大小
void (*__Block_byref_id_object_copy)(void*, void*);//函式指標指向copy函式
void (*__Block_byref_id_object_dispose)(void*); //函式指標指向dispose函式
XLPerson *__strong person; //強引用XLPerson的例項物件
};
//__Block_byref_person_0結構體的建立與賦值
__Block_byref_person_0 person = {
0, //對應isa指標,傳0
&person, //對應forwarding指標,將結構體自身的地址傳給了forwarding指標
33554432, //對應flags
sizeof(__Block_byref_person_0), //當前結構體所需記憶體大小
__Block_byref_id_object_copy_131, //copy函式
__Block_byref_id_object_dispose_131,//dispose函式
objc_msgSend(objc_msgSend(objc_getClass("XLPerson"),
sel_registerName("alloc")),
sel_registerName("init")) //通過objc_msgSend建立XLPerson物件,並且將物件的指標傳入結構體中
};
複製程式碼
可以明顯看出,在結構體__Block_byref_person_0中,存在如下成員變數
- isa指標,此處賦值為0,同時也能說明此結構體也是一個OC物件
- forwarding指標,指向結構體自身的記憶體地址
- flags,標誌位,此處傳33554432
- size,結構體大小,通過sizeof(__Block_byref_person_0)獲得,此處可以簡單計算出結構體所需記憶體大小為48個位元組
- __Block_byref_id_object_copy,copy函式,因為在結構體中引用到了person物件,所以呼叫此方法來根據person指標的引用型別決定是否對person物件進行retain操作,此處person物件是使用__strong來修飾,所以copy函式的作用就是對person物件進行一次retain操作,引用計數+1。
- __Block_byref_id_object_dispose_131,dispose函式,在結構體從記憶體中移除的時候,會呼叫dispose函式,對person物件進行一次release操作,引用計數-1
- person指標,因為我們外部建立的是XLPerson的例項物件,所以結構體內部直接儲存了person指標來指向我們建立的XLPerson物件。
前文提到過,因為block封裝了函式呼叫環境,所以一旦它內部引用了外部的auto物件,就需要對外部物件的記憶體進行管理,所以才有了copy函式和dispose函式。此處也一樣,因為使用__block修飾的XLPerson物件的指標存放在了結構體內部,所以需要使用copy函式和dispose函式來管理物件的記憶體。
__block修飾基本資料型別auto變數
如果使用__block來修飾基本資料型別的auto變數,就會將變數封裝成__Block_byref_a_1型別的結構體,內部結構如下
#將變數a封裝成結構體__Block_byref_a_1
struct __Block_byref_a_1 {
void *__isa; //isa指標
__Block_byref_a_1 *__forwarding;//forwarding指標
int __flags; //標識位
int __size; //結構體大小
int a; //變數a
};
//封裝變數a
__Block_byref_a_1 a = {
0, //isa,傳0
(__Block_byref_a_1 *)&a, //傳入當前結構體a的地址
0, //flags
sizeof(__Block_byref_a_1), //結構體的大小
10 //外部變數a的值
};
複製程式碼
相對於__block修飾auto物件,如果修飾基本資料型別,則結構體中少了copy函式和dispose函式,因為基本資料型別不需要進行記憶體管理,所以不需要呼叫這兩個函式。
- isa指標,此處傳0
- forwarding指標,指向結構體自身的記憶體地址
- flags,此處傳0
- size,結構體的大小,使用sizeof(__Block_byref_a_1)來獲取,此處結構體佔用記憶體大小為32個位元組(結構體本身需要的記憶體大小為28個位元組)
- a,儲存了外部變數a的值,此處為10
總結
- __block可以用來解決在block內部無法修改auto變數值的問題。
- __block只能用來修飾auto型別變數,無法用來修飾全域性變數、靜態變數等等
- 使用__block修飾的auto變數,編譯器會將此變數封裝成一個結構體(其實也是一個物件),結構體內部有以下幾個成員變數
- isa指標
- forwarding指標,指向自身記憶體地址
- flags
- size,結構體的大小
- val(使用的外部變數,如果是基本資料型別,就是變數的值,如果是物件型別,就是指向物件的指標)
- __block修飾基本資料型別的auto變數,例如__block int a,那麼封裝的結構體內部成員變數如上,如果是修飾物件型別的auto變數,如__block XLPerson *person,那麼生成的結構體中會多出copy和dispose兩個函式,用來管理person物件的記憶體。
__block的記憶體管理
當block訪問外部__block修飾的auto變數時,會將變數封裝成結構體,並且將結構體的地址值存放在block內部
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_person_0 *person; // by ref
__Block_byref_a_1 *a; // by ref
};
複製程式碼
其中person和a就是指向兩個__block結構體的指標,正因為在block中有引用到__Block_byref_person_0和__Block_byref_a_1,那麼block就必須對這兩個結構體的記憶體進行管理,所以相應的在__main_block_desc_0中就生成了兩個函式copy和dispose,專門用來管理person和a所指向的結構體(也是物件)的記憶體。如下
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*); //copy函式
void (*dispose)(struct __main_block_impl_0*); //dispose函式
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0),
__main_block_copy_0, //copy函式
__main_block_dispose_0 //dispose函式
};
複製程式碼
相應的copy函式和dispose函式如下
//copy函式
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign(&dst->person,src->person, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign(&dst->a, src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
//dispose函式
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
複製程式碼
這裡和上文中說到的block中訪問外部物件的記憶體管理相同
- 當block在棧上的時候,block內部並不會對__block變數產生強引用,在上文中已經使用demo驗證過
- 當block被copy到堆上時,首先會呼叫copy函式,在copy函式內部會呼叫_Block_object_assign函式來對__block變數形成強引用。這裡和之前說到的block訪問外部auto物件有點不同,如果block訪問外部物件,_Block_object_assign會根據外部物件的修飾符是否是__strong還是__weak來決定是否對物件形成強引用,但是如果是訪問__block變數,block就一定會對__block變數形成強引用。
當圖中的Block0被賦值到堆上時,會將他所引用的__block變數一起賦值到堆上,並且對堆上的__block變數產生強引用
當圖中的Block1被複制到堆上時,因為之前__block變數已經被複制到了堆上,所以Block1只是對堆上的__block變數產生強引用。
- 當block從堆中移除時,會呼叫block內部的dispose函式,dispose函式內部又會呼叫_Block_object_dispose函式來自動釋放引用的__block變數,相當於對__block變數執行一次release操作。
當Block0和Block1都被廢棄時,Block0和Block1對__block變數的引用會被釋放,所以__block變數最終因為沒有持有者而被廢棄
__block中的__forwarding指標
__block修飾的auto變數所對應的結構體如下
在結構體中有一個__forwarding指標指向自己,在後續訪問__block變數的時候也是通過__forwarding指標來進行訪問
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_1 *a = __cself->a; // bound by ref
__Block_byref_person_0 *person = __cself->person; // bound by ref
(a->__forwarding->a) = 20; //通過__forwarding指標來拿到a進行修改
(person->__forwarding->person) = __null; //通過__forwarding指標來拿到person進行修改
NSLog((NSString *)&__NSConstantStringImpl__var_folders_f3_lg91hwts5rjdlzjph0sn82m80000gp_T_main_fbc4b7_mi_0,(a->__forwarding->a),(person->__forwarding->person));
}
複製程式碼
當block在棧上時,__block變數也存放在棧上,它內部的__forwarding指標指向它本身
當block被複制到堆上之後,block所引用的__block變數也會被複制到堆上,這樣在棧上和堆上各存在一份__block變數,此時將棧上__block變數中的__forwarding指標指向堆上__block變數的地址,同時,堆上的__block變數中的__forwarding指標指向它本身,那麼此時,不管我們是訪問棧上__block變數中的屬性值還是堆上__block變數中的屬性值,都是通過__forwarding指標訪問到堆上的__block變數。
__block修飾的物件型別記憶體管理總結
- 當__block變數存放在棧上時,他內部不會對指向的物件產生強引用
- 當block被copy到堆上時,它訪問的__block變數也會被copy到堆上
- 會首先呼叫__block變數內部的copy函式,copy函式內部會呼叫_Block_object_assign函式
- _Block_object_assign函式會更加所指向物件的修飾符(__strong、__weak、__unsafe_unretained)來做出相應的操作,如果是__strong修飾的物件,則會對它進行強引用
- 如果block從堆上移除
- 會呼叫__block變數內部的dispose函式,dispose函式內部會呼叫_Block_object_dispose函式
- _Block_object_dispose函式會對__block變數內部引用的物件進行釋放操作,相當於執行一次release。
block訪問物件型別的auto變數和__block變數對比
相同點
當block存放在棧上是,對物件型別的auto變數和__block變數都不會產生強引用
不同點
當block被copy到堆上時
- 訪問物件型別的auto變數時,block內部會呼叫copy函式,根據物件的修飾符(__strong、__weak、__unsafe_unretained)來決定是否對物件進行強引用,如果是__strong修飾的物件,則進行強引用。copy函式如下
//copy函式
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
複製程式碼
- 訪問__block變數時,block內部會直接呼叫copy函式,對__block變數進行強引用,copy函式如下
//copy函式
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign(&dst->a, src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
複製程式碼
當block從堆中移除
當block從堆中移除時,都會呼叫dispose函式來對引用的物件進行釋放
- 引用物件型別的auto變數時呼叫的dispose函式
//dispose函式
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
複製程式碼
- 引用__block變數時呼叫的dispose函式
//dispose函式
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
複製程式碼
雖然呼叫的都是copy函式,但是傳遞的引數型別不同,訪問物件型別的auto變數時,傳遞的引數為3(BLOCK_FIELD_IS_OBJECT),訪問__block變數時,傳遞的引數為8(BLOCK_FIELD_IS_BYREF)。
補充
block迴圈引用的問題
在使用block時,如果block作為一個物件的屬性,並且在block中也使用到了這個物件,則會產生迴圈引用,導致block和物件相互引用,無法釋放。Demo如下
typedef void(^TestBlock)(void);
@interface XLPerson : NSObject
@property(nonatomic, copy)NSString *name;
@property(nonatomic, copy)TestBlock block;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
XLPerson *person = [[XLPerson alloc] init];
person.name = @"張三";
person.block = ^{
NSLog(@"%@",person.name);
};
person.block();
}
return 0;
}
複製程式碼
解決方式有兩種(此處主要講解ARC的情況下):
- 使用__weak來修飾物件
__weak typeof(person) weakPerson = person;
person.block = ^{
NSLog(@"%@",weakPerson.name);
};
複製程式碼
- 使用__unsafe_unretained來修飾物件
__unsafe_unretained XLPerson *weakPerson = person;
person.block = ^{
NSLog(@"%@",weakPerson.name);
};
複製程式碼
__weak和__unsafe_unretained的區別
__weak和__unsafe_unretained最終的效果都是能shi使block不對外部訪問的物件形成強引用,而是形成弱引用。也就是說外部物件的引用計數不會增加。但是__weak和__unsafe_unretained也有區別,__weak在物件被銷燬後會自動將weak指標置為nil,而__weak和__unsafe_unretained修飾的物件在被銷燬後,指標是不會被清空的,如果後續訪問到了這個指標,會報野指標的錯誤,因此在遇到迴圈引用的時候,優先使用__weak來解決。更多的關於__weak的內容會在後續文章中進行學習。
面試題
1、block的本質是什麼?
block其實就是封裝了函式呼叫與呼叫環境的OC物件,它的底層其實是一個結構體。
2、__block的作用是什麼?
在block中如果想要修改外部訪問的auto變數,就需要使用__block來修飾auto變數,它會將修飾的變數封裝成一個結構體,結構體內部存放著變數的值。如果__block修飾的是物件型別,那麼在結構體中會儲存著儲存物件記憶體地址的指標,同時在結構體中還多出兩個函式指標copy和dispose,用來管理物件的記憶體。
3、block作為屬性時為什麼要用copy來修飾?
在ARC中,block如果使用copy來修飾,會將block從棧上覆制到堆上,方便我們手動管理block的記憶體,如果不用copy來修飾的話,那麼block就會存在棧上,由系統自動釋放記憶體。
4、使用block會遇到什麼問題?怎麼解決?
在使用block過程中,會遇到迴圈引用的問題,解決方式就是使用__weak或者__unsafa_unretain來修飾外部引用的物件。優先使用__weak。
結束語
以上內容純屬個人理解,如果有什麼不對的地方歡迎留言指正。
一起學習,一起進步~~~