Block循环引用

木木木大约 5 分钟iOSBlock底层原理

Block循环引用

在Objective-C中,Block是一种类似于匿名函数的语法结构,它可以用来封装一段代码,并在需要时执行该代码。尽管Block提供了便利的语法结构,但是在实际使用中,我们经常会遇到一个问题,即Block循环引用。

Block循环引用是指在Block内部捕获了一个对象,并且该对象又持有了该Block,从而导致两者互相持有,无法释放内存。这个问题不仅会导致内存泄漏,还会影响程序的性能和稳定性。

Block的本质

在深入探讨Block循环引用之前需要了解一下Block的本质。在Objective-C中,Block实际上是一种OC对象,它是由以下三个部分组成的:

  • 函数指针:指向实际要执行的代码。

  • Block descriptor:描述Block的内存布局和引用的对象。

  • Block invoke function:执行Block的函数。 其中,Block descriptor是关键部分,它包含了Block的内存布局和Block捕获的对象信息。具体来说,Block descriptor包含了以下信息:

  • Block的引用计数。

  • Block的标志位,用于标识Block的一些特性,例如是否为全局Block、是否为栈上Block等。

  • Block捕获的对象信息,包括捕获的对象数量、对象类型和对象的值。 在实际使用中,Block会自动捕获其所在函数的局部变量,这些局部变量会作为Block的捕获对象,存储在Block descriptor中。

Block循环引用的原因

由于Block descriptor中包含了Block捕获的对象信息,因此如果Block内部捕获了一个对象,并且该对象又持有了该Block,就会导致两者互相持有,从而产生循环引用。这个问题可以通过以下示例代码来说明:

@interface Person : NSObject

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

@end

@implementation Person

- (void)dealloc {
    NSLog(@"Person dealloc");
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc] init];
    person.block = ^{
        NSLog(@"%@", person);
    };
}

@end

创建了一个Person对象,并将一个Block赋值给其属性block。这个Block内部捕获了person对象,而person对象又持有了这个Block,从而产生了循环引用。在这种情况下,即使我们将person对象设置为nil,它也无法被释放,因为Block仍然持有了person对象。

解决循环引用的方法

为了解决Block循环引用问题,我们需要避免在Block内部捕获持有Block的对象。下面介绍两种常用的解决方法。

__weak

在Block内部使用__weak指针可以解决循环引用问题。__weak指针是一种弱引用,它可以指向一个对象,但不会增加该对象的引用计数。当被指向的对象被释放时,__weak指针会自动置为nil。

以下是使用__weak指针解决循环引用的示例代码:

@interface Person : NSObject

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

@end

@implementation Person

- (void)dealloc {
    NSLog(@"Person dealloc");
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc] init];
    __weak typeof(person) weakPerson = person;
    person.block = ^{
        NSLog(@"%@", weakPerson);
    };
}

@end 在上述代码中,我们使用__weak指针weakPerson来引用person对象,并在Block内部使用weakPerson来打印person对象。由于weakPerson是弱引用,它不会持有person对象,因此不会产生循环引用。

__block

另一种解决循环引用的方法是在Block内部使用__block修饰符来捕获持有Block的对象。__block修饰符是一种特殊的指针类型,它可以让变量在Block内部被修改,并且不会被复制到Block中。

以下是使用__block修饰符解决循环引用的示例代码:

@interface Person : NSObject

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

@end

@implementation Person

- (void)dealloc {
    NSLog(@"Person dealloc");
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    __block Person *person = [[Person alloc] init];
    person.block = ^{
        NSLog(@"%@", person);
        person = nil;
    };
}

@end

使用__block修饰符来捕获person对象,并在Block内部打印person对象。由于使用了__block修饰符,person对象可以在Block内部被修改,从而可以在Block内部将其置为nil,避免了循环引用。

底层分析Block循环引用的原因

Block 结构体的定义:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __block_literal {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
  struct {
    unsigned long int reserved;
    unsigned long int Size;
    // 这里是存储捕获变量的结构体
    void (*copy)(struct __block_literal*, struct __block_literal*);
    void (*dispose)(struct __block_literal*);
    // 这里是存储捕获变量的具体值
    // 注意:这里的数组长度是通过 Size 计算得到的
    char data[];
  } *descriptor;
};

最后,探讨一下底层是如何实现Block循环引用的。在Objective-C中,对象之间的循环引用通常是通过强引用产生的。在Block循环引用中,强引用主要是由Block捕获对象产生的。

在Block捕获对象时,Block会通过copy函数将捕获的对象复制到堆上,并且将复制后的对象存储在Block descriptor中。由于Block descriptor是一个结构体,它不包含任何引用计数的信息,因此当Block中捕获了一个对象时,该对象的引用计数不会被增加。这意味着如果Block持有一个对象,该对象就可能被Block循环引用。

例如,在上面的示例代码中,当Block持有person对象时,由于Block没有增加person对象的引用计数,person对象就可能被Block循环引用。

为了解决这个问题,Objective-C提供了__weak指针和__block修饰符。使用__weak指针可以避免在Block内部持有对象,从而避免循环引用。使用__block修饰符可以让Block内部修改对象,从而在Block内部将对象置为nil,避免循环引用。

总结

在Objective-C中,Block是一种强大的语言特性,可以用于实现回调、事件处理等功能。但是,由于Block循环引用的问题,如果不注意使用方式,就容易造成内存泄漏等问题。

上次编辑于:
贡献者: perhapsdone