iOS Block(4)-block内存管理,block循环引用

1. Block内存管理

demo代码.png

OC代码转换成C++代码

void(*block)(void);

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int 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__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_3fd458_mi_0,(age->__forwarding->age));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}

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};
int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};

        block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));
    }
}

_block的内部要调用外边的变量,_block的desc0的结构体里会多出copydispose函数方法,copy的函数方法中会调用_Block_object_assign进行内存管理.

  1. 当block在栈上时,并不会对__block变量产生强引用.
  2. 当block被copy到堆时
        ①. 会调用block内部的copy函数.
        ②. copy函数内部会调用_Block_object_assign函数.
        ③. _Block_object_assign函数会对__block变量形成强引用(retain).
copy操作第一步.png
copy操作第二步.png

当block从堆中移除时

  • 会调用block内部的dispose函数
  • dispose函数内部会调用_Block_object_dispose函数
  • _Block_object_dispose函数会自动释放引用的__block变量(release)
dispose操作的第一步.png
dispose操作的第二步.png

对象类型的auto变量、__block变量

  1. 当block在栈上时,对它们都不会产生强引用.
  2. 当block拷贝到堆上时,都会通过copy函数来处理它们.
  • __block变量(假设变量名叫做a)
    _Block_object_assign((void*)&dst->a, (void*)src->a,8/*BLOCK_FIELD_IS_BYREF*/);
  • 对象类型的auto变量(假设变量名叫做p)
    _Block_object_assign((void*)&dst->p, (void*)src->p,3/*BLOCK_FIELD_IS_OBJECT*/);
  1. 当block从堆上移除时,都会通过dispose函数来释放它们
  • __block变量(假设变量名叫做a)
    _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
  • 对象类型的auto变量(假设变量名叫做p)
    _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
_block变量.png

2. __block的__forwarding指针

_forwarding指针的示意图.jpg

用"__block"修饰auto变量xxx的时候,系统会将这个auto变量xxx转换成一个__Block_byref_xxx_0结构体类型,结构体中有个成员__forwarding。当block在栈区的时候,__forwarding指向栈区的__Block_byref_xxx_0结构体本身内存地址;当block被copy到堆区的时候,栈上block变量内的__forwarding将会指向堆上的block变量,从而进一步访问block变量内部的成员。这样,前文中访问age的时候通过" (age->__forwarding->age) = 20;"这种做法也就明白了。

3. __block修饰对象类型

  • block内部的指针指向包装好的结构体就是强指针,没有弱引用.结构体内部的对象指向外部的变量的指针是强指针还是弱指针是由外部的变量的修饰词决定的.
  • 如果__block修饰的是对象类型,block从栈区copy到堆区的时候,包装好的对象会增加copy和dispose两个函数对包装好的对象做内存管理.
  1. __block变量在栈上时,不会对指向的对象产生强引用
  2. __block变量被copy到堆时
       ①. 会调用__block变量内部的copy函数
       ②. copy函数内部会调用_Block_object_assign函数
       ③._Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain
  3. 如果__block变量从堆上移除
       ①. 会调用__block变量内部的dispose函数
       ②. dispose函数内部会调用_Block_object_dispose函数
       ③. _Block_object_dispose函数会自动释放指向的对象(release)

4. block循环引用

一. demo1

self的循环引用的常例.png

demo1中,TestClass有一个block实例对象,self对block的关系为强持有。block实现中,也引用了当前实例self,并且也为强引用。这样一来,self持有block,block持有self,所以两者都无法释放,就造成内存泄露。将该.m文件转换为C++实现,看看block结构体__TestClass__test_block_impl_0和block代码块函数__TestClass__test_block_func_0:

