iOS的内存管理

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中提供了两个拷贝方法

  1. copy 不可变拷贝,产生不可变副本
  2. 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需要如下几个步骤,如果不这么做就会报方法找不到

  1. 首先类要遵守协议

  2. 实现- (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];
}

你可能感兴趣的:(iOS的内存管理)