从声明定义到底层原理,搞懂block的全部内容系列文章(三)block的变量捕获及内存管理

本文详细介绍了Objective-C中block的三种类型(全局、栈、堆),并探讨了在MRC和ARC模式下的内存管理规则,包括block的复制行为和如何处理循环引用问题,特别是__weak和__strong指针的使用策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

三 block的类型及内存管理
回顾及前言:

上篇文章中,我们看到了block的底层实现,知道了block作为oc对象,会封装函数及函数调用环境,而且我们知道函数指的是block代码体,函数调用环境其实就是block代码体所引用的外部变量。我们也可以换个常见的说法,block对象会捕获block代码体用到的外部变量。这篇文章,重头戏就在外部变量这个概念上。外部变量不同,block对象的类型也会不同,而block类型不同,就会导致内存管理的差别。所以本文将分别介绍block的类型和block的内存管理。

(一) block的类型

在上一篇,我们可以看到__main_block_impl构造函数中将isa指针赋值为_NSConcreteStackBlock,_NSConcreteStackBlock就是block的一种类型,那么block一共几种类型呢?先说结论,block一共有三种类型(全局、栈、堆的概念,属于编程基础知识,不在本文的介绍范围):

  1. _NSConcreteGlobalBlock:全局block,当代码体捕获了全局变量、静态局部变量。
  2. _NSConcreteStackBlock:栈block,当代码体没有捕获变量或捕获了非静态局部变量(甭管是基本类型如int还是引用类型如NSObject *,都是静态局部变量,关于引用类型如果不明白啥意思,推荐去看一下C语言指针部分内容)时。
  3. _NSConcreteMallocBlock:堆block,栈block执行copy操作,会变成堆block。

下面用代码实际展示一下上述三种情况,注意是mrc模式下

//MRC
int a = 10;
int main(int argc, const char * argv[]) {
   @autoreleasepool {
       //global block
       void (^block1)(void) = ^{
           NSLog(@"%d",a);
       };
       NSLog(@"%@",[block1 class]); //__NSGlobalBlock__
       
       static int b = 20;
       void (^block2)(void) = ^{
           NSLog(@"%d",b);
       };
       NSLog(@"%@",[block2 class]);//__NSGlobalBlock__
       
       void (^block3)(void) = ^{
           
       };
       NSLog(@"%@",[block3 class]);//__NSGlobalBlock__
       
       //stack block
       int c = 10;
       void (^block4)(void) = ^{
           NSLog(@"%d",c);
       };
       NSLog(@"%@",[block4 class]);//__NSStackBlock__
       
       NSObject *obj = [[NSObject alloc]init];
       void (^block5)(void) = ^{
           NSLog(@"%@",obj);
       };
       NSLog(@"%@",[block5 class]);//__NSStackBlock__
       NSLog(@"下面是copy操作");
       NSLog(@"%@",[[block1 copy] class]); //__NSGlobalBlock__
       NSLog(@"%@",[[block2 copy] class]); //__NSGlobalBlock__
       NSLog(@"%@",[[block3 copy] class]); //__NSGlobalBlock__
       NSLog(@"%@",[[block4 copy] class]); //__NSMallocBlock__
       NSLog(@"%@",[[block5 copy] class]); //__NSMallocBlock__
   }
   return 0;
}

那么在arc模式下呢?代码如下:

//ARC
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int c = 10;
        NSLog(@"%@",[^{
            NSLog(@"%d",c);
        } class]); //__NSStackBlock__
        void (^block1)(void) = ^{
            NSLog(@"%d",c);
        };
        NSLog(@"%@",[block1 class]); //__NSMallocBlock__
    }
    return 0;
}

我们可以看到,在ARC模式下,block创建后,只要被强引用,编译器就会自动对其进行copy操作,block就从__NSStackBlock__类型变为__NSMallocBlock__。
在这里,有必要总结下,在ARC模式下,什么情况下block会被自动copy,什么情况下block需要手动copy。
自动copy:

  1. block被强制针引用;
  2. block作为函数返回值或函数参数时;
  3. GCD API中的block;
  4. OC的API中包含有usingBlock字样的函数;
    手动copy:
    TODO
(二) block的内存管理
  1. block既然是OC对象,那么它也使用引用计数进行内存管理。根据block的类型不同,规则如下:
    __NSMallocBlock__的block copy操作后,引用计数如何改变呢?这个问题需要确定下。TODO
