Block 编程

这是 Objective-C 高级编程 第二章。这一章的内容,囧,太底层了,还是要把知识往实际上靠。下周就是最后一章,就看完它了,应该过不久又要学 Swift 了,囧囧囧囧_

一些测试代码

栈上 Block 对象捕获强引用和弱引用变量:

NSObject *a = [NSObject new];
NSObject * __weak b = a;

NSLog(@"%@ %lu", ^{
    a;
}, _objc_rootRetainCount(a));
// 输出:<__NSStackBlock__: 0x7fff5fbff7c0> 2

NSLog(@"%@ %lu", ^{
    b;
}, _objc_rootRetainCount(a));
// 输出:<__NSStackBlock__: 0x7fff5fbff7f8> 2

NSLog(@"%@ %lu", ^{
    a;
}, _objc_rootRetainCount(a));
// 输出:<__NSStackBlock__: 0x7fff5fbff798> 3

栈上和堆上对 __strong 变量的捕获,以及堆上 Block 调用 copy:

NSObject *obj = [NSObject new]; // 引用计数为 1
NSLog(@"%@ %lu", ^{ NSLog(@"blk0: %@", obj); }, _objc_rootRetainCount(obj));
// 输出:<__NSStackBlock__: 0x7fff5048aac0> 2,因为被 Stack Block 持有
    
id a = ^{ NSLog(@"blk1: %@", obj); };
NSLog(@"%lu", _objc_rootRetainCount(obj));
// 输出:4。因为赋值时,将 Block 复制到堆上,就有了两个 Block 对象持有 obj。
    
id b = [a copy];
NSLog(@"%lu", _objc_rootRetainCount(obj));
// 输出:4。已经在堆上的 Block 再调用 copy 不会对 obj 有影响,可能在 Block 上后再调用 copy 不会做任何事。

栈上和堆上对 __block 对象的变量的持有情况:

void (^blk)() = nil;
{
    __block NSMutableArray * __strong a = [NSMutableArray new];
    __block NSMutableArray * __strong b = a;    
    NSLog(@"%lu", _objc_rootRetainCount(b)); // 输出:2
        
    blk = ^{
        [a addObject:[NSObject new]];
        NSLog(@"%p %@ %lu", a, a, _objc_rootRetainCount(a));
        // 输出:0x7f8064318a80 ("<NSObject: 0x7f8064006490>") 1
    };
    NSLog(@"%p %@ %lu", a, a, _objc_rootRetainCount(a));
    // 输出:0x7f8064318a80 () 2
    // 这里如果按上一个解释的话应该输出 4 才对,栈和堆各持有一次,实际上不是
}
blk();
// 这里的解释是:1. Block 栈对象是直接持有的 __block a 对象的指针,所以不会对 array a 对象持有引用。
// 2. 当 Block 从栈复制到堆时,
//    若 Block 使用的变量为附有 __block 说明符的 id 类型或对象类型的自动变量,不会被 retain;
//    若 Block 使用的变量为没有 __block 说明符的 id 类型或对象类型的自动变量,则被 retain 。

上一份测试代码的补充:

// 上面代码不会 crash,但是 blk() 里的 array 确实已经是野指针了。
NSMutableArray * __unsafe_unretained c;
{
    NSMutableArray * __strong a = [NSMutableArray new];
    c = a;
}
[c addObject:[NSObject new]];
// c 是野指针,也不会 crash 。<这要看运气咯>

id __strong array = __cself->array 并不会 retain。

void (^blk)(id) = nil;
{
    id array = [[NSMutableArray alloc] init];
    blk = [^(id obj) {
    [array addObject:obj];
    NSLog(@"%ld %lu", [array count], _objc_rootRetainCount(array)); // 输出:1 1
    // 在 ARC 中是不允许在结构体中使用 Objective-C 对象的,所以这里总有些不合逻辑的地方。
    // 在 Block 实现的函数中,最开始就有语句:id __strong array = __cself->array;
    // 按理应该会再持有一次 array 的,但是 array 的引用计数没有变。
    // 仍然只是 Block 对象的成员变量持有的一次 array。
    } copy];
}    
blk([NSObject new]);

截获自动变量

int main(int argc, char * argv[]) {
    @autoreleasepool {
        int dmy = 256;
        int val = 10;
        const char *fmt = "val = %d";
        NSObject *a = [NSObject new];
        NSObject __weak *b = a;
        
        void (^blk)() = ^{
            printf(fmt, val);
            b; // 测试弱引用
        };
        blk();
    }
    
    return 0;
}

