Objective-C高级编程(中):Block

《Objective-C高级编程:iOS与OS X多线程和内存管理》是iOS开发中一本经典书籍,书中有关ARC、Block、GCD的梳理是iOS开发进阶路上必不可少的知识储备。笔者读完此书后为了加强理解,特以笔记记之。本文为中篇,主要谈论Objective-C中的Block。

Block

鉴于本书翻译自日文原版且翻译偏向书面,笔者希望采用通俗的语言记录,文章结构略有调整。

本文首发于Rachal's blog

Block概要

定义Block是带有自动变量(局部变量)的匿名函数。

匿名函数就是不带名称的函数。

C语言的函数中可能使用的变量:

  • 自动变量(局部变量)
  • 函数的参数
  • 静态变量(静态局部变量)
  • 静态全局变量
  • 全局变量

前两种在超出作用域会销毁,后三种可以在函数的多次调用之间传递。

在计算机科学中,“带有自动变量值的匿名函数”这一概念称为闭包,Block就是Objective-C对闭包的实现。

Block模式

Block语法

Block的完整语法:^ 返回值类型 参数列表 表达式

^ int (int count) {return count + 1;}

完整形式的Block语法与一般C语言函数定义相比,仅有两点不同:

  • 没有函数名
  • 带有^

其中返回值类型和参数列表可省略,省略后为:^ 表达式

^ {printf("Blocks\n");}

Block变量类型

在Block语法中下,可将Block语法赋值给声明为Block类型的变量。

int (^blk)(int);// 声明Block类型

int (^blk1)(int) = ^(int count) {return count + 1};// 变量blk1
int (^blk2)(int);// 变量blk2

blk2 = blk1;// Block类型变量赋值

通过typedef可声明Block类型变量,函数定义就变得更容易理解。

typedef int (^blk)(int);

blk blk1 = ^(int count) {return count + 1;};
blk blk2 = blk1; // blk类型变量赋值

截获自动变量

  • Block可以截获自动变量的值。
NSInteger aa = 10;

void (^blk)(void) = ^{
    NSLog(@"%ld",aa);
};

aa = 2;

blk();

// 输出结果为10
  • Block中使用时向截获的自动变量进行赋值会出错。
id array = [[NSMutableArray alloc] init];

void (^blk) (void) = ^{

    id obj = [[NSObject alloc] init];

    /* 使用array变量调方法,没问题 */
    [array addObject:obj];
};

blk();
id array = [[NSMutableArray alloc] init];

void (^blk) (void) = ^{

    /* 想array变量赋值,出错 */
    array = [[NSMutableArray alloc] init];
};

blk();

__block说明符

若想在Block语法的表达式中将值赋给在Block语法外声音的自动变量,需要在该自动变量上附加__block说明符。

__block int val = 0;

void (^blk)(void) = ^{val = 1;};

blk();

printf("val = %d\n",val);// 输出结果为val = 1;
__block id array = [[NSMutableArray alloc] init];

void (^blk) (void) = ^{

    /* 向array变量赋值,不会出错 */
    array = [[NSMutableArray alloc] init];
};

blk();

Block实现

Block的实质

将Objective-C代码转化成可读源码(C++)的方法:

// 终端cd 源代码文件夹
clang -rewrite-objc 源代码文件名

书中将一段Block代码转化为可读的C++源码,简化源码,关注其中的结构体:

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

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

Block的结构体中含有isa指针,足以证明Block即为Objective-C的对象

isa = &_NSConcreteStackBlock;

isa被初始化,说明在将Block作为Objective-C的对象处理时,关于该类的信息放置于_NSCocreteStackBlock中。

三种Block

前面提到了_NSConcreteStackBlock类型的Block,与之对应的还有_NSConcreteGlobalBlock_NSConcreteMallocBlock

名称 Block的类 对象存储域 复制效果
栈Block _NSConcreteStackBlock 从栈复制到堆
堆Block _NSConcreteMallocBlock 引用计数增加
全局Block _NSConcreteGlobalBlock 程序的数据区(.data区) 什么也不做

全局Block产生途径:

  • 记述全局变量的地方有Block语法时
  • Block语法的表达式中不使用应截获的自动变量时

除此之外的Block语法生成的Block为栈Block,将栈上的Block复制到堆上,Block就会变成堆Block。

Blobk如何截获自动变量值

通过clang源码转换得知:

Block语法表达式中使用的自动变量被作为成员变量追加到Block的结构体中。Block语法表达式中没有使用的自动变量不会被追加。Block的自动变量截获只针对Block中使用的自动变量。

所谓“截获自动变量值”意味着在执行Block语法时,Block语法表达式所使用的的自动变量值被保存到Block的结构体实例(即Block自身)中。

__block变量

__block说明符能用来指定Block中想变更值的自动变量

准确的表述方式为“__block存储域类说明符”,C语言中有以下存储域类说明符:

  • typedef
  • extern
  • static
  • auto
  • register

它们用于指定将变量值设置在哪个存储域中。例如:auto表示作为自动变量存储在栈中,static表示作为静态变量存储在数据区。

自动变量加上__block,源代码会急剧增加,变量的转换如下:

__block int val = 10;
__Block_byref_val_0 val = {
    0,
    &val,
    0,
    sizeof(__Block_byref_val_0),
    10
};

自动变量转换后竟然变成了结构体实例,即栈上生成的__Block_byref_val_0结构体实例。

结构体声明如下:

struct __Block_byref_val_0 {
    void *__isa;
    __Block_byref_val_0 *__forwarding;
    int __flags;
    int __size;
    int val;
};

__Block_byref_val_0结构体实例的成员变量__forwarding持有指向该实例自身的指针,也就是原自动变量。

访问__block变量

另外,__block变量的__Block_byref_val_0结构体并不在Block的结构体中,这样做是为了在多个Block中使用__block变量。

Block从栈复制到堆时,__block变量也会一并从栈复制到堆并被该Block所持有。在多个Block中使用__block变量时,被复制的Block持有__block变量,并增加__block变量的引用计数。如果配置在堆上的Block被废弃,那么它所使用的__block变量也就被释放。

栈上的__block变量的结构体实例在复制到堆上时,会将成员变量__forwarding的值替换为复制目标堆上的__block变量的结构体的地址。

复制__block变量

通过该功能,无论是在Block语法中、Block语法外使用__block变量,还是__block变量配置在栈上或堆上,都可以顺利访问同一个__block变量

Block截获的对象和__block变量

- (void)blockTest {

    blk_t blk;

    {
        id array = [[NSMutableArray alloc] init];

        blk = ^(id obj){

        [array addObject:obj];

            NSLog(@"array count is %ld", [array count]);
        };
    }

    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
}

以上blockTest方法在ARC无效和有效情况下执行结果:

  • ARC无效时,程序会强制结束。因为对象array超出作用域后不存在。
  • ARC有效时,输出如下:
array count is 1
array count is 2
array count is 3

ARC有效时,大部分情况下编译器会适当的进行判断,自动生成将Block从栈上复制到堆上的代码。被栈上Block截获的对象可以超出作用域而存在。

通过编译器转换后的源码查看Block结构体,发现Block结构体中有__strong修饰的成员变量array

struct __main_block_impl_0 {
    struct __blobk_impl impl;
    struct __main_block_desc_0* Desc;
    id __strong array;
}

C语言结构体中不能含有__strong修饰的变量,因为编译器不能很好地对其内存管理。但是Objective-C运行时库能够准确把握从栈复制到堆上的Block被废弃的时机,恰当地对变量进行初始化和废弃

在此对比截获对象和使用__block变量时持有(copy)和废弃(dispose)函数源码:

  • 截获对象:
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) {
    _Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}
static void __main_block_dispose_0(struct __main_block_impl_0 *src) {
    _Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);
}
  • 使用__block变量:
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) {
    _Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}
static void __main_block_dispose_0(struct __main_block_impl_0 *src) {
    _Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}

通过对比发现:

  • 截获对象和__block变量使用_Block_object_assign函数
  • 废弃对象和__block变量使用_Block_object_dispose函数
  • 通过Block_FIELD_IS_OBJECTBLOCK_FIELD_IS_BYREF参数区分copydispose函数的对象类型是对象还是__block变量。

由此可知:Block中使用的赋值给__strong修饰(ARC下常省略)的自动变量的对象和复制到堆上的__block变量,由于被堆上的Block所持有,因而可超出其变量作用域而存在。

栈上的Block复制到堆上的时机

  • 调用Block的copy方法时
  • Block作为函数返回值返回时
  • 将Block赋值给__strong修饰的id类型或Block类型变量时
  • 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时

栈上的Block为何需要复制到堆上

