iOS开发:block的分析及循环引用的解决方案

iOS开发中block随处可见,什么是block呢?block是一个匿名函数,是一个代码块,把代码放在这个代码块中,在需要使用的时候进行调用。block会封装函数以及函数的调用环境:
-封装函数:是指block会把block内部的参数返回值、执行体封装成一个函数,并且存储该函数的内存地址。
-封装函数的调用环境:是指block会捕获变量,并把这些变量存储起来。

将OC上层的代码还原成c++代码的方式:

  • clang -rewrite-objc Input.m -o Output.cpp
  • xcrun -sdk iphoneos clang -arch arm64e -rewrite-objc Input.m -o Output.cpp

一、block的类型和使用

iOS开发中有三种类型的block,他们分别是全局block,堆block、栈block
全局block(__NSGlobalBlock__):没有访问外界局部普通变量的block就是全局block,存储在全局区。
block(__NSMallocBlock__):对栈block进行copy操作返回的block就是堆block,存储在堆区。
block(__NSStackBlock__):访问了外界普通局部变量的block就是栈block,存储在栈区。

注意:本章节的代码均在MRC环境下开启的调试,MRC下新建的仅访问局部普通变量的block是栈block,进行copy之后变成堆blockARC下编译器会自动将创建的栈block转换成堆block,如果不做转换可以在block前添加__weak关键字。例如void(^__weak testBlock)() = ^{}这样定义即可。

测试代码

- (void)main{
    int sValue = 20;
    static int gValue = 20;
    
    void(^gBlock)(void) = ^(){
        NSLog(@"value=%d", gValue);
    };
    id cgBlock = [gBlock copy];

    
    void(^sBlock)(void) = ^(){
        NSLog(@"value=%d", sValue);
    };
    
    id csBlock = [sBlock copy];
    
    NSLog(@"gBlock=%@,copy之后%@",gBlock,cgBlock);
    NSLog(@"sBlock=%@,copy之后%@",sBlock,csBlock);
}

运行结果:

gBlock=<__NSGlobalBlock__: 0x107728490>,copy之后<__NSGlobalBlock__: 0x107728490>
sBlock=<__NSStackBlock__: 0x7ffee84e1b68>,copy之后<__NSMallocBlock__: 0x600002e20960>

通过如上代码,我们知道如果仅访问的局部静态变量,那么他仍然是个全局block,全局block copy之后仍然是个全局block(仍指向原来的内存区域)。如果访问了局部普通变量,那么他就是栈block,拷贝之后成了一个全新的堆block(指向的内存区域地址发生了变化)。

在使用的过程中,包括三个部分block的声明(定义)、block的实现,block的调用:
block的声明的完整写法:返回值类型 + ( + ^ + 属性名称 + ) + 参数列表:

//作为属性
@property (nonatomic, copy) int (^calcHashValue)(id value);
//作为函数参数:
- (void)getHashValue:(int(^)(id value)) calcHashValue


//可以采用别名的方式简化block的定义:
typeof  int (^CalcHashValue)(id value);
//作为属性
@property (nonatomic, copy) CalcHashValue calcHashValue;
//作为函数参数:
- (void)getHashValue:(CalcHashValue)calcHashValue;

block的实现完整写法:^ + 返回值类型 + 参数列表 + 函数体 +;

//返回值类型:没有的话可以不写,也可以写`void`;
//参数列表 :用小括号把参数列表包裹起来,参数之间用逗号分隔。没有参数的话可以写成`(void)`或者不写。
self.calcHashValue = ^int(id value){ return 22222222;};

//无参数,无返回值案例:
typedef void(^TodoValue)(void);
@property (nonatomic, copy) TodoValue todoValue;
self.todoValue = ^void(void){};或者self.todoValue = ^{};

block的调用:

//需要注意的是在不能保证block有实现的时候,一定要检查是否为空
if(self.calcHashValue) self.calcHashValue(self);

