美文网首页iOS面试知识点iOS DeveloperiOS开发攻城狮的集散地
Block原理探究(下篇)-捕获变量分析及__block原理

Block原理探究(下篇)-捕获变量分析及__block原理

作者: 梧雨北辰 | 来源:发表于2019-10-07 21:29 被阅读0次

主要内容:
1.分析Block捕获外部变量的过程
2.理解Block修改外部变量的限制
3.分析__block存储域类说明符的原理
4.理解__block变量的存储域
5.探究Block对对象的捕获过程
6.Block的循环引用问题

一、分析Block捕获外部变量的过程

为了保证block内部能够正常访问外部的变量,Block有一个变量捕获机制,即Block语法表达式所使用变量可以被保存到Block的结构体实例(Block自身)中。

关于捕获,Block对不同的外部变量的处理有所不同,根据OC中使用变量的分类,大概包括以下几种情况:

  • 函数参数(这里研究Block捕获,所以此处不涉及)
  • 自动变量(常简称,局部变量)
  • 静态局部变量(常简称,静态变量)
  • 静态全局变量
  • 全局变量

那么,现在对Block捕获外部变量的四种情况进行测试,相关代码如下:

#import <Foundation/Foundation.h>

//使用如下的命令,可将OC代码编译为C++代码
//clang -rewrite-objc main.m

int global_val = 1;                  //全局变量
static int static_global_val = 1;    //静态全局变量

int main(int argc, char * argv[]) {
    int val = 1;                     //自动变量
    static int static_val = 1;       //局部静态变量
    
    void (^myBlock)(void) = ^{
        global_val ++;
        static_global_val ++;
        static_val ++;
        //val++//直接修改会报错(Variable is not assignable (missing __block type specifier)
        
        NSLog(@"\nBlock内:\nglobal_val = %d,\nstatic_global_val = %d,\nval = %d,\nstatic_val= %d",global_val,static_global_val,val,static_val);
    };
    
    global_val ++;
    static_global_val ++;
    val ++;
    static_val ++;
    
    NSLog(@"\nBlock外:\nglobal_val = %d,\nstatic_global_val = %d,\nval = %d,\nstatic_val= %d",global_val,static_global_val,val,static_val);
    myBlock();
    return 0;
}

运行的结果如下:

Block外:
global_val = 2,
static_global_val = 2,
val = 2,
static_val= 2

Block内:
global_val = 3,
static_global_val = 3,
val = 1,
static_val= 3

观察代码运行结果,我们会发现四种情况下,只有静态局部变量、静态全局变量、全局变量可以在Block里被修改,而且直接修改自动变量就会报错;所以此时需要考虑以下两个问题:
1.为什么在Block里不允许更改自动变量?
2.Block捕获不同的变量并修改时,有什么区别吗?

现在将上述代码转化为C++源码来具体分析,转换后的代码如下:

int global_val = 1;
static int static_global_val = 1;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *static_val;  //对应静态局部变量
  int val;          //对应自动变量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int _val, int flags=0) : static_val(_static_val), val(_val) {
    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_val = __cself->static_val; // bound by copy
  int val = __cself->val; // bound by copy

        global_val ++;
        static_global_val ++;
        (*static_val) ++;

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_78fd5a_mi_0,global_val,static_global_val,val,(*static_val));
    }

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, char * argv[]) {
    int val = 1;
    static int static_val = 1;

    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val, val));

    global_val ++;
    static_global_val ++;
    val ++;
    static_val ++;

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_78fd5a_mi_1,global_val,static_global_val,val,static_val);

    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

    return 0;
}

在代码分析之前,我们有必要对程序中的内存区域划分有所了解,其大致的分类如下:

内存区域 具体说明
栈区 存放局部变量的值,系统自动分配和释放;
特点:容量小,速度快,有序
堆区 存放通过malloc系列函数或new操作符分配的内存,如对象;
一般由程序员分配和释放,如果不释放,则出现内存泄露;
特点:容量大,速度慢,无序;
静态区 存放全局变量和静态变量(包括静态局部变量和静态全局变量);
当程序结束时,系统回收;
常量区 存放常量的内存区域;
程序结束时,系统回收;
代码区 存放二进制代码的区域

