iOS Block用法与实现原理

2020-03-18  本文已影响0人  KOLee

一、何为Block?

二、Block语法

  1. Block表达式语法:
^ int (int count) {
      return count + 1;
  };

其中返回值类型 参数都可省略

   ^ {
       NSLog(@"No Parameter");
   };
  1. Block类型变量

返回值类型 (^变量名)(参数列表) = Block表达式

例如,如下声明了一个变量名为blk的Block:

    int (^blk)(int) = ^(int count) {
        return count + 1;
    };
- (void)func:(int (^)(int))blk {
    NSLog(@"Param:%@", blk);
}

借助typedef可简写:

typedef int (^blk_k)(int);

- (void)func:(blk_k)blk {
    NSLog(@"Param:%@", blk);
}

三、截获自动变量值

Block表达式可截获所使用的自动变量的值。
截获:保存自动变量的瞬间值。
因为是“瞬间值”,所以声明Block之后,即便在Block外修改自动变量的值,也不会对Block内截获的自动变量值产生影响。

  int i = 10;
    void (^blk)(void) = ^{
        NSLog(@"In block, i = %d", i);
    };
    i = 20;//Block外修改变量i,也不影响Block内的自动变量
    blk();//i修改为20后才执行,打印: In block, i = 10
    NSLog(@"i = %d", i);//打印:i = 20

四、__block说明符号

自动变量值为一个变量情况
  __block int i = 10;//i为__block变量,可在block中重新赋值
    void (^blk)(void) = ^{
        NSLog(@"In block, i = %d", i);
    };
    i = 20;
    blk();//打印: In block, i = 20
    NSLog(@"i = %d", i);//打印:i = 20
自动变量值为一个对象情况
    NSMutableArray *array = [[NSMutableArray alloc] initWithObjects:@"1", @"2",nil ];
    NSLog(@"Array Count:%ld", array.count);//打印Array Count:2
    void (^blk)(void) = ^{
        [array removeObjectAtIndex:0];//Ok
        //array = [NSNSMutableArray new];//没有__block修饰,编译失败!
    };
    blk();
    NSLog(@"Array Count:%ld", array.count);//打印Array Count:1

Block实现原理

clang -rewrite-objc 源码文件名

注:如果使用该命令报错:’UIKit/UIKit.h’ file not found,可参考《Objective-C编译成C++代码报错》解决。

    int main() {
        int count = 10;
        void (^ blk)() = ^(){
            NSLog(@"In Block:%d", count);
        };
        blk();
    }

如上所示的最简单的Block使用代码,经clang转换后,可得到以下几个部分(有代码删减和注释添加):

static void __main_block_func_0(
    struct __main_block_impl_0 *__cself) {
    int count = __cself->count; // bound by copy
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_d2f8d2_mi_0, 
    count);

这是一个函数的实现,对应Block中{}内的内容,这些内容被当做了C语言函数来处理,函数参数中的__cself相当于Objective-C中的self。

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc; //描述Block大小、版本等信息
      int count;
      //构造函数函数
      __main_block_impl_0(void *fp,
              struct __main_block_desc_0 *desc,
              int _count,
              int flags=0) : count(_count) {
        impl.isa = &_NSConcreteStackBlock; //在函数栈上声明,则为_NSConcreteStackBlock
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };  

__main_block_impl_0即为main()函数栈上的Block结构体,其中的__block_impl结构体声明如下:

    struct __block_impl {
      void *isa;//指明对象的Class
      int Flags;
      int Reserved;
      void *FuncPtr;
    };

去除掉复杂的类型转化,可简写为:

    int main() {
        int count = 10;
        sturct __main_block_impl_0 *blk = &__main_block_impl_0(__main_block_func_0,         //函数指针
                                                               &__main_block_desc_0_DATA)); //Block大小、版本等信息
    
        (*blk->FuncPtr)(blk);   //调用FuncPtr指向的函数,并将blk自己作为参数传入
    }

由此,可以看出,Block也是Objective-C中的对象
Block有三种类(即__block_impl的isa指针指向的值,isa说明参考《Objective-C isa 指针 与 runtime 机制》),根据Block对象创建时所处数据区不同而进行区别:

一、如何截获自动变量

二、Block的存储域

    void (^blk)(void) = ^{
        NSLog(@"Global Block");
    };
    int main() {
        blk();
        NSLog(@"%@",[blk class]);//打印:__NSGlobalBlock__
    }
    int main() {
        void (^blk)(void) = ^{//没有截获自动变量的Block
            NSLog(@"Stack Block");
        };
        blk();
        NSLog(@"%@",[blk class]);//打印:__NSGlobalBlock__
        
        int i = 1;
        void (^captureBlk)(void) = ^{//截获自动变量i的Block
            NSLog(@"Capture:%d", i);
        };
        captureBlk();
        NSLog(@"%@",[captureBlk class]);//打印:__NSMallocBlock__
    }

可以看到截获了自动变量的Block打印的类是NSGlobalBlock,表示存储在全局数据区。
但为什么捕获自动变量的Block打印的类却是设置在堆上的NSMallocBlock,而非栈上的NSStackBlock?这个问题稍后解释。

三、Block复制

impl.isa = &_NSConcreteMallocBlock;

在ARC有效时,大多数情况下编译器会进行判断,自动生成将Block从栈上复制到堆上的代码,以下几种情况栈上的Block会自动复制到堆上:

