【iOS老生常谈】-Blocks知多少

一、Block的概念

1、是什么Block

Block是带有局部变量的匿名函数。

iOS4引入,是对c语言的扩充功能,先来理解一下局部变量匿名函数的含义

1.1.局部变量

那带有局部变量又是什么意思? 先理解一下c语言的都有哪些变量

  • 自动变量(局部变量)
  • 函数的参数
  • 静态变量(静态局部变量)
  • 静态全局变量
  • 全局变量

每个变量的作用域不同。

c 复制代码
int aa = 2; //全局变量
static int bb = 3; //静态全局变量
int main(int argc, const char * argv[]) { //argc 函数的参数
   int a = 1;  //auto修饰局部变量
   static int b = 1; //静态局部变量
   return 0;
}
变量 作用域 存储位置 生命周期 关键特性
全局 aa 整个程序(文件间) 全局数据区.data 程序全程 外部链接,多文件可访问
静态全局 bb 仅当前源文件 全局数据区.data 程序全程 内部链接,仅本文件可见
局部 a main 函数内 栈区 main 执行期间 自动销毁,每次调用重新初始化
静态局部 b main 函数内 全局数据区.data 程序全程 仅初始化一次,保留值
argc/argv main 函数内 栈区 main 执行期间 函数参数,栈上分配

1.2 匿名函数

所谓匿名函数,就是不带有函数名称的函数。

而在c语言中是不允许函数不带有名称的。

先理解一下c语言的函数定义:

c 复制代码
int func(int count); //声明了名为func的函数,参数为int类型的count,返回值为int类型

调用func

c 复制代码
int result = func(3);

使用函数指针funcPtr直接调用函数func,这种也是需要函数名才能通过函数指针调用。

c 复制代码
int (*funPtr)(int) = &fun;
int result = (*funcPtr)(3);

二、Block的语法

2.1.Block的定义

官方文档:Blocks Programming Topics

c 复制代码
//官方实例
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    return num * multiplier;
};

其中myBlock是声明的快对象,返回类型为int,myBlock 快对象有一个int类型的参数,myBlock的主体部分为 return num * multiplier;

上面表达式的特点:

  • 没有函数名 (匿名函数)
  • 带有^ ,插入记号,便于查找。

Block的表达式: ^返回值类型(参数列表){表达式}

c 复制代码
^int (int count){ return count +1 };

Block可以省略如返回值,参数列表,如果用不到的话

  • 省略返回值类型

    c 复制代码
    //省略返回值类型:^(参数列表){表达式};
    ^(int count){return count+1};
  • 省参数列表

    c 复制代码
    ^int (void){ return  1};
    ^int {return  1};
  • 省略返回值类型、参数列表:

    c 复制代码
    ^{  }; //最简洁的block

2.2.Block类型的变量

通过Block语法将Block赋值为Block类型的变量

c 复制代码
int (^blk) (int) = ^(int count) { return  count+1}; 

此时的表达式和c语言的指针函数表达式对比

c 复制代码
int (*funPtr)(int) = &fun; //指针函数

block的变量声明就是把声明指针函数类型的* 变为^

  • 在函数参数中使用 Block类型的变量

    c 复制代码
    //作为函数参数的block变量
    void func(int (^blk)(int)){
        
    }
  • 作为函数的返回值

    c 复制代码
    int (^blk1(void))(int){
        return ^(int count){
          return count+1 ;
        };
    }
    //作为函数返回值时,需要注意:
    // 1. 调用 blk1 函数,并且是无参函数,它返回一个Block
    int (^myBlock)(int) = blk1();
    
    // 2. 调用(执行)这个返回的Block,并传入整数参数 5
    int result = myBlock(5);
    NSLog(@"结果是: %d", result); // 输出:结果是: 6

    上述当block作为参数或返回值时,可以通过typedef声明类型,来简化,如下

    c 复制代码
    typedef int ^(Blk_t)(int); //声明一个Blk_t类型的block
    Blk_t blck = ^{
    }
    
    //作为函数参数和返回值就可以简化为
    void func(Blk_t blck){
    
    }
    
    //作为函数返回值时可以简化为
    Blk_t func1(){
    
    }

2.3. 截获自动变量值

c 复制代码
void test1(void){
   //默认为auto修饰局部变量
   int a = 1;
   void (^bck1)(void)=^(){
       NSLog(@"访问量局部变量a:%d",a);
   };
    a = 2;
    NSLog(@"访问量局部变量a1:%d",a);
   bck1();
}
//访问量局部变量a1:2
//访问量局部变量a:1

block访问局部变量 auto修饰,此时block截获了变量a的当前的瞬间值,底层为值传递,所以block内部不能直接赋值修改,block外侧修改了局部变量,block内部变量值不会修改。

如果在block内部尝试修改局部变量会报错

报错信息:变量不可赋值(缺少__block类型说明符)

Variable is not assignable (missing __block type specifier)

2.4. __block 说明符

__block说明符更准确的描述方式为"__block存储域类说明符" __block storage-class-specifier,c语言的存储域类说明符右以下几种:

  • typedef
  • extern
  • static
  • auto
  • register

__block类似于static、auto、register等说明符。用于指定将变量值设置到哪个存储域中,例如auto作为自动变量存储在栈中,static表示作为静态变量存储在数据区中。

如果非要在block内部修改局部变量,就需要再局部变量前通过__block修饰

c 复制代码
void test1(void){
   //默认为auto修饰局部变量
   __block int a = 1;
   void (^bck1)(void)=^(){
       NSLog(@"访问量局部变量修改前a:%d",a);
       a = 3;
       NSLog(@"访问量局部变量修改后a:%d",a);
   };
    a = 2;
    NSLog(@"访问量局部变量a1:%d",a);
   bck1();
}
//访问量局部变量a1:2
//访问量局部变量修改前a:2
//访问量局部变量修改后a:3

2.5.截获的自动变量

block截获自动变量会报错,那截获OC对象呢,比如NSMutableArray还会报错吗?

c 复制代码
//截获可变数组,
void test11(void){
   //默认为auto修饰局部变量
    NSMutableArray *array = [NSMutableArray array];
   void (^bck1)(void)=^(){
       [array addObject:[[NSObject alloc]init]];
       NSLog(@"访问量局部变量array.count:%lu",(unsigned long)array.count);
   };
    NSLog(@"访问量局部变量array1.count:%lu",(unsigned long)array.count);
   bck1();
}
//访问量局部变量array1.count:0
//访问量局部变量array.count:1

此时block截获的变量值是NSMutableArray类对象,及NSMutableArray类对象的结构体实例指针,因此,对变量值进行addObject操作,是没有影响的,如果在block内部,要对array进行赋值时是不行的。依然需要用__block修饰

