【iOS】block小记

本篇文章并不会剖析block的原理,笔者对原理也不甚了解。

声明:对某个属性的访问(access)包括getter和setter。以下所说的访问是指setter。

1.1 block内访问外部局部变量

block内访问的外部局部变量是值传递,block会捕获外部局部变量的瞬间值。
block内不能直接set外部局部变量,否则会报错,错误代码如下:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    int var = 10;

    self.block = ^{
        var = 20; // block内不可以直接访问外部局部变量
    };
    self.block();
    NSLog(@"var == %d",var);
}

错误截图:

block内不可以直接访问外部局部变量

要想访问外部局部变量,外部局部变量必须用__block修饰,代码如下:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    __block int var = 10;

    self.block = ^{
        var = 20;
    };
    self.block();
    NSLog(@"var == %d",var);
}

运行效果如下:

block内访问__block修饰的局部变量

block内访问__block关键字修饰的外部局部变量是地址传递(此处的地址传递不准确,后续文章深入剖析)。block内修改这个局部变量会影响到外部局部变量的值。反之亦然。

1.2 block内访问静态变量

block内访问的静态变量也是地址传递。block内修改这个静态变量会影响到静态变量的值。反之亦然。

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    static int staticVar = 10;

    self.block = ^{
        staticVar = 20;
    };
    self.block();
    NSLog(@"staticVar == %d",staticVar);
}
block内访问静态变量

1.3 block内访问全局变量

block对全局变量也是地址传递。block内修改这个全局变量会影响到全局变量的值。反之亦然。

代码如下:

int globalVar = 10;
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.block = ^{
        globalVar = 20;
    };
    self.block();
    NSLog(@"globalVar == %d",globalVar);
}

运行效果如下:

block内访问全局变量

block内要想修改外部局部变量,外部局部变量必须要用__block修饰。但,block内部可以直接修改外部的静态变量或者全局变量,静态变量和全局变量无需用__block修饰。


block分为三种:全局block、栈区block、堆block

block循环引用问题

ARC

ARC下产生循环引用

解决方法:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 使用__weak
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        [weakSelf handleBlock];
    };
    self.block();
    
}

- (void)handleBlock {
    NSLog(@"调用block");
}

ARC防止循环引用

MRC

把viewController.m文件切换到MRC,切换方式如下:

MRC

使用_ _block修饰block内部引用的外部变量,如下:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // MRC下使用__block
    __block typeof(self) weakSelf = self;
    self.block = ^{
        [weakSelf handleBlock];
    };
    self.block();
    
}

- (void)handleBlock {
    NSLog(@"调用block");
}
MRC下使用__block

block类型

关于block的种类,网上有很多文章说是三种,其实不然,block的类型其实是六种,只不过我们接触过得只有3中而已(另外3种主要用于GC),如下:

  • NSConcreteGlobalBlock (全局block)
  • NSConcreteStackBlock (栈block)
  • NSConcreteMallocBlock (堆block)

什么是全局block

如果一个block没有访问外界的任何变量或者对象,那么这个block就是一个全局block。全局block既不存储于栈区也不存储于堆区。下面的blk就是一个全局block:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // block内部没有访问外界的任何变量或对象
    customBlock blk = ^{
    };
    NSLog(@"%@",blk);
}

输出block,查看blk类型,验证结果如下:

__NSGlobalBlock__

注意:只有block内部不访问任何外部的变量,才是全局block。如果block没有访问外部变量,但是访问了block自己本身,此时也不再是全局block,代码如下:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // block内部没有访问外界的任何变量或对象
    // block 内部也不能访问block自己本身
    customBlock blk = ^{
        NSLog(@"%@",blk);
    };
    NSLog(@"%@",blk);
}

输出block,查看blk类型,验证结果如下:

block访问自己也不再是全局block

如上图,block内部访问了block自己(或者访问外部变量),就会变成堆block。那么问题来了,问题一:为什么block的类型会变呢?为题二:为什么block会变成堆block而不是栈block呢?
MRC下,访问了外界变量的block会变成栈block。栈上的Block,如果其所属的变量作用域结束,该Block就被释放,如同栈上的其他变量。为了解决栈block在其变量作用域结束之后被释放的问题,我们需要把Block复制到堆中,使用引用计数管理block,就像堆上的其他对象一样,延长其生命周期。所以在MRC下,我们经常会对block进行copy操作(当然,我没有彻头彻尾的经历过MRC时代的项目)。ARC下,编译器会判断是否有需要将Block从栈复制到堆,如果需要复制到堆上,编译器会自动生成将Block从栈复制到堆上的代码。Block的复制操作执行的是copy实例方法。Block只要调用了copy方法,栈block就会变成堆block。所以再ARC下,开发者不需要对block进行额外的copy操作,编译器会替我们做这些事情。而上面的工程代码是在ARC下编写的,所以block访问了外界变量,会经历两个过程:全局block -> 栈block -> 堆block。最终,控制台输出的block的类型变成了堆NSMallocBlock