block在使用的时候,会有一些需要注意的问题:

  • 1.block内部不能修改局部变量(直接报错:Variable is not assignable (missing __block type specifier))。如果需要修改则需要在局部变量的定义时加上__block关键字。
__block int localValue = 10;
void(^sBlock)(void) = ^(){
    NSLog(@"localValue=%d", localValue);//localValue=10
    localValue = 20;
};
sBlock();
NSLog(@"localValue=%d", localValue);//localValue=20
  • 2.block实现之后调用之前修改局部变量,block中拿到的仍然是旧值。如果要获得最新值则需要在局部变量定义时加上__block关键字。
//不加block关键字
int localValue = 10;
void(^sBlock)(void) = ^(){ 
    NSLog(@"localValue=%d", localValue); //localValue=10
};
localValue = 20;
sBlock();

//加block关键字
__block int localValue = 10;
void(^sBlock)(void) = ^(){
    NSLog(@"localValue=%d", localValue);//localValue=20
};
localValue = 20;
sBlock();

这个block关键字到底是做了什么呢?

二、block的底层实现

我们通过clang看下block底层是如何实现的

xcrun -sdk iphoneos clang -arch arm64e -rewrite-objc NXBlock.m

其中原始代码如下:

@interface NXBlock : NSObject
- (void)t1;
- (void)t2;
@end

@implementation NXBlock

- (void)t1{
    int localValue = 10;
    void(^noneBlock)(void) = ^(){
        NSLog(@"localValue=%d", localValue);
    };
    noneBlock();
}

- (void)t2{
    __block int localValue = 10;
    void(^withBlock)(void) = ^(){
        NSLog(@"localValue=%d", localValue);
    };
    withBlock();
}
@end

生成.cpp文件后我们拷贝出相关的代码片段:
如下本段是localValue没有使用__block修饰的:

struct __NXBlock__t1_block_impl_0 {
    struct __block_impl impl;
    struct __NXBlock__t1_block_desc_0* Desc;
    int localValue;
    __NXBlock__t1_block_impl_0(void *fp, struct __NXBlock__t1_block_desc_0 *desc, int _localValue, int flags=0) : localValue(_localValue) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static void __NXBlock__t1_block_func_0(struct __NXBlock__t1_block_impl_0 *__cself) {
    int localValue = __cself->localValue; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_fz_f2dd5f4545ggwp26d9pyjfwc0000gn_T_NXBlock_7c795f_mi_0, localValue);
}

static struct __NXBlock__t1_block_desc_0 {
    size_t reserved;
    size_t Block_size;
} __NXBlock__t1_block_desc_0_DATA = { 0, sizeof(struct __NXBlock__t1_block_impl_0)};

static void _I_NXBlock_t1(NXBlock * self, SEL _cmd) {
    int localValue = 10;
    void(*noneBlock)(void) = ((void (*)())&__NXBlock__t1_block_impl_0((void *)__NXBlock__t1_block_func_0, &__NXBlock__t1_block_desc_0_DATA, localValue));
    ((void (*)(__block_impl *))((__block_impl *)noneBlock)->FuncPtr)((__block_impl *)noneBlock);
}

如下本段是localValue使用__block修饰的:

struct __Block_byref_localValue_0 {
    void *__isa;
    __Block_byref_localValue_0 *__forwarding;
    int __flags;
    int __size;
    int localValue;
};

