Block浅探

116 阅读16分钟

Block的本质

  • block本质上也是一个OC对象,它内部也有个isa指针
  • block是封装了函数调用以及函数调用环境的OC对象
  • block底层结构如下:
//block的底层结构
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  //构造函数(C++) 
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//其中struct __block_impl结构
struct __block_impl {
  void *isa;//isa指针
  int Flags;
  int Reserved;
  void *FuncPtr;
};

PS:以上是通过clang指令后的cpp文件代码(截取主要代码):xcrun-sdk iphoneos clang-arch arm64-rewrite-objc main.m-main.cpp 。其中main.m文件如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block1)(void) = ^{
                NSLog(@"block1");
            };
    }
    return0;
}

Block的基本声明和表现形式

1、关于声明

(1)在interface中声明Block时,有两种方法。如下的block1和block2:

typedef void(^BLOCK2)(void); 
@interface ViewController () 
//第一种:直接声明,按照block的声明样式写就可以
@property (copy, nonatomic) void(^block1)(void); 
//第二种:先将BLOCK2进行定义类型,然后再定义变量block2
@property (copy, nonatomic) BLOCK2 block2; 
@end

(2)在MRC下,声明block使用copy修饰; 在ARC下,声明block使用strong和copy修饰都可以,一般建议使用copy。

2、表现形式(返回值或参数以int为例)

(1)无参数,无返回值

void(^block1)(void) = ^{ 
    NSLog(@"block1"); 
};

(2)无参数,有返回值()

int(^block2)(void) = ^int{ 
    NSLog(@"block2"); 
    return 1
};
// 定义的时候,返回值可以省略,如下:
int(^block2)(void) = ^{ 
    NSLog(@"block2"); 
    return 1
};

(3)有参数,无返回值

void(^block3)(int) = ^(int a){ 
    NSLog(@"block3---%d",a); 
};

(4)有参数,有返回值

int(^block4)(int) = ^int(int a){ 
    return a; 
};

Block的变量捕获

为了保证block内部能够正常访问外部的变量,block有个变量捕获机制

1、局部变量

(1)auto变量类型

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // int age = 10;也即 auto int age = 10;只是默认去掉auto来展示
        int age = 10;
        // 定义block变量
        void(^block)(void) = ^{
            NSLog(@"block---%d",age);
        };
        int age = 20;
        // 执行block内部代码
        block();
    }
    return 0;
}

// 输出结果为:block---10

输出结果为何是block---10,而不是block---20呢? 首先我们通过clang命令来窥探一下底层的实现原理。

// 下面看一下main函数的底层结构(去掉没必要的干扰,简化后的)
int main(int argc, const char * argv[]) {
   { __AtAutoreleasePool __autoreleasepool;
        int age = 10;
        // 定义block变量
        void(*block)(void) = &__main_block_impl_0(__main_block_func_0,
                                                   &__main_block_desc_0_DATA,
                                                   age);
        int age = 20;
        // 执行block内部代码
        block->FuncPtr(block);
    }
    return 0;
}

// __main_block_impl_0 结构体
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  // 结构体的构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

  • 在__main_block_impl_0 结构体中可以看到,定义了一个同名变量age,用来接收传过来的值,其中age(_age)是将_age的值赋给age。PS: age(_age)是c++的写法,代表age=_age

  • main中,执行block内部代码,block内部通过调用__main_block_impl_0结构体的构造函数(其中传入的参数age = 10),从而使得结构体中的age赋值为10。仅仅只是值的传递,因此外面的age=20,只是改变外面的age,和__main_block_impl_0结构体内定义的age无关。

(2)static变量类型

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        static int age = 10;
        // 定义block变量
        void(^block)(void) = ^{
            NSLog(@"block---%d",age);
        };
        int age = 20;
        // 执行block内部代码
        block();
    }
    return 0;
}

// 输出结果为:block---20

我们通过clang命令来窥探一下底层的实现原理:

// 下面看一下main函数的底层结构(去掉没必要的干扰,简化后的)
int main(int argc, const char * argv[]) {
   { __AtAutoreleasePool __autoreleasepool;
        static int age = 10;
        // 定义block变量
        void(*block)(void) = &__main_block_impl_0(__main_block_func_0,
                                                   &__main_block_desc_0_DATA,
                                                   &age);
        int age = 20;
        // 执行block内部代码
        block->FuncPtr(block);
    }
    return 0;
}