调用Block的copy方法
将Block作为函数返回值时
将Block赋值给__strong修改的变量时
向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时
    int count = 0;
    blk_t blk = ^(){
        NSLog(@"In Stack:%d", count);
    };
    
    NSLog(@"blk's Class:%@", [blk class]);//打印:blk's Class:__NSMallocBlock__
    NSLog(@"Global Block:%@", [^{NSLog(@"Global Block");} class]);//打印:Global Block:__NSGlobalBlock__
    NSLog(@"Copy Block:%@", [[^{NSLog(@"Copy Block:%d",count);} copy] class]);//打印:Copy Block:__NSMallocBlock__
    NSLog(@"Stack Block:%@", [^{NSLog(@"Stack Block:%d",count);} class]);//打印:Stack Block:__NSStackBlock__

关于ARC下和MRC下Block自动copy的区别,查看《Block 小测验》里几道题目就能区分了。
另外,原书存在ARC和MRC混合讲解、区分不明的情况,比如书中几个使用到栈上对象导致Crash的例子是MRC条件下才会发生的,但书中没做特殊说明。

四、使用__block发生了什么

int main() {
    __block int count = 10;
    void (^ blk)() = ^(){
        count = 20;
        NSLog(@"In Block:%d", count);//打印:In Block:20
    };
    count ++;
    NSLog(@"Out Block:%d", count);//打印:Out Block:11
    blk();

将上面的代码段clang,发现Block的结构体__main_block_impl_0结构如下所示:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_count_0 *count; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_count_0 *_count, int flags=0) : count(_count->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }

最大的变化就是count变量不再是int类型了,count变成了一个指向__Block_byref_count_0结构体的指针,__Block_byref_count_0结构如下:

struct __Block_byref_count_0 {
  void *__isa;
__Block_byref_count_0 *__forwarding;
 int __flags;
 int __size;
 int count;
};

它保存了int count变量,还有一个指向__Block_byref_count_0实例的指针__forwarding,通过下面两段代码__forwarding指针的用法可以知道,该指针其实指向的是对象自身:
//Block的执行函数

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_count_0 *count = __cself->count; // bound by ref
 
        (count->__forwarding->count) = 20;//对应count = 20;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_fafeeb_mi_0, 
        (count->__forwarding->count));
    }

//main函数
int main() {
    __attribute__((__blocks__(byref))) __Block_byref_count_0 count = {(void*)0,
    (__Block_byref_count_0 *)&count, 
    0, 
    sizeof(__Block_byref_count_0), 
    10};
    
    void (* blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, 
    &__main_block_desc_0_DATA, 
    (__Block_byref_count_0 *)&count, 
    570425344));
    
    (count.__forwarding->count) ++;//对应count ++;
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_fafeeb_mi_1, 
    (count.__forwarding->count));
    
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}

为什么要通过__forwarding指针完成对count变量的读写修改?
为了保证无论是在栈上还是在堆上,都能通过都__forwarding指针找到在堆上创建的count这个__main_block_func_0结构体,以完成对count->count(第一个count是__main_block_func_0对象,第二个count是int类型变量)的访问和修改。

五、Block的循环引用

有三种常用形式:

  1. 使用__weak ClassName
    __block XXViewController* weakSelf = self;
    self.blk = ^{
        NSLog(@"In Block : %@",weakSelf);
    };

2.使用__weak typeof(self)

    __weak typeof(self) weakSelf = self;
    self.blk = ^{
        NSLog(@"In Block : %@",weakSelf);
    };

3.Reactive Cocoa中的@weakify和@strongify

    @weakify(self);
    self.blk = ^{
        @strongify(self);
        NSLog(@"In Block : %@",self);
    };

其原理参考《@weakify, @strongify》,自己简便实现参考《@weak - @strong 宏的实现》

方法二:对Block内要使用的对象A使用__block进行修饰,并在代码块内,使用完__block变量后将其设为nil,并且该block必须至少执行一次。

   __block XXController *blkSelf = self;
        self.blk = ^{
            NSLog(@"In Block : %@",blkSelf);
            blkSelf = nil;//不能省略
        };
   self.blk();//该block必须执行一次,否则还是内存泄露

在block代码块内,使用完使用完__block变量后将其设为nil,并且该block必须至少执行一次后,不存在内存泄露,因为此时:

XXController对象持有Block对象blk
blk对象持有__block变量blkSelf(类型为编译器创建的结构体)
__block变量blkSelf在执行blk()之后被设置为nil(__block变量结构体的__forwarding指针指向了nil),不再持有XXController对象,打破循环

第二种使用__block打破循环的方法,优点是:

可通过__block变量动态控制持有XXController对象的时间,运行时决定是否将nil或其他变量赋值给__block变量
不能使用__weak的系统中,使用__unsafe_unretained来替代__weak打破循环可能有野指针问题,使用__block则可避免该问题

其缺点也明显:

必须手动保证__block变量最后设置为nil
block必须执行一次,否则__block不为nil循环应用仍存在

因此,还是避免使用第二种不常用方式,直接使用__weak打破Block循环引用。
方法三:将在Block内要使用到的对象(一般为self对象),以Block参数的形式传入,Block就不会捕获该对象,而将其作为参数使用,其生命周期系统的栈自动管理,不造成内存泄露。
即原来使用__weak的写法:

  __weak typeof(self) weakSelf = self;
  self.blk = ^{
      __strong typeof(self) strongSelf = weakSelf;
      NSLog(@"Use Property:%@", strongSelf.name);
      //……
  };
  self.blk();

改为Block传参写法后:

    self.blk = ^(UIViewController *vc) {
        NSLog(@"Use Property:%@", vc.name);
    };
    self.blk(self);

优点:

该种用法的详细思路,和clang后的数据结构,可参考《Heap-Stack Dance》

上一篇下一篇

猜你喜欢

热点阅读