OC底层原理-Block
對于block,在日常開發(fā)中經(jīng)常會用到,但有時候會出現(xiàn)一些奇奇怪怪的問題,比如block的里面值沒更新上,又比如block里面要改變一個外部變量的值,就得加上__block,不然編譯器就會報錯。
那么,這次就來一次過搞清楚block背后不為人知的真相。
Block的基本結(jié)構(gòu)
void blockTest() {void (^block)(void) = ^{NSLog(@"Hello World!");};block(); }int main(int argc, char * argv[]) {@autoreleasepool {blockTest();} }通過Clang在終端輸入clang -rewrite-objc main.m,會生成一個cpp文件,打開可得:
struct __blockTest_block_impl_0 {struct __block_impl impl;struct __blockTest_block_desc_0* Desc;__blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int flags=0) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;} };static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself) {NSLog((NSString *)&__NSConstantStringImpl__var_folders_04_xwbq8q6n0p1dmhhd6y51_vbc0000gp_T_main_0048d2_mi_0);}static struct __blockTest_block_desc_0 {size_t reserved;size_t Block_size; } __blockTest_block_desc_0_DATA = { 0, sizeof(struct __blockTest_block_impl_0)};void blockTest() {void (*block)(void) = ((void (*)())&__blockTest_block_impl_0((void *)__blockTest_block_func_0, &__blockTest_block_desc_0_DATA));((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); }int main(int argc, char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; blockTest();} }static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };其實block的背后會生成一個結(jié)構(gòu)體,這個結(jié)構(gòu)體就是__blockTest_block_impl_0。
__blockTest_block_impl_0
struct __blockTest_block_impl_0 {struct __block_impl impl;struct __blockTest_block_desc_0* Desc;__blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int flags=0) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;} };__blockTest_block_impl_0是Block的C++實現(xiàn),是一個結(jié)構(gòu)體。包含了:
impl、__blockTest_block_desc_0* Desc和一個構(gòu)造函數(shù)。
__block_impl
struct __block_impl {void *isa;int Flags;int Reserved;void *FuncPtr; };- 這里面有一個isa指針,指向一個類對象,有三種類型:_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock。
- Flags:block 的負(fù)載信息(引用計數(shù)和類型信息),按位存儲。
- *FuncPtr:一個指針,指向Block執(zhí)行時調(diào)用的函數(shù),也就是Block需要執(zhí)行的代碼塊。在本例中是__blockTest_block_func_0函數(shù)。
__blockTest_block_desc_0
static struct __blockTest_block_desc_0 {size_t reserved;size_t Block_size; } __blockTest_block_desc_0_DATA = { 0, sizeof(struct __blockTest_block_impl_0)};__blockTest_block_desc_0包含了兩個變量:
- reserved:Block版本升級所需的預(yù)留區(qū)空間,在這里為0。
- Block_size:Block大小(sizeof(struct __blockTest_block_impl_0))。
可見,block也是會占用一定的內(nèi)存大小的。
__blockTest_block_func_0
static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself) {NSLog((NSString *)&__NSConstantStringImpl__var_folders_04_xwbq8q6n0p1dmhhd6y51_vbc0000gp_T_main_0048d2_mi_0);}__blockTest_block_func_0就是Block的執(zhí)行時調(diào)用的函數(shù),參數(shù)是一個__blockTest_block_impl_0類型的指針。
blockTest
void blockTest() {void (*block)(void) = ((void (*)())&__blockTest_block_impl_0((void *)__blockTest_block_func_0, &__blockTest_block_desc_0_DATA));((void (*)(__block_impl *)) ((__block_impl *)block)->FuncPtr)((__block_impl *)block); }第一句就是定義一個block指針,指向一個通過__blockTest_block_impl_0構(gòu)造函數(shù)構(gòu)造出的實例對象。
第二句就是調(diào)用block,通過block->FuncPtr指針找到__blockTest_block_func_0,并轉(zhuǎn)成((void (*)(__block_impl *))類型,最后將block作為參數(shù)傳給函數(shù)調(diào)用。
Flags
在__block_impl中我們看到Flags。
在這里Block_private.h中可以看到:
也就是說,一般情況下,一個 block 的 flags 成員默認(rèn)設(shè)置為 0。如果當(dāng) block 需要 Block_copy() 和 Block_release 這類拷貝輔助函數(shù),則會設(shè)置成 1 << 25 ,也就是 BLOCK_HAS_COPY_DISPOSE 類型。可以搜索到大量講述 Block_copy 方法的博文,其中涉及到了 BLOCK_HAS_COPY_DISPOSE 。
總結(jié)一下枚舉類的用法,前 16 位即起到標(biāo)記作用,又可記錄引用計數(shù)
BLOCK_DEALLOCATING:釋放標(biāo)記。一般常用 BLOCK_NEEDS_FREE 做 位與 操作,一同傳入 Flags ,告知該 block 可釋放。
BLOCK_REFCOUNT_MASK:一般參與判斷引用計數(shù),是一個可選用參數(shù)。
BLOCK_NEEDS_FREE:通過設(shè)置該枚舉位,來告知該 block 可釋放。意在說明 block 是 heap block ,即我們常說的 _NSConcreteMallocBlock 。
BLOCK_HAS_COPY_DISPOSE:是否擁有拷貝輔助函數(shù)(a copy helper function)。
BLOCK_HAS_CTOR:是否擁有 block 析構(gòu)函數(shù)(dispose function)。
BLOCK_IS_GC:是否啟用 GC 機(jī)制(Garbage Collection)。
BLOCK_HAS_SIGNATURE:與 BLOCK_USE_STRET 相對,判斷是否當(dāng)前 block 擁有一個簽名。用于 runtime 時動態(tài)調(diào)用。
block截獲變量
void blockTest() {int num = 10;void (^block)(void) = ^{NSLog(@"%d",num);};num = 20;block(); }int main(int argc, char * argv[]) {@autoreleasepool {blockTest();} }打印出來的結(jié)果是10,我們來看看c++代碼:
struct __blockTest_block_impl_0 {struct __block_impl impl;struct __blockTest_block_desc_0* Desc;int num;__blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int _num, int flags=0) : num(_num) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;} };static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself) {int num = __cself->num; // bound by copyNSLog((NSString *)&__NSConstantStringImpl__var_folders_04_xwbq8q6n0p1dmhhd6y51_vbc0000gp_T_main_3c2714_mi_0,num); }void blockTest() {int num = 10;void (*block)(void) = ((void (*)())&__blockTest_block_impl_0((void *)__blockTest_block_func_0, &__blockTest_block_desc_0_DATA, num));num = 20;((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); }對比一開始的代碼,這塊代碼多了個int num;
而構(gòu)造函數(shù)里也多了個參數(shù),__blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int _num, int flags=0),所以在調(diào)用構(gòu)造函數(shù)的時候,只是想num的值傳了過去,這就能解釋為什么這里打印出來的是10,而不是20。
使用static修飾變量
void blockTest() {static int num = 10;void (^block)(void) = ^{NSLog(@"%d",num);num = 30;};num = 20;block();NSLog(@"%d",num); }打印出來的結(jié)果分別是20、30。
C++代碼:
這次和上次又有點(diǎn)不一樣,不是int num而是int *num,這里傳進(jìn)來的是num的指針,所以,共用的是同一個變量,所以在block改變了值,在block外面也會發(fā)現(xiàn)值改變了。
為什么這里就用指針,而剛剛那里不用指針呢?
因為num是棧上的變量,如果用指針去訪問,一旦num的作用域結(jié)束了,num被釋放了這時候block再通過指針去訪問變量就會有問題,就是個野指針。而對于static修飾的就不一樣了,不用擔(dān)心會被釋放掉。
全局變量
int num = 10;void blockTest() {void (^block)(void) = ^{NSLog(@"%d",num);num = 30;};num = 20;block();NSLog(@"%d",num); }打印出來的結(jié)果也是20、30.
int num = 10;struct __blockTest_block_impl_0 {struct __block_impl impl;struct __blockTest_block_desc_0* Desc;__blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int flags=0) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;} };static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself) {NSLog((NSString *)&__NSConstantStringImpl__var_folders_04_xwbq8q6n0p1dmhhd6y51_vbc0000gp_T_main_1875c6_mi_0,num);num = 30;}在這里,構(gòu)造函數(shù)并沒有把num作為參數(shù)傳進(jìn)去,直接使用全局的num。
使用__block修飾變量
void blockTest() {__block int num = 10;void (^block)(void) = ^{NSLog(@"%d",num);num = 30;};num = 20;block();NSLog(@"%d",num); }clang改寫后的代碼如下:
struct __Block_byref_num_0 {void *__isa; __Block_byref_num_0 *__forwarding;int __flags;int __size;int num; };struct __blockTest_block_impl_0 {struct __block_impl impl;struct __blockTest_block_desc_0* Desc;__Block_byref_num_0 *num; // by ref__blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, __Block_byref_num_0 *_num, int flags=0) : num(_num->__forwarding) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;} };static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself) {__Block_byref_num_0 *num = __cself->num; // bound by refNSLog((NSString *)&__NSConstantStringImpl__var_folders_04_xwbq8q6n0p1dmhhd6y51_vbc0000gp_T_main_018b76_mi_0,(num->__forwarding->num));(num->__forwarding->num) = 30;}static void __blockTest_block_copy_0(struct __blockTest_block_impl_0*dst, struct __blockTest_block_impl_0*src) {_Block_object_assign((void*)&dst->num, (void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);}static void __blockTest_block_dispose_0(struct __blockTest_block_impl_0*src) {_Block_object_dispose((void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);}static struct __blockTest_block_desc_0 {size_t reserved;size_t Block_size;void (*copy)(struct __blockTest_block_impl_0*, struct __blockTest_block_impl_0*);void (*dispose)(struct __blockTest_block_impl_0*); } __blockTest_block_desc_0_DATA = { 0, sizeof(struct __blockTest_block_impl_0), __blockTest_block_copy_0, __blockTest_block_dispose_0};void blockTest() {__attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 10};void (*block)(void) = ((void (*)())&__blockTest_block_impl_0((void *)__blockTest_block_func_0, &__blockTest_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344));(num.__forwarding->num) = 20;((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);NSLog((NSString *)&__NSConstantStringImpl__var_folders_04_xwbq8q6n0p1dmhhd6y51_vbc0000gp_T_main_018b76_mi_1,(num.__forwarding->num)); }我們可以先從blockTest開始看:
__Block_byref_num_0
__attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 10};這里定義了一個__Block_byref_num_0的實例,再看看這個實例有什么:
struct __Block_byref_num_0 {void *__isa; __Block_byref_num_0 *__forwarding;int __flags;int __size;int num; };有一個isa指針,有一個指向和自己類型一樣的__forwarding指針,還有一個flags、size,最后,會有一個num,num估計就是保存著變量的值。至于__forwarding指針,后面會講到。
__blockTest_block_copy_0和__blockTest_block_dispose_0
__blockTest_block_copy_0中調(diào)用的是_Block_object_assign,__blockTest_block_dispose_0中調(diào)用的是_Block_object_dispose。
調(diào)用時機(jī):
__blockTest_block_copy_0: __block變量結(jié)構(gòu)體實例從棧拷貝到堆時
__blockTest_block_dispose_0:__block變量結(jié)構(gòu)體實例引用計數(shù)為0時
BLOCK_FIELD_IS_BYREF
// Runtime support functions used by compiler when generating copy/dispose helpers// Values for _Block_object_assign() and _Block_object_dispose() parameters enum {// see function implementation for a more complete description of these fields and combinationsBLOCK_FIELD_IS_OBJECT = 3, // id, NSObject, __attribute__((NSObject)), block, ...BLOCK_FIELD_IS_BLOCK = 7, // a block variableBLOCK_FIELD_IS_BYREF = 8, // the on stack structure holding the __block variableBLOCK_FIELD_IS_WEAK = 16, // declared __weak, only used in byref copy helpersBLOCK_BYREF_CALLER = 128, // called from __block (byref) copy/dispose support routines. };BLOCK_FIELD_IS_OBJECT:OC對象類型
BLOCK_FIELD_IS_BLOCK:是一個block
BLOCK_FIELD_IS_BYREF:在棧上被__block修飾的變量
BLOCK_FIELD_IS_WEAK:被__weak修飾的變量,只在Block_byref管理內(nèi)部對象內(nèi)存時使用
BLOCK_BYREF_CALLER:處理Block_byref內(nèi)部對象內(nèi)存的時候會加的一個額外標(biāo)記(告訴內(nèi)部實現(xiàn)不要進(jìn)行retain或者copy)
Block的內(nèi)存管理
其實Block總共有三種類型:
分為全局 Block(_NSConcreteGlobalBlock)、棧 Block(_NSConcreteStackBlock)、堆 Block(_NSConcreteMallocBlock)三種形式
在ARC環(huán)境下,有哪些情況編譯器會自動將棧上的把Block從棧上復(fù)制到堆上呢?
- 調(diào)用Block的copy實例方法時
- Block作為函數(shù)返回值返回時
- 在帶有usingBlock的Cocoa方法或者GCD的API中傳遞Block時候
- 將block賦給帶有__strong修飾符的id類型或者Block類型時
以上四種情況會把Block從棧復(fù)制到堆上。
當(dāng)Block復(fù)制到堆上,__block修飾的變量也會跟著變化:
1、當(dāng)Block在棧上的時候,__block變量也會在棧上,被棧上的Block持有著。
2、當(dāng)Block被復(fù)制到堆上時,會通過調(diào)用Block內(nèi)部的copy函數(shù),copy函數(shù)內(nèi)部會調(diào)用_Block_object_assign函數(shù)。此時__block變量的存儲域是堆,__block變量被堆上的Block持有。
3、當(dāng)堆上的Block被釋放,會調(diào)用Block內(nèi)部的dispose,dispose函數(shù)內(nèi)部會調(diào)用_Block_object_dispose,堆上的__block被釋放。
1、當(dāng)多個棧上的Block使用棧上的__block變量,__block變量被棧上的多個Block持有。
2、當(dāng)Block0被復(fù)制到堆上時,__block也會被復(fù)制到堆上,被堆上Block0持有。Block1仍然持有棧上的__block,原棧上__block變量的__forwarding指向拷貝到堆上之后的__block變量。
3、當(dāng)Block1也被復(fù)制到堆上時,堆上的__block被堆上的Block0和Block1只有,并且__block的引用計數(shù)+1。
4、當(dāng)堆上的Block都被釋放,__block變量結(jié)構(gòu)體實例引用計數(shù)為0,調(diào)用_Block_object_dispose,堆上的__block被釋放。
下面來描述__forwarding指針的變化:
一開始,在棧上的forwarding指針是指向自身,當(dāng)復(fù)制到堆以后,forwarding指針指向堆上的__Block_byref_num_0。
forwarding存在意義就是:
__forwarding 保證在棧上或者堆上都能正確訪問對應(yīng)變量
接下來,舉個例子
int main(int argc, char * argv[]) {int num = 10;NSLog(@"%@",[^{NSLog(@"%d",num);} class]);void (^block)(void) = ^{NSLog(@"%d",num);};NSLog(@"%@",[block class]); }打印的結(jié)果分別是:
NSStackBlock、NSMallocBlock
因為第一個block沒有copy,所以只是棧上面的block,而第二個block是進(jìn)行了copy,所以是堆上的block。
block截獲對象
@interface Person : NSObject @property (nonatomic, strong) NSString *name; @end@implementation Person- (void)dealloc {NSLog(@"-------dealloc-------"); }@endtypedef void(^Block)(void);int main(int argc, char * argv[]) {{Person *person = [[Person alloc] init];person.name = @"roy";NSLog(@"%@",[^{NSLog(@"%@",person.name);} class]);NSLog(@"%@",@"+++++++++++++");}NSLog(@"%@",@"------------"); }打印結(jié)果:
2022-01-04 22:50:00.490032+0800 HelloWorld[11404:3495395] __NSStackBlock__ 2022-01-04 22:50:00.490791+0800 HelloWorld[11404:3495395] +++++++++++++ 2022-01-04 22:50:00.490904+0800 HelloWorld[11404:3495395] -------dealloc------- 2022-01-04 22:50:00.491035+0800 HelloWorld[11404:3495395] ------------我們看到棧上的block內(nèi)部訪問了局部變量,是不會對局部變量強(qiáng)引用的,局部變量還是能釋放掉。
strong對象
@interface Person : NSObject @property (nonatomic, strong) NSString *name; @end@implementation Person- (void)dealloc {NSLog(@"-------dealloc-------"); }@endtypedef void(^Block)(void);int main(int argc, char * argv[]) {Block block;{Person *person = [[Person alloc] init];person.name = @"roy";block = ^{NSLog(@"%@",person.name);};person.name = @"david";NSLog(@"%@",@"+++++++++++++");}NSLog(@"%@",@"------------");NSLog(@"%@",[block class]);block ();block = NULL; }打印結(jié)果:
2022-01-04 22:59:32.072849+0800 HelloWorld[11771:3503869] +++++++++++++ 2022-01-04 22:59:32.076322+0800 HelloWorld[11771:3503869] ------------ 2022-01-04 22:59:32.076494+0800 HelloWorld[11771:3503869] __NSMallocBlock__ 2022-01-04 22:59:32.076629+0800 HelloWorld[11771:3503869] david 2022-01-04 22:59:32.077177+0800 HelloWorld[11771:3503869] -------dealloc-------Clang改寫代碼:
struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;Person *__strong person;__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__strong _person, int flags=0) : person(_person) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;} };__main_block_impl_0里面多了個Person *__strong person。
這里Block截獲了局部變量,當(dāng)Block被拷貝到堆上的時候,使用了強(qiáng)引用指向這個person對象,這就是為什么只有block=NULL的時候(block會被釋放),才會打印dealloc方法。
weak對象
typedef void(^Block)(void);int main(int argc, char * argv[]) {Block block;{Person *person = [[Person alloc] init];person.name = @"roy";__weak Person *weakPerson = person;block = ^{NSLog(@"%@",weakPerson.name);};weakPerson.name = @"david";NSLog(@"%@",@"+++++++++++++");}NSLog(@"%@",@"------------");block (); }打印結(jié)果:
2022-01-04 23:05:53.482355+0800 HelloWorld[11992:3508889] +++++++++++++ 2022-01-04 23:05:53.482986+0800 HelloWorld[11992:3508889] -------dealloc------- 2022-01-04 23:05:53.483113+0800 HelloWorld[11992:3508889] ------------ 2022-01-04 23:05:53.483217+0800 HelloWorld[11992:3508889] (null)Clang改寫以后的代碼:
struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;Person *__weak weakPerson;__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;} };在這里,__main_block_impl_0中多了個Person *__weak weakPerson成員變量。
總結(jié):
- 當(dāng)Block內(nèi)部訪問了對象類型的局部變量對象時,如果Block是在棧上,將不會對auto對象產(chǎn)生強(qiáng)引用。
- 如果block被拷貝到堆上,會調(diào)用Block內(nèi)部的copy函數(shù),copy函數(shù)內(nèi)部會調(diào)用_Block_object_assign函數(shù),_Block_object_assign會根據(jù)auto對象的修飾符(__strong,__weak,__unsafe_unretained)做出相應(yīng)的操作,當(dāng)使用的是__strong時,將會對person對象的引用計數(shù)加1,當(dāng)為__weak時,引用計數(shù)不變。
- 如果Block從對上移除,會調(diào)用block內(nèi)部的dispose函數(shù),內(nèi)部會調(diào)用_Block_object_dispose函數(shù),這個函數(shù)會自動釋放引用的局部變量對象。
block循環(huán)引用
@interface Person : NSObject@property (nonatomic, strong) NSString *name; @property (nonatomic, copy) void (^block)(void);- (void)testReferenceSelf;@end@implementation Person- (void)testReferenceSelf {self.block = ^ {NSLog(@"self.name = %s", self.name.UTF8String);};self.block(); }- (void)dealloc {NSLog(@"-------dealloc-------"); }@endint main(int argc, char * argv[]) {Person *person = [[Person alloc] init];person.name = @"roy";[person testReferenceSelf]; }這是典型的循環(huán)引用,person的dealloc方法沒有執(zhí)行。
clang改寫后的代碼如下:
這里,__Person__testReferenceSelf_block_impl_0有一個Person *const __strong self;
在person中block強(qiáng)引用了self,而self又強(qiáng)引用了block,導(dǎo)致了循環(huán)引用。
解除循環(huán)引用
@implementation Person- (void)testReferenceSelf {__weak typeof(self) weakself = self;self.block = ^ {__strong typeof(self) strongself = weakself;NSLog(@"self.name = %s", strongself.name.UTF8String);};self.block(); }- (void)dealloc {NSLog(@"-------dealloc-------"); }@end打印結(jié)果里面會打印出dealloc,person對象被正常釋放了。
clang改寫后的代碼如下:
此時,block中對self只是個弱引用。
在Person對象的Dealloc方法中會調(diào)用weak引用的處理方法,從weak_table中尋找弱引用的依賴對象,進(jìn)行清除處理。
總結(jié)
以上是生活随笔為你收集整理的OC底层原理-Block的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 智能化养猪场需要哪些设备?
- 下一篇: vue项目打包后,favorite.ic