类型copy操作
NSGlobalBlockNSGlobalBlock
NSStackBlockNSMallocBlock
NSMallocBlockNSMallocBlock
  1. 需要注意的是,当block捕获对象类型的自动变量时,block需要对捕获的对象进行内存管理,谁使用谁负责,请看代码:
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSObject *__strong obj;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _obj, int flags=0) : obj(_obj) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

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};
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) 
{
	_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

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

我们看到,当block捕获对象类型的自动变量时,block内部会有一个__strong类型的成员变量指向这个被捕获的对象。同时相对于捕获int类型的变量,desc内部增加了copy和dispose两个函数指针。
当对stack类型的block进行copy操作时,会调用desc内的copy,对block内的__strong类型的成员变量进行_Block_object_assign操作,导致其引用计数+1,从而确保,block的代码体在执行的时候,obj没有被释放掉,dispose同理。

(三) 循环引用问题

我们知道,面向对象的编程语言都需要处理内存管理的问题,内存管理的方式有很多,比如垃圾回收,引用计数等。引用计数方式的内存管理存在一个致命缺陷,那就是循环引用导致的内存泄漏。block也存在这种问题,请看下面代码:

@interface Person : NSObject
@property(nonatomic,copy)void (^block1) (void);
@end

int main(int argc, const char * argv[]) {    
    Person *person = [[Person alloc]init];
    person.block1 = ^{
        NSLog(@"%@",person);
    };
    return 0;
}

上述代码,person和它的成员变量block1循环引用,下面是它的循环引用关系图:
使用__strong指针的的引用计数关系图
循环引用关系一目了然,如何破解呢?当然是使用__weak指针

int main(int argc, const char * argv[]) {
    
    Person *person = [[Person alloc]init];
    __weak Person *weakPerson = person;
    person.block1 = ^{
        NSLog(@"%@",weakPerson);
    };
    
    return 0;
}

使用__weak指针后,block结构体中person成员即为Person *__weak weakPerson,对person是弱引用,不会导致person的引用计数+1。

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

使用__weak指针后,block结构体中person成员即为Person *__weak weakPerson,对person是弱引用,不会导致person的引用计数+1,所以它们的关系图变为下图,从而解决循环引用的问题:
使用__weak指针后的引用计数关系图
在block循环引用这个话题下,有一个相对复杂且值得注意的问题:block的嵌套和__weak、__strong的配合使用,下面我们展开来讲一下:

int main(int argc, const char * argv[]) {
    Person *person = [[Person alloc]init];
    person.block1 = ^{
        void (^blockInter)(void) = ^{
            NSLog(@"%@",person);
        };
        blockInter();
    };
    return 0;
}

观察上述代码,会有循环引用的问题吗?block1和blockInter是两个block,乍看之下,感觉block1并未引用person,而强引用person的blockInter并不属于person的属性,block1和person之间并未形成循环引用,真的如此吗?并非如此。
想直观的弄明白这个问题,我们需要分析一下block的构造函数以及block代码体对应的函数。
同样编译为c++,我们先看blockInter的构造函数:__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__strong _person, int flags=0) ,其中包含一个__strong person,这个很容易理解,因为blockInter代码体引用了__strong person。然后我们看一下block1的代码体对应的函数:static void __main_block_func_1(struct __main_block_impl_1 *__cself),我们知道,blockInter的构造函数是在这个函数中调用的,且需要传入一个__strong _person的参数,那么这个参数从哪来呢?看来只能来自于block1代码体对应的函数的入参了,即struct __main_block_impl_1 *__cself),而这个类型正是block1对象,所以,block1对象必须有个成员变量指向person,而编译后的c++代码也正是如此:

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

所以,总结一下,当block嵌套时,如果内层的block引用了外部的某个对象,那么从这个对象所在层定义的block到内层block,每一层block都会对这个对象形成引用,我个人称之为block的穿透引用。所以上述例子中,外层的block1和person形成了循环引用,而内层的blockInter由于没有被person强引用,只是单方面引用person,并不会形成循环引用。所以正确的代码应该如下:

int main(int argc, const char * argv[]) {
    Person *person = [[Person alloc]init];
    __weak Person *weakPerson = person;
    person.block1 = ^{
        __strong Person *strongPerson = weakPerson;
        void (^blockInter)(void) = ^{
            NSLog(@"%@",strongPerson);
        };
        blockInter();
    };
    return 0;
}

那么__strong啥时候使用呢?这就要具体分析每一个block的引用计数关系图了。还是上述例子,对于block1,person强引用block1,而block1内部又强引用person,故会发生循环引用,所以通过__weak指针,取消block1对person的强引用。对于内部blockInter,person并未引用blockInter,所以blockInter内部是可以强引用person的,由于blockInter没有被其他对象强引用且blockInter的声明和调用都在block1的代码体内,所以当block1代码体对应的函数执行的时候,blockInter的代码体就会执行,而此时,person是没有销毁的,所以这里__weak和__strong都可以,但是当blockInter被person外的其他对象强引用,且blockInter的代码体的调用是在block1代码体之外进行,那么,blockInter内部对person的引用就必须是__strong,因为blockInter的代码体执行的时候,person可能已经销毁,所以必须用strong保着person的命。
故,block的多层嵌套时,__weak、__strong的使用需要分析每层block对象的引用关系来确定。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值