struct __NXBlock__t2_block_impl_0 {
    struct __block_impl impl;
    struct __NXBlock__t2_block_desc_0* Desc;
    __Block_byref_localValue_0 *localValue; // by ref
    __NXBlock__t2_block_impl_0(void *fp, struct __NXBlock__t2_block_desc_0 *desc, __Block_byref_localValue_0 *_localValue, int flags=0) : localValue(_localValue->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static void __NXBlock__t2_block_func_0(struct __NXBlock__t2_block_impl_0 *__cself) {
    __Block_byref_localValue_0 *localValue = __cself->localValue; // bound by ref
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_fz_f2dd5f4545ggwp26d9pyjfwc0000gn_T_NXBlock_7c795f_mi_1, (localValue->__forwarding->localValue));
}

static void __NXBlock__t2_block_copy_0(struct __NXBlock__t2_block_impl_0*dst, struct __NXBlock__t2_block_impl_0*src) {
    _Block_object_assign((void*)&dst->localValue, (void*)src->localValue, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static void __NXBlock__t2_block_dispose_0(struct __NXBlock__t2_block_impl_0*src) {
    _Block_object_dispose((void*)src->localValue, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static struct __NXBlock__t2_block_desc_0 {
    size_t reserved;
    size_t Block_size;
    void (*copy)(struct __NXBlock__t2_block_impl_0*, struct __NXBlock__t2_block_impl_0*);
    void (*dispose)(struct __NXBlock__t2_block_impl_0*);
} __NXBlock__t2_block_desc_0_DATA = { 0, sizeof(struct __NXBlock__t2_block_impl_0), __NXBlock__t2_block_copy_0, __NXBlock__t2_block_dispose_0};

static void _I_NXBlock_t2(NXBlock * self, SEL _cmd) {
    __attribute__((__blocks__(byref))) __Block_byref_localValue_0 localValue = {(void*)0,(__Block_byref_localValue_0 *)&localValue, 0, sizeof(__Block_byref_localValue_0), 10};
    void(*withBlock)(void) = ((void (*)())&__NXBlock__t2_block_impl_0((void *)__NXBlock__t2_block_func_0, &__NXBlock__t2_block_desc_0_DATA, (__Block_byref_localValue_0 *)&localValue, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)withBlock)->FuncPtr)((__block_impl *)withBlock);
}

我们通过一个表格来对比两者的异同:

对比 无修饰 __block修饰 static修饰 __weak修饰
定义 int localValue = 10; __block int localValue = 10; static int localValue = 10; __weak id self = self;
完整结构与构造函数 struct __NXBlock__t1_block_impl_0{}:
-1.包括impl、Desc和localValue三个变量。
2.其中localValue这个外部局部变量在内部的定义仍然是int localValue
3.构造函数包括调用的函数指针+描述+int型参数_localValue+flags,分别赋值给impl.FuncPtr、Desc、localValue和impl.Flags。
struct __NXBlock__t2_block_impl_0{}:
1.包括impl、Desc和localValue三个变量。
2.其中localValue这个外部局部变量在内部的定义为__Block_byref_localValue_0 *localValue;是一个结构体指针;
3.构造函数包括调用的函数指针+描述+__Block_byref_localValue_0型参数_localValue+flags,分别赋值给impl.FuncPtr、Desc、localValue和impl.Flags。
struct __NXBlock__t3_block_impl_0{};
1.包括impl、Desc和localValue三个变量。
2.其中localValue这个外部变量在内部的定义是int *localValue
3.构造函数包括调用的函数指针+描述+int*类型参数_localValue+flags,分别赋值给impl.FuncPtr、Desc、localValue和impl.Flags。
struct __NXBlock__t4_block_impl_0{};
1.包括impl、Desc和localValue三个变量。
2.其中localValue这个外部变量在内部的定义是__weak id localValue
3.构造函数包括调用的函数指针+描述+weak id类型参数_localValue+flags,分别赋值给impl.FuncPtr、Desc、localValue和impl.Flags
外部变量在内部的定义 struct->localValue访问原始数据; 封装在struct __Block_byref_localValue_0{}结构体中
1. 整形int类型变量localValue存储了外部传入的localValue值.
2.结构体指针类型__Block_byref_localValue_0 *的变量指向自己(localValue(_localValue->__forwarding)说明了这一点)。
3.struct->localValue->__forwarding->localValue即是原始localValue;
(*struct.localValue)访问原始数据。 struct.localValue访问原始数据
block函数指针 __NXBlock__t1_block_func_0(...)即是外部调用block的执行函数。
1.函数包括一个完整block的参数__cself。
2.通过__cself->localValue获得外界的值。
__NXBlock__t2_block_func_0(...)即是外部调用block的执行函数。
1.函数包括一个完整block的参数__cself。
2.通过__cself->localValue->__forwarding->localValue获得外界的值。
__NXBlock__t3_block_func_0(...)即是外部调用block的执行函数。
1.函数包括完整block的参数__cself。
2.通过(*__cself.localvalue)获得外界的值。
__NXBlock__t4_block_func_0(...)即是外部调用block的执行函数。
1.函数包括完整block的参数__cself。
2.通过__cself.localvalue获得外界的值
描述结构 __NXBlock__t1_block_desc_0:
1:保留字段reserved默认值为0;
2.Block_size记录完成结构体的大小;
__NXBlock__t2_block_desc_0;
1.保留字段reserved默认值为0。
2.Block_size记录完成结构体的大小;
3.copy函数指针;
4.dispose函数指针;
__NXBlock__t3_block_desc_0:
1:保留字段reserved默认值为0;
2.Block_size记录完成结构体的大小;
__NXBlock__t4_block_desc_0:
1:保留字段reserved默认值为0;
2.Block_size记录完成结构体的大小;
copy函数 / __NXBlock__t2_block_copy_0; / /
dispose函数 / __NXBlock__t2_block_dispose_0; / /
  • __Block_byref_localValue_0构造过程中传给__forwarding的是__Block_byref_localValue_0类型,是变量自身,也就是src.__forwarding指向的是自己,这个是block在栈区的情况。
  • 当栈区block被复制到堆区的时候,src结构体会被复制一份,复制出来的在堆区copy. __forwarding = copy;并且栈区src. __forwarding = copy;了。
  • 这样以来,无论是通过访问原有的栈区的block还是新拷贝的堆block,那么通过block. byref. __forwarding获取到的都是堆内存中的那一份。

我们可以通过一段代码验证这个结果:

__block int localValue = 10;
NSString *(^stackBlock)(void) = ^NSString *(){
    return [NSString stringWithFormat:@"&localValue=%p", &localValue];
};
NSLog(@"拷贝前:%@-%@", stackBlock, stackBlock());
NSString *(^mallocBlock)(void) = [stackBlock copy];
NSLog(@"拷贝后:%@-%@", stackBlock, stackBlock());
NSLog(@"拷贝后:%@-%@", mallocBlock, mallocBlock());

打印结果

拷贝前:<__NSStackBlock__: 0x7ffee17f6b60>-&localValue=0x7ffee17f6ba8
拷贝后:<__NSStackBlock__: 0x7ffee17f6b60>-&localValue=0x600000472878
拷贝后:<__NSMallocBlock__: 0x600000a5d200>-&localValue=0x600000472878

可以看到拷贝后,新生成了一个堆block对象并且block捕获的localValue的地址发生了变化,由0x7ffee17f6ba8变成了0x600000472878,而且原有的栈block捕获的localValue的地址也由0x7ffee17f6ba8变成了0x600000472878,跟堆block保持一致。

细节逻辑可以在源码中看到:

// src points to stack
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy;  // patch stack to point to heap copy
copy->size = src->size;

简单总结:
1.没有使用__block关键字修饰的localValue是一个简单int类型,传入内部也是的int类型的localValue变量.
2.使用__block关键字修饰的localValue会生成为__Block_byref的结构体,传入内部也是__Block_byref类型的localValue结构体指针。
3.栈上的block进行copyblock本身会在堆上开辟内存;__Block_byref在堆上新开辟内存;捕获的外部变量也会在堆上新开辟内存。原有的栈block__Block_byref__forwarding会指向堆block__Block_byref

三、block中的循环引用

如果对象A强持有对象B:
-初始化完成后A的引用计数为1,B的引用计数为1。B的引用计数会因为A的持有而+1变成2。
-在AB出作用域后系统会给AB分别发送一个release消息,A的引用计数-1变成0;B的引用计数-1变成1。
-A引用计数变成0调用dealloc方法进行销毁,A调用dealloc方法时也会给B发送一个release消息,B的引用计数-1变成0,则B会调用dealloc方法进行销毁。

如果对象A强持有对象B,B也强持有A:
-初始化完成后AB引用计数为1,相互赋值后两者的引用计数都变成2。
-AB出作用域后系统分别给AB发送release消息。A的引用计数变成1,B的引用计数变成1。
-两者维持引用计数为1而得不到释放,造成内存泄漏。

而block中的循环引用出现的通常是由于self强制有block,而block又强持有self造成的循环引用。如果能通过weak断开引用环那么问题就解决了。

2.1.使用weak(/strong)修饰断开环的方式避免循环引用:

准备一段代码:

@interface NXTester: NSObject
@property (nonatomic, copy) void(^work)(void);
@property (nonatomic, copy) void(^test)(void);
@end

@implementation NXTester
- (void)dealloc{
    NSLog(@"NXTester-dealloc");
}
@end

案例1:如下代码会造成循环引用,tester持有work,work持有tester,形成环构成循环引用。解决方案看案例2.

NXTester *tester = [[NXTester alloc] init];
tester.work = ^{
   tester;
};
tester.work();

案例2:如下代码不会造成循环引用,tester持有work,work持有weakself, weakself弱持有tester。弱引用断开了这个闭环,不会循环引用。这一点原因可以参考上述表格的__weak修饰的局部变量部分,在内部定义成__weak NXTester *类型。

NXTester *tester = [[NXTester alloc] init];
__weak NXTester *weakself = tester;
tester.work = ^{
   weakself;
};
tester.work();

案例3:如下代码会造成循环引用,注释掉的部分也会循环引用。因为strongself强持有test,test又强持有strongself,形成环构成循环引用,解决办法参考案例4.

NXTester *tester = [[NXTester alloc] init];
__weak NXTester *weakself = tester;
tester.work = ^{
   //weakself.test = ^{
   //   weakself;
   //};
   //weakself.test();
  //或者
   __strong NXTester *strongself = weakself;
   strongself.test = ^{
      strongself;
   };
   strongself.test();
};
tester.work();

案例4:如下代码不会造成循环引用。通过weak断开了引用环。

NXTester *tester = [[NXTester alloc] init];
__weak NXTester *weakself = tester;
tester.work = ^{
   __strong NXTester *strongself = weakself;
   __weak NXTester *weakweakself = strongself;
   strongself.test = ^{
      weakweakself;
   };
   strongself.test();
};
tester.work();

如上我们看到第一种解决循环引用的方式是weak-stong修饰。block内部需要访问的变量用在block外先用weak修饰,内部使用时在用strong修饰。

2.2、使用参数传递来解决循环引用的问题:

以上案例中,我们需要在block内部捕获外部的变量。我们也可以通过参数传递的方法来解决。

@interface NXTester: NSObject
@property (nonatomic, copy) void(^work)(NXTester *value);
@end

@implementation NXTester
- (void)dealloc{
    NSLog(@"NXTester-dealloc");
}
@end

使用:

NXTester *tester = [[NXTester alloc] init];
tester.work = ^(NXTester *value){
    value;
};
tester.work(tester);

2.3.使用完毕后将对象设置为nil

如果一个通过block执行任务后不再需要保留了,那么可以在block中将对象设置为nil,或在不使用之后把block或者对象设置为nil都可以解决问题。这种方案不推荐。

注意事项:
有些场景下不一定会造成循环引用,关键看有没有无法断开的引用环。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容