iOS - block

image

三年前根竿,第一次寫關(guān)于 block 的東西鸿捧,就是初識(shí) block税肪,了解了些皮毛阅悍,但發(fā)現(xiàn)鸡典,那么僅僅是 block 的冰山一角套才,關(guān)于 block 還有很多需要參透和理解乎完。

block 本質(zhì)

block 的本質(zhì)是一個(gè) Objective-C 對(duì)象掰读,其內(nèi)部也有 isa 指針松逊,block 中封裝了函數(shù)的調(diào)用以及函數(shù)調(diào)用環(huán)境的 Objective-C 對(duì)象躺屁。它的結(jié)構(gòu)如下:

image

  1. 函數(shù)的調(diào)用相當(dāng)于函數(shù)的調(diào)用地址
  2. 函數(shù)調(diào)用環(huán)境指參數(shù),訪問(wèn) block 外部的值等

一段下面的 block:

void(^block)(int a, int b) = ^(int a, int b) {
            NSLog(@"a + b = %d", a + b);
};
block(1, 2);

用命令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m(下同) 重寫后 C++ 代碼是這樣的:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
};

__block_impl 的聲明:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

__main_block_desc_0 的聲明:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
};

Block_size 表示 __main_block_impl_0 能占多少內(nèi)存棺棵。

假如 block 內(nèi)使用了外部變量楼咳,如:

int outter = 35;
void(^block)(int a, int b) = ^(int a, int b) {
    NSLog(@"outter is %d", outter);
    NSLog(@"a + b = %d", a + b);
};
block(1, 2);

本質(zhì)結(jié)構(gòu)為:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int outter;
};

若我們?cè)?.m 文件中自行實(shí)現(xiàn)這些結(jié)構(gòu)體:
[圖片上傳失敗...(image-e5da00-1555345050037)]
然后進(jìn)行轉(zhuǎn)換:

struct __main_block_impl_0* blockStruct = (__bridge struct __main_block_impl_0*)block;

加斷點(diǎn)運(yùn)行后進(jìn)入 LLDB 調(diào)試環(huán)境可看到 blockStruct 的信息:


image

發(fā)現(xiàn) outter 已經(jīng)封裝到 blockStruct 的內(nèi)存中去了。

我們記錄下 __FuncPtr 后面的內(nèi)存地址 0x0000000100000ee0烛恤,然后在 block 塊內(nèi)增加斷點(diǎn)并過(guò)掉當(dāng)前斷點(diǎn)母怜,當(dāng)程序停留在 block 塊內(nèi)的斷點(diǎn)的時(shí)候,然后 Debug -> Debug Workflow -> Always Show Disassembly 會(huì)看到如下界面:
[圖片上傳失敗...(image-fe45c2-1555345050037)]
第一行 0x100000ee0 <+0>: pushq %rbp 的地址就是__FuncPtr 的地址缚柏,這說(shuō)明 block 塊內(nèi)的代碼都封裝到了函數(shù)里面苹熏,這個(gè)函數(shù)的首地址(例子中的 0x0000000100000ee0)在 block 結(jié)構(gòu)體的成員結(jié)構(gòu)體 __block_impl 中。

深入探究

底層數(shù)據(jù)結(jié)構(gòu)

main 函數(shù)中例子的代碼 C++ 的實(shí)現(xiàn)為:

int outter = 35;
// 定義 block 變量
void(*block)(int a, int b) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, outter));
// 執(zhí)行 block 內(nèi)部代碼
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);

去除強(qiáng)制轉(zhuǎn)換的干擾代碼币喧,簡(jiǎn)化后:

int outter = 35;
// 定義 block 變量
void(*block)(int a, int b) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, outter));
// 執(zhí)行 block 內(nèi)部代碼
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);

這里的 block 會(huì)指向什么轨域?首先得明白 _main_block_impl_0() 會(huì)返回什么?我們?cè)?.cpp 文件中發(fā)現(xiàn)該函數(shù)在 __main_block_impl_0 的結(jié)構(gòu)體中:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int outter;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _outter, int flags=0) : outter(_outter) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

該函數(shù)接收 4 個(gè)參數(shù)杀餐,flags 默認(rèn)為 0干发,并且函數(shù)名和結(jié)構(gòu)體名相同,是 C++ 中的構(gòu)造函數(shù)史翘,和 Java 的構(gòu)造函數(shù)道理類似枉长,也和 Objective-C 中的 init 方法類似,并且無(wú)任何返回琼讽。

outter(_outter) 表示傳進(jìn)來(lái)的 _outter 的值會(huì)賦給結(jié)構(gòu)體成員變量 outter必峰,相當(dāng)于 outter = _outter

4 個(gè)傳入的參數(shù)中 outter 不必多言钻蹬,那么來(lái) __main_block_func_0 是什么:

static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
  int outter = __cself->outter;
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_f61ac8_mi_0, outter);
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_f61ac8_mi_1, a + b);
}

