iOS 关于Block代码块的详解

概述

block

上图就是一个block简单使用,它包括了block的声明赋值实现调用 三个部分,其中,实现部分可以看作是一种匿名函数;跟函数一样,block也是需要调用才能执行内部代码的;赋值的行为又让block看起来跟数据类型类似

代码块Block是在iOS4开始引入的,是对C语言的扩展,用来实现匿名函数的特性

Block是一种特殊的数据类型,可以像基本数据类型一样定义成变量、作为参数、返回值来使用

Block还可以保存一段代码,在需要的时候调用

在iOS开发中,Block被系统应在很多地方,例如:GCD、UIView动画、排序等,我们开发者也可以应用在各类回调、传值、传消息等

Block的声明、赋值实现、调用

Block的声明样式。

返回类型 (^Block名称)(参数列表)

void(^myBlock)(NSString *, NSString *)

Block的返回类型分为有返回类型和无返回类型(void),参数列表也可有也可以没有,具体看需求

// 无返回类型无参数列表
void(^block)();
// 无返回类型有参数列表
void(^block)(int);
// 有返回类型无参数列表
int(^block)();
// 有返回列表有参数列表
int(^block)(int);

Block的简单使用:声明、赋值、调用

Block变量的赋值格式为:

Block变量 = ^(参数列表){函数体}; // 这里的参数列表一定要和声明时的参数列表一致

// 声明
void(^block)();
// 赋值
block = ^(){
    NSLog(@"Hello World");
};
// 调用
block();

也可以在声明时完成赋值

// 声明、赋值
void(^block)() = ^(){
    NSLog(@"Hello World");
};
// 调用
block();

定义Block类型

前面提到过,Block是一种特殊的数据类型,我们可以使用 typedef 来定义 Block 类型,这样我们就可以使用该类型来声明很多相同的Block变量了。

typedef 返回类型(^Block名称)(参数列表);

示例:

// 声明一个Block类型
typedef void(^Block)();

// 使用定义两个block变量
Block myBlock,myNewBlock;
// 赋值实现
myBlock = ^(){
    NSLog(@"Hello World");
};
myNewBlock = ^(){
    NSLog(@"Hello World, I am lolita0164.");
};
// 调用
myBlock();
myNewBlock();

ARC模式下简单应用

  • 作为对象属性实现消息传递

前面说到,Block保存一段代码,在需要的时候调用。我们可以将使用Block的三个步骤拆开,实现消息传递、传值功能。

// 定义Block类型
typedef void(^Block)(NSString *);

@interface Person : NSObject
// 声明Block变量
@property (nonatomic, copy) Block myBlock;
-(void)sayHello;
@end

@implementation Person
-(void)sayHello{
    // Block调用
    self.myBlock(@"Hello, I am lolita0164");
}
@end

在定义声明、调用之后,还缺少实现的部分,这一步通常由外部实现。

Person *p = [Person new];
// Block赋值实现
p.myBlock = ^(NSString *string) {
    NSLog(@"%@",string);
};
[p sayHello];

这样,我们就可以在Block的赋值实现部分里拿到 p类里的数据了。

  • 作为函数参数实现数据回调

我们将之前的例子稍加改动

// 定义Block类型
typedef void(^Block)(NSString *);
@interface Person : NSObject
// 将Block作为参数
-(void)sayHelloUseBlock:(Block)myBlock;
@end

@implementation Person
-(void)sayHelloUseBlock:(Block)myBlock{
    // Block调用
    myBlock(@"Hello, I am lolita0164");
}
@end

在外部进行实现。

Person *p = [Person new];
// Block实现
[p sayHelloUseBlock:^(NSString *string) {
    NSLog(@"%@",string);
}];

作为参数和作为属性传递消息,在应用场景稍稍有些不同。
作为参数时,通常和当前的方法有着紧密的联系,函数体内部需要与调用的外部进行交互。例如在请求方法中,经常会使用到block进行回调,而这个block和当前的方法关系紧密,通常是该方法的结果回调。又或者是方法执行期间需要外部提供一定的信息,从而通过block获取外部提供的数据。
作为属性时,通常是和当前类相关,作为类与类之间的交互代表。

  • 作为返回值实现链式语法

将block作为返回值的经典例子就是约束库 masonry,这个库在做完每次约束设置之后通过 block 将实例再次回调,就形成了链式语法。

[view makeConstraints:^(MASConstraintMaker *make) {
    make.left.equalTo(view1.mas_right).offset(10); 
    make.top.equalTo(view1).offset(0);
    make.right.equalTo(-10);
    make.size.equalTo(viewWidth);
}];

下面通过创建颜色类来演示 block 作为参数的使用。

// block 作为返回值
+(UIColor* (^)(CGFloat, CGFloat, CGFloat))rgb{
    // block 的声明和实现
    UIColor* (^rgbBlock)(CGFloat, CGFloat, CGFloat) = ^id(CGFloat r, CGFloat g, CGFloat b) {
        return [UIColor colorWithRed:(r)/255.0f green:(g)/255.0f blue:(b)/255.0f alpha:1];
    };
    return rgbBlock;
}

解析

返回值是一个有返回值参数有三个的block:UIColor * (^)(CGFloat, CGFloat, CGFloat)。我们在该方法的内部进行了block的声明和具体实现,并且将其作为返回值返回了出去,那么外部在调用该方法之后接收到的是一个block,可以使用该值。

那么外部使用情况如下。

