美文网首页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是什么东东

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