欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

block本质探寻八之循环引用

程序员文章站 2022-08-10 09:50:50
说明:阅读本文,请参照之前的block文章加以理解; 一、循环引用的本质 //代码——ARC环境 //打印 分析:main函数日志输出之前,Person实例对象就被销毁了——因为在test1()方法中,强指针per持有[[Person alloc] init]对象会执行retain操作导致Perso ......

说明:阅读本文,请参照之前的block文章加以理解;

一、循环引用的本质

//代码——arc环境

void test1()
{
    person *per = [[person alloc] init];
    per.age = 10;
    per.block = ^{
        nslog(@"-------1");
    };
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test1();
//        test2();
    }
    
    nslog(@"----");
    return 0;
}
#import <foundation/foundation.h>

ns_assume_nonnull_begin

typedef void(^myblock)(void);

@interface person : nsobject

@property (nonatomic, assign) int age;
@property (nonatomic, copy) myblock block;

@end

ns_assume_nonnull_end



#import "person.h"

@implementation person

- (void)dealloc
{
//    [super dealloc];
    nslog(@"%s", __func__);
}

@end

//打印

2019-01-17 16:46:28.353740+0800 mj_test[2990:240693] -[person dealloc]
2019-01-17 16:46:28.354013+0800 mj_test[2990:240693] ----
program ended with exit code: 0

分析:main函数日志输出之前,person实例对象就被销毁了——因为在test1()方法中,强指针per持有[[person alloc] init]对象会执行retain操作导致person实例对象的retaincount值为2(此前alloc操作,其retaincount值就设置为1),当test1()方法结束时,per被存放在栈区也随之销毁,故per不会再持有person实例对象即执行release操作导致该对象的retaincount指减1;当自动销毁池autoreleasepool结束时,会自动向池中的所有对象再次发送一条release消息,那么此时person实例对象的retaincount值再次减1变成0,对象的引用计数一旦为0,其所占内存会被自动回收,因此person实例对象就会销毁;

补充:我们知道blcok的内存管理模式为copy策略(原因就不分析了),因为在arc环境下强指针持有block对象,系统会自动将block对象copy到堆区中,所以arc模式下,系统会自动帮助我们对block进行copy的管理策略,我们写成strong的策略是没有任何问题的——但是,mrc模式下必须是copy策略,系统不会帮你管理内存,只能手动;这点请注意!

至此,以上person实例对象销毁是正常的,那么什么情况下是不正常的?往下看:

//代码

void test2()
{
    person *per = [[person alloc] init];
    per.age = 10;
    per.block = ^{
        nslog(@"-------%d", per.age);
    };
}

//打印

2019-01-17 15:00:31.859710+0800 mj_test[2486:187534] -----
program ended with exit code: 0

//clang

main.cpp

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* desc;
  person *__strong per;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, person *__strong _per, int flags=0) : per(_per) {
    impl.isa = &_nsconcretestackblock;
    impl.flags = flags;
    impl.funcptr = fp;
    desc = desc;
  }
};

person.cpp

struct person_impl {
    struct nsobject_impl nsobject_ivars;
    int _age;
    myblock  _nonnull _block;
};


struct nsobject_impl {
    class isa;
};


static void(* _i_person_block(person * self, sel _cmd) )(){ return (*(myblock  _nonnull *)((char *)self + objc_ivar_$_person$_block)); }


static void _i_person_setblock_(person * self, sel _cmd, myblock  _nonnull block) { objc_setproperty (self, _cmd, __offsetofivar__(struct person, _block), (id)block, 0, 1); }

分析:

<1>我们知道,oc对象编译成c++后的本质就是一个结构体person_impl,该结构体的第一个成员变量就是isa指针,指向类对象本身;同时,@property修饰的属性,系统会自动生成一个结构体成员变量,还为之生成getter和setter方法——这些之前的文章已经说过,此处不再赘述!

<2>per实例对象结构体person_impl中含有_block变量,通过setter法_i_person_setblock_将block对象(等号右边)赋值给该_block变量,因此_block指向block对象(强引用);

<3>在__main_block_impl_0结构体中,我们看到person *__strong per,所以,block对象本身对person实例对象也是强引用;

综上:block对象结构体__main_block_impl_0通过其内部成员指针变量person *__strong per持有person实例对象(强引用),而person实例对象结构体person_impl通过其内部成员指针变量_block持有block对象(强引用)——因此二者构成循环引用,当autoreleasepool大括号结束时,block对象和person实例对象所占内存依然没有被系统回收,因为他们的引用计数依然大于0;