使用c语言的数组时必须小心使用其指针,下面这个例子,看似并没有像截获的自动变量text赋值,但还是编译不通过,报错信息:Cannot refer to declaration with an array type inside block(不能引用块内数组类型的声明)

需要把text声明为指针来解决

c 复制代码
void test111(void){
   //使用c语言的数组时必须小心使用其指针,
   const char *text = "hello";
   void (^bck1)(void)=^(){
       //截获自动变量的方法并没有实现对c语言数组的截获,此时需要用指针来解决该问题
       NSLog(@"截获的局部变量:%c",text[2]);
   };
   bck1();
}

三、Block的底层实现

3.1、Block的实质

一开始讲了Blocks是带有局部变量匿名函数,但是Block实质究竟是什么,类型、 变量、还是其他什么?

先说结论:Block其实是一个对象。因为它的结构体里有isa指针

为了探究到底,我们需要通过clang 把oc转为c++源码来探个究竟

进入main.m的文件夹下,执行:clang -rewrite-objc main.m

执行后,会生成一个main.cpp文件

  • 转换前的oc代码

    c++ 复制代码
    //为了简化生成的c++代码,把原来oc代码的main方法传参(int argc, const char * argv[])省略。以及
    int main(void) {
            void (^donyBck)(void)=^(){
                //这里的日志用英文,转c++后,不会转义,方便查看,中文的话,会转义,不方便查看
                NSLog(@"The block prints logs internally");
            };
            //donyBck的调用
            donyBck();
        return 0;
    }
  • 转换后的C++源码

    c++ 复制代码
    //包含block实际函数指针的结构体
    struct __block_impl {
      void *isa; //有isa
      int Flags;
      int Reserved;  //今后升级所需区域大小
      void *FuncPtr; //函数指针
    };
    
    //
    static __NSConstantStringImpl __NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_5a37bd_mi_0 __attribute__ ((section ("__DATA, __cfstring"))) = {__CFConstantStringClassReference,0x000007c8,"The block prints logs internally",32};
    
    //block结构体
    struct __main_block_impl_0 {
      struct __block_impl impl; //Block的实际指针,
      struct __main_block_desc_0* Desc;
      //block构造函数
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock; //isa指针 默认block创建在栈上
        impl.Flags = flags; //block标志位
        impl.FuncPtr = fp; //Block执行的函数指针
        Desc = desc; //Block描述信息,Block大小等元信息
      }
    };
    
    //Block内部函数调用
    /* 
    	void (^donyBck)(void)=^(){
                //这里的日志用英文,转c++后,不会转义,方便查看,中文的话,会转义,不方便查看
                NSLog(@"The block prints logs internally");
            };
    */
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_5a37bd_mi_0);
            }
    
    //Block底层编译后自动生成的,Block描述信息结构体变量,静态匿名结构体+变量
    static struct __main_block_desc_0 {
      size_t reserved; //成员1 :保留字段
      size_t Block_size; //成员2:Block实例的内存大小
    } __main_block_desc_0_DATA = { //初始化变量
      0, //给Reserved赋值
      sizeof(struct __main_block_impl_0) //给Block------size赋值
    };
    
    
    //main函数
    int main(void) {
      	//1.构造block对象,并将其强制转换为无参无返回值的函数指针。
        void (*donyBck)(void)=((void (*)())&__main_block_impl_0(
          (void *)__main_block_func_0, //block要执行的代码逻辑(函数指针)->FuncPtr
          &__main_block_desc_0_DATA //block的描述信息(版本,大小等)
        ));
      
      	//2.调用block的核心逻辑(donyBck的调用)
      	// ((void (*))(donyBck):将函数指针转回Block结构体指针
      	//->FuncPtr :取出Block的执行函数指针
      	//最后调用该函数,并传入Block自身作为参数,(block的隐式self)
       	((void (*)(__block_impl *))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck);
        return 0;
    }

3.2 、源码分析

3.2.1 Block结构体-__main_block_impl_0

先看看 __main_block_impl_0 结构体

c++ 复制代码
//block结构体
struct __main_block_impl_0 {
  struct __block_impl impl; //Block的实际指针,
  struct __main_block_desc_0* Desc;
  //block构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock; //isa指针 默认block创建在栈上
    impl.Flags = flags; //block标志位
    impl.FuncPtr = fp; //Block执行的函数指针
    Desc = desc; //Block描述信息,Block大小等元信息
  }
};

从源码里可以看出,__main_block_impl_0结构体,包含三部分

  • 成员变量 impl,是结构体__block_impl

  • 成员变量 Desc指针,是结构体 __main_block_desc_0

  • __main_block_impl_0 构造函数

分别分析一下这三部分

3.2.1.1 struct __block_impl impl
c++ 复制代码
//包含block实际函数指针的结构体
struct __block_impl {
  void *isa; //有isa
  int Flags;  //标志位
  int Reserved;  //今后升级所需区域大小
  void *FuncPtr; //函数指针
};
  • isa指针,保存Block结构体的实例指针
  • Flags 标志位
  • Reserved 后续版本升级所需区域大小
  • FuncPtr 函数指针,指向了Block的主体部分,也及时对应oc代码里的 { NSLog(@"The block prints logs internally");};

3.1.1里的__main_block_impl_0 里的impl 是__block_impl结构体,而__block_impl 包含了Block实际函数指针 FuncPtr

总结:impl 主要就是包含了Block的函数指针 FuncPtr

3.2.1.2 struct __main_block_desc_0* Desc
c++ 复制代码
//Block底层编译后自动生成的,Block描述信息结构体变量,静态匿名结构体+变量
static struct __main_block_desc_0 {
  size_t reserved; //成员1 :保留字段
  size_t Block_size; //成员2:Block实例的内存大小
} __main_block_desc_0_DATA = { //初始化变量
  0, //给Reserved赋值
  sizeof(struct __main_block_impl_0) //给Block------size赋值
};
  • reserved 版本升级后所需的区域大小
  • Block_size Block的大小。

总结:__main_block_desc_0是block的描述信息,也就是附加信息

3.2.1.3 __main_block_impl_0构造函数
c++ 复制代码
 //block构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock; //isa指针 默认block创建在栈上
    impl.Flags = flags; //block标志位
    impl.FuncPtr = fp; //Block执行的函数指针
    Desc = desc; //Block描述信息,Block大小等元信息
  }

传三个参数:

  • void *fp :主要赋值给impl的FuncPtr ,Block执行的函数指针
  • struct __main_block_desc_0 *desc, Block的描述信息
  • int flags=0 ,默认值为0,标志位。

总结:构造函数主要用来初始化__block_impl的成员变量,以及把描述信息赋值给Desc