Console 使用 clang -rewrite-objc main.m 转换为下面 C语言代码(并不难看懂):

struct __block_impl {
  void *isa;  // _NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock
  int Flags;  // 标志位
  int Reserved; // 版本升级可能用到的保留位
  void *FuncPtr; // Block 函数
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt;
  int val;
  NSObject *__weak b;
  
  // 构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, NSObject *__weak _b, int flags=0) : fmt(_fmt), val(_val), b(_b) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt; // bound by copy
  int val = __cself->val; // bound by copy
  NSObject *__weak b = __cself->b; // bound by copy

  printf(fmt, val);
  b;
}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->b, (void*)src->b, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->b, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int dmy = 256;
        int val = 10;
        const char *fmt = "val = %d";
        NSObject *a = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"));
        NSObject __attribute__((objc_gc(weak))) *b = a;

        void (*blk)() = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val, b, 570425344);
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }

    return 0;
}

main 函数看到,Block 实际上被转换成了位于栈上的 __main_block_impl_0 自动变量。__main_block_copy_0 中的 _Block_object_assign 函数相当于 retain 实例方法,使 Block 的成员变量 b 持有捕获到的对象。 __main_block_dispose_0 中的 _Block_object_dispose 函数相当于 release 实例方法,释放 Block 的成员变量 b 持有的对象。

Block 不能捕获数组的原因是:在 __main_block_fun_0() 中需要对捕获到的变量赋值,而 C语言不支持数组赋值。

int a[20];
int b[20] = a; // 编译错误

截获的自动变量不能在 Block 中赋值,产生编译错误。因为就算改变 Block 中该变量的值也不能改变 Block 外部变量的值,既然如此,编译器就不让你改了,但可以在 Block 中再声明一个变量,然后去修改这个新的变量。但对于 静态全局变量(static) 和 全局变量,因为它们本身就可以被全局访问,所以 Block 不会捕获它们,且可以在 Block 中修改它们的值;对于 静态局部变量 因为它不能被全局访问,当截获它时会自动截获它的地址作为 Block 的成员变量,也可以在 Block 中修改它们的值。为什么不对所有变量都保存指针呢?因为不安全,局部变量在作用域结束就会被释放,但 Block 仍然保留着它的地址,就形成野指针了。

__block storage-class-specifier

__block 类似于 static auto register 说明符,它们用于指定将变量设置到那个存储域中。

int main(int argc, char * argv[]) {
    @autoreleasepool {
        __block int val = 10;
        void (^blk)(void) = ^{ val = 1; };
    }   
    return 0;
}

Console 使用 clang -rewrite-objc main.m 转换为下面 C语言代码:

// 声明为 __block 的变量会被转换成该类型的结构体
struct __Block_byref_val_0 {
  void *__isa;
  __Block_byref_val_0 *__forwarding;
  int __flags;
  int __size;
  int val;  // __block 原自动变量
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  
  // 构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// Block 函数体
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref
 (val->__forwarding->val) = 1; 
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) 
{_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) 
{_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

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

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
        void (*blk)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344);

    }
    return 0;
}

从转换后的代码中可以看到 __block 指定的变量被转换成了 __Block_byref_val_0 结构体,并且在 __main_block_impl_0 中以前捕获自动变量的位置被替换成了对应的 __Block_byref_val_0 指针。__main_block_impl_0__block_impl 成员的 isa 指针一般为 _NSConcreteStackBlock,但当满足以下任一情况时,Block 将被指定为:_NSConcreteGlobalBlock 类对象。

  • 在全局变量的地方使用 Block
  • Block 的表达式中没有截获自动变量

配置在全局变量上的 Block,从变量作用域外也可以通过指针安全访问(全局变量作用域好大的)。但设置在栈上的 Block,如果其所属的变量作用域结束,该 Block 就被废弃。由于 __block 变量也配置在栈上,同样地,如果其所属的变量作用域结束,该 Block 就被废弃。

实际上当 ARC 有效时,大多数情况下编译器会恰当地进行判断,自动生成 Block 从栈上复制到堆上的代码。只有在 向方法或函数的参数中传递 Block 时 需要手动生成代码。但是在函数或方法中适当地复制了传递过来的参数,那么就不必调用 copy 方法手动复制了。以下方法或函数不用手动复制:

  • Cocoa 框架的方法且方法名中含有 usingBlock 等
  • Grand Central Dispatch 的 API

