美文网首页block相关iOS开发今日看点
iOS 一句话搞懂block是什么东东

iOS 一句话搞懂block是什么东东

作者: Allan_野草 | 来源:发表于2017-03-06 09:28 被阅读591次

片头

一句话搞懂block:可以理解为,block是对代码段的打包,然后在适当的时机执行。

正文

1. 语法

变量声明:返回值类型 (^block对象指针变量名)([形参列表])
类型声明:typedef 返回值类型 (^block类型名)([形参列表])
定义:^([形参列表]){block体}
调用:block对象指针变量([入参列表])

语法例子1:
int (^func)(int, int) = ^(int a, int b) {return a+b};
int result = fun(1,2);// 3

解释:
int (^func)(int, int)声明了一个返回值类型为int,有两个int型参数的block对象指针func,
^(int a, int b){return a+b;}定义了一个block体为return a+b的block对象,
int result = func(1,2);调用block,入参为1和2,block返回了3。

语法例子2:
typedef int (^Func)(int, int);
Func func = ^(int a, int b) {return a-b;};
int result = fun(1,2);// -1

解释:
typedef int (^Func)(int, int);声明了block的类型,
Func func声明了Func类型的block指针func,
= ^(int a, int b) {return a-b;};定义了block对象并赋值给func,
func(1,2);调用block。

(有C基础的可以继续往下看)
block语法和C的函数指针语法非常相似,比如C函数指针的声明:

// C函数指针声明
// 返回值类型为int,有两个int型参数的函数指针
int (*ptr)(int, int);
typedef (*Ptr)(int, int);

那么现在把 * 换成 ^ ,相信会很好理解了

// block声明
int (^func)(int, int);
typedef (^Func)(int, int);

**实际上block体代码段会被编译器用C重写成静态函数,block的调用的结果是C函数的调用,所以在语法设计上两者会比较相似。

作为形参:( 返回值类型 (^)([block的形参列表]) )形参名 or (block类型)形参名

语法例子3:
// typedef void (^Callback)(NSData *);
// -(void)asyncHttpAndCallback:(Callback)block
-(void)asyncHttpAndCallback:(void (^)(NSData *))block
{
  dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 异步加载
    NSData *data = ..;
    dispatch_async(dispatch_get_main_queue(), ^{
        // 主线程回调
        block(data);
    });
  });
}

调用:

// Callback block = ..
void (^block)(NSData *) = ^(NSData *data) {
  NSLog(@"下载完成");
  NSLog(@"%@",data);
  UIImage *image = [UIImage imageWithData:data];
  ..
};
[self asyncHttpAndCallback:block];

2. 特性

捕获:
block能够跟据上下文捕获外界的变量。通过编译器在编译期,将外界与block体中同名的变量"编译"进block体,从而实现捕获。为了便于说明,用伪代码演示:

特性例子1:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
  int i = 1;
  void (^block)(void) = ^{
    int j = i+1;
    NSLog(@"%d", j);
  };
  block();
  return 0;
}

经过编译器的处理后,在block体构造局部变量_block_i,完成"捕获"变量i:

int i = 1;
void (^block)(void) = ^{
  int _block_i = i;// 构造局部变量
  int j = _block_i+1;
  NSLog(@"%d", j);
};

举一反三,为什么如果在block体中进行i++操作(不加__block修饰),编译器会报错:

特性例子2:
int i = 1;
void (^block)(void) = ^{
  i++;// 这里报错
};
int i = 1;
void (^block)(void) = ^{
  int _block_i = i;
  _block_i++;
};

因为i++操作编译后,实际上是对局部变量_block_i++操作,所以编译器很智能地给出了不能在block体内修改变量i的错误提示。

加了__block修饰局部变量之后,就可以在block体内对变量值进行修改了,编译器会做如下处理:

特性例子2:
__block int i = 1;
void (^block)(void) = ^{
  i++;// 通过编译
};
int i = 1;
void (^block)(void) = ^{
  int *_block_i = &i;// 指向i的指针
 (*_block_i)++;
};

可以发现_block_i是指向变量i的指针,所以在block体修改_block_i值,对变量i同样生效。需要一提的是,对全局变量(全局变量,合局静态变量)并不需要用__block修饰。因为对于全局变量“捕获”进来的也是引用(在表现上与天生加上了__block一样)。

上面例子都是用伪代码的表示的。更准确来说,对于每个block,编译后会自动生成一个表示block的结构体,block体被编译成一个静态C函数,“捕获”进来的变量是结构体的成员变量,调用block相当于调用静态C函数来对结构体的成员变量进行操作。比如,下面将Objective-C源码用Clang编译后得到的代码片段,可以看到编译生成了其它很多代码:
(代码片段引用自深入研究Block捕获外部变量和__block实现原理,为读者方便理解,我加上了较多的注释)