可見(jiàn)吼蚁,__main_block_func_0 封裝了 block 執(zhí)行邏輯的函數(shù)。__main_block_func_0 對(duì)應(yīng) __main_block_impl_0 構(gòu)造方法中的 void *fp, fp 賦值給了 impl.FuncPtr问欠。這樣 impl.FuncPtr 存儲(chǔ)的就是執(zhí)行邏輯的函數(shù)的地址肝匆。

在該構(gòu)造方法中同時(shí)初始化了 isa 指針:

impl.isa = &_NSConcreteStackBlock;

說(shuō)明 block 的類型為 _NSConcreteStackBlock粒蜈。

回過(guò)頭我們?cè)倏磦魅氲牡诙€(gè)參數(shù) &__main_block_desc_0_DATA,有關(guān)它的完整代碼為:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

該處 0 賦值給了 reserved术唬,sizeof(struct __main_block_impl_0) 計(jì)算該 block 結(jié)構(gòu)體的大小并將結(jié)果賦值給 Block_size薪伏。

&__main_block_desc_0_DATA 對(duì)應(yīng) __main_block_impl_0 構(gòu)造方法中的 struct __main_block_desc_0 *desc, 并賦值給了 Desc。換而言之 __main_block_impl_0 中的 Desc 指向的是 __main_block_desc_0 結(jié)構(gòu)體變量粗仓。

所以在執(zhí)行結(jié)構(gòu)體的構(gòu)造函數(shù)的時(shí)候嫁怀,outter 為 35。倘若在外部將 outter 重新賦值借浊,結(jié)構(gòu)體中的 outter 是不會(huì)更改的塘淑。也就是說(shuō) outter 是以值傳遞的形式傳遞的。

__main_block_func_0 中:

int outter = __cself->outter; 

該步驟為取出 outter 的值(35)蚂斤。

block 的變量捕獲

為確保 block 能正確訪問(wèn)外部變量存捺,block 有變量捕獲機(jī)制,如下圖:


auto: 局部變量默認(rèn)是 auto 修飾的:int a = 0; 等價(jià)于 auto int a = 0;曙蒸,它表示自動(dòng)變量捌治,離開(kāi)作用域后自動(dòng)銷毀。

那么 block 中的捕獲是什么意思纽窟?就是 block 內(nèi)部會(huì)新增一個(gè)成員變量用來(lái)存儲(chǔ)外部變量的值肖油,這個(gè)過(guò)程為捕獲。

auto 修飾的變量

上一節(jié)例子中的 int 型 outter 就是自動(dòng)變量臂港,默認(rèn) auto 修飾森枪。其訪問(wèn)方式是值傳遞

static 修飾的變量

我們添加靜態(tài)變量 outter2:

int outter = 35;
static int outter2 = 1210;
void(^block)(int a, int b) = ^(int a, int b) {
            NSLog(@"outter is %d", outter);
            NSLog(@"outter2 is %d", outter2);
            NSLog(@"a + b = %d", a + b);
};
block(10, 20);

運(yùn)行后打印了 outter 和 outter2 的值审孽。說(shuō)明無(wú)論是 auto 修飾還是 static 修飾的外部變量县袱,block 內(nèi)部都是能捕獲到的。
那么內(nèi)部訪問(wèn)方式是否一樣佑力?重寫 C++ 代碼后發(fā)現(xiàn):

void(*block)(int a, int b) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, outter, &outter2));

outter2 是以 &outter2 傳入 __main_block_impl_0 結(jié)構(gòu)體的構(gòu)造方法的式散,并且 __main_block_impl_0 中的 outter2 是:

struct __main_block_impl_0 {
  ...
  int outter;
  int *outter2;
  ...
};

發(fā)現(xiàn)這里的 outter2 是通過(guò)傳址的方式傳進(jìn)去的,在打印的 C++ 實(shí)現(xiàn)中 outter2 是這樣取值的:

NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_e5faae_mi_1, (*outter2));

*outter2 這樣的取值方式是直接取出外面靜態(tài)變量?jī)?nèi)存里的值打颤。

Q:為什么會(huì)有這樣的差異杂数?
A:因?yàn)?auto 修飾的變量是可能自動(dòng)銷毀的,而 block 執(zhí)行的時(shí)機(jī)未定瘸洛,所以存在 block 執(zhí)行內(nèi)部代碼的時(shí)候變量已經(jīng)銷毀的情況,這會(huì)導(dǎo)致程序的 Crash次和,所以外部變量需進(jìn)行值傳遞反肋。而 static 修飾的變量會(huì)一直存在于內(nèi)存當(dāng)中,不存在 block 執(zhí)行的時(shí)候變量已經(jīng)銷毀的情況踏施。

全局變量

我們驗(yàn)證全局變量的捕獲機(jī)制石蔗,添加一個(gè)全局的成員變量 outter3:

int static outter3 = 1314;