将栈上的Block复制到堆上,这样即使Block记述的变量作用域结束,堆上的Block还可以继续存在。

- (void)blockTest {
    id obj = [self getBlockArray];

    typedef void (^blk_t)(void);

    blk_t blk = (blk_t)[obj objectAtIndex:0];

    blk();
}

- (id)getBlockArray {
    int val = 10;

    return [[NSArray alloc] initWithObjects:^{NSLog(@"blk0:%d",val);},^{NSLog(@"blk1:%d",val);},nil];
}

执行以上blockTest方法会出错,原因在于变量作用域。如下修改后没问题:

- (id)getBlockArray {
    int val = 10;

    return [[NSArray alloc] initWithObjects:[^{NSLog(@"blk0:%d",val);} copy],[^{NSLog(@"blk1:%d",val);} copy],nil];
}

注意:将Block从栈上复制到堆上是相当消耗CPU的

Block循环引用

Block为何会引起循环引用

Block中使用__strong修饰的对象类型自动变量,当Block从栈复制到堆时,该对象为Block所持有,这样容易引起循环引用。

typedef void(^blk_t) (void);

    @interface MyObject : NSObject {
    blk_t _blk;
}

@end


@implementation MyObject

- (id)init {

    self = [super init];

    _blk = ^{
        NSLog(@"self = %@", self);//循环引用警告
        };

    return self;
}

@end


int main() {

    id o = [[MyObject alloc] init];
    NSLog(@"%@", o);

    return 0;
}

MyObject类对象实例self持有Block,Block语法中使用了self,Block从栈复制到堆并持有所使用的self。self和Block相互持有,引起循环引用。

__weak解决循环引用

为避免循环引用,可声明__weak修饰的变量,并将self赋值使用。

- (id)init {

    self = [super init];

    id __weak tmp = self;

    _blk = ^{
        NSLog(@"self = %@", tmp);
        };

    return self;
}

__weak打破了Block对self的强引用,解决了循环引用问题。

另外,Block内使用MyObject类的实例变量也会引起循环引用,因为会造成Block对self的间接持有。同样使用__weak可解决循环引用问题。

typedef void(^blk_t) (void);

@interface MyObject : NSObject {
    blk_t _blk;
    id _obj;
}

@end


@implementation MyObject

- (id)init {

    self = [super init];

    id __weak tmp = _obj;

    _blk = ^{
        NSLog(@"_obj = %@", _obj);
    };

    return self;
}

@end

__block解决循环引用

使用__block变量也可以避免循环引用。

typedef void(^blk_t) (void);

@interface MyObject : NSObject {
    blk_t _blk;
}

- (void)execBlock;

@end


@implementation MyObject

- (id)init {

    self = [super init];

    __block id tmp = self;

    _blk = ^{

        NSLog(@"self = %@", tmp);
        tmp = nil;
    };

    return self;
}

- (void)execBlock {
    blk();
}

@end


int main() {

    id o = [[MyObject alloc] init];
    [o execBlock];

    return 0;
}

使用__block变量来避免循环引用注意以下两点:

  • __block变量在使用完时要被赋值nil
  • 必须执行Block

PS:笔者认为这种方式其实是在模拟__weak功能。因为__weak修饰的变量在超出作用域销毁时会自动被赋值nil,而执行Block刚好就是促使变量被赋值nil这一操作执行。

注意:ARC有效和无效时,__block说明符的区别很大。ARC无效时,__block说明符用来避免Block中的循环引用,因为有__block说明符的变量不会被retain

以上为Block篇的学习内容。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,348评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,122评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,936评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,427评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,467评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,785评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,931评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,696评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,141评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,483评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,625评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,291评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,892评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,492评论 2 348

推荐阅读更多精彩内容

  • 世界上最美妙的声音是来自孩子的,因为纯真。我们每个人都曾经纯真过,只是岁月的风一点点把它吹走,我们自以为沧海桑田的...
    遇见思享阅读 283评论 0 0
  • 今天大年初二,那我们就接着说说大年初二那些事。 NO.1 回娘家。 大年初二,出嫁的女...
    大唐坐在白日梦上阅读 242评论 0 0
  • 我不是一个擅长坚持的人,从来都不是。 曾经喜欢过吉他,兜兜转转到大学才报个了班学,不到两个月便弃掉了,原因无非骨子...
    无理七阅读 129评论 3 1