iOS 内存管理

2021-01-31  本文已影响0人  陈盼同学

iOS程序的内存布局

低地址
    \|/
     |保留
     |
     |代码段(_TEXT)    代码段:编译之后的代码
     |
     |
     |数据段(_DATA)    数据段:
     |                |  字符串常量       字符串常量:比如NSString *str = @"123"
     |                |  已初始化数据     已初始化数据:已初始化的全局变量、静态变量等
     |               \|/ 未初始化数据     未初始化数据:未初始化的全局变量、静态变量等
     |
     |
     |
     |堆(heap)        堆: 通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址值越来越大(由低地址往高地址分)
     |       \|/
     |
     |
     |       /|\
     |栈(stack)       栈:函数调用开销,比如局部变量。分配的内存空间地址值越来越小(由高地址往低地址分)
     |
     |内核区
    \|/
高地址

Tagged Pointer

从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储

在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值

使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中,Tagged Pointer 指针的值不再是地址了,而且包含了真正的值.所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。其中tag为标记。

当指针不够存储数据时,才会使用动态分配内存的方式来存储数据

因为tagged pointer 不是一个真正的对象,如果使用isa指针在编译时会报错。

NSNumber *number1 = @1;
po number1->isa会报error
☆☆☆待考证☆☆☆
比如NSNumber *number1 = @4;想把它转为int时,要调用number1.intValue;,相当于objc_msgSend(number1,@selector(intValue));,但是number1在这里面被Tagged Pointer了不是对象啊,其实objc_msgSend会识别是不是Tagged Pointer后的指针,如果是,直接从指针提取数据,节省了以前的调用开销。

如何判断一个指针是否为Tagged Pointer?
iOS平台,最高有效位是1(第64bit)
Mac平台,最低有效位是1

//下面代码执行时会怎么样

@property (nonatomic , copy) NSString *name;

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

for (int i = 0; i < 1000; i++) {
    dispatch_async(queue, ^{
        self.name = [NSString stringWithFormat:@"abcdefghijk"];

    });
}

会崩溃。崩溃如下:

libobjc.A.dylib`objc_release:
    0x7fff51411000 <+0>:  testq  %rdi, %rdi
    0x7fff51411003 <+3>:  je     0x7fff51411007            ; <+7>
    0x7fff51411005 <+5>:  jns    0x7fff51411008            ; <+8>
    0x7fff51411007 <+7>:  retq
    0x7fff51411008 <+8>:  movq   (%rdi), %rax
->  0x7fff5141100b <+11>: testb  $0x4, 0x20(%rax)
    0x7fff5141100f <+15>: je     0x7fff5141101b            ; <+27>
    0x7fff51411011 <+17>: movl   $0x1, %esi
    0x7fff51411016 <+22>: jmp    0x7fff51411028            ; objc_object::sidetable_release(bool)
    0x7fff5141101b <+27>: movq   0x389f549e(%rip), %rsi    ; "release"
    0x7fff51411022 <+34>: jmpq   *0x36625268(%rip)         ; (void *)0x00007fff513f7780: objc_msgSend

可以看到在objc_release时候开始崩溃

ARC代码会转成MRC,self.name在MRC里如下

- (void)setName:(NSString *)name
{
    if (_name != name) {
        [_name release];
        _name = [name copy];
    }
}

多条线程同时调用"[_name release]"进而导致程序崩溃,坏内存访问
解决办法就是加锁,在
self.name = [NSString stringWithFormat:@"abcdefghijk"];这句话前后加锁.比如直接设置属性为atomic

//把代码改动下成为下面这样,会如何

@property (nonatomic , copy) NSString *name;

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

for (int i = 0; i < 1000; i++) {
    dispatch_async(queue, ^{
        self.name = [NSString stringWithFormat:@"abc"];
    });
}

不会崩溃,字符串内容变少了,通过Tagged Pointer技术能够存在指针里,self.name就不用走set方法里的"某些"操作,直接把数据存在指针里,所以就不会产生崩溃。

MRC

在iOS中,使用引用计数来管理OC对象的内存

一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1

内存管理的经验总结
当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1

可以通过以下私有函数来查看自动释放池的情况

extern void _objc_autoreleasePoolPrint(void);

下面用MRC来实现一个简单的调用

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

@interface MJPerson : NSObject
{
    MJDog *_dog;
    int _age;
}

- (void)setAge:(int)age;
- (int)age;

- (void)setDog:(MJDog *)dog;
- (MJDog *)dog;

@end

#import "MJPerson.h"

@implementation MJPerson

- (void)setAge:(int)age
{
    _age = age;
}

- (int)age
{
    return _age;
}

- (void)setDog:(MJDog *)dog
{
    //判定是不是同一只狗,同一只狗不做事情,是因为同一只狗的话,如果MJPerson拥有dog后,外界释放dog,此时dog引用计数为1,走到这儿,在释放的话引用计数就为0了,导致dog成为僵尸对象了,在进行retain就有问题了
    if (_dog != dog) {
    
        [_dog release]; //先释放上次的狗是因为如果一个人调setDog初始化一只狗1,再调setDog初始化一只狗2,那么一个人就有了两只狗,但是只使用狗2,所以要把MJPerson的狗1释放,狗2的释放交给MJPerson负责(销毁时释放)
        _dog = [dog retain]; //在set方法里面进行计数器的增加,是想让person对dog负责,从而让j外界初始化的dog尽情释放。因为MJPerson拥有dog,如果想正常的使用dog,就不能让dog出现MJPerson还没销毁就找不到dog问题。比如这里不进行增加的话,外界初始化MJPerson和MJDog后就要releasse,这时候通过MJPerson调MJDog的run方法(MJDog里有一个run方法)就会出现坏内存访问。
    }
}

- (MJDog *)dog
{
    return _dog;
}

- (void)dealloc
{
    //    [_dog release];
    //    _dog = nil;
    self.dog = nil;
    
    NSLog(@"%s", __func__);
    
    // 父类的dealloc放到最后
    [super dealloc];
}

@end

//在viewcontroller里调用
- (void)viewDidLoad {
    [super viewDidLoad];
    
    MJDog *dog = [[MJDog alloc] init];
    MJPerson *person = [[MJPerson alloc] init];
    [person setDog:dog];

    [dog release];

    [person setDog:dog];
    [person setDog:dog];
    [person setDog:dog];
    [person setDog:dog];
    [person setDog:dog];
    [[person dog] run];
    
    [person release];
}

@property在MRC下也是能用的。修饰词为retain,assign什么的。自动生成成员变量和属性的setter、getter方法的声明
.m写@synthesize自动生成成员变量和属性的setter、getter实现
@synthesize age = _age;

现在xcode自动实现@synthesize,所以写@property声明属性后就包含了成员变量、声明、实现了。

修饰词写成retain后,生成的set方法里会自动管理这个属性的引用计数器。但是dealloc里以及其他的方法里还是需要自己对额外的引用做内存的增加或释放。

//声明一个属性
@property (retain, nonatomic) NSMutableArray *data;
//之后,下面开始调用时
//这几句代码
    NSMutableArray *data = [[NSMutableArray alloc] init];
    self.data = data;
    [data release];
//可简化为
    self.data = [[NSMutableArray alloc] init];
    [self.data release];
//还可以简化为
    self.data = [[[NSMutableArray alloc] init] autorelease];
//再可以简化为
    self.data = [NSMutableArray array]; //这种可以通过类方法来创建的,不需要自己写release,内部实现了这次释放

"注意",在ARC下手动调用retain、release、retainCount、autorelease这几个方法会报错

copy

拷贝的目的:产生一个副本对象,跟"源对象互不影响"
修改了源对象,不会影响副本对象
修改了副本对象,不会影响源对象

浅拷贝和深拷贝
1.浅拷贝:指针拷贝,没有产生新的对象,但会影响引用计数
2.深拷贝:内容拷贝,产生新的对象,不会影响引用计数

//无论是不可变字符还是可变字符,不可变拷贝拷贝出来类型都变成NSString,可变拷贝拷贝出来类型都变成NSMutableString
NSString *str1 = [NSString stringWithFormat:@"test"];
NSString *str2 = [str1 copy]; // 返回的是NSString
NSMutableString *str3 = [str1 mutableCopy]; // 返回的是NSMutableString

NSMutableString *str4 = [[NSMutableString alloc] initWithFormat:@"test"];
NSString *str5 = [str4 copy]; // 返回的是NSString
NSMutableString *str6 = [str4 mutableCopy];// 返回的是NSMutableString


NSString *str1 = [NSString stringWithFormat:@"test"]; //地址一样
NSString *str2 = [str1 copy]; //地址一样  虽然跟str1是同一个对象,不过这句话相当于[str1 retain]
NSMutableString *str3 = [str1 mutableCopy]; //地址不一样
//为什么NSString的copy地址和本身地址一样,mutableCopy地址和本身地址不一样呢?
首先,针对不能随意再次改变字符长度的NSString来说,copy之后还是一样不可变的字符,也不能随意再次改变字符长度,既然都不可变,所以干脆地址一样,都指向一块内存。针对NSString的mutableCopy是一个新对象,所以地址不一样,指向的内存也不一样。


NSMutableString *str1 = [[NSMutableString alloc] initWithFormat:@"test"];
NSString *str2 = [str1 copy]; // 深拷贝
NSMutableString *str3 = [str1 mutableCopy]; // 深拷贝
//为什么NSMutableString的copy和mutableCopy都是深拷贝呢?
因为str1是一个可变的,经过copy后变成NSString了,是一个新对象。经过mutableCopy后变成NSMutableString了,也是一个新对象。
很好奇为什么经过NSMutableString的copy后返回的是NSString?是因为是不可变拷贝拷贝后是不可变的原因吧,不可变不可变......令人窒息

表格

                     |      copy        |   mutableCopy
------------------------------------------------------------
NSString             |       NSString   |   NSMutableString
                     |       浅拷贝      |      深拷贝
------------------------------------------------------------
NSMutableString      |      NSString    |   NSMutableString
                     |       深拷贝      |     深拷贝
------------------------------------------------------------
NSArray              |     NSArray      |   NSMutableArray
                     |      浅拷贝       |     深拷贝
------------------------------------------------------------
NSMutableArray       |     NSArray      |   NSMutableArray
                     |     深拷贝        |    深拷贝
------------------------------------------------------------
NSDictionary         |  NSDictionary    |  NSMutableDictionary
                     |    浅拷贝         |    深拷贝
------------------------------------------------------------
NSMutableDictionary  |  NSDictionary    |  NSMutableDictionary
                     |    深拷贝         |    深拷贝
------------------------------------------------------------

@property (nonatomic , retain) NSArray *datArr;

@property (nonatomic , copy) NSArray *datArr;
的区别

用retain修饰,会释放旧值,然后保留新值,再然后将新值设置上去

- (void)setDatArr:(NSArray *)datArr {
    if (_datArr != datArr) {
        [_datArr release];
        _datArr = [datArr retain];
    }
}

用copy修饰,会释放旧值,但是不保留新值,而是将其“拷贝”

- (void)setDatArr:(NSArray *)datArr {
    if (_datArr != datArr) {
        [_datArr release];
        _datArr = [datArr copy];
    }
}

有个例子

假设Person类里有一个属性
@property (nonatomic , copy) NSMutableArray *dataArr;

我们在另一个控制器里使用这个东西
Person *per = [Person alloc]init];

per.dataArr = [NSMutableArray array];
[per.dataArr addObject:@""];

[Person release];

那么代码会怎么样?
会因找不到addObject方法崩溃。因为使用per.dataArr的时候已经把dataArr通过copy变成不可变数组了,再次[per.dataArr addObject:@""]就会没有addObject方法

waring

属性的修饰只有copy,没有"mutableCopy".

mutableCopy只是单独针对几种特定的类才有的权力,比如字典(含可变字典)、数组(含可变数组)、字符串(含可变字符串)、NSData(含可变NSData)、NSSet(含可变NSSet)等

copy其他补充

NSString、NSArray、NSDictionary 等等经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary;
block 也经常使用 copy 关键字。block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”,他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。

用@property声明的NSString(或NSArray,NSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?
1.因为父类指针可以指向子类对象,使用 copy 的目的是为了让本对象的属性不受外界影响,使用 copy 无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本.
2.如果我们使用是 strong ,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性.

eg:
@property (nonatomic ,readwrite, strong) NSArray *array;
@property (nonatomic ,readwrite, copy) NSArray *array1;

然后进行下面的操作:
NSArray *array = @[ @1, @2, @3, @4 ];
NSMutableArray *mutableArray = [NSMutableArray arrayWithArray:array];

self.array = mutableArray; //俩指针指向了同一个区域
self.array1 = mutableArray;
[mutableArray removeAllObjects];;
NSLog(@"%@",self.array);
NSLog(@"%@",self.array1);

[mutableArray addObjectsFromArray:array];
self.array = [mutableArray copy];
[mutableArray removeAllObjects];;
NSLog(@"%@",self.array);

打印结果如下所示:
2019-05-10 10:34:55.636762+0800  Test[10681:713670] (
)
2019-05-10 14:26:39.048384+0800 Test[4445:427749] (
                                                   1,
                                                   2,
                                                   3,
                                                   4
                                                   )
2019-05-10 10:34:55.636762+0800  Test[10681:713670] (
                                                    1,
                                                    2,
                                                    3,
                                                    4
                                                    )

什么情况下不会autosynthesis(自动合成)?

1.同时重写了 setter 和 getter 时
2.重写了只读属性的 getter 时
3.使用了 @dynamic 时
4.在 @protocol 中定义的所有属性
5.在 category 中定义的所有属性
6.重载的属性。当你在子类中重载了父类中的属性,你必须 使用 @synthesize 来手动合成ivar。

自定义对象的copy

想让一个自定义对象可以copy还不报错,需要让自定义对象遵守<NSCopying>协议,并实现- (id)copyWithZone:(NSZone *)zone方法。在- (id)copyWithZone:(NSZone *)zone方法里分配空间,赋值属性。

引用计数的存储

在64bit中,引用计数可以直接存储在优化过的isa指针中,也可能存储在SideTableS()中,SideTableS()是散列表结构,其中SideTableS()中有很多SideTable结构,实例对象的地址作为key,SideTable结构作为value。非嵌入式系统中,使用64个SideTable

struct SideTable {
    spinlock_t slock; //自旋锁,是一种“忙等”的锁
    RefcountMap refcnts; //引用计数表,哈希表
    weak_table_t weak_table; //弱引用表,实际上也是哈希表
}

"为什么不用一个SideTable呢?
假如说只有一个SideTable,那么所有实例对象的引用计数和弱引用什么的都放在一个结构里。这时候如果我们要操作某一个对象的引用计数值进行修改,由于多个对象可能是在不同线程进行分配和销毁的,那么我们想正确的修改某一个对象就要进行加锁才能够保证资源访问正确。这时候大大减弱了效率。系统为了解决这种效率问题,采用了分离锁技术。将8个SideTable共用一把锁,共8把锁(N为64)。这样能保证最大并发量。但是劣势在于与采用单个锁来实现独占访问相比,要获取多个锁来实现独占访问将更加困难并且开销更高
"那么系统如何快速的实现SideTableS()分流呢?
SideTableS()本质是哈希表。通过对象地址经过hash函数计算后得出SideTable地址。

"RefcountMap"结构
引用计数表也就是RefcountMap是一个哈希表,通过一个指针快速找到对象的引用计数进行操作,避免循环遍历。
RefcountMap结构的key是指针(大牛没说是否是对象地址),value为size_t,size_t是一个无符号的long型的变量。
size_t在这里面是64位来表示,最低的一位(也就是第一个二进制位)用来表示是否有弱引用;第二位用来表示当前对象是否正在释放;其他62位用来存储这个对象的引用计数值,当我们想要获得这个对象的引用计数值时需要将这个值向右偏移两位才能取到真实的值。

"weak_table_t"弱引用表,实际上也是哈希表
weak_table_t的key为对象指针,通过hash运算,可以获得一个结构体为weak_entry_t,weak_entry_t为一个结构体数组,weak_entry_t这个结构体数组里面存的就是一个个的WeakPtr(对象),即弱引用指针地址。

alloc实现

经过一系列函数调用,最终调用了C函数calloc,此时并没有设置引用计数为1(疑问:那为啥此时打印retainCount时是1)

retain实现原理(源码可以查看,源码片段如下)

//通过对象的指针,去SideTableS()中获取SideTable
SideTable& table = SideTableS()[this];
//拿到SideTable中的引用计数表,然后获取引用计数值size_t
size_t& refcntStorage = table.refcnts[this];
//将引用计数值加上一个宏定义,这个宏是偏移两位的1,即4,因为size_t最后两位是记录其他信息,所以需要加偏移两位的1
refcntStorage += SIDE_TABLE_RC_ONE

release实现原理(刚好跟retain相反,但是代码为啥不一样不晓得)

//通过对象的指针,去SideTableS()中获取SideTable
SideTable& table = SideTableS()[this];
//拿到SideTable中的引用计数表进行查找
RefcountMap::iterator it = table.refcnts.find(this);
//将引用计数值减上一个宏定义,这个宏是偏移两位的1
it->second -= SIDE_TABLE_RC_ONE;

retainCount实现原理

//通过对象的指针,去SideTableS()中获取SideTable
SideTable& table = SideTableS()[this];
//声明一个局部变量为1
size_t refcnt_result = 1;
//拿到SideTable中的引用计数表进行查找
RefcountMap::iterator it = table.refcnts.find(this);
//将查找的结果向右偏移两位然后+1 (所以说新alloc的对象,在引用计数表里实际没有相关联的key、value,那么此时读出来的值就为0,然后偏移后加1即上面所说的新alloc的对象打印retainCount时是1)
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;

dealloc实现原理

        开始
         |
        \|/
    _objc_rootDealloc()
        \|/                     |- nonpointer_isa 是否使用非指针ISA
    rootDealloc()               |  weakly_referenced 是否有weak指针指向
        \|/                     |  has_assoc 是否有关联对象
yes--是否可以直接释放?-条件-需全否->|  has_cxx_dtor 是否有C++析构函数
 |            |NO               |- has_sidetable_rc 是否是通过sidetable()存储引用计数
\|/           |
C函数free()   |
            \|/
        object_dispose()

object_dispose()内部实现

针对上面没法直接释放的五个条件,全部清除后再调用C函数free()释放。
比如如果有关联对象,object_dispose()内部需先移除关联对象后才能释放
比如针对是否有weak指针指向,会将weak指针置nil

ARC是啥?

自动引用计数
ARC是LLVM编译器和Runtime共同协作的结果。
禁止手动调用retain、release、retainCount、autorelease这几个方法
ARC新增weak、strong属性关键字

ARC和MRC区别?

1 MRC是手动管理内存,ARC是LLVM编译器和Runtime共同协作来自动进行引用计数的内存管理。
2 MRC可以手动调用retain、release、retainCount、autorelease这几个方法,ARC不能手动调用

ARC

一、简介
ARC是自iOS 5之后增加的新特性,完全消除了手动管理内存的烦琐,编译器会自动在适当的地方插入适当的retain、release、autorelease语句。你不再需要担心内存管理,因为编译器为你处理了一切 注意:ARC 是编译器特性,而不是 iOS 运行时特性(除了weak指针系统),它也不是类似于其它语言中的垃圾收集器。因此 ARC 和手动内存管理性能是一样的,有时还能更加快速,因为编译器还可以执行某些优化

二、原理

ARC 的规则非常简单:只要还有一个变量指向对象,对象就会保持在内存中。当指针指向新值,或者指针不再存在时,相关联的对象就会自动释放。这条规则对于实例变量、synthesize属性、局部变量都是适用的

strong
strong修饰的属性一般不会自动释放,在OC中,对象默认是强指针;

strong的实现
使用strong关键字,引用计数自动加1,该指针指向的对象不会由于其他指针指向的改变而销毁。

weak
我们都知道weak表示的是一个弱引用,这个引用不会增加对象的引用计数,并且在所指向的对象被释放之后, weak指针会被设置的为nil。weak引用通常是用于处理循环引用的问题,如代理及block的使用中,相对会较多的使用到weak。

weak

先看个例子

// ARC是LLVM编译器和Runtime系统相互协作的一个结果

__strong MJPerson *person1;
__weak MJPerson *person2;
__unsafe_unretained MJPerson *person3;


NSLog(@"111");

{
    MJPerson *person = [[MJPerson alloc] init];
    
//❤1    person1 = person;
//❤2    person2 = person;
    person3 = person;
}
//❤1  NSLog(@"222 - %@", person1);    //有值
//❤2  NSLog(@"222 - %@", person2);    //null
NSLog(@"222 - %@", person3);      //崩溃:野指针访问

__unsafe_unretained 和 __weak 区别: _unsafe_unretained:和__weak 一样,唯一的区别便是,对象即使被销毁,指针也不会自动置空, 此时指针指向的是一个无用的野地址。如果使用此指针,程序会抛出 BAD_ACCESS 的异常。

weak的实现原理 (大牛讲解,MJ讲解没听懂)

http://www.cocoachina.com/ios/20170328/18962.html

{
    id __weak obj1 = obj;
}
上面代码通过编译之后成为下面这个样子
{
    id obj1;
    //objc_initWeak接收两个参数,一个弱引用地址,一个对象
    objc_initWeak(&obj1,obj);
}

那么objc_initWeak函数做了什么呢?(源码可看。下面过程就是弱引用变量添加的过程)

objc_initWeak函数会调用 objc_storeWeak()函数,objc_storeWeak() 会调用一个weak_register_no_lock()函数,weak_register_no_lock()函数会进行弱引用变量的添加,具体添加位置是通过hash运算找到对应位置,如果查找位置已经有当前对象对应的弱引用数组,就把弱引用变量添加进数组当中;如果没有那么久创建一个弱引用数组,把弱引用变量添加到第0个位置。

那么一个弱引用指针又是如何置nil的呢?

dealloc()经过一系列的调用,最终会调用weak_clear_no_lock(),weak_clear_no_lock()接收两个参数,一个弱引用表,一个dealloc对象。weak_clear_no_lock()会根据当前对象指针查找弱引用表,进而拿到当前对象的所有弱引用(是一个数组),遍历这个弱引用数组,把所有指针置为nil

/*
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组.

weak 的实现原理可以概括一下三步:
1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
*/

自动释放池(大牛讲解,MJ讲解没听懂)

AutoreleasePool原理是怎样的?

编译器会将
autoreleasepool{}
改写成
void *ctx = objc_autoreleasePoolPush();
{}中的代码
objc_autoreleasePoolPop(ctx);

其中objc_autoreleasePoolPush()的内部实现

void* objc_autoreleasePoolPush()
        |
        |会调用C++类AutoreleasePoolPage类里的push函数
        |
       \|/
void* AutoreleasePoolPage::push(void)

其中objc_autoreleasePoolPop()的内部实现

void* objc_autoreleasePoolPop(void* ctxt)
|
|会调用C++类AutoreleasePoolPage类里的pop函数
|
\|/
void* AutoreleasePoolPage::pop(void* ctxt)

一次pop相当于一次批量pop操作,即在pop时会往{}的所有对象都会发送一次release

自动释放池的数据结构

是以栈为结点通过双向链表的形式组合而成。
是和线程一一对应的。

双向链表

NULL <——父指针——       <——父指针——             <——父指针——
                头结点              后续结点...            最后结点              NULL
                      ——child指针——>          ——child指针——>     ——child指针——>


             |—————————    低地址
 栈顶  ——————>|obj(n)        /|\
             |obj(n-1)       |
             |...            |
 栈底  ——————>|obj(1)         |
             |————————     高地址

AutoreleasePoolPage类的结构如下

id* next;    //next指针指向栈当中下一个可填充的位置
AutoreleasePoolPage* const parent; //双向链表的父指针
AutoreleasePoolPage* child; //双向链表的child指针
pthread_t const thread; //线程

[obj autorelease]实现原理

下面先讲push过程

                                  开始
                                  \|/
增加一个栈结点到链表上<——yes——————next == 栈顶?
   |                               |NO
   |                              \|/
   |————————————————————————————>add(obj) //入栈添加

现在来讲下pop流程

根据传入的哨兵对象找到对应位置。
给上次push操作后添加的对象依次发送release
回退next指针到正确位置。

讲解上述三个步骤:
比如一个栈里有五个元素,栈底为5,依次,栈顶为1,next指针此时指向1的位置,即栈顶
这时候接收到一次AutoreleasePop操作,那么要给AutoreleasePool包含的元素全部release。假设AutoreleasePool包含的是3、2、1这三个指针,那么清空3、2、1。此时next指针此时指向3的位置.

总结自动释放池

在当次runloop将要结束的时候调用objc_autoreleasePoolPop

为什么AutoreleasePool可以嵌套使用呢?

多层嵌套就是多次插入哨兵对象。

什么场景下会需要手动创建AutoreleasePool呢?

在for循环中alloc图片数据等内存消耗较大的场景手动插入AutoreleasePool(比如每一次for就就释放防止内存峰值)

循环引用

三种类型:
自循环引用
相互循环引用
多循环引用

自循环引用

对象强持有成员变量obj,然后我们给obj赋值为原对象,那么就自循环引用

相互循环引用

A对象成员变量obj引用B对象,B对象成员变量obj引用A对象

多循环引用

A对象成员变量obj引用B对象,B对象成员变量obj引用C对象,C对象成员变量obj引用A对象

循环引用考点
代理
block
NSTimer
大环引用

上一篇下一篇

猜你喜欢

热点阅读