3.2.1.4 在main方法里的过程

关于:__main_block_impl_0 基本概念就了解完了,那在main方法里,__main_block_impl_0 又是怎么赋值的呢

c++ 复制代码
//原函数
 void (^donyBck)(void)=^(){
            //这里的日志用英文,转c++后,不会转义,方便查看,中文的话,会转义,不方便查看
            NSLog(@"The block prints logs internally");
 };

//转换c++后
//1.构造block对象,并将其强制转换为无参无返回值的函数指针。
    void (*donyBck)(void)=((void (*)())&__main_block_impl_0(
      (void *)__main_block_func_0, //block要执行的代码逻辑(函数指针)->FuncPtr
      &__main_block_desc_0_DATA //block的描述信息(版本,大小等)
    ));

可以看出通过 __main_block_impl_0构造函数,生成 __main_block_impl_0结构体(Block结构体)的实例指针,并赋值给donyBck

然后对 __main_block_impl_0构造函数传了两个参数

  • __main_block_func_0

    c++ 复制代码
    //Block内部函数调用
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    	//对应的就是原oc  NSLog(@"The block prints logs internally");
       NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_5a37bd_mi_0);
    }

    可以看出 对应的是oc block的主体部分也就是 {NSLog(@"The block prints logs internally"); };

    这里的参数 __cself就是指向Block的值的指针变量,相当于oc的self;

    🎯这里画个重点:从这里可以看出,block内部,把block的^{}执行函数{}在Block定义完后,当成一个参数类型为__main_block_func_0传入到 __main_block_impl_0->__block_impl-> FuncPtr 里了。

  • __main_block_desc_0_DATA

    __main_block_desc_0_DATA 是包含了Block的描述信息,

    c++ 复制代码
    static struct __main_block_desc_0{
      size_t reserved; //成员1 :保留字段
      size_t Block_size; //成员2:Block实例的内存大小
    } __main_block_desc_0_DATA = { //初始化变量
      0, //给Reserved赋值
      sizeof(struct __main_block_impl_0) //给Block------size赋值
    };

3.2.2 总结

至此Block的内部原理就浮出水面了。

Block内部是由 __main_block_impl_0结构体组成的,内部isa指针,指向所属类的结构体的实例指针,_NSConcreteStackBlock相当于Block的结构体实例,对象impl.isa = &_NSConcreteStackBlock ,将Block结构体的指针赋值给impl的成员变量isa ,相当于Block结构体成员变量保存了Block结构体的指针,和OC的对象处理方式是一致的。

所以Block的实质就是对象,和NSObject一样,都是对象。

3.3 Block截获局部变量和特殊区域变量

3.3.1 Block截获局部变量的实质

在2.3里我们知道了Block可以截获局部变量,那背后Block是怎么截获的,为什么不能在block内部直接修改截获的局部变量呢?

先说结论:Block截获的局部变量是值传递的方式传入Block结构体中,并保存为Block的成员变量。因此当外部局部变量值发生修改后,Block内部对应的成员变量的值并没有发生改变。

为了探究到底,我们需要通过clang 把oc转为c++源码来探个究竟

进入main.m的文件夹下,执行:clang -rewrite-objc main.m

  • oc代码

    c 复制代码
    int main(void) {
        int a = 2;
        void (^donyBck)(void)=^(){
            NSLog(@"The block Capture local variables:%d",a);
        };
        
        a = 4;
        //donyBck的调用
        donyBck();
        NSLog(@"The block prints local variables:%d",a);
        return 0;
    }
    //The block Capture local variables:2
    //The block prints local variables:4
  • 转c++源代码

    c++ 复制代码
    //block结构体
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int a; //截获的局部变量,在block内部变成了成员变量
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    //block执行函数
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int a = __cself->a; // bound by copy
    
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_268414_mi_1,a);
        }
    
    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(void) {
        int a = 2;
    
        //block定义
        void (*donyBck)(void)=((void (*)())&__main_block_impl_0(
        (void *)__main_block_func_0,
         &__main_block_desc_0_DATA, 
         a));
    
        a = 4;
    
        //block 执行方法
        ((void (*)(__block_impl *))((__block_impl *)donyBck)->FuncPtr)(
        (__block_impl *)donyBck
        );
      //打印
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_268414_mi_2,a);
        return 0;
    }
    • 从源码可以看出,结构体__main_block_impl_0 多了一个成员变量a,这个变量就是Block捕获的局部变量
    • 方法__main_block_func_0 看出 int a = __cself->a在block内部访问这个变量a时,通过值传递的方式,而不是指针传递,这也就说明了a是block的内部变量,外部修改a,Block内部捕获的a是不会发生变化的。

3.3.2 使用__block说明符更改局部变量

那使用__block来修饰局部变量后,就能让Block内部来修改这个变量,那背后__block 又做了什么呢?

先说结论:通过 __block修饰后,使这个局部变量在block内部通过指针传递,所以修饰后的局部变量,在block内部可以修改了

为了探究到底,我们需要通过clang 把oc转为c++源码来探个究竟

进入main.m的文件夹下,执行:clang -rewrite-objc main.m

  • oc代码

    c 复制代码
    int main(void) {
        __block int a = 2;
        void (^donyBck)(void)=^(){
            a = 3;
            NSLog(@"The block Capture local variables:%d",a);
        };
        
        a = 4;
        //donyBck的调用
        donyBck();
        NSLog(@"The block prints local variables:%d",a);
        return 0;
    }
  • c++代码

    c++ 复制代码
    //__block修饰的
    struct __Block_byref_a_0 {
      void *__isa; //isa指针
    __Block_byref_a_0 *__forwarding; //传入变量的地址
     int __flags; //标志位
     int __size; //结构体大小
     int a; //存放变量a的实际的值,相当与原局部变量的成员变量。
    };
    
    //block内部
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_a_0 *a; // by ref //加入__Block修饰后,这里的a是__Block_byref_a_0类型了
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
        impl.isa = &_NSConcreteStackBlock; //栈block
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    //block执行函数
    static void __main_block_func_0(struct  __main_block_impl_0 *__cself) {
      __Block_byref_a_0 *a = __cself->a; // bound by ref 
            (a->__forwarding->a) = 3;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_3b64fc_mi_3,(a->__forwarding->a));
        }
    
    //新增了 
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
        _Block_object_assign(
        (void*)&dst->a, 
        (void*)src->a,
         8/*BLOCK_FIELD_IS_BYREF*/);
    }
    
    //新增了
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
      _Block_object_dispose((void*)src->a, 
                            8/*BLOCK_FIELD_IS_BYREF*/);
    }
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
      void (*dispose)(struct __main_block_impl_0*);
    } __main_block_desc_0_DATA = { 
        0, 
        sizeof(struct __main_block_impl_0),
         __main_block_copy_0,
          __main_block_dispose_0
    };
    
    //main函数
    int main(void) {
      
      	//__block修饰的局部变量a
        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {
          (void*)0,
          (__Block_byref_a_0 *)&a, 
          0, 
          sizeof(__Block_byref_a_0), 
          2};
        
        void (*donyBck)(void)=((void (*)())&__main_block_impl_0(
          (void *)__main_block_func_0, 
          &__main_block_desc_0_DATA, 
          (__Block_byref_a_0 *)&a, 
          570425344));
    
      	//修改局部变量a的值
      	(a.__forwarding->a) = 4;
      
      	//block调用
        ((void (*)(__block_impl *))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck);
      
      	//block打印
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_3b64fc_mi_4,(a.__forwarding->a));
        return 0;
    }

    可以看出局部变量a加上 __block后,c++代码里新增了 __Block_byref_a_0__main_block_copy_0__main_block_dispose_0

    • 从结构体 __main_block_impl_0 可以看出原来oc的 被__block修饰后的局部变量a,在结构体__main_block_impl_0内部变成了 __Block_byref_a_0 *a,也就是说Block内部的结构体__main_block_impl_0实例持有指向__block变量的__Block_byref_a_0结构体实例指针。
    • __main_block_copy_0__main_block_dispose_0留在后面在说。