了解了这些之后,我们再来具体分析代码和执行结果:

1.全局变量和静态全局变量

这两种变量都存储在静态区,在任何时候都可以访问,所以Block无所谓捕获,而是采用了直接访问的方式成功的修改了它们的值;这一点从Block对应的构造函数中就可以看出来:

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_var, int _var, int flags=0) : static_var(_static_var), var(_var);

我们看到,用于创建Block的构造函数使用到了静态局部变量和自动变量作为参数,并没有涉及到全局变量和静态全局变量。而且我们也在Block的结构体中只发现了对应的静态变量和自动变量的属性,这进一步说明Block是直接使用全局变量和静态全局变量,而非捕获;

int *static_val;  //对应静态局部变量
int val;          //对应自动变量
2.自动变量与静态局部变量

虽然自动变量与静态局部变量都被Block捕获,但是只有静态局部变量才可以被修改成功;通过Block中对应的函数__main_block_func_0,可以观察到Block对外部变量的修改过程,相关代码如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_var = __cself->static_var; // bound by copy
  int var = __cself->var; // bound by copy
            global_var ++;
            static_global_var ++;
            (*static_var) ++;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_TestBlock_b539f1_mi_0,global_var,static_global_var,var,(*static_var));
}

我们发现,Block为了访问到对应的自动变量和静态局部变量都使用了__cself,这些操作其实都是针对Block自身属性的,但不同的是:
外部静态局部变量,由于是指针传递,所以修改的是同一个变量,可以修改成功;
外部自动变量,由于是值传递,所以即使修改成功,也无法改变外部自动变量的值;

因此,也许是出于安全的目的,在编译阶段我们就会收到错误提示:Block不能修改其捕获的外部自动变量,即:

Variable is not assignable(missing __block type specifier)

这里还有两个问题值得我们思考:
1.为什么静态局部变量的存储域也在静态区,却不可以像全局变量一样直接修改呢?
关键原因还是"局部"两个字,我们看到C++代码中的函数__main_block_func_0被设置在了包含Block语法的函数(main函数,静态局部变量在此处声明定义)之外,所以__main_block_func_0和静态局部变量和作用域不同,自然不能像全局变量一样随时访问它,所以采用捕获和指针传递的方式来修改静态变量;

2.为什么自动变量不能像静态变量一样指针传递呢?
其实,这主要还是因为自动变量和静态变量的存储域的不同,自动变量存在栈上被销毁的时间不定,这很有可能导致Block执行的时候自动变量已经被销毁,那么此时访问被销毁的地址就会产生野指针错误。

二、理解Block修改外部变量的限制

通过以上的代码示例,我们可以将Block修改外部变量成功的情况分为两种:
第一种:Block直接访问全局性的变量,如全局变量、静态全局变量;
第二种:Block间接访问静态局部变量,捕获外部变量并使用指针传递的方式;

Block中不允许修改外部变量的值的问题,变成了不允许修改自动变量的值的问题;但这也并非最终答案,其实最根本的原因还是Block不允许修改栈中指针的内容
下面的一段代码,可以从侧面来验证我们的想法:

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
  NSMutableString *mStr = @"mStr".mutableCopy;
    void (^myBlock)(void) = ^{
        //mStr = @"newMstr".mutableCopy; //代码1:直接修改了mStr指针内容;
        [mStr appendString:@"-ExtraStr"]; //代码2:修改mStr指向的堆中内容;
        NSLog(@"Block内:mStr:%@",mStr);
    };
    NSLog(@"Block外:%@",mStr);
    myBlock();   
    return 0;
}
//打印结果:
//Block外:mStr
//Block内:mStr:mStr-ExtraStr

上述代码是操作一个自动变量的可变字符串,经过测试mStr不可以直接赋值,却可以通过appendString修改字符串,这其中的原因是什么呢?
首先还是将代码转化为C++源码,具体如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSMutableString *mStr;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableString *_mStr, int flags=0) : mStr(_mStr) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself){
  NSMutableString *mStr = __cself->mStr; // bound by copy

        ((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)mStr, sel_registerName("appendString:"), (NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_fe0cca_mi_1);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_fe0cca_mi_2,mStr);
    }
    
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->mStr, (void*)src->mStr, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->mStr, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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};

