iOS OC对象的内存管理
在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);
@synthesize
自动生成成员变量和属性的setter、getter实现
//@synthesize age = _age; 属性名在左边 ,成员变量名在右边
随着编译器的发展@synthesize也不用写了 只要我们用@property修饰就会自动生成setter getter方法的声明 ,并且是_(下划线)开头的成员变量,并且sette方法 getter方法都帮你生成好了,而且一旦你传进去的修饰值是@property (nonatomic, assign) assign也就意味着是直接赋值,那么既然是直接赋值,到时候你的setter方法就是直接赋值,没有其他内存管理的操作
如果对象类型还是用assign修饰,那么生成setter方法也是直接赋值,那么就会产生问题,一旦外界,一旦外界的
- (void)setDog:(MJDog *)dog
{
_dog = dog;
}
一旦外界的dog释放 里面的 dog就不能用了 ,如果要保证里面的dog可以使用,所以就需要写(MRC实现思路)
- (void)setDog:(MJDog *)dog
{
if (_dog != dog) {
[_dog release];
_dog = [dog retain];
}
}
这时候用的关键字就是@property (nonatomic, retain) MJDog *dog; 这时候代码就是上面的形式。帮你生成内存管理的代码
使用autorelease注意
self.data = [NSMutableArray array];
这种方法内部调用了autorelease 所以不需要我们release操作
如果是alloc 、new 可以直接调用self.data = [[[NSMutableArray alloc] init] autorelease];
然后在
- (void)dealloc {
//要在super 之前调用释放
self.data = nil;
[super dealloc];
}
+ (instancetype)person
{
return [[self alloc] init] ;
}
这种类方法相当于工厂方案,相当于工厂一样,调用一下这个工厂方法 就生产一个person
Copy
拷贝的目的 产生一个副本(副本的特点就是改变一个文件 他不会影响另一个文件)跟源对象是互不影响,修改了副本对象不会影响源对象,修改了源对象不会影响副本对象
需求:有一个对象A里面有很多属性,这个时候你可能需要另一个对象B里面也有这么多属性 这个时候可以拷贝A,这样就不用一个个重新设置了,有点类似于克隆技术
iOS中提供了两个拷贝方法
- copy 不可变拷贝,产生不可变副本
- mutableCopy 可变拷贝,产生可变副本
NSString *str1 = [[NSString alloc] initWithFormat:@"test"];
NSString *str2 = [str1 copy]; // 浅拷贝,指针拷贝,没有产生新对象
NSMutableString *str3 = [str1 mutableCopy]; // 深拷贝,内容拷贝,有产生新对象
NSLog(@"%@ %@ %@", str1, str2, str3);
NSLog(@"%p %p %p", str1, str2, str3);
分析:首先str1和str2指针指向同一个对象 原因是str2 copy操作拷贝的后得到是一个不可变对象,原对象str1也是一个不可变对象,他们指向的对象都是不可变的不能更改的字符串,所以为了避免资源浪费 他俩指向的都是同一个对象,因为指向的对象不可更改,不存在更改对象的值操作;
为什么str3 mutableCopy会产生新的对象呢?因为str3 返回的是一个可变字符串,原来的不可变字符串属性都改变了 所以是一个新的对象,修改了对象的值并不会影响原来的值,因为副本的本质是需要克隆一个对象 但是mutableCopy会产生一个可变对象,也就是可变字符串,所以以后需要有修改操作。所以为了不影响原值 会产生新的对象。
深拷贝和浅拷贝的区别?
1、深拷贝、内容拷贝、产生新的对象。
2、浅拷贝,指针拷贝、没有产生新的对象。
如果原对象是是可变对象,不管是copy或者是mutableCopy都是深拷贝,因为copy的话产生的是一个不可变对象,拷贝的意义是要产生副本,因为原对象是一个
下面的代码会出什么问题?
@interface MJPerson : NSObject
@property (copy, nonatomic) NSMutableArray *data;
@end
MJPerson *p = [[MJPerson alloc] init];
p.data = [NSMutableArray array];
[p.data addObject:@"jack"];
[p.data addObject:@"rose"];
回答:会崩溃,因为data是copy修饰,用copy修饰就会set方法的时候就会调用copy操作,生成一个不可变对象,既然是生成不可变对象,就不会存在addObject:方法,所以在使用NSMutableArray不要用copy修饰,NSArray才可以
NSString底层用的都是copy修饰符,用copy修饰能保证不管外面传进来的是可变或者不可变,这个NSString是不可变的,要想改字符串,需要对整一个属性进行新的赋值,直接修改值,这样即使是外面那一个不可变字符串接受,我用copy修饰 NSSMutableString 用copy也会进行深拷贝,拷贝一个新的副本,这样修改NSSMutableString就不会影响原来的NSString
自定义copy
有这样一个场景创建一个Person对象,Person对象中有一大堆属性,这个时候想拷贝一个Person对象的副本,进行拷贝他的属性,这时候就可以自定义copy操作,既然Person属性可以调用copy那么可以对一个类的属性进行mutableCopy操作吗?答案是不行的,mutableCopy是指给Fundition框架自带的一些类进行操作的,比如
NSString,NSMutableString
NSArray,NSMutableArray
NSData,NSMutableData
NSSet,NSMutableSet
NSDictionary,NSMutableDictionary
那么属性为什么没有mutableCopy呢?因为属性是通用的,可以定义很多不同的属性和自定义属性(比如如一个person,里面有个属性是cat,dog),既然是通用的只能用copy,因为mutableCopy是对某些特定的类操作的。
自定义copy 要想实现copy需要如下几个步骤,如果不这么做就会报方法找不到
首先类要遵守
协议 -
实现- (id)copyWithZone:(NSZone *)zone方法
- (id)copyWithZone:(NSZone *)zone { MJPerson *person = [[MJPerson allocWithZone:zone] init]; person.age = self.age; // person.weight = self.weight; return person; }
当p1调用copy方法实际上相当于调用copyWithZone:(NSZone *)zone
MJPerson *p1 = [[MJPerson alloc] init];
p1.age = 20;
p1.weight = 50;
MJPerson *p2 = [p1 copy];
这时候我把p1所有的属性否赋值给了这个全新的对象,所以这个全新的对象里面就有p1里面所有的属性,这两个对象是全新的对象,互不影响,哪些属性想copy是可以自己控制的 上述代码里age进行了copy而 weight并没有
引用计数值的存储?
可以存储在isa指针里面 如果不够存储就存储在SideTable的散列表中
从64bit开始,引用计数可以直接存储在优化过的isa指针中,也可能存储在SideTable类中
从arm64架构开始,对isa进行了优化,变成了一个公用体(union)结构,还使用位域来存储更多信息
isa位域里的19位extra_rc里面存储的值是引用计数减1,还有一个就是has_sidetable_rc,如果这19位不够存储我们的引用计数的话,到时候has_sidetable_rc就会为1,一旦变成1,引用计数就会存储在SideTable的类里面
-
has_sidetable_rc
- 引用计数器是否过大无法存储在isa中
- 如果为1,那么引用计数会存储在一个叫SideTable的类属性中
weak指针的实现原理?
将弱引用存到一个hash表里面,到时候这个对象要销毁,他就会取出当前对象对应的那个弱引用表,把弱引用表存储的那些弱引用都清除掉
ARC都帮我们做了什么?
LLVM+Runtime结合协调帮我们达到ARC的效果
- 首先ARC利用LLVM的编译器自动帮我们生成release、retain、autorealse代码,说明我们开启了ARC以后 ,自动根据根据函数结束自动补上release代码
- 像弱引用这样的存在,是需要运行时runtime来支持的,他是在程序运行的过程中,监听到对象销毁的过程中,就会把这个对象对应的弱引用都清除掉,需要在程序运行中还要操作这个弱指针
autoreleasePool原理
atautoreleasepoolobj = objc_autoreleasePoolPush();
MJPerson *person = [[[MJPerson alloc] init] autorelease];
objc_autoreleasePoolPop(atautoreleasepoolobj);
自动释放池的主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage
调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的
autoreleasePool 内部包含一个pthread_t thread是有专属于某一个线程的,他不能同时用于两个线程里面
AutoreleasePoolPage
每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址
所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起
每一个AutoreleasePoolPage对象都有一个AutoreleasePoolPage* parent和AutoreleasePoolPage* child ,不同的AutoreleasePoolPage之间通信是通过child指向下一个AutoreleasePoolPage的child,AutoreleasePoolPage之间通信是通过parent指向上一个AutoreleasePoolPage的parent。最后一个child的下一个指向nil,第一个parent的上一个也指向nil
其实是在堆区
objc_autoreleasePoolPush()
首先他会把调用了autorelease方法的这个对象的内存地址,会存放到AutoreleasePoolPage的内存里面去,因为AutoreleasePoolPage有4096个字节,它本身里面有七个成员,每个占8个字节就还剩4040个字节这里面开始的地址叫begin 结束的叫end,如果使用autorelease方法的对象过多,地址不够用他就会创建一个新的AutoreleasePoolPage,这些字节就是用来存储就会存储调用了autorelease的这些对象的地址,调用objc_autoreleasePoolPush()方法的时候会将一个POOL_BOUNDARY(这个值就是个nil 相当于0)也就是相当于将0入栈,并且返回其存放的内存地址,
objc_autoreleasePoolPop()
pop传进去的是当初你所压进去的POOL_BOUNDARY的内存地址,我告诉objc_autoreleasePoolPop()这个函数当初我POOL_BOUNDARY在哪里,objc_autoreleasePoolPop()拿到POOL_BOUNDARY这个边界地址,他会从最后压倒AutoreleasePoolPage进去的内存地址开始调用他们的release方法,直到遇到刚才传进来的POOL_BOUNDARY为止,也就是说他会从最后一个对象开始(从栈顶开始),一直调用到POOL_BOUNDARY,调用到你刚才传进来这个POOL_BOUNDARY,只要调用push 他就会按照顺序添加一个POOL_BOUNDARY,到时候pop的时候传进来是哪个对象 就会给pop传进来哪个POOL_BOUNDARY,这样就会返回到哪个POOL_BOUNDARY为止
next指针,哨兵对象
永远都执行下一个能够存储AutoreleasePoolPage对象内存的地方
autorelease对象会在什么时机被调用release?
如果是直接被AutoreleasePool包住的话,他的release时机就是括号结束,也就是下面代码1的位置
@autoreleasepool {//0
MJPerson *person = [[MJPerson alloc] init];
}//1
Runloop和Autorelease
iOS在主线程的Runloop中注册了2个Observer,obsever就是用来监听runloop的状态的
第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
第2个Observer
监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()
//这个Person什么时候调用release,是由RunLoop来控制的
// 它可能是在某次RunLoop循环中,RunLoop休眠之前调用了release
MJPerson *person = [[[MJPerson alloc] init] autorelease];
方法里面有局部对象,是不是方法结束他就立即释放
如果ARC是下面这样是立即释放
- (void)viewDidLoad {
[super viewDidLoad];
MJPerson *person = [[MJPerson alloc] init];
[person release];
NSLog(@"%s", __func__);
}
如果是下面这养是在下一次runloop休眠之前释放
- (void)viewDidLoad {
[super viewDidLoad];
MJPerson *person = [[[MJPerson alloc] init] autorelease];
}