这里有点绕,我们用白话文理解一下

可以把整个过程想象成寄送一个易碎品(变量a)

角色 对应代码 比喻说明
易碎品本身 局部变量 int a = 2 比如一个玻璃杯。
加固包装盒 __Block_byref_a_0结构体 一个专门用来固定玻璃杯的防震包装盒。
快递单/指针 __Block_byref_a_0 *a(Block结构体里的成员) 一张写着包装盒地址的快递单。
整个Block __main_block_impl_0结构体实例 快递仓库。
🎯 整个过程是这样的:
  1. 打包: 当你用 __block修饰变量 a时,编译器会自动创建一个"包装盒"(__Block_byref_a_0结构体),然后把你的变量 a(玻璃杯)放进这个盒子里。
  2. 填写快递单: Block(快递仓库)想要操作这个玻璃杯,但它并不直接把玻璃杯拿进来(那样就成了它的私有物品,无法和外界共享了)。Instead,它只拿了一张写着"包装盒地址"的快递单 (即指针 __Block_byref_a_0 *a)。
  3. 共享修改: 当Block内部或外部代码需要修改 a的值时,它们会凭着这张"快递单"找到同一个"包装盒",然后打开盒子修改里面的玻璃杯。因为大家访问的是同一个盒子里的东西,所以任何一方的修改,另一方都能立刻看到

白话文总结:被__block修饰后的变量a,在Block内并没有直接把变量装在自己口袋里,而是记下了变量所在包装盒的地址。通过这个共享的地址,Block和外部代码就能共同修改同一个变量了。

在看看__Block_byref_a_0 结构体

c++ 复制代码
//__block修饰的
struct __Block_byref_a_0 {
  void *__isa; //isa指针
__Block_byref_a_0 *__forwarding; //传入变量的地址
 int __flags; //标志位
 int __size; //结构体大小
 int a; //存放变量a的实际的值,相当与原局部变量的成员变量。
};

在看一下在main()中原oc代码为

c 复制代码
     __block int a = 2;

转为c++为

c 复制代码
    //__block修饰的局部变量a
    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {
      (void*)0,
      (__Block_byref_a_0 *)&a, 
      0, 
      sizeof(__Block_byref_a_0), 
      2};

从赋值里可以看出在main()__block修饰后的变量a,底层赋值给__Block_byref_a_0结构体时,传入的值如下

  • isa传入空

  • __forwarding 传入了局部变量a的本身地址

  • __flags :分配了0

  • sizeof:结构体的大小

  • a值赋值为2.

总结一下:到此知道了__forwarding就是局部变量a的本身地址,可以通过 __forwarding指针来访问局部变量。同时也能对其修改了。

c 复制代码
    //block执行函数
    static void __main_block_func_0(struct  __main_block_impl_0 *__cself) {
      __Block_byref_a_0 *a = __cself->a; // bound by ref 
            (a->__forwarding->a) = 3;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_3b64fc_mi_3,(a->__forwarding->a));
        }

可以看出 (a->__forwarding->a) = 3;,是通过指针取值的方式来改变局部变量的值,

🎯总结:

从而解释了通过__block修饰后的变量,在Block内部通过指针传递的方式修改局部变量

另外__block__Block_byref_a_0 结构体并不在Block的 __main_block_impl_0结构体中,这样做是为了多个Block同时使用__block变量。

  • 继续看一下OC代码

    c 复制代码
            int main(void) {
                    //__block被多个Block使用
                __block int aa = 2;
    
                void (^donyBck)(void)=^(){
                    aa = 3;
                    NSLog(@"The block Capture local variables:%d",aa);
                };
    
                void (^donyBck1)(void)=^(){
                    aa = 4;
                    NSLog(@"The block Capture local variables1:%d",aa);
                };
            }
  • 转换为c++

    c++ 复制代码
      //__block修饰后
      __Block_byref_aa_0 aa = {
         0,
         &aa, 
         0, 
         sizeof(__Block_byref_aa_0), 2};
         //donyBck
         donyBck)=&__main_block_impl_0(
            __main_block_func_0, 
            &__main_block_desc_0_DATA, 
            (__Block_byref_aa_0 *)&aa, 
            570425344));
            //donyBck1
          donyBck1)=&__main_block_impl_1(
            __main_block_func_1, 
            &__main_block_desc_1_DATA, 
            (__Block_byref_aa_0 *)&aa, 
            570425344));

可以看出donyBckdonyBck1 都是用了__Block_byref_aa_0结构体的实例aa的指针。反过来一个block中使用多个 __block也是可以的。

3.3.3 更改特殊区域变量值

除了通过__block修饰局部变量外,其他变量如静态局部变量、静态全局变量、全局变量能否在block内部进行修改?

为了探究到底,我们继续需要通过clang 把oc转为c++源码来探个究竟

