前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Block原理探究(下篇)-捕获变量分析及__block原理

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

作者头像
梧雨北辰
发布2019-10-08 14:43:37
1.6K0
发布2019-10-08 14:43:37
举报

主要内容: 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.png

使用__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环境下的用途有很大区别,因此在编写代码时我们必须区分好这两种环境。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019.10.07 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、分析Block捕获外部变量的过程
    • 1.全局变量和静态全局变量
      • 2.自动变量与静态局部变量
      • 二、理解Block修改外部变量的限制
      • 三、理解__block存储域类说明符的原理
      • 四、__block变量的存储域
        • 1.单个Block中使用__block变量
          • 2.多个Block使用__block变量
            • 3.__block变量的释放
            • 五、理解Block对对象的捕获
              • 1.__main_block_copy_0函数
                • 2.__main_block_dispose_0函数
                  • 3.Block捕获对象与__block变量的区别
                  • 六、Block的循环引用问题
                    • 1.解决ARC环境下的循环引用问题
                      • 2.解决MRC环境下的循环引用问题
                      相关产品与服务
                      腾讯云代码分析
                      腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档