// __main_block_impl_0 结构体
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *age;
  // 结构体的构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

  • 在__main_block_impl_0 结构体中可以看到,定义了一个age的指针int *。
  • main中,执行block内部代码,block内部通过调用__main_block_impl_0结构体的构造函数,将外面age的指针&age作为参数传入,并赋值给block内部定义的age指针。age和block里面的定义的age指向的是同一块内存,因此输出为20。

2、全局变量

int age = 10;
static int height = 190;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 定义block变量
        void(^block)(void) = ^{
            NSLog(@"age---%d",age);
            NSLog(@"height---%d",height);
        };
        age = 20;
        height = 170;
        // 执行block内部代码
        block();
    }
    return 0;
}

// 输出结果为:age---20,height---170

我们通过clang命令来窥探一下底层的实现原理:

int age = 10;
static int height = 190;

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

int main(int argc, const char * argv[]) {
   { __AtAutoreleasePool __autoreleasepool;
        static int age = 10;
        // 定义block变量
        void(*block)(void) = &__main_block_impl_0(__main_block_func_0,
                                                   &__main_block_desc_0_DATA,
                                                   &age);
        age = 20;
        height = 170;
        // 执行block内部代码
        block->FuncPtr(block);
    }
    return 0;
}

在__main_block_impl_0 结构体中可以看到,内部没有新定义age,或者height,说明block内部并没有捕获外部的全局变量。最后调用函数,打印的age和height是全局变量。

3、block访问外部变量的总结

image.png

Block的类型

block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型,再往上是继承NSObject类型。

  • NSGlobalBlock ( _NSConcreteGlobalBlock )
  • NSStackBlock ( _NSConcreteStackBlock )
  • NSMallocBlock ( _NSConcreteMallocBlock )
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 定义block变量
        void(^block)(void) = ^{
            
        };
        NSLog(@"1 -- %@",[block class]);
        NSLog(@"2 -- %@",[[block class] superclass]);
        NSLog(@"3 -- %@",[[[block class] superclass] superclass]);
        NSLog(@"4 -- %@",[[[[block class] superclass] superclass] superclass]);
    }
    return 0;
}
/*
输出结果为:
1 -- __NSGlobalBlock__
2 -- NSBlock
3 -- NSObject
4 -- (null)
*/

block的3种类型具体在哪些环境中体现呢?

block类型环境
NSGlobalBlock没有访问auto变量
NSStackBlock访问了auto变量
NSMallocBlockNSStackBlock类型调用了copy

其中NSStackBlock只针对于MRC环境,因为ARC环境系统会自动帮我们做了一次copy操作。比如:

// 1、ARC环境下
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 定义block变量
        int age = 10;
        void(^block)(void) = ^{
            NSLog(@"age----%d",age);
        };
        NSLog(@"blockClass---%@",[block class]);
    }
    return 0;
}
/* 输出的class结果为:blockClass---__NSMallocBlock__ */

// 2、MRC环境下(没有进行copy操作)

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 定义block变量
        int age = 10;
        void(^block)(void) = ^{
            NSLog(@"age----%d",age);
        };
        NSLog(@"blockClass---%@",[block class]);
    }
    return 0;
}

/* 输出的class结果为:blockClass---__NSStackBlock__ */

// 3、MRC环境下(对block进行了copy操作)

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 定义block变量
        int age = 10;
        void(^block)(void) = [^{
            NSLog(@"age----%d",age);
        } copy];
        NSLog(@"blockClass---%@",[block class]);
    }
    return 0;
}

/* 输出的class结果为:blockClass---__NSMallocBlock__ */

每一种类型的block调用copy后的结果如下:

Block类型副本源的配置存储域复制的效果
NSGlobalBlock程序的数据区域什么也不做
NSStackBlock从栈复制到堆
NSMallocBlock引用计数增加

在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况

  • block作为函数返回值时
  • 将block赋值给__strong指针时
  • block作为Cocoa API中方法名含有usingBlock的方法参数时
  • block作为GCD API的方法参数时

MRC下block属性的建议写法

  • @property (copy, nonatomic) void (^block)(void);

ARC下block属性的建议写法

  • @property (strong, nonatomic) void (^block)(void);
  • @property (copy, nonatomic) void (^block)(void);

Block内部关于访问对象类型的auto变量

首先先创建一个MyPerson对象

// MyPerson.h
@interface MyPerson : NSObject
@property (assign, nonatomic) int age;
@end

// MyPerson.m
#import "MyPerson.h"

@implementation MyPerson