进入main.m的文件夹下,执行:clang -rewrite-objc main.m

  • oc代码

    c 复制代码
    int global_a = 2;
    static int static_global_a = 3;
    int main(void) {
        static int static_a = 4;
        void (^donyBck)(void)=^(){
            global_a = 1;
            static_global_a = 2;
            static_a = 3;
            NSLog(@"The block Capture global_a:%d ,static_global_a:%d,static_a:%d",global_a,static_global_a,static_a);
        };
        
        //donyBck的调用
        donyBck();
        return 0;
    }
  • C++代码

    c++ 复制代码
    //全局变量
    int global_a = 2;
    //静态全局变量
    static int static_global_a = 3;
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int *static_a; //静态局部变量
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_a, int flags=0) : static_a(_static_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int *static_a = __cself->static_a; // bound by copy
    
            global_a = 1;
            static_global_a = 2;
            (*static_a) = 3;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_6417ea_mi_3,global_a,static_global_a,(*static_a));
        }
    
    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(void) {
        static int static_a = 4;
        void (*donyBck)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_a));
        ((void (*)(__block_impl *))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck);
        return 0;
    }

    __main_block_impl_0可以看出,静态局部变量static_a以指针形式添加添加为成员变量,而静态全局变量 static_global_a 和全局变量global_a并没有添加到__main_block_impl_0内部。

    c++ 复制代码
    //全局变量
    int global_a = 2;
    //静态全局变量
    static int static_global_a = 3;
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int *static_a; //静态局部变量
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_a, int flags=0) : static_a(_static_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };

    再从__main_block_func_0,可以看出全局变量global_a和全局静态变量static_global_a是在block内部访问直接访问的,而静态局部变量static_a是通过指针传递的方式进行访问和赋值的。

    c++ 复制代码
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int *static_a = __cself->static_a; // bound by copy
    
            global_a = 1; //全局变量
            static_global_a = 2; //全局静态变量
            (*static_a) = 3; //局部静态变量
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_6417ea_mi_3,global_a,static_global_a,(*static_a));
        }

3.4 Block的存储域

在3.2 中的Block实质源码分析里,看出impl.isa = &_NSConcreteStackBlock; //栈block,可以看出该Block存储在栈区,那Block还可以存在哪个区呢?

先说结论:Block分别可以存储在_NSConcreteGlobalBlock_NSConcreteStackBlock_NSConcreteMallocBlock

设置对象的存储域
_NSConcreteGlobalBlock 数据区域(.data区)
_NSConcreteStackBlock
_NSConcreteMallocBlock

3.4.1 _NSConcreteGlobalBlock

为了探究到底,我们继续需要通过clang 把oc转为c++源码来探个究竟