struct __TestClass__test_block_impl_0 {
  struct __block_impl impl;
  struct __TestClass__test_block_desc_0* Desc;
  TestClass *const __strong self;
  __TestClass__test_block_impl_0(void *fp, struct __TestClass__test_block_desc_0 *desc, TestClass *const __strong _self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __TestClass__test_block_func_0(struct __TestClass__test_block_impl_0 *__cself) {
  TestClass *const __strong self = __cself->self; // bound by copy


        NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_TestClass_e9b143_mi_0, ((NSInteger (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("age")));
    }

正如上面分析的一样:由"TestClass *const __strong self;"可见block结构体中成员变量self为当前类实例的强指针;并且block的代码块中TestClass *const __strong self = __cself->self; // bound by copy也强引用着当前类TestClass的实例.
block与self的互相持有:

block与self的互相持有.png

二. demo2

#import <Foundation/Foundation.h>
typedef void(^CSBlock)(void);
@interface Person : NSObject
/** age*/
@property(nonatomic,assign)int age;
/** blokc*/
@property(nonatomic,copy) CSBlock block;
@end

@implementation Person
- (void)dealloc {
    NSLog(@"%s",__func__);
}
@end

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

将上方的OC代码转换为C++:

Person循环引用OC->C++.png

person的循环引用形成

person的循环引用形成.png

block内部有个强指针person指向MJPerson,MJPerson有个成员变量_block指向block.

person的循环引用1.png

当我们执行完main函数中的20行代码的时候,main中的person指向MJPerson的指针就销毁了.

person的循环引用2.png

三. 解决block与持有对象间的强引用关系.

在ARC环境下有以下三种解决方案:
① 使用"__weak";
② 使用"__unsafe_unretained";
③ 使用"__block"(必须要调用block)。

方案①大家应该都清楚,在开发的过程中都应该使用过.
方案②:“__unsafe_unretained”字面理解就是不安全的、不会导致引用计数增加。简单说就是:不安全的弱引用。
“__weak”与“ __unsafe_unretained”对比:
"__weak":不会产生强引用,当指向的对象销毁时,会自动让指针置为nil;
“ __unsafe_unretained”:不会产生强引用,不安全。当指向的对象销毁时,指针存储的地址值不变,这个时候指向的是一块已经被系统回收的内存,这个时候继续访问会引发"野指针异常"。
对于方案③demo:

#import <Foundation/Foundation.h>
#import "TestClass.h"

@implementation TestClass

- (void)test{
    
    self.age = 20;
    
//    __unsafe_unretained TestClass *weakself = self;
//    __weak TestClass *weakself = self;
    
    __block TestClass* weakSelf = self;
    
    self.block = ^{
        
        NSLog(@"%ld", weakSelf.age);
        weakSelf = nil;
    };
    self.block();

}

- (void)dealloc{
    NSLog(@"TestClass - %@",NSStringFromSelector(_cmd));
}

当testClass实例销毁的时候,block也释放了,不会循环引用。
我们分析一下转换后的C++代码:

struct __TestClass__test_block_impl_0 {
  struct __block_impl impl;
  struct __TestClass__test_block_desc_0* Desc;
  __Block_byref_weakSelf_0 *weakSelf; // by ref
  __TestClass__test_block_impl_0(void *fp, struct __TestClass__test_block_desc_0 *desc, __Block_byref_weakSelf_0 *_weakSelf, int flags=0) : weakSelf(_weakSelf->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};


struct __Block_byref_weakSelf_0 {
  void *__isa;
__Block_byref_weakSelf_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 TestClass *__strong weakSelf;
};

首先block结构体内持有__block类型的"__Block_byref_weakSelf_0"对象;
其次“__block”类型对象中“TestClass *__strong weakSelf;”,即持有TestClass实例。
由于self持有了block,所以当前对象self、block已经__block变量三者的关系为:

三者关系的循环引用.png

如此一来:又是一个循环引用问题,我们尝试在block代码块内部去掉"weakSelf = nil",实际结果是TestClass实例不会释放掉。针对这种状况,打破三者之间的循环链即可消除循环引用,解释如下:
首先a. 对象(也就是持有block的对象)对block的持有关系肯定是强持有;
其次b. block对__block变量也是强持有的关系,这两条线无法改动!如果突破__block变量持有对象这条线,就可以了,这样就可以通过调用block后,手动设置__block对象为nil。

在本demo中__block变量定义如下:

struct __Block_byref_weakSelf_0 {
  void *__isa;
__Block_byref_weakSelf_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 TestClass *__strong weakSelf;
};

也就是将结构体__Block_byref_weakSelf_0中的成员变量"TestClass *__strong weakSelf"置为nil,即weakSelf = nil,这样__block就不会持有当前类的实例了,所以循环被打破。打破后三者关系见下图:

打破三种的循环引用关系.png

由此针对方案③:该方案唯一的缺点就是需要执行block。这么麻烦的关键在于:执行完block之后,在block体内设置引用对象为nil,从而达到手动将__block变量内部的关键成员置为nil,这样就可以打破循环关系.

在ARC环境下有以下三种解决方案:
① 用__unsafe_unretained解决;
② 用__block解决(block可以不调用)。

__unsafe_unretained”同ARC一致;
在使用"_block"时,我们先总结一下block被copy到堆上时,底层做了啥(⊙⊙)?

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

正是因为MRC环境下,__block变量对所引用的对象为弱引用关系,所以“对象”、“block”与"block变量"三种之间处于开环状态,也就不存在循环引用问题,因此在MRC下用__block修饰被引用对象,block可以不调用。正如下面demo:

// ATTENTION:MRC环境
#import <Foundation/Foundation.h>
#import "TestClass.h"

@implementation TestClass

- (void)test{
    
    self.age = 20;
    
    __block TestClass* weakSelf = self;
    
    self.block = ^{
        
        NSLog(@"%ld", weakSelf.age);
    };
}

- (void)dealloc{
    [super dealloc];
    NSLog(@"TestClass - %@",NSStringFromSelector(_cmd));
}

@end


int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        {
            TestClass *testClass = [[TestClass alloc] init];
           
            [testClass test];
            [testClass release];
            
        }
        NSLog(@"---------");
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}


//2018-08-31 11:25:27.800865+0800 Block[74957:7622148] TestClass - dealloc
//2018-08-31  11:25:27.803116+0800 Block[74957:7622148] ---------

结果是TestClass实例被销毁的时候,block也一起销毁了。

四. __weak搭配__strong使用

__weak TestClass* weakSelf = self;
    self.block = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        NSLog(@"%ld", strongSelf.age);
    };

在本demo中,在block内部重新使用__strong修饰weakSelf(被引用)变量是为了在block内部有一个强指针指向weakSelf(弱引用)避免在block调用的时候weakSelf已经被销毁。有些时候block内部访问的对象并不是当前类的实例,考虑到block可能很久才会销毁,因此被block引用的对象应该是弱引用,否则可能造成被引用对象毫无意义地存在于内存中。既然是弱引用,一旦该对象在其他地方被销毁,则block内部的弱引用对象也就销毁了,继续访问也就会返回null,还是用demo说话吧:

// 1. 新建一个Dog类,并实现dealloc方法;
@interface Dog : NSObject

@property (nonatomic, copy) NSString *name;

@end

@implementation Dog

- (void)dealloc{
  
    NSLog(@"Dog - %@",NSStringFromSelector(_cmd));
}


// 2. 在另一个类的block函数体类访问dog的成员属性name;
#import <Foundation/Foundation.h>
#import "TestClass.h"
#import "Dog.h"

@implementation TestClass

- (void)test{
    
    Dog *dog = [[Dog alloc] init];
    dog.name = @"小黑";

    __weak Dog *weakDog = dog;
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"开始执行block");
//        __strong typeof(weakDog) strongDog = weakDog;
        sleep(2);
        NSLog(@"狗的名字:%@",weakDog.name);
    });
  
    sleep(1);
    NSLog(@"模拟weakDog被释放");
    dog = nil;
    
    /*
    2018-09-04 14:43:32.966624+0800 Block[80253:7808973] 开始执行block
    2018-09-04 14:43:33.956807+0800 Block[80253:7808911] 模拟weakDog被释放
    2018-09-04 14:43:33.957256+0800 Block[80253:7808911] Dog - dealloc
    2018-09-04 14:43:34.972230+0800 Block[80253:7808973] 狗的名字:(null)
    */
}