- (void)dealloc
{
//    [super dealloc];
    NSLog(@"MyPerson - dealloc");
}

@end

然后定义main函数中的block访问auto对象类型

typedef void(^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 定义block变量
        MyBlock block;
        {
            MyPerson *person = [[MyPerson alloc] init];
            person.age = 20;
            block = ^{
                NSLog(@"age----%d",person.age);
            };
        }
        NSLog(@"blockClass---%@",[block class]);
    }
    return 0;
}
/*
打印的结果为:
blockClass---__NSMallocBlock__
MyPerson - dealloc
*/

从打印的结果,可以看出,block类型为__NSMallocBlock__。即存在于堆上。等block释放后MyPerson对象才释放,可以猜测到block对person对象进行了强引用。接下来我们通过clang指令后的cpp文件代码(截取主要代码)

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  MyPerson *__strong person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, MyPerson *__strong _person, int flags=0) : person(_person) {
    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->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 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};
  • 可以看出__main_block_impl_0结构体中,捕抓了MyPerson *person变量(强指针)。而block为NSMallocBlock类型,位于堆上,可拥有person变量。而block里面的person变量指向MyPerson对象,因此,block没有销毁时,MyPerson对象不能自动销毁,只有block销毁了,MyPerson对象才能销毁;
  • 而我们发现 __main_block_desc_0结构体多了void (*copy)和void (*dispose)两个函数指针,void (*copy)函数指针指向__main_block_copy_0函数,而void (*dispose)函数指针指向__main_block_dispose_0函数;
  • 当block进行copy操作时,block内部会自动调用__main_block_copy_0函数,而__main_block_copy_0内部调用了_Block_object_assign,_Block_object_assign的作用是根据auto变量的强或者弱修饰符来做出相应操作,形成强引用或者弱引用;
  • 当block从堆上移除时,block内部会调用__main_block_dispose_0函数中的_Block_object_dispose来释放auto变量。

对于弱引用,如下所示:

typedef void(^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 定义block变量
        MyBlock block;
        {
            __weak MyPerson *person = [[MyPerson alloc] init];
            person.age = 20;
            block = ^{
                NSLog(@"age----%d",person.age);
            };
        }
        block();
        NSLog(@"blockClass---%@",[block class]);
    }
    return 0;
}

/* 
断点在block(),运行到断点处,打印了:Person - dealloc。说明person在block没有销毁前就已经提前释放了,所以block没有强引用auto变量。
*/

// 通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-15.0.0 main.m(兼容带有__weak的转换)转换成cpp文件代码。提取出不一样的地方如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  MyPerson *__weak person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, MyPerson *__weak _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

看出,加__weak后,__main_block_impl_0中的person变量变成了__weak修饰。

当我们切换到MRC环境:

typedef void(^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 定义block变量
        MyBlock block;
        {
            MyPerson *person = [[MyPerson alloc] init];
            person.age = 20;
            block = ^{
                NSLog(@"age----%d",person.age);
            };
            [person release];
        }
        block();
        NSLog(@"blockClass---%@",[block class]);
    }
    return 0;
}

断点到block()这一行,运行到断点时,打印MyPerson - dealloc,说明person对象提前被释放了。此时打印blockClass的类型为__NSStackBlock__,得出block是在栈上,不会对auto变量强引用。所以block还没释放前,auto变量已提前释放。

如果在MRC环境对block实行copy操作会是什么情况呢?如下:

typedef void(^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 定义block变量
        MyBlock block;
        {
            MyPerson *person = [[MyPerson alloc] init];
            person.age = 20;
            block = [^{
                NSLog(@"age----%d",person.age);
            } copy];
            [person release];
        }
        block();
        [block release];
        NSLog(@"blockClass---%@",[block class]);
    }
    return 0;
}

同样断点到block()这一行,运行到断点时,person不会释放,只有释放block后,auto变量才会被释放。打印的blockClass为__NSMallocBlock__。这说明block执行copy操作后,被拷贝到了堆上,而block指向对象是有强指针引用的,因此会默认对其进行copy操作,将block指向的对象存放在堆区,因此是可以拥有其内部的变量person。

总结:当block内部访问了对象类型的auto变量时

  1. 如果block是在栈上,将不会对auto变量产生强引用

  2. 如果block被拷贝到堆上

  • 会调用block内部的copy函数
  • copy函数内部会调用_Block_object_assign函数
  • _Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
  1. 如果block从堆上移除
  • 会调用block内部的dispose函数
  • dispose函数内部会调用_Block_object_dispose函数
  • _Block_object_dispose函数会自动释放引用的auto变量(release)
  1. 当栈上的block复制到堆时,调用copy函数;当堆上的block被废弃时,调用dispose函数