int main(int argc, const char * argv[]) {
    NSMutableString *mStr = ((id (*)(id, SEL))(void *)objc_msgSend)((id)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_fe0cca_mi_0, sel_registerName("mutableCopy"));
    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, mStr, 570425344));

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_fe0cca_mi_3,mStr);
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    return 0;
}

作为对象的字符串会涉及到释放的问题,所以此处转换后的源码与基本类型有所区别(但不影响此处分析,后续会讲到),我们发现Block捕获了mStr,而且采用了指针传递的方式,这与上面的静态局部变量被捕获的方式很相似,但是mStr依然不可以直接赋值新的字符串,其实弄清楚问题的关键是理解下面这句代码究做了什么?

mStr = @"newMstr".mutableCopy;

这句代码的含义可以归纳为:@"mStr".mutableCopy创建了新的字符串对象,并将新对象的地址返回,最后又赋值给了mStr;可我们知道mStr指针是在栈上的,它随时可能被释放,直接修改就有可能造成野指针错误,这刚好对应了先前自动变量不可修改的问题;

但通过appendString为什么又可以修改字符串呢?这主要因为mStr通过指针传递被Block捕获后,Block只是借助其内部的指针(和mStr同名,且指向同一个地址),找到了可变字符串的位置,向这块内存追加新的内容,但是并未改变mStr的内存地址;

重要总结:Block修改外部变量的限制,其实是指Block不允许修改栈中指针的内容

三、理解__block存储域类说明符的原理

通过以上的分析,我们可以将Block理解为"可以带有自动变量值的匿名函数",但由于存储域的关系,Block并不能直接修改捕获的自动变量。为了解决这个问题,总结起来有两种方案:
1.使用存储域在静态区的变量(如全局变量、静态全局变量、静态局部变量);
2.使用存储域类说明符__block;

第一种方案我们已经分析过了,现在重点来理解__block存储域说明符的用法,其实C语言中的还有许多其他存储域类说明符,如:
typedef
extern
static
auto
register
__block说明符就类似于static、auto、register它们可以用于指定变量值设置到哪个存储域中。例如,auto表示自动变量存储在栈中(默认),static表示静态变量存储在数据区中。

下面我们来实际使用__block,使用它来修改被Block捕获的自动变量,具体的代码如下:

//__block存储域修饰符
int main(int argc, const char * argv[]) {
    __block int val = 10;
    void (^myBlock)(void) = ^{ val = 20;};

    val = 30;
    myBlock();
    NSLog(@"val: %@",val);
    return 0;
}

此处代码在Block中修改自动变量却没有像之前那样报错,说明__block说明符是有效的,为了探究其中原理,现在我们再次把上述代码转换C++代码,具体如下:

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref
 (val->__forwarding->val) = 20;}
 
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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};

int main(int argc, const char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));

    (val.__forwarding->val) = 30;
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_wd_fhcn9bn91v56nlzv9mt5z8ym0000gn_T_main_a9f88e_mi_0,(val.__forwarding->val));
    return 0;
}

分析代码,我们会发现__block变量的初始化已经发生了根本的变化,此时的自动变量val对应的是C++源码中的__Block_byref_val_0结构体。该结构体包含了五个成员变量,具体定义如下:

struct __Block_byref_val_0 {
  void *__isa;                      //isa指针
__Block_byref_val_0 *__forwarding;  //初始化传递的是自身结构体实例的指针
 int __flags;                       //标记flag
 int __size;                        //大小
 int val;                           //对应原自动变量val的值
};

我们看到__block变量val的初始值为10,而这个值也出现在了调用__Block_byref_val_0结构体构造方法的时候,总结__block变量被捕获的过程如下:
1.自动变量__block int varl被封装为__Block_byref_val_0结构体;
2.__Block_byref_val_0结构体包含一个与__block变量同名的成员变量val,对应外部自动变量的值;
3.__Block_byref_val_0结构体包含一个__forwarding指针,初始化传递的是自己的地址;
4.在Block初始化的过程中,调用__main_block_impl_0结构体构造函数时,会将__block变量__Block_byref_val_0结构体实例的指针作为参数;