并在內(nèi)部打印罕邀,發(fā)現(xiàn)打印 1314,若在執(zhí)行 block 之前修改 outter3 的值:

void(^block)(int a, int b) = ^(int a, int b) {
    NSLog(@"outter is %d", outter);
    NSLog(@"outter2 is %d", outter2);
    NSLog(@"outter3 is %d", outter3);
    NSLog(@"a + b = %d", a + b);
};
outter3 = 999;
block(10, 20);

打印得 outter3 = 999养距,看似和局部靜態(tài)變量的道理一樣诉探,我們看下 C++ 實(shí)現(xiàn)得 __main_block_impl_0 結(jié)構(gòu)體:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int outter;
  int *outter2;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _outter, int *_outter2, int flags=0) : outter(_outter), outter2(_outter2) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

發(fā)現(xiàn)并無(wú) outter3,在 block 內(nèi)部打印的地方為:

NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_147262_mi_2, outter3);

也就是說(shuō)棍厌,全局變量并沒(méi)有捕獲到 block 內(nèi)部肾胯,而且,內(nèi)部訪問(wèn)全局變量是直接訪問(wèn)的耘纱。

那么假如 block 中是如何捕獲 self 的呢敬肚?
我們新建 Test 類:
.h:

@interface Test : NSObject

@property(nonatomic, copy) NSString* param;

- (void)test;
- (instancetype)initWithParam:(NSString*)param;

@end

.m:

@implementation Test

- (void)test {
    void(^block)(void) = ^{
        NSLog(@"====>%p", self);
    };
    block();
}

- (instancetype)initWithParam:(NSString*)param
{
    self = [super init];
    if (self) {
        self.param = param;
    }
    return self;
}

@end

外部調(diào)用 test() 方法便可執(zhí)行 block,并訪問(wèn)內(nèi)部的 self束析。打友蘼:

====>0x10070be20

重寫 Test.m 文件后發(fā)現(xiàn)其 block 結(jié)構(gòu)為:

struct __Test__test_block_impl_0 {
  struct __block_impl impl;
  struct __Test__test_block_desc_0* Desc;
  Test *self;
  __Test__test_block_impl_0(void *fp, struct __Test__test_block_desc_0 *desc, Test *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

發(fā)現(xiàn) self 是通過(guò)指針傳遞進(jìn)來(lái)的。而且可推導(dǎo)员寇,既然能捕獲弄慰,說(shuō)明 self 是局部變量。
我們可看到 test() 函數(shù)的底層為:

static void _I_Test_test(Test * self, SEL _cmd) {
    void(*block)(void) = ((void (*)())&__Test__test_block_impl_0((void *)__Test__test_block_func_0, &__Test__test_block_desc_0_DATA, self, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

發(fā)現(xiàn)底層函數(shù)默認(rèn)添加了兩個(gè)參數(shù):self_cmd蝶锋,這也是我們?yōu)槭裁茨茉诤瘮?shù)內(nèi)可以調(diào)用到 self 和 _cmd 的原因陆爽。
若假如打印 _param 呢?運(yùn)行

Test* t = [[Test alloc] initWithParam:@"something"];[t test];

發(fā)現(xiàn)可打由馈:

something

此時(shí)是捕獲的 _param墓陈?錯(cuò),_param 等價(jià)于 self->_param第献,所以捕獲的還是 self贡必。

block 的類型

block 有三種類型,亦是可以通過(guò) class 方法或者查看 isa 指針查看其具體類型庸毫,但最終都是繼承自 NSBlock仔拟。

類型
_NSGlobalBlock_ 全局 block
_NSStackBlock_ 棧區(qū) block
_NSMallocBlock_ 堆區(qū) block

我們?yōu)樘骄科漕愋停\(yùn)行:

void(^block)(void) = ^{
     NSLog(@"This is a block");
};
block();
NSLog(@"%@", [block class]);

打屿摺:

This is a block
__NSGlobalBlock__

追加打永ā:

NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);

得:

__NSGlobalBlock
NSBlock
NSObject

現(xiàn)在得到繼承鏈:


那么現(xiàn)在打印:

void(^block)(void) = ^{
            NSLog(@"This is a block");
        };
        
int num = 10;
void(^block1)(void) = ^{
    NSLog(@"The num is %d", num);
};
        
NSLog(@"%@ %@ %@", [block class], [block1 class], [^{
    NSLog(@"The num is %d", num);
} class]);

得:

__NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__

重寫后我們可看見(jiàn)有三個(gè) block:__main_block_impl_0载佳、__main_block_impl_1炒事、__main_block_impl_2。
但奇怪的是三者得 isa 指針都指向的是 _NSConcreteStackBlock蔫慧。

為什么會(huì)出現(xiàn)這樣的問(wèn)題挠乳,是因?yàn)樵谥貙懨钪型ㄟ^(guò) clang 轉(zhuǎn)成的 C++ 代碼并不能完全代表 Objective-C 最終的底層實(shí)現(xiàn)。