进入main.m的文件夹下,执行:clang -rewrite-objc main.m

  • oc代码

    c 复制代码
    //全局block
    void (^donyBck)(void) =^(){
        NSLog(@"global block");
    };
    
    int main(void) {
        donyBck();
        return 0;
    }
  • c++源码

    c++ 复制代码
    struct __donyBck_block_impl_0 {
      struct __block_impl impl;
      struct __donyBck_block_desc_0* Desc;
      __donyBck_block_impl_0(void *fp, struct __donyBck_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteGlobalBlock; //全局block
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };

通过源码可以看出 impl.isa = &_NSConcreteGlobalBlock,说明该Block为 _NSConcreteGlobalBlock类型。

这里需要注意使用全局block时,因为本身已经在全局区域,所以不会捕获自动变量(局部变量),存储在数据区域

3.4.2 _NSConcreteStackBlock

除了全局block外,其他基本都存储在栈上,也就是StackBlock。

NSConcreteStackBlock类的block,存储在栈区,如果所属的变量作用域结束,该Block就会被废弃。由于 __block变量也配置在栈上,同样的所属变量作用域结束后,该 __block变量同样也被废弃。

3.4.3 _NSConcreteMallocBlock

既然在栈上的Block在变量作用域结束后就立即被废弃,那如果不想废弃怎么办?

Block提供了 【复制copy】操作,可以将Block对象和 __block变量从栈区复制到堆区上,当Block从栈区复制到堆区后,即时变量作用域结束时,堆区上的Block和 __block还可以继续使用。

此时的在堆区的Block就是_NSConcreteMallocBlock 对象,Block结构体成员变量isa赋值为 imp.isa = &_NSConcreteMallocBlock;

而此时被__block修饰的变量用结构体成员变量 __forwarding可以实现无论__block变量配置在堆上还是在栈上,都能够正确访问__block变量

3.5 Block的自动拷贝和手动拷贝

3.5.1 Block的自动拷贝

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

  • 将Block作为函数返回值返回时,会自动拷贝

  • 向方法或函数的参数中传递Block时:(以下两种情况内部底层实现了copy操作,其他都需要手动拷贝)

    • Cocoa框架的方法且方法名中含有usingBlock等 如NSArray类的enumerateObjectsUsingBlock方法
    • GCD的API时如dispatch_async函数
    c 复制代码
    //在MRC下,initWithObjects后面的Block,编译器不会主动给Block添加copy操作,所以Block还存在栈上,所以会报错。
    //在ARC下,编译器主动添加了copy操作,此时的block被复制到堆上了。
    @implementation SDPerson
    
    - (id) getBlockArray{
        int val = 10;
      	void (^blk0)(void) = ^{NSLog(@"blk0:%d",val);};
        void (^blk1)(void) = ^{NSLog(@"blk1:%d",val);};
        NSLog(@"blk01:%@",blk0);
      NSLog(@"blk02:%@",blk1);
        /*
      blk01:<__NSStackBlock__: 0x7ff7bfeff0d8>
    		blk02:<__NSStackBlock__: 0x7ff7bfeff0a8>
      */
      	//array 的initWithObjects 纯容器存储AIP,框架内部不会自动copy
      return [[NSArray alloc]initWithObjects:blk0,blk1, nil];
    }
    
    @end

    报错原因是:在执行完getBlockArray后栈上的Block被废弃,MRC 无任何自动优化,initWithObjects: 仅存栈 Block 指针 → 方法返回栈帧销毁 → 执行 Block 访问野内存 → 崩溃。此时我们需要手动复制下即可。

    修改一下getBlockArray,即可

    c 复制代码
    - (id) getBlockArray{
        int val = 10;
        
        void (^blk0)(void) = ^{NSLog(@"blk0:%d",val);};
        void (^blk1)(void) = ^{NSLog(@"blk1:%d",val);};
        blk0 = [blk0 copy];
        blk1 = [blk1 copy];
        NSLog(@"blk01:%@",blk0);
        NSLog(@"blk02:%@",blk1);
        //blk01:<__NSMallocBlock__: 0x600000c00540>
        //blk02:<__NSMallocBlock__: 0x600000c00570>
        return [[NSArray alloc]initWithObjects:blk0,blk1, nil];
    }
3.5.2 Block的手动拷贝

所有需要让 Block「脱离原栈帧存活」的场景,都必须手动调用[block copy]

关于Block不同类的拷贝效果总结如下

block类 副本源的存储域 复制效果
_NSConcreteStackBlock 栈区 从栈拷贝到堆区
_NSConcreteGlobalBlock 程序的数据区域 不做改变
_NSConcreteMallocBlock 堆区 引用计数增加

不管Block配置在何处,用copy复制不会引起任何问题,在不确定时,调用copy方法即可

3.6 __block变量存储域

在使用 __block变量的Block从栈复制到堆上,__block变量也受到了如下影响

__block变量的配置存储区域 Block从栈上复制到堆上时的影响
堆区 从栈复制到堆区,并被Block所持有
栈区 被Block所持有

和OC引用计数方式内存管理完全相同。

  • __block修饰的变量被Block所持有,如果Block废弃,持有的__block变量也跟着废弃

3.7 截获对象

在Block语法中使用局部变量array来添加元素。

理论上在变量作用域的同时,变量array被废弃,因此赋值给变量array的NSMutableArray类对象必定释放并废弃,但上述代码在main方法里的内{}外仍然可以执行,并打印日志。 这意味着array在Block的执行部分超出其变量作用域而存在。

先说结论:Block从栈复制到堆,归结为__Block_copy函数被调用,使__strong修饰的自动变量对象和__block修饰的变量,被堆上的Block所持有,所以可以超出其变量作用域而存在。

那我们转为c++代码再探个究竟

  • OC

    c 复制代码
    typedef int (^Blk_t)(id obj);
    Blk_t donyBck;
    
    int main(void) {
        
        {
            id array = [NSMutableArray array];
            donyBck = [^(id obj){
                [array addObject:obj];
                NSLog(@"access local variables array.count:%lu",[array count]);
            } copy];
        }
        
        donyBck([[NSObject alloc]init]);
        donyBck([[NSObject alloc]init]);
        donyBck([[NSObject alloc]init]);
        return 0;
    }
    /*
    access local variables array.count:1
    access local variables array.count:2
    access local variables array.count:3
    */
  • C++

    c++ 复制代码
    typedef int (*Blk_t)(id obj);
    Blk_t donyBck;
    
    //Block用的结构体
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      id array; //这里array被Block强引用 __strong
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
    };
    
    //block执行方法
    static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
      id array = __cself->array; // bound by copy
    
      					//添加方法
                ((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)array, sel_registerName("addObject:"), (id)obj);
      					//打印日志
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_d__csgptyg57sqczzwhcvq7mcpr0000gn_T_main_b8c0c1_mi_8,((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));
     }
    
    //copy
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    //废弃
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    //block描述
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
      void (*dispose)(struct __main_block_impl_0*);
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
    
    //main方法
    int main(void) {
        {
            id array = ((NSMutableArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array"));
            donyBck = (Blk_t)((id (*)(id, SEL))(void *)objc_msgSend)((id)((void (*)(id))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344)), sel_registerName("copy"));
        }
      
        //Block执行
        ((int (*)(__block_impl *, id))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
        ((int (*)(__block_impl *, id))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
        ((int (*)(__block_impl *, id))((__block_impl *)donyBck)->FuncPtr)((__block_impl *)donyBck, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
        return 0;
    }

    通过源码可以看出 array被Block截获,并成为__strong修饰成员变量,这里虽然没有显示__strong,默认就是强引用

c 复制代码
        //Block用的结构体
        struct __main_block_impl_0 {
          struct __block_impl impl;
          struct __main_block_desc_0* Desc;
          id array; //这里array被Block强引用 __strong
        };

那Block捕获的array是在什么时候进行初始化和废弃的呢?

我们可以从__main_block_desc_0可以看出结构体新增了copydispose,以及对应__main_block_copy_0__main_block_dispose_0,这两个结构体在3.3.2里__block时也遇到了。 不过__block修饰的变量和捕获的对象有一点点区别:

对象 BLOCK_FIELD_IS_OBJECT
__block变量 BLOCK_FIELD_IS_BYREF

仅仅主要用来区分是对象还是__block变量

c++ 复制代码
   //block描述
   static struct __main_block_desc_0 {
     size_t reserved;
     size_t Block_size;
     void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
     void (*dispose)(struct __main_block_impl_0*);
   } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

   //copy
   static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
     //_Block_object_assign对使Block内部对array持有
     _Block_object_assign(
       (void*)&dst->array, 
       (void*)src->array, 
       3/*BLOCK_FIELD_IS_OBJECT*/
     );
   }

   //废弃
   static void __main_block_dispose_0(struct __main_block_impl_0*src) {
     _Block_object_dispose(
       (void*)src->array, 
       3/*BLOCK_FIELD_IS_OBJECT*/
     );
   }
  • __main_block_copy_0可以看出,内部通过_Block_object_assign方法对array持有,_Block_object_assign相当于retain,将对象赋值在对象类型结构体成员变量中。

  • __main_block_dispose_0使用_Block_object_dispose函数,相当于release,释放赋值在Block用结构体成员变量的array中的对象。

  • 因此_Block_object_assign__main_block_dispose_0指针赋值在__main_block_desc_0的copy和dispose中。

    但是在源代码里没有看到这些函数以及指针被调用,那他们的调用时机在什么时候?

那什么时候栈上的Block会复制到堆上呢?

  • 调用Block的copy实例方法时
  • Block作为函数返回值返回时
  • 将Block赋值给附有__strong修饰符id类型或Block类型成员变量时
  • 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时。

array默认为__strong修饰,那如果改为__weak呢?

c 复制代码
typedef void (^Blk_t)(id obj);
Blk_t donyBck;

int main(void) {
    {
        id array = [NSMutableArray array];
        id __weak array2 = array;
        donyBck = [^(id obj){
            [array2 addObject:obj];
            NSLog(@"access local variables array.count:%lu",[array2 count]);
        } copy];
    }
    
    donyBck([[NSObject alloc]init]);
    donyBck([[NSObject alloc]init]);
    donyBck([[NSObject alloc]init]);
    return 0;
}
/**
access local variables array.count:0
access local variables array.count:0
access local variables array.count:0
*/

这是因为array在变量作用域结束后同时被释放、废弃。nil被赋值给__weak修饰的array2。

那如果__block__weak同时修饰呢?

c 复制代码
__block id __weak array2 = array;

结果一样:

c 复制代码
    access local variables array.count:0
    access local variables array.count:0
    access local variables array.count:0

即使被附加了__block说明符,__strong修饰符的变量array也会在变量作用域结束的同时被释放掉,nil被赋值给附有__weak的变量array2中。

另外被__unsafe_unretained修饰符的变量只不过与指针相同,所以不管在Block中使用还是附加到__block变量中,也不会想 __strong__weak那样进行处理,使用__unsafe_unretained修饰符需要注意不能通过悬垂指针访问已被废弃的对象。

__autoreleasing修饰符也不能和__block同时使用

3.8 Block循环引用

我们知道Block内部使用__strong修饰符的对象类型的自动变量,那当Block从栈复制到堆的时候,该对象就会被Block所持有

那么如果这个对象还同时持有Block的话,就容易发生循环引用。正所谓你中有我,我中有你

  • 示例1:

    c 复制代码
    // 文件SDPerson.m
    typedef void (^blk_t)(void);
    @interface SDPerson()
    {
        blk_t blk_;
    }
    @end
    
    
    @implementation SDPerson
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            blk_ = ^{
                NSLog(@"self = %@",self);
            };
        }
        return self;
    }
    
    - (void)dealloc{
        NSLog(@"dealloc");
    }
    c 复制代码
    //文件main.m
    #import "SDPerson.h"
    int main(void) {
        SDPerson *person = [[SDPerson alloc]init];
        NSLog(@"%@",person);
        return 0;
    }

    最终执行结果 <SDPerson: 0x60000020d340> SDPerson的dealloc没有执行,发生了Block的循环引用

    具体分析:

    SDPerson内部blk_t持有了self,而self也同时持有作为成员变量的blk_t

    另外编译器也会有提示

    如果Block内部不使用self,还会造成循环引用吗?

  • 示例2:

    c 复制代码
    typedef void (^blk_t)(void);
    @interface SDPerson()
    {
        blk_t blk_;
        id obj_;
    }
    @end
    
    
    @implementation SDPerson
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            blk_ = ^{
                NSLog(@"obj_ = %@",obj_);
            };
        }
        return self;
    }

    答案:会

    分析一下:表面上看obj_没有使用self,但是它是self的成员变量,因此Block想持有obj_,就必须引用self,所以同样造成循环引用。

    那如果这个属性使用weak修饰符呢

  • 实例3 :

    c 复制代码
    typedef void (^blk_t)(void);
    @interface SDPerson()
    @property (nonatomic, weak) NSArray *array;
    @end
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            blk_ = ^{
                NSLog(@"obj_ = %@",_array);
            };
        }
        return self;
    }

    答案:还是会循环引用,因为循环引用是self和block之间的事情,这个被block持有的成员变量是strong或weak没有关系,即使是基本类型assign也是一样的。