接下来分析给__block变量赋值的代码,转换后的源码如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref
 (val->__forwarding->val) = 20;
}

在这里,我们看到函数首先通过cself->val拿到了对应__block变量的结构体实例,然后又通过__Block_byref_val_0结构体实例的成员变量__forwarding,最终访问到了结构体成员变量val;具体过程如下图所示:

访问__block变量.png

分析当前情况,我就会发现这里有两个很关键问题:
1.为什么要使用多余的__forwarding指针来间接访问变量?
2.当前__block说明符的作用仅仅体现在:将__block变量封装为__Block_byref_val_0结构体;这并未从根本上改变自动变量的性质,自动变量究竟是如何被修改的呢?

为了理解上述问题,我们首先应该对下面的代码有一个更加清晰的了解:

void (^myBlock)(void) = ^{ val = 10;};

代码中创建后的Block直接赋值给了强指针,这其实满足了ARC环境下编辑器对Block的优化:编译器会自动将Block从栈拷贝到堆上,而Block中的用到的__block变量也会被一并拷贝,并且被堆上的Block持有。这样即使Block语法所在的作用域结束,堆上的Block和__block变量依然继续存在,自然也就不存在自动变量创建在栈上被释放的问题了,借助图示理解如下:

在一个Block中使用__block变量.png

另外,当__block变量结构体实例在从栈上被拷贝到堆上时,会将成员变量的__forwarding的值替换为复制目标堆上的__block变量结构体实例的地址。通过这种功能,无论是在Block语法中、Block语法外使用__block变量,还是__block变量配置在栈上或堆上,都可以顺利访问同__block变量。这就是__forwarding指针存在的意义。使用图示理解如下:

复制__block变量后__forwarding指针的变化.png

重要总结:__block修饰的自动变量被封装为结构体,作为一个对象随着Block被拷贝到了堆上,解决了自动变量容易因作用域结束而释放的问题。而__block变量结构体中的__forwarding则保证了无论在栈上还是堆上访问的都是同一个__block变量;我们能够成功修改__block变量的值,其实是修改了堆上被Block持有的__block变量的内部成员变量val。

其他问题:
1.ARC存在编译器的自动优化,自动拷贝Block的情况还包含了很多种,这里只是其中一种情况,上篇已分析过;
2.上述代码中,__block说明符将基本类型的数据封装为结构体类型(其中包含了isa指针),这其实就说明__block变量已经是作为了一个对象在使用,而对象类型被Block捕获之后都会涉及一些释放的问题,所以源码也出现了许多与对象释放相关的函数如:__main_block_copy_0__main_block_dispose_0等。这个问题后续会详细分析;

四、__block变量的存储域

Block的存储域通常涉及到拷贝的操作,那么对于__block变量又是如何处理的呢?使用__block变量的Block从栈上拷贝到堆上时,__block变量也会受到影响。

1.单个Block中使用__block变量

若一个Block中使用__block变量,则当该Block从栈拷贝到堆上时,使用的所有__block变量也全部被从栈上拷贝到堆上。使用图示理解如下:

在一个Block中使用__block变量.png
2.多个Block使用__block变量

多个Block使用__block变量时,任何一个Block从栈上拷贝到堆上,__block变量就会一并从栈上拷贝到堆上并被该Block所持有。当剩下的Block从栈拷贝到堆上时,被拷贝的Block持有__block变量,并增加__block变量的引用计数。使用图示理解如下:

在多个Block中使用__block变量.png
3.__block变量的释放

如果拷贝到堆上的Block被释放,那么它使用的__block变量的引用计数会减一,如果引用计数为0就会被释放。使用图示理解如下:

Block和__block变量的释放.png

重要总结:无论是对基本类型还是对象使用__block修饰符,从转化后的源码来看,它们都会被转化为对应的结构体实例来使用,具有引用类型数据的特性。因此__block变量随着Block被拷贝到堆上后,它们的内存管理与普通的OC对象引用计数内存管理模式完全相同。

五、理解Block对对象的捕获

仔细观察之前的源码我们就会发现,Block捕获对象类型和__block类型的变量(在底层被封装为结构体,也属于对象)明显比基本类型要复杂多,其实这里主要是因为对象类型还要涉及到释放的问题。下面的代码演示了Block对对象的捕获的过程,具体如下:

typedef void(^AddBlock)(NSString *); //定义一种携带字符串参数的Block
int main(int argc, const char * argv[]) {
    AddBlock blk = nil;
    {
        NSMutableArray *mArr = @[].mutableCopy;
        blk = ^(NSString *string){
            [mArr addObject:string];
            NSLog(@"mArr count = %ld",[mArr count]);
        };
    }//NSMutableArray所在的作用域结束
    
    blk(@"A");
    blk(@"B");
    blk(@"C");
    return 0;
}

//打印结果:
mArr count = 1
mArr count = 2
mArr count = 3

分析代码:当前为ARC环境下,编译器自动对访问了自动变量的mArrblk进行了拷贝;所以mArr离开其所在的作用域结束时并没有被释放。虽然mArr指针已经不能使用,但是blk依然保留有对mArr的引用可以找到这块内存。所以代码也是运行正常的;

现在查看编译器转换后的源码如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSMutableArray *mArr;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_mArr, int flags=0) : mArr(_mArr) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->mArr, (void*)src->mArr, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->mArr, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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};

由于代码量较大,这里只提供了与捕获基本类型不同的部分;我们发现,当Block捕获对象类型的变量时,此处的__main_block_desc_0结构体中多了copydispose两个成员变量,而且它们的初始化分别使用了__main_block_copy_0__main_block_dispose_0的函数指针;

这里主要的原因是,在Objective-C中,C语言结构体不能含有__strong、__weak修饰符的变量,因为编译器不知道应该如何进行C语言结构的初始化和废弃操作,不能很好地管理内存。但是OC的运行时库能够准确把握Block从栈复制到堆以及堆上Block被废弃的时机,所以这里才会增加与内存管理相关的变量和函数。

1.__main_block_copy_0函数

结构体__main_block_desc_0中的copy成员变量对应了__main_block_copy_0函数。

当Block从栈上拷贝到堆上时,__main_block_copy_0函数会被调用,然后再调用其内部的_Block_object_assign函数。_Block_object_assign函数就相当于retain操作,会自动根据__main_block_impl_0结构体内部的mArr是什么类型的指针,对mArr对象产生强引用或者弱引用。如果mArr指针是__strong类型,则为强引用,引用计数+1,如果mArr指针是__weak类型,则为弱引用,引用计数不变。

2.__main_block_dispose_0函数

结构体__main_block_desc_0中的dispose成员变量对应了__main_block_dispose_0函数。
当Block被废弃时,__main_block_dispose_0函数会被调用,__main_block_dispose_0函数就相当于release操作,将mArr对象的引用计数减1,如果此时引用计数为0,那么遵循引用计数的规则mArr也就被释放了。

3.Block捕获对象与__block变量的区别

其实Block捕获对象与__block变量后,对于它们的内存管理的方式相同,也都是使用copy函数持有和disposde函数释放;两者体现在源码上的不同,我们可以观察下面的函数:

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->mArr, (void*)src->mArr, 3/*BLOCK_FIELD_IS_OBJECT*/);}

_Block_object_assign函数中的最后一个参数用于区分Block捕获的是对象还是__block变量。

对象变量 __block变量
BLOCK_FIELD_IS_OBJECT BLOCK_FIELD_IS_BYREF

六、Block的循环引用问题

Block在从栈拷贝到堆上时,如果其中捕获了强类型的对象,该对象就会被Block所持有。这样很容易就会引起循环引用,我们来看下面的代码:

typedef void(^MyBlock)(void);

@interface MyObject : NSObject
@property(nonatomic,copy) MyBlock block;
@end

@implementation MyObject
- (instancetype)init {
    self = [super init];
    return self;
}

- (void)dealloc {
    NSLog(@"MyObject dealloc!");
}
@end

int main(int argc, char * argv[]) {
    @autoreleasepool {
        MyObject *myObject = [[MyObject alloc] init];
        myObject.block = ^{
            //Capturing 'myObject' strongly in this block is likely to lead to a retain cycle
            NSLog(@"捕获对象:%@", myObject );
        };
    }
    NSLog(@"myObject的作用域结束了");
    return 0;
}

