iOS之内存管理
1.ARC、MRC
ARC:automatic reference counting 自引用用计数
MRC:manual reference counting 手动引用计数
MRC
在2011年、IOS5之前,iOS的开发只支持MRC模式。
MRC的六个特有方法:
- alloc
- retain
- release
- retainCount
- autorelease :
[[[NSObject alloc] init] autorelease];
在AutoreleasePool结束的时候会自动release对象 - dealloc
使用new、alloc、copy和mutableCopy产生的对象,在以后不用的时候,也需要release:
# 注意 initWithFormat的字符串一定要长,如果字符串短系统会采用taggepointer优化,引用计数为-1
# str1的引用计数为1
id str1 = [[NSString alloc] initWithFormat:@"asfdasasdfasdfasdfasdfasdfasdfasdfasdfasdfsadfasdfasdf"];
# str1、str2的引用计数为均为2 因为是不可变对象的拷贝,所以str2和str1指向同一块内存空间,即[str1 copy]相当于retain操作,引用计数+1
NSString *str2 = [str1 copy];
# str3的引用计数为1,因为产生的是可变对象,所以相当于str3指向一个新的内存空间
NSString *str3 = [str1 mutableCopy];
[str3 release];
[str2 release];
[str1 release];
当时每当一个新的指针引用了一块堆空间(也就是对象),
就必须手动的把此块堆空间内的 retainCount + 1。
Person* p = [Person new];//默认就是1 ,所以这里的p不需要手动操作。
Person* p2 = p;
[p2 retain];//将retainCount的值+1;
当p2指针不使用此堆空间了。要手动把 retainCount 值 - 1
[p2 release];
p不用了,也需要release
[p release];
手动计数器使用规则:
谁申请(retain),谁释放(release)
关于内存释放的本质:
当一块内存释放的时候,本质上只是给这部分字节打了标签。并没有把字节里的二进制数据全部清成0或者1.
什么是僵尸对象?
堆空间已经被标记清空,能被其他数据使用。但此时此刻,新的二进制数据还没有进来。
我们此时用一个指针指向已经标记释放了的堆空间。这个就叫僵尸对象和野指针。
ARC
- ARC是编译器和runtime共同作用的结果
- ARC中禁用MRC的六个方法
- ARC中新增
weak
和strong
关键字
2.AutoreleasePool
iOS系统针对不同场景下提供的内存方案:
- TaggedPointer:对于NSNumber、NSString、NSDate等对象,可以直接从指针提取数据内容,而不需要使用指针访问内存再提取内容
- NONPOINTER_ISA: 64位架构下,ISA指针占64bite位,实际使用中不需要这么多,苹果就在剩余的ISA比特位中存储了一些内存管理的相关信息
- 散列表 :包括引用计数表和弱引用计数表
在使用@autoreleasePool {}
后,编译器会将其改写为:
//@autoreleasePool {
创建了一个autoreleasePoolPage对象 push进一个标记 然后把数据依次放autoreleasePoolPage对象中
{代码}
依次释放掉表中的数据,直到碰到标记位停止
//}
- Autoreleasepool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组成
- AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址
- iOS里的TaggedPointer不适用autorelesepool
- NSAutoreleasePool可以创建一个autorelease pool,但该对象本身也需要被释放 drain
- 在ARC下,应当使用@autoreleasepool{}
- 对于不同线程,应当创建自己的autorelease pool。如果应用长期存在,应该定期drain和创建新的autorelease pool。
- runloop 与 AutoreleasePool 是协同合作关系
- AutoreleasePool 与 runloop 与线程是一一对应的关系
- AutoreleasePool 在 runloop 在开始时被push,在runloop休眠时(beforewaiting状态)pop
思考 使用autorelease的对象什么时候会被释放?
- 如果在autoreleasepool中,是在autoreleasepool 执行到后括号的时候释放
NSLog(@"1");
@autoreleasepool {
NSObject *str1 = [[NSObject alloc] init];
} # 此时释放
NSLog(@"2");
- 如果没有单独放到
autoreleasepool
中的时候,是在runloop即将休眠的时候统一释放,因为程序入口main函数是放在autoreleasepool
中:
3.循环引用
循环引用分类:
- 自循环引用
- 相互循环引用
- 多循环引用
自循环引用
对象的强持有变量指向自身,就会造成自循环引用
相互循环引用
多循环引用
如何破除循环引用?
-
使用__weak
-
使用 __block
MRC下,__block修饰对象不会增加引用计数,避免了循环引用
ARC下,__block修饰对象会被强引用,无法避免循环引用,需要手动街环 -
使用__unsafe_unretained
修饰对象不会增加引用计数,避免了循环引用
如果修饰对象在某一时机被释放了,会产出悬垂指针
CADisplayLink、NSTimer的循环引用问题:
CADisplayLink、NSTimer会对target产生强引用,如果target也对他们进行了强引用,就会出现循环引用问题
解决方案1:使用中间人
NSTimer的循环引用问题解决.png通过创建一个中间对象,令中间对象持有两个弱引用变量分别是原对象和NSTimer,NSTimer的回调是在中间对象中实现的。在中间对象实现的NSTimer的回调方法中,对中间对象持有的weak弱引用target值的判断,如果当前target值存在,则把NSTimer的回调给原对象,如果值为nil,则把NSTimer设为无效即可解除当前runloop对NSTimer的强引用和NSTimer对中间对象的强引用。
解决方案2:使用动态消息解析
ViewController中:
@interface ViewController (){
NSTimer *_timer;
}
- (void)viewDidLoad {
[super viewDidLoad];
_timer = [NSTimer scheduledTimerWithTimeInterval: 1 target: [TimerMiddleware initWithTarget: self] selector: @selector(sayhaha) userInfo: nil repeats: YES];
}
- (void)dealloc {
[_timer invalidate];
_timer = nil;
}
新建一个消息解析类:
@interface TimerMiddleware : NSObject
+ (instancetype)initWithTarget:(id)target;
@property (nonatomic, weak) NSObject *target;
@end
@implementation TimerMiddleware
+ (instancetype)initWithTarget:(id)target {
TimerMiddleware *mid = [TimerMiddleware new];
mid.target = target;
return mid;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.target;
}
@end
解决方案3:使用NSProxy转发消息
NSProxy是跟NSObjec一个级别的基类,用来设计做消息转发的。
NSProxy是抽象类,使用时候我们需要使用其子类
NSProxy不会跟NSObject类一样去父类搜索方法实现,会直接进入消息转发流程
@interface MyProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (nonatomic, weak) NSObject *target;
@end
@implementation MyProxy
+ (instancetype)proxyWithTarget:(id)target {
MyProxy *proxy = [MyProxy alloc];
proxy.target = target;
return proxy;
}
# NSProxy接收到消息会自动进入到调用这个方法 进入消息转发流程
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector: sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget: self.target];
}
@end
Demo详见:https://www.jianshu.com/p/9c91ee60b0dc
面试总结:
1.什么是ARC?
ARC是由LLVM和runtime共同协作来为我们实现自动引用计数管理
2.为什么weak指针指向的对象在被废弃之后会被自动置为nil?
当对象被废弃之后,dealloc的内部实现当中会调用清除弱引用的一个方法。然后在清楚弱引用的方法当中,会通过哈希算法来查找被废弃对象在弱引用表当中的位置,来提取所对应的弱引用指针的列表数组,然后进行for循环遍历,把每一个weak指针都置为nil
3.苹果是如何实现AutoreleasePool的?
AutoreleasePool是以栈为节点,以双向链表形式合成的一个数据结构