那如何解决这样的循环引用呢?

3.8.1 __weak修饰符

为了避免循环引用,可以通过__weak修饰符,来打破互相持有

c 复制代码
- (instancetype)init
{
    self = [super init];
    if (self) {
      	//使用__weak修饰符,使block内部为弱引用关系
        id __weak tmp = self;
        // id __unsafe_unretained tmp = self;
        blk_ = ^{
            NSLog(@"self = %@",tmp);
        };
    }
    return self;
}

常见用 __weak typeof(self) weakSelf = self; 来进行弱引用self

c 复制代码
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
    [weakSelf doSomething];
};

上述存存在一个问题,当block执行过程中,weakSelf可能被释放,导致后续操作无效

这里需要再block内部进行强化弱引用,使用__strong在局部作用域内临时强引用弱引用对象,确保在执行期间对象存活。__strong在Block内部栈上创建局部强指针,不会造成循环引用。

c 复制代码
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
    __strong typeof(self) strongSelf = weakSelf;
    if (strongSelf) {
        [strongSelf doSomething];
        [strongSelf doAnotherThing];
    }
};

除了__weak typeof(self) weakSelf = self; 这里还可以参考第三方开源库ReactiveObjC这样的简洁写法 @weakify(self); 作用是一样的,都是对self进行弱引用。

c 复制代码
@weakify(self);
self.myBlock = ^{
		@strongify(self);
    if (self) {
        [self doSomething];
        [self doAnotherThing];
    }
};

除了__weak修饰符外,还可以使用__unsafe_unretained,如id __unsafe_unretained tmp = self; 效果是一样的,都是使对象为弱引用,那两者有什么区别?更推荐使用__weak

两者的区别在于当所指向的对象被释放时,如何处理指针

  • __weak(安全)当对象释放后,所有指向它的__weak变量会被运行时自动设置为nil,意味着后续在访问这个指针,就像nil发送消息一样,在OC中是安全的,不会导致程序崩溃 。
  • __unsafe_unretained(不安全)当对象释放后,__unsafe_unretained指针不会自动置空,仍然保存着对象被释放前的那个内存地址,也就是变成了"悬垂指针"或"野指针",如果此时访问了这个指针,就会发生BAD_ACCESS
3.8.2 __block修饰符

除了__weak外,还可以使用 __block解决block循环引用问题

c 复制代码
typedef void (^blk_t)(void);
@interface SDPerson()
{
    blk_t blk_;
}
@end
@implementation SDPerson

- (instancetype)init
{
    self = [super init];
    if (self) {
        __block id tmp = self;
        blk_ = ^{
            NSLog(@"obj_ = %@",tmp);
            tmp = nil;
        };
    }
    return self;
}

//执行block
- (void)execBlock{
    blk_();
}
c 复制代码
int main(void) {
    SDPerson *person = [[SDPerson alloc]init];
    [person execBlock];
    NSLog(@"%@",person);
    return 0;
}
//执行结果
//obj_ = <SDPerson: 0x600000c08690>
//<SDPerson: 0x600000c08690>
//dealloc 

如果Block不执行execBlock,依然会存在循环引用

此时-SDPerson持有Block,Block持有__block变量,__block持有SDPerson类对象。三者互相持有,导致引用循环

如何解决?

就是执行Block execBlock方法,Block内部,会把tmp置为nil 。并执行block execBlock方法,因此__block持有类对象的强引用就失效了,

c 复制代码
blk_ = ^{
     NSLog(@"obj_ = %@",tmp);
     tmp = nil;
};
//并执行block execBlock方法

所以__block需要执行Block来解决循环引用,基于此特点,可以通过__block控制对象的持有时间。

这里需要区分,这里利用__block解决循环引用,不是因为__block本身直接解决的,而是利用了__block的**「可写特性」+ 手动执行 tmp = nil**,并执行block的execBlock方法,使block内部tmp=nil生效,从而主动打破了循环链。

在实际开发过程中,需要具体根据实际情况,来使用__weak还是__block.

四、常见面试题

4.1 说一下什么是Block?

Block是带有局部变量的匿名函数,本质是一个对象,内部有isa指针,内部是由结构体**__main_block_impl_0->__block_impl**组成,Block的执行函数通过在__block_impl->FuncPtr函数指针,找到封装的函数,并将block地址作为参数传给这个函数进行执行。Block捕获的变量,存入__main_block_impl_0结构体内,并通过block地址拿到捕获变量的值。