所以我們還是按照打印的標(biāo)準(zhǔn)也判斷 block 的類型,可發(fā)現(xiàn)睡扬, block 的存儲(chǔ)類型和捕獲外部的局部變量也有關(guān)系盟蚣。

image

text 區(qū)存放的是程序代碼,data 區(qū)存放的是全局變量卖怜,堆區(qū)放的是 alloc 出來(lái)的對(duì)象屎开,動(dòng)態(tài)分配內(nèi)存,需要開(kāi)發(fā)者手動(dòng)調(diào)用马靠,也需要開(kāi)發(fā)者主動(dòng)管理內(nèi)存(現(xiàn)在有 ARC 了)奄抽,棧區(qū)放的是局部變量,系統(tǒng)自動(dòng)銷毀內(nèi)存虑粥。

具體的 block 類型是區(qū)分的如孝?如下表:

block 類型 區(qū)別
_NSGlobalBlock_ 沒(méi)有訪問(wèn) auto 變量
_NSStackBlock_ 訪問(wèn) auto 變量
_NSMallocBlock_ _NSStackBlock_ 調(diào)用了 copy

對(duì)于 NSStackBlock 的 block 存在一個(gè)問(wèn)題,代碼如下:

void(^block)(void);

void test() {
    int num = 35;
    block = ^{
        NSLog(@"The num is %d", num);
    };
    NSLog(@"%@", [block class]);
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();
    }
}

運(yùn)行結(jié)果為:

__NSStackBlock__
The num is -272632472

做這個(gè)實(shí)驗(yàn)請(qǐng)將內(nèi)存管理改為手動(dòng)(MRC):Build Setting -> Objective-C Automatic Reference Counting -> No

為何會(huì)出現(xiàn) -272632472娩贷?是因?yàn)榈谖瑘?zhí)行過(guò) test() 后棧區(qū)的 對(duì)應(yīng)數(shù)據(jù)被回收,存在的可能就是垃圾數(shù)據(jù)彬祖,那么再訪問(wèn)結(jié)構(gòu)體內(nèi)的成員的時(shí)候得到的就是這些垃圾數(shù)字茁瘦。

將上述代碼稍作改動(dòng),test 內(nèi)的 block 改為:

block = [^{
    NSLog(@"The num is %d", num);
} copy];

打印結(jié)果為:

__NSMallocBlock__
The num is 35

此時(shí)的 block 已經(jīng)進(jìn)行了 copy 操作储笑,棧 block 變?yōu)槎?block甜熔,內(nèi)存需要我們手動(dòng)釋放,而我并沒(méi)有釋放突倍,所以打印的 num 是正確的腔稀。

產(chǎn)生疑惑,_NSGlobalBlock_ 類型的棧進(jìn)行了 copy 操作會(huì)變成 _NSMallocBlock_ 類型嗎羽历?
去掉 block 內(nèi)部對(duì) num 的打印再來(lái)運(yùn)行發(fā)現(xiàn):

__NSGlobalBlock__

即使使用了 copy 操作焊虏,block 依然為 _NSGlobalBlock_ 類型。

copy 操作

由上節(jié)可知秕磷,對(duì)于 _NSStackBlock_ 類型的 block 有太多的不確定性诵闭,所以在對(duì)這種 block 使用的時(shí)候需要對(duì)其進(jìn)行一次 copy 操作將棧 block 復(fù)制到堆區(qū)。

但上節(jié)的例子是基于 MRC 的環(huán)境下操作的澎嚣,在 ARC 的環(huán)境下疏尿,編譯器會(huì)根據(jù)情況自動(dòng)講 block 進(jìn)行 copy 操作。
在 ARC 環(huán)境下執(zhí)行:

void(^block)(void);

void test() {
    int num = 35;
    block = ^{
        NSLog(@"The num is %d", num);
    };
    NSLog(@"%@", [block class]);
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();
    }
}

得:

__NSMallocBlock__
The num is 35

在以下條件下易桃,編譯器會(huì)自動(dòng)將 block 進(jìn)行 copy 操作:

  • block 作為返回值
  • 將 block 復(fù)制給 __strong 指針時(shí)
  • block 作為 Cocoa API 中方法名含有 usingBlock 的方法參數(shù)時(shí)
    如:
NSArray* arr = ...;
[arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];

對(duì)象類型的 auto 變量捕獲

前面的例子內(nèi)部捕獲外部變量都是基本類型褥琐,如 int,那么對(duì)象類型的外部變量是如何捕獲的晤郑?
將 Test 類踩衩,添加 NSInteger 類型的屬性 num嚼鹉。
外部:

Test* test = [[Test alloc] init];
test.num = 35;
        
TestBlock block = ^{
    NSLog(@"The num is %ld", (long)test.num);
};
block();

TestBlock 定義為:typedef void(^TestBlock) (void);
運(yùn)行得:

The num is 35

我們稍作改動(dòng):