- (void)dealloc{
    NSLog(@"TestClass - %@",NSStringFromSelector(_cmd));
}

@end

模拟block内部访问的对象在外部被提前释放的情况,我在调用block的过程中特意将dog设置为nil,访问的结果是:“Block[76269:7674002] 狗的名字:(null)”,项目中block调用的时机是不确定的,被访问的对象何时候释放也是不确定的,故而这种情况下仅仅使用__weak修饰被访问对象肯定存在问题,为了更好解决这样的问题,我们用“__strong”修饰符在block内部搭配外部的"__weak"修饰被访问对象,针对上面demo,正确的做法如下:

- (void)test{
    
    Dog *dog = [[Dog alloc] init];
    dog.name = @"小黑";

    __weak Dog *weakDog = dog;
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"开始执行block");
        __strong typeof(weakDog) strongDog = weakDog;
        sleep(2);
        NSLog(@"狗的名字:%@",strongDog.name);
    });
  
    sleep(1);
    NSLog(@"模拟weakDog被释放");
    dog = nil;
    /*
     2018-09-04 14:46:32.969188+0800 Block[80345:7811829] 开始执行block
     2018-09-04 14:46:33.961744+0800 Block[80345:7811757] 模拟weakDog被释放
     2018-09-04 14:46:33.962013+0800 Block[80345:7811757] ---------
     2018-09-04 14:46:34.974592+0800 Block[80345:7811829] 狗的名字:小黑
     2018-09-04 14:46:34.974973+0800 Block[80345:7811829] Dog - dealloc
    */
}

在block内将weakDog对象强引用为strongDog,执行block过程中将dog设置为nil,结果仍能继续访问。

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

推荐阅读更多精彩内容