Block的__block修饰符

__block可以用于解决block内部无法修改auto变量值的问题, 但不能修饰全局变量、静态变量(static)。

__block int age = 10;
MyBlock block = ^{
    age = 20;
    NSLog(@"age---%d",age);
};
block();

过clang指令后的cpp文件代码(截取主要代码)

// __Block_byref_age_0结构体
struct __Block_byref_age_0 {
  void *__isa; //isa指针,代表该类型是一个对象
  __Block_byref_age_0 *__forwarding; //接收自己的地址
  int __flags;
  int __size; //改类型值的大小
  int age; //__block修饰的变量age=10
};

// __main_block_impl_0新创建了一个__Block_byref_age_0类型的age对象
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,
  __Block_byref_age_0 *_age, int flags=0) : age(_age->__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_age_0 *age = __cself->age; // bound by ref

            (age->__forwarding->age) = 20;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_m2_p61bk9fj11106mwvj765gjbr0000gn_T_main_469f18_mi_0,(age->__forwarding->age));
        }
        
// 从以下简化后的main函数中,我们可以清楚看到
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        // __block int age = 10
        __Block_byref_age_0 age = {
            0,
            &age,
            0,
            sizeof(__Block_byref_age_0),
            10};
        // 定义block
        MyBlock block = &__main_block_impl_0(__main_block_func_0,
                                             &__main_block_desc_0_DATA,
                                             &age,
                                             570425344);
        // 调用block
        block->FuncPtr(block);
    }
    return 0;
}
  • 上面可以看出,block并没有直接捕获age值,而是编译器将__block变量包装成一个类型为__Block_byref_age_0的对象。
  • main函数中__block int age = 10变成了__Block_byref_age_0结构体,将&age(定义好的结构体变量的地址即自己的地址)传给了结构体中的__forwarding,将sizeof(__Block_byref_age_0)自己的大小传给了结构体中的size,将10传给了结构体中的age。
  • 定义block中,&age(block外面定义的age的地址)传给了定义的__main_block_impl_0构造函数中的__Block_byref_age_0,使得block内部有个__Block_byref_age_0 *age指针指向结构体__Block_byref_age_0,而__Block_byref_age_0结构体内部又有个age的成员存储了10的值。
  • 当修改age值时,找到block内部函数__main_block_func_0,(age->__forwarding->age) = 20;通过age拿到__forwarding,也就是拿到自己的指针地址,然后再通过地址拿到结构体里面的age,从而实现了修改。

__block的内存管理

  1. 当block在栈上时,并不会对__block变量产生强引用。
  2. 当block被copy到堆时
  • 会调用block内部的copy函数
  • copy函数内部会调用_Block_object_assign函数
  • _Block_object_assign函数会对__block变量形成强引用(retain)

当在栈上两个block(分别命名为block1和block2)同时引用一个__block修饰的变量时,对block1进行copy操作后,block1会从栈复制到堆上面,同时,会将block1内部访问的__block修饰的变量也复制到堆上面,在堆上面的block1还是会对堆上面的__block修饰的变量进行强引用。如果block2也进行copy操作,拷贝到堆上,因为__block修饰的变量已经拷贝到了堆上了,就不用重复拷贝,只需要把堆上的block1也强引用堆上__block修饰的变量就可以了。

  1. 当block从堆中移除时
  • 会调用block内部的dispose函数
  • dispose函数内部会调用_Block_object_dispose函数
  • _Block_object_dispose函数会自动释放引用的__block变量(release)

当只有一个堆上的block对堆上的__block修饰的变量进行强引用,block进行dispose操作后,block会废弃。__block修饰的变量没有了强引用指向,也会被释放掉。 如果是两个堆上的block同时对堆上的__block修饰的变量进行强引用,等两个block进行dispose操作后分别废弃掉后,__block修饰的变量没有了强引用指向,也会被释放掉。

__block的__forwarding指针

为什么不直接用age,而使用age->__forwarding->age的方式来访问?

  • 如果我们访问的age变量在栈上的时候,可以直接访问age,但是如果age变量被拷贝到堆上,直接访问,还是访问到栈上的age,这样就出问题了;(复制并不是剪切,栈上面是还有的。)
  • 通过__forwarding找到它自己,然后访问里面的age,在栈上也很容易找到age,这是行的通的,虽然复杂了一步,但是age变量拷贝到堆上后,__forwarding指针会指向堆上面的__Block_byref_age_0结构体,这样里面访问的__block修饰的变量,确保了是堆上面的。