TestBlock block;
{
    Test* test = [[Test alloc] init];
    test.num = 35;
            
    block = ^{
         NSLog(@"The num is %ld", (long)test.num);
    };
}
NSLog(@"=====end=====");

也重寫了 Test 的 dealloc() 方法打印 dealloc。我們?cè)黾訑帱c(diǎn)在打印 “end” 的一行驱富,運(yùn)行發(fā)現(xiàn)斷點(diǎn)處,并沒(méi)有打印 Test 的 delloc 信息匹舞,也就是說(shuō)褐鸥,內(nèi)部 {} 執(zhí)行完了 Test 也沒(méi)有立即被銷毀。
我們將代碼改成:

Test* test = [[Test alloc] init];
test.num = 35;
        
TestBlock block = ^{
    NSLog(@"The num is %ld", (long)test.num);
};
 NSLog(@"=====end=====");

重寫后發(fā)現(xiàn) block 的結(jié)構(gòu)體中有 Test *test 成員變量赐稽〗虚牛回到修改之前的代碼,在執(zhí)行:

block = ^{
    NSLog(@"The num is %ld", (long)test.num);
};

的時(shí)候姊舵,block 進(jìn)行了 copy 操作成為堆區(qū)的 block晰绎,不會(huì)輕易銷毀,那么意味著對(duì) test 也是強(qiáng)引用持有括丁,test 亦不會(huì)輕易被釋放荞下,所以 dealloc 信息延后打印:

=====end=====
=====dealloc=====

若是 MRC 環(huán)境(需添加 [t release] 操作史飞,并且 dealloc 方法內(nèi)須調(diào)用父類的 dealloc 方法)尖昏,即使 block 還在,也會(huì)先執(zhí)行 Test 的 dealloc 方法构资。結(jié)果為:

=====dealloc=====
=====end=====

若在 MRC 環(huán)境下改為:

block = [^{
    NSLog(@"The num is %ld", (long)test.num);
} copy];

則會(huì)達(dá)到 ARC 下同樣的效果抽诉,因?yàn)檫M(jìn)行了 copy 操作后在 block 內(nèi)部相當(dāng)于調(diào)用了一次 [t reatain] 操作。結(jié)果為:

=====end=====
=====dealloc=====

回到 ARC 環(huán)境吐绵,假如 Test 對(duì)象進(jìn)行 __weak 修飾迹淌,則情況又有所不同:

=====dealloc=====
=====end=====

在用 __weak 修飾的情況下重寫 C++ 代碼會(huì)報(bào)錯(cuò):

cannot create __weak reference because the current deployment target does not support weak references

是因?yàn)?strong>命令需要支持 ARC 并且指定運(yùn)行時(shí)系統(tǒng)版本,如:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-9.0.0 main.m

重寫成功后發(fā)現(xiàn) block 結(jié)構(gòu)體為:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Test *__weak test;
  ...
};

test 對(duì)象為 weak 修飾己单,所以在離開(kāi)作用域后立即釋放唉窃。去掉 weak 后的結(jié)構(gòu)體再用上命令重寫,得到:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Test *__strong test;
  ...
};

發(fā)現(xiàn) weak 默認(rèn)用了 strong 修飾荷鼠,所以“延長(zhǎng)了”其壽命句携。
最后來(lái)個(gè)總結(jié):

  • 當(dāng) block 在棧上,不會(huì)對(duì) auto 變量產(chǎn)生強(qiáng)引用
  • 當(dāng) block 在堆上允乐,會(huì)根據(jù) auto 是否由 __strong 或者 —__weak 修飾來(lái)決定是否產(chǎn)生強(qiáng)引用 [下有說(shuō)明]
  • 當(dāng) block 從堆上移除矮嫉,將放棄對(duì) auto 變量的引用,相當(dāng)于進(jìn)行了一次 release 操作

copy 操作后的 block 其 Desc 是有變化的:

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};

原本只有 reservedBlock_size 現(xiàn)在又多了兩個(gè)函數(shù)指針: copydispose牍疏。copy 保存的是 __main_block_copy_0蠢笋,dispose 保存的是 __main_block_dispose_0
當(dāng) block 執(zhí)行了 copy 操作后鳞陨,這兩個(gè)函數(shù)便會(huì)執(zhí)行昨寞。
__main_block_copy_0 和 __main_block_dispose_0是現(xiàn)實(shí):

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
// 會(huì)根據(jù) test 對(duì)象是 strong 還是 weak 修飾來(lái)決定是否對(duì) test 對(duì)象產(chǎn)生強(qiáng)引用
_Block_object_assign((void*)&dst->test, (void*)src->test, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
// 對(duì) test 對(duì)象進(jìn)行釋放
_Block_object_dispose((void*)src->test, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
函數(shù) 調(diào)用時(shí)機(jī)
copy 棧上的 block 復(fù)制到堆時(shí)
dispose 堆上的 block 被收回時(shí)

__block

我們?cè)賮?lái)新建一個(gè)例子工程:

typedef void(^TestBlock) (void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        int num = 10;
        TestBlock block = ^{
            NSLog(@"The num is %d", num);
        };
        block();
    }
    return 0;
}

運(yùn)行上面這段代碼瞻惋,結(jié)果為:

The num is 10

那么實(shí)際情況中,我們常常需要在 block 內(nèi)部改變外面變量的值援岩,在 block 內(nèi)部直接修改是不允許的:

^{
    num = 35; // ?
}

這是因?yàn)?num 的作用域?qū)儆?main 函數(shù)歼狼,而 block 內(nèi)執(zhí)行邏輯屬于另一個(gè)函數(shù) __main_block_func_0,是無(wú)法跨域進(jìn)行修改的享怀。

但是通過(guò) static 修飾的局部變量是可以用這種方式修改的:

static int num = 10;
TestBlock block = ^{
    num = 35;
    NSLog(@"The num is %d", num);
};
block();

結(jié)果為:

The num is 35

因?yàn)?static 修飾的是引用傳遞羽峰,block 的結(jié)構(gòu)體存儲(chǔ)的是指向 num 的指針,所以在內(nèi)部修改 num 的值是可以成功的添瓷。

那么如何修改非 static 修飾的的局部變量梅屉?就是 __block 關(guān)鍵字。

__block int num = 10;
TestBlock block = ^{
    num = 35;
    NSLog(@"The num is %d", num);
};
block();

結(jié)果:

The num is 35

__block 本質(zhì)

__block 變量不能修飾全局變量鳞贷、靜態(tài)變量坯汤。并且編譯器會(huì)將 __block 變量包裝成一個(gè)對(duì)象。
重寫 C++ 代碼后發(fā)現(xiàn) block 結(jié)構(gòu)體 num 的成員變量和之前未用 __block 修飾的 num 有本質(zhì)的區(qū)別:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_num_0 *num; // by ref
  ...
};

這里的 num 為 __Block_byref_num_0 * 類型搀愧。__Block_byref_num_0 也是個(gè)結(jié)構(gòu)體惰聂,其內(nèi)部定義是這樣的:

struct __Block_byref_num_0 {
  void *__isa;
__Block_byref_num_0 *__forwarding;
 int __flags;
 int __size;
 int num;
};

我們可推斷一開(kāi)始 num 的值為 10,這個(gè)值一定是存儲(chǔ)在 __Block_byref_num_0 的成員變量 num 中妈橄。那么 __forwarding 表示什么庶近?
首先我們看到由 __block 修飾后的 num,在 main 函數(shù)的源碼中變成了:

__attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 10};

簡(jiǎn)化版本:

__Block_byref_num_0 num = {(0,
                            &num,
                            0,
                            sizeof(__Block_byref_num_0),
                            10};

此時(shí)第一個(gè) 0 賦值給 __isa眷蚓,第二個(gè) 0 賦值給 __flags鼻种,第四個(gè)參數(shù)是計(jì)算當(dāng)前結(jié)構(gòu)體有多大并賦值給 __size,最后 10 賦值給 num沙热,推斷得到驗(yàn)證叉钥。第二個(gè)參數(shù) &num 就是 num 結(jié)構(gòu)體本身,也就是說(shuō)它將自身的結(jié)構(gòu)體地址傳遞給了 __forwarding篙贸。換而言之 __forwarding 指向的是自己投队。

image

同時(shí) &num 也傳給了 __main_block_impl_0 的 *num

TestBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344));

block 修改 num 的源碼為:

// 首先拿到 __Block_byref_num_0 中的 __forwarding
__Block_byref_num_0 *num = __cself->num;
// 取得 num 再修改
(num->__forwarding->num) = 35;

倘若多加了一個(gè)對(duì)象類型的局部變量:

__block int num = 10;
__block NSObject* obj = [[NSObject alloc] init];
TestBlock block = ^{
    obj = nil;
    num = 35;
    NSLog(@"The num is %d", num);
};
block();

num 和 obj 在底層會(huì)生成兩個(gè)機(jī)構(gòu)體:

struct __Block_byref_num_0 {
  void *__isa;
__Block_byref_num_0 *__forwarding;
 int __flags;
 int __size;
 int num;
};

struct __Block_byref_obj_1 {
  void *__isa;
__Block_byref_obj_1 *__forwarding;
 int __flags;
 int __size;
 // copy 操作
 void (*__Block_byref_id_object_copy)(void*, void*);
 // dispose 操作
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *__strong obj;
};

block 結(jié)構(gòu)體會(huì)有兩個(gè)成員變量指向它們?cè)谶@里不貼出。

我們?nèi)サ魧?duì)象類型的 obj 回到最簡(jiǎn)狀態(tài)爵川,在 block() 后打印 num 的內(nèi)存地址敷鸦,得:

0x10051e968

