ObjectiveC基礎之四(深入理解Block)

NO IMAGE

什麼是Block

Block其實就是一個代碼塊,通常被稱為“閉包”,它封裝了函數調用以及函數調用環境,以便在合適的時機進行調用,在OC中,Block其實就是一個OC對象,它可以當做參數傳遞。

Block的結構如下:

ObjectiveC基礎之四(深入理解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指向的結構體中,多了兩個函數指針分別為copydispose,這兩個函數和block內部對象的內存管理有關,後面會具體說明。

ObjectiveC基礎之四(深入理解Block)

block變量捕獲總結

block使用變量捕獲機制來保證在block內部能夠正常的訪問外部變量。

ObjectiveC基礎之四(深入理解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;
}

運行結果如下:

ObjectiveC基礎之四(深入理解Block)

  • 運行後可以發現,block可以有三種類型,分別是NSGlobalBlockNSStackBlockNSMallocBlock,這三種類型分別存放在.data區、棧區堆區。對應的結構圖如下
ObjectiveC基礎之四(深入理解Block)

圖中的block類型和上文中打印出來的block類型對應關係如下

class方法返回類型isa指向類型
NSGlobalBlock_NSConcreteGlobalBlock
NSStackBlock_NSConcreteStackBlock
NSMallocBlock_NSConcreteMallocBlock

但是不管它是哪種block類型,最終都是繼承自NSBlock類型,而NSBlock繼承自NSObject,所以這也說明了block本身就是一個對象。

如何區分block類型

在上述示例中,提到了四種生成不同類型的block的方法,分別如下:

  1. 沒有訪問局部變量的block,並且沒有強指針指向block,則此block為NSGlobalBlock
  2. 訪問了局部變量的block,但是沒有強指針指向block,則此block為NSStackBlock
  3. 訪問了局部變量的block,並且有強指針指向block,則此block為NSMallocBlock
  4. NSStackBlock類型的block,執行了copy操作之後,生成的block為NSMallocBlock

其實第三點和第四點生成的都是NSMallocBlock,由此我們就可以得到下面的結論

block的類型block執行的操作
NSGlobalBlock沒有訪問auto類型的變量
NSStackBlock訪問了auto類型的變量
NSMallocBlock__NSStackBlock__類型的block執行了copy操作

block的copy操作

block執行copy操作後的內存變化

NSGlobalBlockNSStackBlockNSMallocBlock三種類型的block分別存放在了數據區、棧區和堆區。將三種類型的block分別進行copy操作之後,產生的結果如下:

  • NSGlobalBlock的block進行copy操作,什麼也不會發生,生成的還是NSGlobalBlock類型的block
  • NSStackBlock類型的block進行操作,會將block從棧上覆制一份到堆中,生成NSMallocBlock類型的block
  • NSMallocBlock類型的block進行copy操作,此block的引用計數會加1

結構圖如下

ObjectiveC基礎之四(深入理解Block)

ARC環境下哪些操作會自動進行copy操作?

在上述示例中,NSStackBlock類型的block,執行了copy操作之後,生成的block為NSMallocBlock,其實不止這一種方式生成NSMallocBlock,以下是OC中在ARC環境下自動觸發copy操作的幾種情況:

  1. block作為返回值時,會自動進行copy
typedef void(^block)(void);
block test(){
return ^{
NSLog(@"NSMallocBlock");
};
}
  1. 使用__strong類型的指針指向block時,會執行copy操作
void(^test2)(void) = ^{
NSLog(@"NSMallocBlock - %d", a);
};
  1. block作為Cocoa API中含有usingBlock的方法的參數時,會執行copy操作
NSArray *arr = @[@"1",@"2"];
[arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"NSMallocBlock");
}];
  1. 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;
}
  • 運行程序,得到如下的打印信息
ObjectiveC基礎之四(深入理解Block)

可以發現,在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)來獲取,此處結構體大小為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
};

其中persona就是指向兩個__block結構體的指針,正因為在block中有引用到__Block_byref_person_0__Block_byref_a_1,那麼block就必須對這兩個結構體的內存進行管理,所以相應的在__main_block_desc_0中就生成了兩個函數copy和dispose,專門用來管理persona所指向的結構體(也是對象)的內存。如下

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變量形成強引用。
ObjectiveC基礎之四(深入理解Block)

當圖中的Block0被賦值到堆上時,會將他所引用的__block變量一起賦值到堆上,並且對堆上的__block變量產生強引用

ObjectiveC基礎之四(深入理解Block)

當圖中的Block1被複制到堆上時,因為之前__block變量已經被複制到了堆上,所以Block1只是對堆上的__block變量產生強引用。

  • 當block從堆中移除時,會調用block內部的dispose函數,dispose函數內部又會調用_Block_object_dispose函數來自動釋放引用的__block變量,相當於對__block變量執行一次release操作。
ObjectiveC基礎之四(深入理解Block)

當Block0和Block1都被廢棄時,Block0和Block1對__block變量的引用會被釋放,所以__block變量最終因為沒有持有者而被廢棄

__block中的__forwarding指針

__block修飾的auto變量所對應的結構體如下

ObjectiveC基礎之四(深入理解Block)

在結構體中有一個__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指針指向它本身

ObjectiveC基礎之四(深入理解Block)

當block被複制到堆上之後,block所引用的__block變量也會被複制到堆上,這樣在棧上和堆上各存在一份__block變量,此時將棧上__block變量中的__forwarding指針指向堆上__block變量的地址,同時,堆上的__block變量中的__forwarding指針指向它本身,那麼此時,不管我們是訪問棧上__block變量中的屬性值還是堆上__block變量中的屬性值,都是通過__forwarding指針訪問到堆上的__block變量。

ObjectiveC基礎之四(深入理解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。

結束語

以上內容純屬個人理解,如果有什麼不對的地方歡迎留言指正。

一起學習,一起進步~~~

相關文章

爬蟲管理平臺Crawlabv0.4.3發佈(界面上點幾下就可安裝pip或npm依賴)

🇮🇷蘇萊曼尼遇刺,竟是因為這個….

ES6import/export靜態編譯

ES6系列之箭頭函數全解析