不仅编译器给出了内存泄漏的警告,而且测试结果也证实了MyObject的dealloc实例方法并没有执行,这里发生了循环引用。原因就在与myObjectblock在被自动拷贝到堆上的过程中持有了myObject,而myObject本身就持有了block,所以两者相互持有就产生了问题。

现在就来总结类似情况下的Block循环引用的处理方法,可分为ARC和MRC两种情况:

1.解决ARC环境下的循环引用问题

方法1:使用弱引用修饰符__weak、和__unsafe_unretained修饰符;
使用__weak解决上述问题,需要改进的代码如下:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        MyObject *myObject = [[MyObject alloc] init];
        __weak typeof(myObject) weakObject = myObject;
        myObject.block = ^{
            NSLog(@"捕获对象:%@", weakObject );
        };
    }
    NSLog(@"myObject的作用域结束了");
    return 0;
}

上述代码使用弱引用修饰符__weak ,在block内部对 myObject设置为弱引用,弱引用不会导致Block捕获对象的引用计数增加(这在上述分析中已经讲过)。

注意__weak__unsafe_unretained的区别:
__weak:iOS4之后才提供使用,而且比__unsafe_unretained更加安全,因为当它指向的对象销毁时,会自动将指针置为nil;推荐使用。
__unsafe_unretained:在__weak出现以前常用修饰符,其指向的对象销毁时,指针存储的地址值不变,所以没有__weak安全。

方法2:使用__block说明符
回忆__block修饰基本类型的C++源码,我们可以知道__block修饰对象时其实也会封装一个结构体类型,而这个结构体中会持有自动变量对象,这样就会造成下图的情况:

__block解决循环引用1.jpeg

使用__block解决上述问题,需要改进的代码如下:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        MyObject *myObject = [[MyObject alloc] init];
        __block MyObject *tempObject = myObject;
        myObject.block = ^{
            NSLog(@"捕获对象:%@", tempObject );
            tempObject = nil;  //关键代码1
        };
        myObject.block();      //关键代码2:执行持有的block;
    }
    NSLog(@"myObject的作用域结束了");
    return 0;
}

上述代码有两句关键,已经通过注释标注;在block中通过tempObject = nil这句代码,__block变量tempObject对于MyObject类对象的强引用失效了,而这句代码生效的前提又是block被调用了(关键代码2);这种方式避免了循环引用的产生的过程如下图:

__block解决循环引用2.png

特别注意:如果关键代码2没有被调用,同样会造成循环引用。

使用__block变量相比弱引用修饰符的优缺点:
优点:
1.通过执行block的方式,可动态决定__block变量可以控制对象的持有时间;
2.在不能使用__weak修饰符的环境下,避免使用__unsafe_unretained(因为要考虑野指针问题);
缺点:
为了避免循环引用,必须执行Block;

2.解决MRC环境下的循环引用问题

方法1:使用弱引用修饰符__unsafe_unretained修饰符;
在MRC环境下不支持使用__weak,所以只能使用__unsafe_unretained;使用原理同ARC环境下相同,这里不再赘述。

方法2:使用__block说明符
MRC环境下,__block说明符被用来避免循环引用。这是因为当Block从栈拷贝到堆时,若Block使用的变量是附有__block说明符的id类型或者对象类型的自动变量,不会被retain,否则就会被retain。这一点和ARC环境是不同的。现在我们在MRC环境下改进代码,具体如下:

int main(int argc, char * argv[]) {
    MyObject *myObject = [[MyObject alloc] init];
    __unsafe_unretained MyObject *tempObject = myObject;
    myObject.block = ^{
        NSLog(@"捕获对象:%@", tempObject );
    };
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [myObject autorelease];
    [pool drain];  //等同于[myObject release];
    return 0;
}
//打印结果:
//MyObject dealloc!

上述操作将代码改为了MRC下的自动释放池,相比之前在ARC中使用__block,这里没有在Block内部置nil的操作,也没有调用block,但同样解决了循环引用的问题;

重要总结:__block说明符在ARC与MRC环境下的用途有很大区别,因此在编写代码时我们必须区分好这两种环境。

相关文章

网友评论

    本文标题:Block原理探究(下篇)-捕获变量分析及__block原理

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