c++ 复制代码
//包含block实际函数指针的结构体
struct __block_impl {
  void *isa; //有isa
  int Flags;
  int Reserved;  //今后升级所需区域大小
  void *FuncPtr; //函数指针
};

//block结构体
struct __main_block_impl_0 {
  struct __block_impl impl; //Block的实际指针,
  struct __main_block_desc_0* Desc;
};

4.2 Block 有几种类型?分别是什么?

有三种类型,分别为 _NSConcreteGlobalBlock_NSConcreteStackBlock_NSConcreteMallocBlock

定义在全局的block,为全局block,存储在数据区的全局区里,因为本身就是全局,所以不会访问局部变量,因此不需要捕获局部变量。

一般用到的是栈block,但是栈上的block是临时的,在它的作用域结束后就被销毁,为了延长生命周期,在arc下系统会默认会copy到堆上,来延迟生命周期,这样可以在定义它的作用域外部使用。mrc下,需要手动进行copy

4.3 Block 自动截取变量

Block外部的变量,可以被block捕获到内部进行使用,这里需要注意的是变量类型

  • 全局变量/静态全局变量 ,block不需要捕获,因为全局变量和静态全局变量数据存储在全局数据区,Block内部直接使用

  • 局部静态变量 捕获变量地址,所以外部变量修改后,通过地址访问到变量的值,也会跟着修改。

  • 静态变量 捕获变量的值,是通过值传递 的方式捕获到block内部,并且捕获的是变量的当前瞬时值,所以外部修改了变量,block内部的变量值不会发生改变,如果需要修改,需要通过__block来修饰,然后通过指针引用传递的方式在内部使用。

    4.3.1 为什么普通局部变量要捕获值,跟静态局部变量一样捕获地址不行吗

    不行,和局部变量的生命周期有关系,因为局部变量在出大括号后就会被释放掉,这事我们在大括号外部调用这个Block,此时局部变量已经被释放了,block内部通过变量的指针访问变量,就会抛出异常。而静态局部变量的生命周期是和整个程序的生命周期一样,也就是在整个程序运行过程中不会释放,所以可以通过指针地址访问。

    4.3.2既然静态局部变量一直都不会被释放,那block为什么还要捕获它,直接拿来用不就可以了吗?

    这是因为静态局部变量作用域只在大括号内,出了括号,它虽然存在,但外面已经访问不了,这时通过block执行函数只能通过捕获的方式。

    4.3.3 静态局部变量一直都不会被释放,会导致内存泄漏吗?

    不会,静态局部变量是语言设计的特性,行为可预测,是一种特殊的局部变量,具有局部作用域,存储在数据区.data(初始化).bss(未初始化),程序在运行期间只被分配一次内存(且占内存有限),生命周期有编译器自动管理,启动时初始化,结束时销毁,不会导致运行时的内存泄漏

    首先明确内存泄漏的概念:程序在运行过程中,不断分配内存而没有适当的释放,导致内存逐渐减少的情况

4.4Block 处理循环引用

如果Block内部捕获了外部的strong(强引用)类型的引用对象,那么这个对象有强引用block,就会形成循环引用,会导致内存泄漏,因为参与循环引用的对象和block无法正常释放,长期下去会导致性能问题。

这时,就需要通过__weak关键字,进行对强引用对象进行弱引用,来打破你中有我,我中有你。

4.4.1 __weak typeof(self) weakSelf = self; 和@weakify(self);有什么区别

都是用来弱引用self,避免循环引用,__weak typeof(self) weakSelf = self;

@weakify(self) 是宏定义,预编译阶段展开就是 __weak typeof(self) weakSelf = self;,设计初衷就是为了更简洁更优雅。

4.5 Block 的内存管理

首先围绕Block的三个核心点

4.6__block 的解释以及在 ARCMRC 下有什么不同

默认情况下,Block捕获的外部自动变量(局部变量)是值捕获,在Block内部是无法修改的。

__block是修饰符,主要用于解决上述问题,block捕获的局部变量,在block内部,可以进行修改。

原理:被__block修饰后的局部变量,编译器会把这个变量包装成一个结构体对象,底层其实一个结构体__Block_byref_a_0,内部有一个__forwarding指针,和当前变量的值等成员,无论Block和__block变量本身被复制到栈上还是堆上,都可以通过这个指针访问和修改值,这样就使从值传递变成了引用传递

在ARC和MRC下的不同

  • 在ARC下

__block修饰的对象是强引用,需要注意循环引用,常用的解决方案为使用__weak弱引用。

  • 在MRC下

对__block修饰对象,不会对对象进行retain,避免循环引用

4.7为什么需要使用copy关键字

Block默认创建在栈上,为了延迟生命周期,需要copy到堆上。

在arc之前,手动管理内存,为了保持block的生命周期,开发者需要手动将栈上的block复制到堆上,通过copy关键字操作,如下

c 复制代码
@property (copy, nonatomic) void (^block)(void);

在ARC之后,虽然编译器会自动进行copy操作,把block复制到堆上,为了和MRC下保持一致,避免在不同内存管理环境下切换的混淆,在ARC之后,还是推荐用copy关键字

4.7.1 那用strong可以修饰block吗?

在ARC下可以,在MRC下不可以。

4.7Dispatch_block_t这个有没有用过?解释一下?

dispatch_block_t是GCD中的一个类型定义,代表无参数,也没有返回值的代码里。

基本定义 typedef void (^dispatch_block_t)(void);

常见的使用场景:将任务放入队列后立即返回,不阻塞当前线程

c 复制代码
    dispatch_async(queue,^{
    	NSLog(@"在后台执行任务");
    })

五、参考文献

相关推荐
2501_9159214316 小时前
iOS App 中 SSL Pinning 场景下代理抓包失效的原因
android·网络协议·ios·小程序·uni-app·iphone·ssl
Swift社区16 小时前
Flutter / RN / iOS 的 UI 渲染机制,本质差异在哪里?
flutter·ui·ios
程序漫游人18 小时前
苹果IOS App Store加快审核进度
android·ios·软件工程·iphone
吴Wu涛涛涛涛涛Tao19 小时前
抖音思路复刻:iOS 卡死(ANR)监控 + 自动符号化全流程实战
ios·性能优化
2501_9160088920 小时前
在 Windows 上使用开心上架(Appuploader)在 Windows 环境下创建与管理 iOS 证书
android·ios·小程序·https·uni-app·iphone·webview
好大哥呀21 小时前
iOS - UIViewController 生命周期
macos·ios·cocoa
好大哥呀21 小时前
如何在iOS中使用UIViewController的生命周期方法?
macos·ios·xcode
二流小码农1 天前
鸿蒙开发:一个简单的滑块验证组件
android·ios·harmonyos