這個(gè)內(nèi)存地址和底層的誰(shuí)有對(duì)應(yīng)關(guān)系?是 __main_block_impl_0 中的 *num寝贡?還是 __Block_byref_num_0 中的 num扒披?我們自己實(shí)現(xiàn)這些低層結(jié)構(gòu):


然后運(yùn)行:

__block int num = 10;
TestBlock block = ^{
           
    num = 35;
    NSLog(@"The num is %d", num);
};
struct __main_block_impl_0* blockStruct = (__bridge struct __main_block_impl_0*)block;
NSLog(@"%p", &num);

在最后一行加斷點(diǎn)發(fā)現(xiàn) __Block_byref_num_0 * 型 num 的地址為:0x000000010204b490,打印局部變量的 num 為 0x10204b4a8圃泡,兩者并不相同碟案。
0x000000010204b490 為 __Block_byref_num_0 * 型 num 的地址也就意味著是 __isa 的地址,那么 age 的地址是什么颇蜡?
__isa 大小為 8价说,__forwarding 大小為 8(地址為 0x000000010204b498)辆亏,__flags 大小為 4(地址為0x000000010204b4a0), __size 大小為 4(地址為0x000000010204b4a4)鳖目,num 的地址為 0x000000010204b4a8扮叨。是不是很眼熟?沒(méi)錯(cuò) num 的地址和外部變量的 num 一樣领迈。
通過(guò):

print/x &(blockStruct->num->num)

命令得到的打印結(jié)果和 NSLog(@"%p", &num); 得到的結(jié)果也是一樣的也可以驗(yàn)證甫匹。

__block 內(nèi)存管理

我們來(lái)看這個(gè)熟悉的例子:

int num = 0;
TestBlock block = ^{
     NSLog(@"%d", num);
};
block();

底層的 __main_block_desc_0 是沒(méi)有 copydispose 兩個(gè)成員函數(shù)的,但是當(dāng) num 用 __block 的時(shí)候就多了這兩個(gè)函數(shù)惦费,并在 copy 函數(shù)中調(diào)用 _Block_object_assign() 對(duì) 結(jié)構(gòu)體中的 __Block_byref_num_0 *num 進(jìn)行內(nèi)存管理。
假如有 Block 0 和 Block 1 分別對(duì) __block 變量引用抢韭,則:

在 ARC 環(huán)境下首先 Block 0 會(huì) copy 到堆上薪贫,然后 __block 修飾的變量也同樣會(huì) copy 到堆上,然后進(jìn)行強(qiáng)引用刻恭。
然后 Block 1 也會(huì) copy 到堆上并對(duì) __block 變量有強(qiáng)引用:


image

當(dāng) block 從堆上移除的時(shí)候瞧省,首先會(huì)調(diào)用內(nèi)部 dispose 函數(shù),其內(nèi)部會(huì)調(diào)用 _Block_object_dispose() 函數(shù)鳍贾,然后釋放 __block 變量:

image

若外部是:

__block int num = 0;
__block  NSObject* obj == ...;
TestBlock block = ^{
     ...
};
block();

則底層對(duì) int 和 obj 都會(huì)產(chǎn)生強(qiáng)引用鞍匾。

_Block_byref名字_0 就是強(qiáng)引用

若:

 __block int num = 0;
NSObject* obj = [[NSObject alloc] init];
__weak NSObject* weakObj = obj;
TestBlock block = ^{
     ...
};
block();

則底層不會(huì)對(duì) weakObj 產(chǎn)生強(qiáng)引用。

另骑科,我們?cè)?C++ 代碼中看到:

__main_block_impl_0*src) {
_Block_object_assign((void*)&dst->num, (void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->weakObj, (void*)src->weakObj, 3/*BLOCK_FIELD_IS_OBJECT*/);}

8 表示 __block 修飾的變量橡淑,對(duì)應(yīng)注釋:BLOCK_FIELD_IS_BYREF
3 表示對(duì)象,對(duì)應(yīng)注釋:BLOCK_FIELD_IS_OBJECT

__block 的 __forwarding 指針

當(dāng) block 在棧上時(shí)咆爽,__forwarding 指針指向自己梁棠。那么堆上的 __forwarding 指向誰(shuí)呢?答案也是自己斗埂,但是需要注意的是符糊,經(jīng)過(guò) copy 操作后,原棧上的 __forwarding 指針指向堆上的 block呛凶,即:

image

循環(huán)引用問(wèn)題

當(dāng)對(duì)象對(duì) block 本身有強(qiáng)引用男娄,而 block 又對(duì)對(duì)象持有,則會(huì)引發(fā)循環(huán)引用漾稀。如:

Test* t = [[Test alloc] init];
t.num = 35;
t.block = ^{
    NSLog(@"%ld", t.num);
};

ARC

使用 __weak 和 __unsafe_unretained 解決

在 ARC 環(huán)境下可通過(guò)模闲,__weak__unsafe_unretained 解決:

Test* t = [[Test alloc] init];
t.num = 35;
__weak Test* weakT = t;
t.block = ^{
    NSLog(@"%ld", weakT.num);
};

或者:

Test* t = [[Test alloc] init];
t.num = 35;
__weak typeof(t) weakT = t;
t.block = ^{
    NSLog(@"%ld", weakT.num);
};

對(duì)于 self 的情況也是同理:

__weak typeof(self) weakSelf = self;
image

__unsafe_unretained 同理,但 __unsafe_unretained 是不安全的县好,若 __weak 指向的對(duì)象銷毀围橡,則 weakXXX 會(huì)自動(dòng)置為 nil但 __unsafe_unretained 不會(huì)缕贡,它還是會(huì)指向那個(gè)銷毀對(duì)象的地址翁授,所以進(jìn)行訪問(wèn) weakXXX 的時(shí)候很有可能產(chǎn)生野指針錯(cuò)誤拣播。

使用 __block 解決

__block 情況下的循環(huán)應(yīng)用如下:

image

必須調(diào)用 block 的情況下還可以使用 __block 來(lái)解決。

__block id weakSelf = self;

并且 block 內(nèi)部的 weakSelf 要職位 nil:

xxx.block = ^{
    ...
    weakSelf = nil;
};

因?yàn)橐坏?weakSelf 置為 nil收擦,三者互相“僵持不下”的狀態(tài)就會(huì)打破贮配,也就不存在循環(huán)引用的問(wèn)題了。