#import <Foundation/Foundation.h>
int global_i = 1;
static int static_global_j = 2;
int main(int argc, const char * argv[]) {
    static int static_k = 3;
    int val = 4;
    void (^myBlock)(void) = ^{
        global_i ++;
        static_global_j ++;
        static_k ++;
        NSLog(@"Block中 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);
    };
    global_i ++;
    static_global_j ++;
    static_k ++;
    val ++;
    NSLog(@"Block外 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);
    myBlock();
    return 0;
}
// 将被捕获的变量
int global_i = 1;
static int static_global_j = 2;
struct __main_block_impl_0 {// block结构体(**编译生成)
  struct __block_impl impl;// 实现结构体
  struct __main_block_desc_0* Desc;// 描述结构体,用来描述block
  // 捕获的变量作为成员变量
  int *static_k;
  int val;
  // 构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val, int flags=0) : static_k(_static_k), val(_val) {
    // 初始化实现结构体impl
    impl.isa = &_NSConcreteStackBlock;// 只要看到is_a指针,就可以知道block在oc中被视为对象
    impl.Flags = flags;// 用于内存管理的flags
    impl.FuncPtr = fp;// 指向下面的C实现函数
    // 描述结构体
    Desc = desc;// 保存block的大小、引用计数信息,引用block的拷贝构造、析构函数
  }
};
// block体编译出来的静态函数(**编译生成)
// 也就是说,在block体写的代码会转化为以下的C代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_k = __cself->static_k;// 指向静态变量的指针
  int val = __cself->val;// 普通局部变量
  global_i ++;
  static_global_j ++;
  (*static_k) ++;// 由于是通过指针(地址)取值,所以进行++后被捕获前的static_k值也会改变
  NSLog((NSString*)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_0,global_i,static_global_j,(*static_k),val);
};
// block的描述结构体(**编译生成)
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)};
//
int main(int argc, const char * argv[]) {
    static int static_k = 3;
    int val = 4;
    // 构造block结构体
    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));
    // 打印调用block前的变量
    global_i ++;
    static_global_j ++;
    static_k ++;
    val ++;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_1,global_i,static_global_j,static_k,val);
    // 访问block的实现结构体,调用C静态函数
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    return 0;
}

引用:
ARC下,被捕获的强指针,其指向的对象会被引用(引用计数增加)。比如 id __strong obj、NSData *data等。obj、data在block被释放前都不会被释放,一个不注意就会容易产生内存泄漏。

使用场景

书写block能使代码可阅读性更强,只有在一个地方(函数)里用到回调的代码,不必特地去写另外一个函数去处理回调结果。

先举个简单的例子

使用场景例子1:
// 获取年龄最大的person对象,其中person有个age属性。
- (Person *)getOldest:(NSArray *)people
{
  // 定义block,用于比较大小时调用
  Person * (^comparer)(Person *, Person *) = ^(Person *a, Person b) {
    return a.age>b.age ? a : b;// 返回年龄比较大的对象  
  }
  //
  Person *oldest = nil;
  for (int i = 0; i<[people count]; i++) {
    Person *cur = people[i];
    Person *older = comparer(cur,oldest);// 调用block
    oldest  = older;
  }
  //
  return oldest;
}

进阶使用:根据不同规则,获取相应的对象

使用场景例子2:
- (void)get:(NSArray *)people rule:(Person * (^)(Person *, Person *))cmper
{
  Person *re= nil;
  for (int i = 0; i<[people count]; i++) {
    Person *cur = people[i];
    re = cmper(cur,re);// 回调block
  }
}
// 获取id最小的对象
[self get:people rule:^(Person *a, Person *b) {
   return a.id<b.id ? a : b;
}];
// 获取年龄最大的对象
[self get:people rule:^(Person *a, Person *b) {
   return a.age>b.age ? a : b;
}];

这种写法跟-enumerateObjectsUsingBlock:类似,如何不使用block的话,这时需要写两个规则的比较函数,并传入@selector(method)进行回调。根据个人的编码风格,可以选择不同的写法。

block最最为主要的应用场景,就是作为回调来使用。
比如常见的网络数据请求完成后回调,可以回看语法例子3
还有一种就是block作为属性,打包到某对象当中,在适当时机让对象访问block并调用。

例子有网上流行的视图控制器之间 block传值:

// 在普通控制器a中,有一个UIImageView用来显示用户头像
// 准备present的登录控制器,有一个block属性
LoginContr *b = [LoginContr new];
b.block = ^(UIImage *img){// 登录成功回调
  [this.pic setImage:img];// 设置头像
  NSLog(@"已登录"); 
};
[this presentViewController:b ..];
..
// 在b中,登录完成之后
- (void)doLogin
{
  // blablabla..
  // 终于登录成功了
  UIImage *img = [self getUserAvatar];
  self.block(img);// 调用block, a.pic的image被设置并打印log
}

面试常问

Q1:block是什么、如何使用、好处、实现原理?

手动翻一翻上面的吐血知识整理。

Q2:以下的代码有什么问题(如何解决block的循环引用/内存泄漏)?
// .h
typedef void (^Block)(void);