__block修饰的对象类型

  1. 当__block变量在栈上时,不会对指向的对象产生强引用

  2. 当__block变量被copy到堆时

  • 会调用__block变量内部的copy函数
  • copy函数内部会调用_Block_object_assign函数
  • _Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain)
  1. 如果__block变量从堆上移除
  • 会调用__block变量内部的dispose函数
  • dispose函数内部会调用_Block_object_dispose函数
  • _Block_object_dispose函数会自动释放指向的对象(release)

Block的关于循环引用

循环引用问题

看如下代码:

// MyPerson.h

typedef void(^MyBlock)(void);

@interface MyPerson : NSObject
@property (assign, nonatomic) int age;
@property (copy, nonatomic) MyBlock block;
@end

// MyPerson.m
#import "MyPerson.h"

@implementation MyPerson

- (void)dealloc
{
//    [super dealloc];
    NSLog(@"MyPerson - dealloc");
}
@end
// person声明了一个block变量

main.m如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        MyPerson *person = [[MyPerson alloc] init];
        person.age = 20;
        person.block = ^{
            NSLog(@"age----%d",person.age);
        };
        NSLog(@"------");
    }
    return 0;
}

程序运行结束只打印:------。person并没有释放,造成了内存泄露(循环引用)。

循环引用原因

接下来我们结合.cpp文件分析一下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  MyPerson *__strong person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, MyPerson *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

由MyPerson *__strong person,可以看出block内部其实是有一个强指针引用了MyPerson类型的person对象,指向着MyPerson;而MyPerson中定义的_block的成员变量,强指针指向着block。程序运行结束,局部变量person被销毁了,但是MyPerson和block还被互相强引用着,没办法销毁,从而造成了循环引用。

如何解决循环引用

  1. 通过__weak来解决
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        MyPerson *person = [[MyPerson alloc] init];
        person.age = 20;
        
        __weak MyPerson *weakPerson = person;
        person.block = ^{
            NSLog(@"age----%d",weakPerson.age);
        };
    }
    return 0;
}
// 程序运行结束打印了:MyPerson - dealloc

通过其.cpp文件可以看出block内部有一个弱指针引用了MyPerson类型的person对象

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  MyPerson *__weak weakPerson;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, MyPerson *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

由于block内部指向MyPerson的为弱指针,当局部变量person被销毁,因为没有强引用,MyPerson对象也跟着销毁,从而解决了循环引用。

  1. 通过__unsafe_unretained来解决

__unsafe_unretained解决循环引用也类似于__weak,这里就不做过多的叙述,不过值得注意的是:

  • __weak:不会产生强引用,指向的对象销毁时,会自动让指针置为nil
  • __unsafe_unretained:不会产生强引用,不安全,指向的对象销毁时,指针储存的地址值不变
  1. 通过__block来解决
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __block MyPerson *person = [[MyPerson alloc] init];
        person.age = 20;
        
        person.block = ^{
            NSLog(@"age----%d",person.age);
            person = nil;
            
        };
        person.block();
    }
    return 0;
}
// 也会输出:MyPerson - dealloc

再看一下.cpp文件


struct __Block_byref_person_0 {
  void *__isa;
__Block_byref_person_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 MyPerson *__strong person;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_person_0 *person; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_person_0 *_person, int flags=0) : person(_person->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
  • 可以看出存在三个对象,一个是__block包装成的对象(__Block_byref_person_0),一个是block对象,一个是MyPerson对象;
  • block对象对__Block_byref_person_0对象产生强引用,而__Block_byref_person_0对象对MyPerson对象产生强引用,MyPerson对象对block对象产生强引用;
  • 当执行到person = nil,__Block_byref_person_0对象对MyPerson对象产生强引用会断开,没了引用,person执行完就会被销毁,从而解决了循环引用问题。

值得注意的是:person.block();必须调用一次,为了执行person = nil;

4、MRC应该注意 在MRC下,可以使用__unsafe_unretained解决循环引用。因为MRC没有__weak,也就不存在使用__weak解决循环引用的问题了。也可以使用__block解决循环引用问题。

--对于Block浅探就到此。有啥纠正、疑问或补充,欢迎踊跃拍板。--

参考和引用