image

MRC

使用 __unsafe_unretained 解決

同 ARC 環(huán)境的方式一樣塞赂。

MRC 下不支持 __weak泪勒。

使用 __block 解決

在 MRC 環(huán)境下使用 __block 修飾的話在底層是不會(huì)對(duì)外部變量進(jìn)行 retain 也就是強(qiáng)引用操作的,而 ARC 會(huì)宴猾。
并且不需要調(diào)用 weakSelf = nil 就可以解決循環(huán)引用的問(wèn)題圆存。

image

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市仇哆,隨后出現(xiàn)的幾起案子沦辙,更是在濱河造成了極大的恐慌,老刑警劉巖讹剔,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件油讯,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡延欠,警方通過(guò)查閱死者的電腦和手機(jī)陌兑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)由捎,“玉大人兔综,你說(shuō)我怎么就攤上這事∮绶” “怎么了邻奠?”我有些...
    開(kāi)封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)为居。 經(jīng)常有香客問(wèn)我碌宴,道長(zhǎng),這世上最難降的妖魔是什么蒙畴? 我笑而不...
    開(kāi)封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任贰镣,我火速辦了婚禮,結(jié)果婚禮上膳凝,老公的妹妹穿的比我還像新娘碑隆。我一直安慰自己,他們只是感情好蹬音,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布上煤。 她就那樣靜靜地躺著,像睡著了一般著淆。 火紅的嫁衣襯著肌膚如雪劫狠。 梳的紋絲不亂的頭發(fā)上拴疤,一...
    開(kāi)封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音独泞,去河邊找鬼呐矾。 笑死,一個(gè)胖子當(dāng)著我的面吹牛懦砂,可吹牛的內(nèi)容都是我干的蜒犯。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼荞膘,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼罚随!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起羽资,我...
    開(kāi)封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤毫炉,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后削罩,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡费奸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年弥激,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片愿阐。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡微服,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出缨历,到底是詐尸還是另有隱情以蕴,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布辛孵,位于F島的核電站丛肮,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏魄缚。R本人自食惡果不足惜宝与,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望冶匹。 院中可真熱鬧习劫,春花似錦、人聲如沸嚼隘。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)飞蛹。三九已至谤狡,卻和暖如春灸眼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背豌汇。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工幢炸, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人拒贱。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓宛徊,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親逻澳。 傳聞我的和親對(duì)象是個(gè)殘疾皇子闸天,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 《Objective-C高級(jí)編程》這本書就講了三個(gè)東西:自動(dòng)引用計(jì)數(shù)、block斜做、GCD苞氮,偏向于從原理上對(duì)這些內(nèi)容...
    WeiHing閱讀 9,810評(píng)論 10 69
  • Block概要 Block:帶有自動(dòng)變量的匿名函數(shù)。 匿名函數(shù):沒(méi)有函數(shù)名的函數(shù)瓤逼,一對(duì){}包裹的內(nèi)容是匿名函數(shù)的作...
    zweic閱讀 503評(píng)論 0 2
  • 一. 查看block內(nèi)部實(shí)現(xiàn) 1.編寫block代碼void (^DemoBlock)(int, int) = ^...
    李永開(kāi)閱讀 149評(píng)論 0 0
  • Blocks Blocks Blocks 是帶有局部變量的匿名函數(shù) 截取自動(dòng)變量值 int main(){ ...
    南京小伙閱讀 929評(píng)論 1 3
  • 工具命令轉(zhuǎn)化C++xcrun -sdk iphoneos clang -arch arm64 -rewrite-o...
    iYeso閱讀 3,127評(píng)論 6 67