举个例子,以下是会 crash 的代码(我是在 ViewController 中测试的)

- (id) getBlockArray {
    int val = 10;
    NSArray *arr = [[NSArray alloc] initWithObjects:
            ^{NSLog(@"blk0: %d", val); },
            ^{NSLog(@"blk1: %d", val); },
            nil];
    // initWithObjects 没有将 Block 从栈上 copy 到堆上,在函数结束时,它们就随着栈一起被释放了
    return arr;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSArray *arr = [self getBlockArray];
    void (^blk)() = [arr objectAtIndex:0];
    blk(); // Block 对象已经被释放,将 crash
}

但是下面的代码不会 crash

- (void)viewDidLoad {
    [super viewDidLoad];
    
    int val = 10;
    NSArray *arr = [[NSArray alloc] initWithObjects:
                    ^{NSLog(@"blk0: %d", val); },
                    ^{NSLog(@"blk1: %d", val); },
                    nil];
    // NSLog(@"%@", [arr objectAtIndex:0]); // => 输出:<__NSMallocBlock__: 0x7fed53f276e0>
    ((void (^)())[arr objectAtIndex:0])();  // 不会 crash 
    // 在 ARC 有效时,如果函数(上面的 objectAtIndex)返回的是 Block 则会被自动 copy 到堆中。
    // 还有,在 ARC 有效时,向 id 或 Block 变量赋值时也会自动将 Block 从栈 copy 到堆中。
}

Block 对象使用 copy 方法的效果总结:

Block 的类 副本源的存储位置 赋值效果
_NSConcreteStackBlock 从栈赋值到堆
_NSConcreteStackBlock 程序数据区 什么也不做
_NSConcreteStackBlock 什么也不做

Note: 但是 Block 对象的引用计数永远是 1,有可能没有加入引用计数表,也可能堆上的 Block 做 copy 时什么也没做。

当 Block 从栈复制到堆时对 __block 变量的影响:

__block 变量的存储区 副本源的存储位置
从栈赋值到堆并被 Block 持有
被 Block 持有

Note:__block 变量实际上是 Objective-C 对象。 我们在程序中不能访问 __block 对象,因为源代码都会被转换成 val->__forwarding->val 。

到此,可以说明 __block 结构体的成员变量 __forwarding 的作用了——不管 __block 变量配置在栈上还是堆上,都能正确地访问到该变量。代码如下:

__block int val = 0; // val 变量在栈上
void (^blk)(void) = [^{val++;} copy]; 
// blk 变量在堆上了(__block 变量也被 copy 到堆上),val->__forwarding 指向堆上的 __block 对象
++val; // ++(val->__forwarding->val)
blk(); // ++(val->__forwarding->val) 它的 val 在堆上,__forwarding 指向自己
NSLog(@"%d", val); // 输出为2

循环引用

typedef void(^blk_t)(void);
@interface MyObject : NSObject {
    blk_t blk_;
    id obj_;
}
@end

@implementation MyObject {
-(id)init{
    self = [super init];
    blk_ = ^{ NSLog(@"obj_ = %@", obj_); }
    // 发生循环引用,因为 Block 捕获到的对象实际上是 self,即 obj_ 被编译为 self->obj_
    // 使用 id __weak obj = obj_; 避免循环引用
    
    return self;
}
}

通过使用 __block 变量可以避免循环引用,在非 ARC 时候经常被这样用,好吧,其实我并不想管非 ARC 是什么情况,只是它真的可以避免循环引用。

typedef void(^blk_t)(void);
@interface MyObject : NSObject {
    blk_t blk_;
}
@end

@implementation MyObject {
-(id)init{
    self = [super init];
    __block MyObject * tmp = self; // 使用 __block 变量避免循环应用,这里 self 的引用计数加 1
    blk_ = ^{ NSLog(@"self = %@", tmp); }
    // blk_ 持有了含有 self 的 __block 变量;self 又持有了 blk_ 循环引用了。继续看下面~
    
    return self;
} // __block 无论是在堆上还是在栈上都不会持有 val->forwarding->val 的对象。
// tmp 在栈上且超出作用域,即被销毁。现在 blk_ 仍然持有 tmp 对应的堆上 __block 对象,
// 但堆上的 __block 对象并不持有 self,即不再是循环引用
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容