//图解——注:self是一个auto型的局部变量,指向的是[[person alloc] init]实例对象

block本质探寻八之循环引用

 

补充:所以block循环引用造成的直接后果是内存泄露(即程序结束而内存没有被回收——>根本原因是对象引用计数大于0(retain和release使用次数不对等)——>是因为强指针引用造成的);

引伸:当对象所占内存被回收时,指向对象的指针(强指针)应当被赋值于nil或者指向其他的合法内存,否则会导致野指针调用(乱指)程序崩溃——但是,用weak做内存管理策略(即修饰指针变量)时,为什么系统会自动将指针变量置为nil?这点后面文章会提到!

 

二、解决方案

思路:

据上分析,打破循环引用,只需要将其中一个强引用变成弱引用即可,那么要改变哪一个弱引用呢?person实例对象内部拥有block属性,当该实例对象销毁时,其block属性也会随之销毁,所以我们只需要将block对象中的person类型指针变成弱指针即可——通常都是这样做!

//图解

 block本质探寻八之循环引用

 

1)arc环境下

方案一:weak修饰

//代码

void test3()
{
    person *per = [[person alloc] init];
    per.age = 10;
    __weak person *weakper = per;
    per.block = ^{
        nslog(@"-------%d", weakper.age);
    };
}

//打印

2019-01-18 14:10:17.451718+0800 mj_test[1458:103419] -[person dealloc]
2019-01-18 14:10:17.452663+0800 mj_test[1458:103419] ----
program ended with exit code: 0

//clang

struct __test3_block_impl_0 {
  struct __block_impl impl;
  struct __test3_block_desc_0* desc;
  person *__weak weakper;
  __test3_block_impl_0(void *fp, struct __test3_block_desc_0 *desc, person *__weak _weakper, int flags=0) : weakper(_weakper) {
    impl.isa = &_nsconcretestackblock;
    impl.flags = flags;
    impl.funcptr = fp;
    desc = desc;
  }
};

分析:

<1>block对象中,person指针变量类型变成了__weak类型,打印前person对象销毁了;

<2>另外一种写法:__weak typeof(per) weakper = per  <=>  __weak person *weakper = per;(前者写法居多)

 block本质探寻八之循环引用

 

方案二:__unsafe_unretained修饰 

block本质探寻八之循环引用

 

注:__weak修饰和__unsafe_unretained,二者有一个非常重要的区别:

经上分析,我们知道person实例对象销毁后,其内部的block属性也会销毁,那么其也就不再指向block对象了,而此时一旦block对象没有任何强引用,作用域结束后,其也会被销毁,其成员变量person指针也会被销毁,这点没问题!————但是,如果block对象还存在呢(被其他指针强引用),此时其内部成员变量person指针也存在,但是依然会指向person实例对象销毁前所占的内存区域,但是该内存区域已经被系统回收了,person指针指向的是不合法的内存区域——如果是weak修饰,系统会自动将指针置为nil(指向合法的内存区域);如果是__unsafe_unretained修饰,什么也不会做,这样就会导致野指针调用!

 

方案三:__block修饰

//代码

void test4()
{
    __block person *per = [[person alloc] init];
    per.age = 10;
    per.block = ^{
        nslog(@"-------%d", per.age);
        per = nil;
    };
    per.block();
}

//打印

2019-01-18 15:20:06.697898+0800 mj_test[1810:136831] -------10
2019-01-18 15:20:06.698250+0800 mj_test[1810:136831] -[person dealloc]
2019-01-18 15:20:06.698300+0800 mj_test[1810:136831] ----
program ended with exit code: 0

//clang

struct __block_byref_per_0 {
  void *__isa;
__block_byref_per_0 *__forwarding;
 int __flags;
 int __size;
 void (*__block_byref_id_object_copy)(void*, void*);
 void (*__block_byref_id_object_dispose)(void*);
 person *__strong per;
};


struct __test4_block_impl_0 {
  struct __block_impl impl;
  struct __test4_block_desc_0* desc;
  __block_byref_per_0 *per; // by ref
  __test4_block_impl_0(void *fp, struct __test4_block_desc_0 *desc, __block_byref_per_0 *_per, int flags=0) : per(_per->__forwarding) {
    impl.isa = &_nsconcretestackblock;
    impl.flags = flags;
    impl.funcptr = fp;
    desc = desc;
  }
};

分析:

<1>block对象持有__block对象,而__block对象又持有person实例对象,而person实例对象又持有block对象——如此,构成一个三角循环: 

block本质探寻八之循环引用 

 

 <2>通过block回调,person实例对象指针被置为nil,而该指针本质是__block对象中的person *__strong per指针,因此该指针不可能再指向person实例对象了,所以,第2条持有就断开了,打破了三角循环;

说明:但是该方案看起来比较麻烦,一旦忘记将指针置为nil,就会造成内存泄露;

注:以上分析不理解,请参考前述block文章,此处不再赘述!

 

2)mrc环境

说明:该环境下不支持__weak修饰;

方案一:__unsafe_unretained修饰

//代码

void test5()
{
//    __unsafe_unretained person *per = [[person alloc] init];
    __block person *per = [[person alloc] init];
    per.age = 10;
    per.block = [^{
        nslog(@"-------%d", per.age);
    } copy];
    [per release];
    per = nil;
}

//打印

2019-01-18 16:42:49.970441+0800 mj_test[2257:177470] -[person dealloc]
2019-01-18 16:42:49.971587+0800 mj_test[2257:177470] ----
program ended with exit code: 0

分析:

<1>根据习惯,mrc环境下,我们通常会将block对象(等号右边)从栈区copy到堆区,以达到手动控制其内存销毁的目的;

<2>原理:调用alloc创建对象时,系统会自动将该实例对象引用计数置为1,而该对象又会随着block的copy而一起被copy到堆区,此时该对象的retaincount会加1(变成2),当对该对象发送release消息时,其retaincount自动减1(由2变成1),所以当程序结束时,person实例对象retaincount为1(>0),其内存并不会被系统回收从而导致内存泄露;

那么,__unsafe_unretained修饰后,无论后面有多少次retain或者copy操作,person实例对象的retaincount始终为1,所以程序结束前release时,其retaincount值变为0,此时内存被回收,而不会导致内存泄露的问题;

 

方案二:__block修饰 

 block本质探寻八之循环引用

 分析:为什么block可以?

<1>前述文章我们分析到,mrc环境下,__block修饰对象类型的auto局部变量,系统生成的__block对象并不会根据其内存成员变量person指针变量(其实就是test5()方法中的per指针)是强指针类型而对person实例对象([[person alloc] init])进行retain操作(强引用);

<2>所以此时,__block的作用相当于__unsafe_unretained的作用,原理一样;

 

补充一个问题:在arc环境下,弱指针不能通过"->"形式来访问对象的成员变量

block本质探寻八之循环引用

原因:就是weakself很可能为为空(即有可能提前被释放了),所以必须使用强指针来访问

//代码

person.m

- (void) test6
{
    __weak typeof(self) weakself = self;
    self.block = ^{
        __strong typeof(weakself) strongself = weakself;
        nslog(@"-------%d", strongself->_age);
    };
}

main.m

int main(int argc, const char * argv[]) {
    @autoreleasepool {
//        test1();
//        test2();
//        test3();
//        test4();
//        test5();
        person *per = [[person alloc] init];
        [per test6];
    }
    
    nslog(@"----");
    return 0;
}

//打印

2019-01-18 17:43:41.010848+0800 mj_test[2557:208583] -[person dealloc]
2019-01-18 17:43:41.011346+0800 mj_test[2557:208583] ----
program ended with exit code: 0

//clang

person.m

struct __person__test6_block_impl_0 {
  struct __block_impl impl;
  struct __person__test6_block_desc_0* desc;
  person *const __weak weakself;
  __person__test6_block_impl_0(void *fp, struct __person__test6_block_desc_0 *desc, person *const __weak _weakself, int flags=0) : weakself(_weakself) {
    impl.isa = &_nsconcretestackblock;
    impl.flags = flags;
    impl.funcptr = fp;
    desc = desc;
  }
};

分析:block对象的内部成员变量weakself依然是weak类型,并不受block代码块内部的__strong转化,该转化只是为了骗取编译器通过编译而已;

 

三、结论

1)原因:block对象与oc对象相互持有(强引用)——oc对象有block属性,block代码块中用到了该实例对象;

2)危害:程序结束时,相互强应用(对象的引用计数>0)导致实例对象所占内存不能及时被系统回收——即内存泄露;

3)解决:

<1>arc:__weak修饰(常用)、__unsafe_unretained(会引起野指针调用,不推荐)、__block(过于繁琐,不推荐);

<2>mrc:__unsafe_unretained和__block;

 

 

github