// 接收 block 类型
UIColor* (^colorBlock)(CGFloat, CGFloat, CGFloat) = [UIColor rgb];
// 使用 block 获取到颜色
UIColor* color = colorBlock(10,33,65);
self.view.backgroundColor = color;

在丢弃不需要的部分后,代码如下。

+(UIColor* (^)(CGFloat, CGFloat, CGFloat))rgb{
    return ^id(CGFloat r, CGFloat g, CGFloat b) {
        return [UIColor colorWithRed:(r)/255.0f green:(g)/255.0f blue:(b)/255.0f alpha:1];
    };
}
// 使用block作为参数的方法
self.view.backgroundColor = UIColor.rgb(10, 33, 65);

Block 和 变量

Block访问局部变量问题

block 内部可以访问局部变量。

int global = 100;
void (^Block)() = ^(){
    NSLog(@"global = %i", global);
};
Block(); // 输出 "global = 100"

但是 block 会把变量 复制 为自己私有的const变量,也就是说block会捕获栈上的变量(或指针),将其复制为自己私有的const变量,当变量被修改时,不会影响到block自己私有的const变量。

int global = 100;
void (^Block)() = ^(){
    NSLog(@"global = %i", global);
};
global = 101;
Block(); // 输出 "global = 100"

在Block中不可以直接修改局部变量。

int global = 100;
void (^Block)() = ^(){
    global ++; // 这句报错
    NSLog(@"global = %i", global);
};
Block();

但是可以通过 __block 修饰符修改局部变量。

__block int global = 100;
void (^Block)() = ^(){
    NSLog(@"global = %i", global);
};
global = 101;
Block();   //输出 "global = 101"
__block int global = 100;
void (^Block)() = ^(){
    global ++; // 这句正确
    NSLog(@"global = %i", global);
};
Block();   //输出 "global = 101"

原因:在局部变量前使用 __block修饰 ,在Block定义时便是将局部变量的指针传给Block变量所指向的结构体,因此在调用Block之前对局部变量进行修改会影响Block内部的值,同时内部的值也是可以修改的。

Block访问全局变量、静态变量问题

可以访问和修改。

全局变量所占用的内存只有一份,供所有函数共同调用,在Block定义时并未将全局变量的值或者指针传给Block变量所指向的结构体,因此在调用Block之前对全局变量进行修改会影响Block内部的值,同时内部的值也是可以修改的。

在Block定义时便是将静态变量的指针传给Block变量所指向的结构体,因此在调用Block之前对静态变量进行修改会影响Block内部的值,同时内部的值也是可以修改的。

ARC下的内存管理

在ARC默认情况下,Block的内存存储在堆中,ARC会自动进行内存管理,我们只需要避免循环引用即可。

// 当Block变量出了作用域,Block的内存会被自动释放
void(^myBlock)() = ^{
    NSLog(@"------");
};
myBlock();

如果在Block中引用了外面的对象,会对所引用的对象进行强引用,但是在Block被释放时会自动去掉对该对象的强引用,因此比并不会造成内存泄漏问题。

Person *p = [[Person alloc] init];
void(^myBlock)() = ^{
    NSLog(@"------%@", p);
};
myBlock();
// Person对象在这里可以正常被释放
// 注:这里的Block只是单方面的强引用,所以不会产生循环引用,也不会内存泄漏

如果对象内部引用一个Block属性,而在Block内部又访问了该对象,那么会造成循环引用,导致内存泄漏。

self.block = ^{
    NSLog(@"------%@", self);
};

解决办法:使用一个弱引用的指针指向该对象,然后在Block内部使用该弱引用指针来进行操作,这样就避免了Block对对象进行强引用。

__weak typeof(self) weakSelf = self;
weakSelf.block = ^{
    NSLog(@"------%@", weakSelf);
};

提示:如果只是Block单方面地对外部变量进行强引用,并不会造成内存泄漏。

补充

1、声明block属性的时候为什么用copy呢?

在说明为什么要用copy前,先思考下block是存储在栈区还是堆区呢?其实block有3种类型:

  1. 全局块(_NSConcreteGlobalBlock)
  2. 栈块(_NSConcreteStackBlock)
  3. 堆块(_NSConcreteMallocBlock)

全局块存储在静态区(也叫全局区),相当于OC中的单例;栈块存储在栈区,超出作用域则马上被销毁。堆块存储在堆区中,是一个带引用计数的对象,需要自行管理其内存。

关于内存分配,请看这篇:C语言内存分配

怎么判断一个block所在的存储位置呢?

  • block不访问外界变量(包括栈中和堆中的变量)

block既不在栈中也不在堆中,此时就为全局块,ARC和MRC下都是如此。

  • block访问外面变量

MRC环境下:默认存储在栈区
ARC环境下:默认存储在堆中,实际上是先放在栈区,在ARC情况下自动又拷贝到堆区,自动释放

因此,使用 copy 修饰符的作用就是将block从栈区拷贝到堆区

为什么要这么做呢?官方给出的答案是:

复制到堆区的主要目的就是 保存 block 的状态,延长其声明周期。因为block如果在栈上的话,其所属的变量作用域结束,该block就被释放掉了,block中的 __block 变量也同时被释放掉了,为了解决超出作用域就被释放的问题,我们就需要把block复制到堆中。

总结

OC 中的 block 是对 C 语言的匿名函数的一种特性是实现。block 具有函数特性,同时也可以作为变量使用。block 可以作为属性、参数、返回值使用。想要在 Block 内部修改外部变量时,需要使用 __Block 将变量指针传递给 block。在使用 Block 时需要特别注意内存泄漏的问题。

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

推荐阅读更多精彩内容