1.1 管理的原因
- 只有OC对象才需要管理内存,非OC对象(如:char、int、folat)则不需要管理内存的本质原因:
OC对象是放在堆内存里,非OC对象是放在栈内存里,栈内存里的东西系统会自动管理
1.2 释放过渡
- 当一个对象创建出来,执行了多次release操作,就会报错--释放过渡
- 简单来说就是
//引用计数器是1
Person *p = [[Person alloc]init];
//此时p的引用计数器是0,(p指向的内存已经是坏内存了,称person对象为僵尸对象)
[p release];
//注意:此时再次调用relesase方法,则会报错`message sent to deallocated instance 0x110201950`(向一个已经释放了的对象发送消息)
[p release];
//此时p指向僵尸对象(坏内存),则称p为野指针 //给空指针发送消息不会报错,
1.3 野指针、空指针
- 僵尸对象:已经被销毁的对象(不能再使用的对象)
- 野指针:指向僵尸对象(不可用内存)的指针
- 给野指针发消息会报EXC_BAD_ACCESS错误
- 空指针:没有指向存储空间的指针(里面存的是nil,也就是0)
- 给空指针发消息是没有任何反应的
- 为了避免野指针错误的常见办法是在对象被销毁后,将指向对象的指针变为空指针
-
默认情况下,Xcode是不会管僵尸对象的,使用一块被释放的内存也不会报错,为了方便调试,应该开启僵尸对象监控。如下图:
1.4 dealloc方法的重写
- 当一个对象的引用计数器值为0时,这个对象即将别销毁,其占用的内存被系统回收,系统会自动给对象发送一条dealloc消息
- 一旦重写了dealloc方法,就必须调用
[supper dealloc]
,且放在最后面调用
1.5 重写setter方法
- (void)set:Room:(Room *)toom
{
//传进来的room和_room不一样的时候
if(_room != room){
//对旧值(当前正在使用的房间)做一次release
[_room release];
//对新房间做一次retain操作
[room retain];
_room = room;
//后两步,也可以简化成_room = [room retain]
}
}
//getter方法
- (Room*)room
{
return _room;
}
原因:举例子来说,试想这样的场景:一个名叫张三的人想开一间房,比方说最开始他开了房号为01的房子,等张三打算入住的时候发现房间01采光不好,想要换一间房子,此时张三该怎么做呢?他应该是先退掉01号房间,再去开02房子(喜新厌旧可以,但是需要对旧的东西负责),还有加上判断条件,是为了防止重复赋值的时候 出现野指针的错误
1.6 @property属性定义
如果只是用@property修饰一个属性,默认生成对应的setter方法和getter方法。但具体实现是这样的:
//声明属性
@property Dog *dog;
//默认的setter方法实现
- (void)setDog:(Dog *)dog
{
_dog = dog;
}
显然不能这么干,所以需要加一些修饰属性的关键字,如retain、assign、copy等
- retain:(MRC下)系统默认重写setter和getter方法,具体实现和1.5的getter、setter方法一样,修饰OC对象,release旧值,retain新值。
- assign:直接赋值,不做任何内存管理(默认,用于非OC对象)。
- copy:release旧值,copy新值(一般用于字符串NSString*)。
1.7 @class和#import
- 作用:#import会包含引用类的所有信息(内容),包括引用类的变量和方法。@class仅仅是告诉编译器有这么一个类,具体这个类里有什么信息,完全不知。
- 效率:如果有上百个头文件都#import了同一个文件,或者这些文件依次被#import,那么一旦最开始的头文件稍有改动,后面引用到这个文件的所有类都需要重新编译一遍,编译效率非常低。相对来说,使用@class方式就不会出现这种问题。
- ** 补充三点:**
- <1>. #import跟#include都能完整的包含某个文件的内容,#import能防止同一个文件被包含多次;
- <2>. @class仅仅是声明一个类,并不会包含类的完整声明;@class还能解决循环包含的问题;
- <3>. #import<>用来包含系统自带的文件,#import""用来包含自定义的文件
1.8 autorelease
- 给对象发送一条autorelease消息,会将对象放到一个自动释放池中
- 当自动释放池被销毁时,会对池子里面的所有对象做一次release操作
- 会返回对象本身,调用完autorelease方法后,对象的计数器不变
//自动释放池什么时候销毁?
kCFRunLoopEntry//创建一个自动释放池
kCFRunLoopBeforeWaiting//销毁自动释放池,创建一个新的自动释放池
kCFRunLoopExit//销毁自动释放池
- 简述一下自动释放池底层怎么实现?
自动释放池以栈的形式实现:当你创建一个新的自动释放池时,它将被添加到栈顶,当一个对象收到发送autorelease消息时,他被添加到当前线程的处于栈顶的自动释放池中,当自动释放池被回收时,他们从栈中被删除,并且会给池子里所有的对象都会做一次release操作。
1.9 string的内存管理
NSString *str1 = @"Jack";
NSString *str2 = [NSString stringWithFormat:@"Rose"];
NSString *str3 = @"Jack";
NSString *str4 = [NSString stringWithFormat:@"Rose"];
注意:直接赋值的,则相应的字符串会放到常量区,常量区的字符串有且只有一份,即str3和str1指向的是同一个字符常量。但通过类方法stringWithFormat
自定义创建的字符串会放在堆里面,即使字符串内容相同,也会再次开辟新空间,然后指针就是在栈里面,保存对象的地址。内存分布空间如下:
- + stringWithFormat:类方法,返回一个autorelease的NSString实例,不用手动Release,在自动释放池中会自动释放。
- – initWithFormat:实例方法,返回一个自己Alloc申请内存的NSString实例,根据OC内存管理黄金法则,管杀管埋,它则需要自己手动Release。
2.0 copy的内存管理
首先理解一下深复制和浅复制:
深复制
- 源对象和副本对象是不同的两个对象;
- 源对象引用计数器不变,副本对象计数器为1(因为是新产生的);
- 本质是:产生了新的对象。
浅复制 - 源对象和副本对象是同一个对象
- 源对象(副本对象)引用计数器+1,相当于做一次retain操作;
- 本质是:没有产生新对象。
注意:只有源对象和副本对象都不可变时,才是浅拷贝,其他都是深拷贝
NSString *str1 = [NSString stringWithFormat:@"address is only one"];
NSString *str2 = [str1 copy];
NSLog(@"%p ,%p",str1,str2);
//结果发现两个地址一样,都是0x610000052990。
/**
1.copy:产生的肯定是不可变副本
2. 如果是不可变对象调用copy方法产生出不可变副本,那么不会产生新的对象
*/
然后数组、字典的情况和字符串类似。总结字符串调用copy和mutableCopy的情况如下:
1. NSMutableString调用mutableCopy : 深复制。
2. NSMutableString调用copy : 深复制。
3. NSString调用mutableCopy : 深复制。
4. NSString调用copy : 浅复制
2.1 单例模式
- 系统单例如:UIApplication 、NSUserDefaults、UIDevice....公用一份,省内存,方便管理,一些整个程序都用的上的数据
- 单例工具类的简单写法
static NetworkTools *_networkTools;
@implementation NetworkTools
+ (instancetype)sharedNetworkTools
{
return [[self alloc]init];
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
if (_networkTools == nil) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_networkTools = [super allocWithZone:zone];
});
}
return _networkTools;
}
- (instancetype)init
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_networkTools = [super init];
});
return _networkTools;
}
2.2 KVO
假设有两个类A和B,其中B有个属性age(int类型),现在让A类监听B类的age属性变化,当发生变化时打印一句话。
/**
* A对象监听B对象的age属性发生变化
* @param options 值变化
1. NSKeyValueObservingOptionNew:新值、
2. NSKeyValueObservingOptionOld:旧值
* @param event
*/
[B addObserver:A forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
//KVO实现过程,其代码在运行时会创建一个B类的派生类NSKVONotifying_B,并在NSKVONotifying_B类里重写属性age的setter方法
- (void)setAge:(int)age
{
[supper setAge:age];
//并在sette方法里调用这两个方法,当调用了这两个方法,就会通知B类执行那个监听方法
[self willChangeValueForKey:@"age"];
[self didChangeValueForKey:@"age"];
}
//MARK:- 在B类里
/**
* 属性发生改变时执行
*
* @param keyPath 检测的属性 此时是age
* @param object 谁的属性
* @param change 改变(oldValue、newValue)
* @param context
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
NSLog(@"监听到%@属性发生改变了,change= %@,context= %@",object,change,context);
}
- KCO的底层实现原理:
KVO是基于runtime机制实现的。当某个类的对象第一次被观察时,系统就会在运行期动态的创建该类的一个派生类,在这个派生类中重写基类的中任何被观察属性的setter方法。派生类在被重写的setter方法实现真正的通知机制(Person -> NSKVONotifying_Person)
2.3 block的内存管理
- 默认情况下block的内存是在栈中(不需要手动去管理block内存),它不会对所引用的对象进行任何操作
- 如果对block进行了copy操作, block的内存会搬到堆里面,它会对所引用的对象做一次retain操作
- 对于普通的局部变量,block只会引用它的初值,不能跟踪它的改变;block内部能够一直应用被__block修饰的变量,block内部能够一直引用被static修饰的变量,block内部能够一直引用全局变量;
- block的本质是“带有自动变量的匿名函数”,其实就是一段代码块的内存的指针
- 非ARC: 如果所引用的对象用了__block修饰,就不会做retain操作。
- ARC: 如果所引用的对象用了__unsafe_unretained、__weak修饰,就不会做retain操作。(注意:__unsafe_unretained和__weak的相同点是两者都不持有该对象,当他拥有的对象被释放的时候,那此时这个若引用也会自动失效,区别是__weak会被置为nil的状态。__unsafe_unretained不会置nil,会有野指针的风险)
- 为什么加上 __block就可以修改外部的变量了?
真正的原因是这样的:我们都知道:Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。__block所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。