为了证明在MRC下,访问外界变量的block会变成堆block,我们把viewController.m后面加上-fno-objc-arc,然后运行代码,结果如下:

MRC下访问外界变量的block是栈block

然后,在MRC下,对这个栈上的block进行copy操作,copy出的副本是堆block,代码如下:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // block内部没有访问外界的任何变量或对象
    // block 内部也不能访问block自己本身
    customBlock blk = ^{
        NSLog(@"%@",blk);
    };
    NSLog(@"拷贝前:%@",blk);
    customBlock copyBlk = [blk copy];
    NSLog(@"拷贝后:%@",copyBlk);

}

输出结果如下:

MRC下对栈上的block进行copy操作会变成堆block
MRC下对栈上的block进行copy操作会变成堆block

从上图也能看出,栈上的block被copy后,堆上多出来一个block。而栈上的block依然存在,只是当其作用于结束后,栈上的block会被销毁。

block property

还是MRC下,如果我们的控制器有一个block属性,那么这个block的内存管理语义(retain、strong、copy)也决定了block的类型。
viewController设置为MRC,如下图:

MRC

strong修饰block,代码如下:

typedef void(^customBlock)();

@interface ViewController ()
@property(nonatomic,strong) customBlock block;
@end
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    __block typeof(self) weakSelf = self;
    self.block = ^{
        [weakSelf printBlockType:weakSelf.block];
    };
    self.block();
}

- (void)printBlockType:(customBlock)block {
    NSLog(@"%@",block);
}

输出结果:


strong修饰block属性

如果property改为copy修饰呢?当然肯定也是变成堆block,如下图:

typedef void(^customBlock)();

@interface ViewController ()
@property(nonatomic,copy) customBlock block;
@end
copy修饰block属性

如果block属性使用retain修饰呢?(MRC下):

typedef void(^customBlock)();

@interface ViewController ()
@property(nonatomic,retain) customBlock block;
@end

输出结果:

MRC下retain修饰block

当然,MRC下assign修饰的block属性也是栈block,编译器也不会对其进行拷贝操作,如下图:

MRC下assign修饰block属性

所以,我们可以得出结论:

MRC下,block属性使用不同的内存管理语义,会有不一样的效果。使用strong/copy,编译器会自动帮我们把block copy到堆内存上。使用retain.assign修饰的block属性依旧是栈block,此时如果有需要延长栈block的生命周期,开发者需要对其手动copy。

其实,MRC下,如果我们使用retain修饰block属性,编译器会对我们进行警告,从警告信息我们也可以看出,使用retain修饰block属性,编译器并不会对block进行copy。如下图:

MRC下编译器警告

那你肯能会问,既然MRC下,strong/copy和retain对block属性有不一样的影响,ARC会不会也有相同的影响呢?答案是否定的,不管ARC下我们使用strong/copy/retain中的哪个关键字修饰block属性,如果有需要,最终编译器都会进行优化,把block copy到堆内存上。

ARC下,虽然,我们使用retain修饰了block属性,但是输出的block类型依然是堆block,如下图所示:

ARC下编译器自动copyblock到堆内存(如果有需要)

ARC下,只有当block属性使用assign修饰时,block才会是栈block,编译器无论如何都不会对其进行copy操作,因为编译器会认为这是开发者有意而为之,并不会多此一举的进行拷贝,如下图:

ARC下assign修饰block属性

所以,ARC下使用retain/strong/copy修饰block属性效果是一样的,编译器会在有必要的时候自动对其进行copy。但assign修饰block是很危险的,不要轻易尝试,除非有特殊需要。

总结

  1. 局部变量对block是值传递;静态变量、全局变量对block是地址传递。
  2. __block关键字修饰的局部变量是地址传递。局部变量也只有被__block关键字修饰后才可以被block修改。否则只能读取。
  3. block分为3类NSConcreteGlobalBlock (全局block)、NSConcreteStackBlock (栈block)、NSConcreteMallocBlock (堆block)。
  4. block循环引用问题:ARC下使用__weak关键字打破引用环;MRC下使用__block关键字打破引用环。
  5. 如果一个block没有读取/修改外界的任何变量或者对象,那么这个block就是一个全局block。
  6. ARC下,访问外部变量的block会变为堆block;MRC下,访问外部变量的block会变为栈block。只有copy后才会变为堆block。
  7. block作为property时。
    ARC下,使用strong/copy/retain修饰的效果是一样的,block都会被copy到堆上(前提是block内部有使用其他变量);使用assign修饰的block依然在栈上。
    MRC下,使用strong/copy修饰的block,运行时也会自动帮我们copy到堆上,使用assign/retain修饰的block依然在栈上。

参考

iOS Block详解
Block技巧与底层解析
关于Block再啰嗦几句

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

推荐阅读更多精彩内容