zoukankan      html  css  js  c++  java
  • 浅析Block闭包

    浅析Block闭包

    简单来说,block就是将函数及其上下文封装起来的对象,从功能上可以把它看作是C++中的匿名函数,也可称之为块。

    Block类型写法:

    返回值+(^块名)+(参数)= ^(参数){ 内容 }

    如下所示:

    int (^myBlock)(int a, int b) = ^(int a, int b){
        return a + b;
    };
    

    Block结构

    Block存储区域

    Block本质上也是OC对象,所以每个Block对象也有isa指针指向它们的类对象。根据Block类对象存储的内存空间的不同可分为三种不同的类,分别是:

    位于全局区的Block类:__NSGlobalBlock__

    位于栈区的Block类:__NSStackBlock__

    位于堆区的Block类:__NSMallocBlock__

    • 全局区Block:当Block不捕获外部变量时,会被编译器分配到全局区。因为无外部变量,所以运行时不会在Block内部进行copy或dispose操作,为了削减开销,所以在编译时就确定了大小,即存储在全局区。如下:
    void (^myBlock)(void)=^(void){
        NSLog(@"global");
    };
    NSLog(@"%@",[myBlock class]);
    
    //输出:
    //__NSGlobalBlock__
    
    • 栈区Block:当Block捕获了外部变量后,会被分配到栈区。但是在ARC环境下,系统会自动为生成的栈区Block进行copy操作,所以为了验证是否是在栈区,需要采用MRC环境,在main.m文件的编译选项设置为: -fno-objc-arc后运行如下代码:

      NSString* flag=@"yes";
      void (^myBlock)(void)=^(void){
          NSLog(@"stack:%@",flag);
      };
      NSLog(@"%@",[myBlock class]);
      
      //输出:
      //__NSStackBlock__
      
    • 堆区Block:在MRC模式下,用copy后,会将栈区block复制到堆区。在ARC模式下,系统自动将初始化的Block复制到堆区。

      //MRC环境下:
      NSString* flag=@"yes";
      void (^myBlock)(void)=[^(void){
          NSLog(@"stack:%@",flag);
      } copy];
      NSLog(@"%@",[myBlock class]);
      
      //输出:
      //__NSMallocBlock__
      

    Block内部结构

    官方的Block定义在 Block_private.h中,具体的源码:Block_private.h

    #define BLOCK_DESCRIPTOR_1 1
    struct Block_descriptor_1 {
        uintptr_t reserved;
        uintptr_t size;
    };
    
    #define BLOCK_DESCRIPTOR_2 1
    struct Block_descriptor_2 {
        // requires BLOCK_HAS_COPY_DISPOSE
        void (*copy)(void *dst, const void *src);
        void (*dispose)(const void *);
    };
    
    #define BLOCK_DESCRIPTOR_3 1
    struct Block_descriptor_3 {
        // requires BLOCK_HAS_SIGNATURE
        const char *signature;
        const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
    };
    
    //Block结构
    struct Block_layout {
        void *isa;
        volatile int32_t flags; // contains ref count
        int32_t reserved; 
        void (*invoke)(void *, ...);
        struct Block_descriptor_1 *descriptor;
        // imported variables
    };
    
    • isa指针:指向类对象的指针,即根据不同分区指向: __NSGlobalBlock__ __NSStackBlock__ __NSMallocBlock__,但是这里的底层isa实际上指向的是父类的结构体(C语言)即:_NSConcreteGlobalBlock _NSConcreteStackBlock _NSConcreteMallocBlock结构体,但意义是一样的。
    • flags:类型为枚举,主要用来保存Block的状态信息。
    • reserved:为之后开发准备的保留信息,暂时无用。
    • invoke:函数指针,指向的是实际的功能运行函数。在invoke函数的参数中还包含了Block结构体本身,这么做的目的是在执行时,可以从内存中获取block中捕获的变量。
    • descriptor:主要存储Block的附加信息,其中包括占址大小、签名等。默认指向Block_descriptor_1结构体,当Block被copy到堆上时,则会添加Block_descriptor_2和Block_descriptor_3,新增copydispose方法用来拷贝和销毁捕获的变量。

    Block内部结构图(来自于Effective-OC):

    Block作用

    在日常的开发中,使用Block的主要用处在以下两个方面:

    1. 作为回调的方式之一,对比于代理模式,Block可将将分散的代码块集中写在一处编写。因为有捕获变量的机制,所以可以很轻松的访问上下文,并且Block的代码是内联的,运行效率会更高。

    2. 正是因为有了以上的优势,所以在编写异步代码,作为异步处理回调时,在封装时往往会采用handler块的方式来编写相关代码。

      在编写handler块时有两种策略,一种是在一个方法中提供提供两个Block块分别处理CompletionHandler和errorHandler,另外一种是只提供一个Block块,在Block块中提供error参数,用户自己来对error值进行判断。一般我们更倾向于后者的方式,因为这样处理数据会更加灵活

    两种Handler风格如下:

    Downloader *myDownloader = [[Downloader alloc] initWithURL:url];
    [myDownloader downloadWithCompletionHandler:^(NSData *onlineData){
      //download success
    }
    failureHandler:^(NSError *error){
      //handle error
    }];
    
    Downloader *myDownloader = [[Downloader alloc] initWithURL:url];
    [myDownloader downloadWithBlock:^(NSData *onlineData, NSError * _Nullable error, BOOL succeeded){
      if(succeeded){
        //download success
      }
      else{
        //handle error
      }
    }];
    

    Block内存泄漏

    当几个oc对象互相强引用成环时,就会导致对象永远都不会被释放,当这些对象的数量很大时,就会造成内存泄漏,从而导致整个系统crash的风险。

    举个例子:

    当A类对象强引用了B类对象,B类对象强引用了C类对象,而C对象又强引用了A类对象。假设它们都在一个代码段中。如下图所示:

    因为a、b、c都被该代码段所强引用,所以retainCount初始化都为1,又因为它们互相强引用,所以在连成环的时候retainCount都变为了2。这时候在代码段中,无论是哪一个对象先从代码段中释放,即retainCount--,都仍然还剩1。当整个代码段执行完后,三个类对象a、b、c的retainCount都从2减为了1,在整个系统中,再也没有其他影响因素会让它们的retainCount减少为0,这样就会导致这三个对象在运行中永不释放,从而造成内存泄漏。

    在使用Block时也会很容易造成这个现象,当在网络异步的handler块中,我们通常会将当前ViewController中的某个网络数据属性捕获到handler中,在网络连接成功后将其进行赋值,这样就相当于Block块间接地强引用了当前VC,而通常来说,VC肯定会强引用下载器,而下载器中的Block块一般也会做为其属性进行强引用。如下图所示:

    为了解决强引用环的问题,可以通过将任意一个连接处断开即可。

    • 断开1:基本不可能,在开发中在ViewController或者时ViewModel中都会将下载器作为属性而非临时变量,因为在调取过程中会一般会根据当前下载状态来进行下一步操作。

    • 断开2:

      方法一:不将_downloadHandler作为属性,而是使用临时Block变量,通常这么做的情况是因为下载器类不需要多次使用该block,对于复杂的下载器,这种策略很难得以保证。

      方法二:(推荐)在下载操作结束后调用的方法中令 self.downloadHandler = nil,只要下载请求执行完毕,_downloadHandler属性就不再强引用该block,就打破了强引用环。

    • 断开3:

      方法一:因为Block强引用了VC的data属性,实际上也就强引用了VC(self),所以我们可以通过: __weak typeof(self) weakSelf=self将当前VC,即self弱引用化,生成一个名为weakSelf的当前vc对象,然后在block中使用 weakSelf.data=_data来进行调用。

      方法二:方法一中大部分情况不会出现问题,但是当block块中有延时操作,而对_data的处理也在延时操作当中时,就会出现问题了,例如:

      [self.myDownloader downloadWithBlock:^(NSData *onlineData, NSError * _Nullable error, BOOL succeeded){
        if(succeeded){
          //download success
          dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
              //延迟2s获取data数据
              weakSelf.data = onlineData;
              NSLog(@"%@",weakSelf.data);
          });
        }
        else{
          //handle error
        }
      }];
      
      //假设成功从网络上获取到data
      //打印为空
      

      这时候就会发现,无论是weakSelf还是self的data属性都为空。这就是因为在block执行完后(延时函数还未执行完),weakSelf所在的弱引用表已经被除名了,虽然延时函数还在执行。这时候当2s过后,weakSelf已经变为了nil,对nil发送getter消息也不会报错,所以这里就会出现取值为空的情况。

      为了解决这一问题,只需要在block内再将weakSelf在代码段内部强引用化(该强引用仅限于Block内部)。例如:

      [self.myDownloader downloadWithBlock:^(NSData *onlineData, NSError * _Nullable error, BOOL succeeded){
        if(succeeded){
          //download success
          //将weakSelf强引用化生成该代码段的strong变量
          __strong typeof(self) strongSelf=weakSelf;
          dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
              //延迟2s获取data数据
              //这里使用strongSelf临时变量
              strongSelf.data = onlineData;
              NSLog(@"%@",strongSelf.data);
          });
        }
        else{
          //handle error
        }
      }];
      

      这里的strongSelf属于临时变量,会加到该代码段(Block内)的autoreleasepool当中,当该处代码段结束时会自动释放掉,所以也就不会出现强引用情况。

      方法三:使用临时变量充当当前VC(self),如下:

      __block XXXViewController* vc = self;  //这里self的retainCount会+1
      [self.myDownloader downloadWithBlock:^(NSData *onlineData, NSError * _Nullable error, BOOL succeeded){
        if(succeeded){
          //download success
          vc.data = onlineData;
          //这里需要注意将该临时变量置为nil,即将retainCount重新减为1
          vc=nil;
        }
        else{
          //handle error
        }
      }];
      

      这里需要注意在赋完值后必须将该临时变量重新置为nil,即将retainCount减1,否则仍会出现强引用的问题。

      方法四:将当前self作为block参数传入,例如:

      [self.myDownloader downloadWithBlock:^(NSData *onlineData, NSError * _Nullable error, BOOL succeeded, XXXViewController* vc){
        if(succeeded){
          //download success
          vc.data = onlineData;
        }
        else{
          //handle error
        }
      }];
      

      这种情况一般很少出现,因为下载器通常作为第三方提供的API,通常参数不会有当前控制类。所以这种情况只能用在自定义block当中使用。

    总结

    • 在ARC环境下开发,我们用到的一般都是堆Block或全局Block,当捕获外界变量时为堆Block,否则为全局Block
    • Block主要用于代码回调以及异步操作以降低代码分散程度。
    • Block在捕获变量时很容易造成循环引用,导致内存泄漏。在不确定调用第三方API是否在最后将block属性置为空,或者没有使用属性而是临时变量作为调用block,所以在不破环封装性的原则下,将其视为未处理,然后在自己的代码中使用waekSelf和strongSelf方式来进行当前self的属性进行操作,这样就实现了在环节[3]中打破强引用环。
  • 相关阅读:
    nginx日志格式
    nginx默认虚拟主机
    php各种编译错误汇总
    php.ini中最好禁止的一些函数
    php常用的操作
    快速扫描某个服务器上所有开放端口
    LOJ6303:水题——题解
    BZOJ5323 & 洛谷4562:[JXOI2018]游戏——题解
    BZOJ5333:[SDOI2018]荣誉称号——题解
    LOJ2587:[APIO2018]铁人两项——题解
  • 原文地址:https://www.cnblogs.com/Solist/p/12923264.html
Copyright © 2011-2022 走看看