@interface Test
@property (nonatomic, strong) Block block;
@property (nonatomic, assign) int j;
@end

// .m
@implementation
{
  int _i = 1;
}

- (instancetype)init
{
  if(self = [super init]) {
    [self doTest];
  }
  return self;
}

- (void)doTest
{
  self.block = ^{
    self.j = 1;
    _i = 2;
  };
  self.block();
  NSLog(@"%d", self.j);
  NSLog(@"%d", _i);
}

- (void)dealloc
{
  NSLog(@"dealloc");// dealloc会打印吗?
}
  • 答:- dealloc不会被回调也不会打印。因为Test对象永不被释放,发生了内存泄漏。
  • 解释:首先self.block作为strong属性,在self不被释放之前,block是不会被释放的。同时之前说过block会引用强指针指向的对象,因此block捕获并引用了self,导致block不被释放,self也不会被释放,形成了引用循环。于是内存泄漏了。
  • 解决思路1:block内不使用强指针。
  __weak typeof(self)weakPtr = self;
 self.block = ^{
    weakPtr.j = 1;
    _i = 2;
 };

是不是这样就解决了呢?经过测试发现,依然会有内存泄漏的情况发生。这个问题是比较隐蔽的,发生在self.block=^{_i = 2;}这句话,其等价于:

self.block = ^{
    self->_i = 2;// _i相当于self->_i,self被引用了
};

比较合适的做法有2,一是将_i声明为属性,通过与j一样地去访问;二是通过KVC来设置成员变量的值:

  __weak typeof(self)weakPtr = self;
 self.block = ^{
    weakPtr.j = 1;
    [weakPtr setValue:@2 forKey:@"_i"];
 };

到这里dealloc就完美打印了。

  • 解决思路2:使用完block后,不再引用block(block置为nil)
  self.block = ^{
    self.j = 1;
    _i = 2;
  };
  self.block();
  self.block = nil;

将self.block置为nil,self不再引用block,于是block引用计数降为0,block被释放。所以self失去了block的引用,当dealloc时能正常释放内存。相比思路1,这种方案适合block只需要被临时使用一次的情况。

Q3:使用block回调和代理回调(有的会问委托模式、协议)有什么区别?

两者主要的区别有三点,

  • 一是在语法使用上:block语法简洁,相对使用代理模式,不要求写额外的协议。而且block自动捕获变量,于是在调用的时候,使用代理方法需要比使用block传入更多的参数。总而言之,使用block突出两个字:“便利”。
  • 二是易用性上:使用block留意防止产生循环引用,引起内存泄漏。在这点上使用代理模式,即仅是调用“方法”更加不容易出错。
  • 三是执行方式上:使用block,其捕获的局部变量要拷贝到堆内存中,生命周期结束时释放。而使用代理模式,代理对象回调方法,数据以参数方式传入,操作发生栈上。不考虑构造block和构造对象的耗费,仅仅是挎贝几个变量值和指针地址,性能相差也不会很大。

以上就是block的定义与使用和相关的注意点啦。

希望文章能对你有所帮助~随便一说,一个小“喜欢”我能开心很久的
End. 原创@夏镇冰茶 | 还有其它干货在我的主页哦!

相关文章

  • iOS 一句话搞懂block是什么东东

    片头 对代码段进行打包,在适当的时机执行,这就是block的作用。而且打包成的block视为对象,内存管理机制对b...

  • ios - Block小结

    对于经常使用的Block,你不得不知的东东~~~先上菜,看看最原始的block使用 block的实质是什么?一共有...

  • iOS-2 Block

    block块 系列文章: iOS Block浅浅析 - 简书 iOS Block实现原理 iOS Block __...

  • iOS Block存储域及循环引用

    系列文章:iOS Block概念、语法及基本使用iOS Block实现原理iOS Block __block说明符...

  • iOS Block实现原理

    系列文章:iOS Block概念、语法及基本使用iOS Block __block说明符iOS Block存储域及...

  • 『iOS概念性解说』SEL详解

    其实这篇文章是对上一篇文章的补充(『iOS 概念性解说』一篇文章搞懂 Block 和 Delegate),因为SE...

  • iOS开发---Block详解

    iOS开发---Block详解 Block的基础 什么是Blocks? 用一句话来描述:带有自动变量的匿名函数(是...

  • iOS底层原理之Block

    前言 Block 是 C 语言的扩充功能, Apple 在 iOS4 引入了这个新功能. 一句话形容 Block,...

  • iOS-block相关

    本篇涵盖block的解析、应用等. 1.Block是什么?2.循环引用,看我就对了3.iOS中block技术小结4...

  • iOS Block __block说明符

    系列文章:iOS Block概念、语法及基本使用iOS Block实现原理iOS Block存储域及循环引用 上一...

网友评论

    本文标题:iOS 一句话搞懂block是什么东东

    本文链接:https://www.haomeiwen.com/subject/cugnwttx.html