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中新增weakstrong关键字

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.循环引用

循环引用分类:

  • 自循环引用
  • 相互循环引用
  • 多循环引用
自循环引用
1751468-5735e0cf6bbaeac5.png

对象的强持有变量指向自身,就会造成自循环引用

相互循环引用
1751468-54aef3fd17308109.png
多循环引用
1751468-799e2fce1908f1cd.png
如何破除循环引用?
  • 使用__weak

  • 使用 __block
    MRC下,__block修饰对象不会增加引用计数,避免了循环引用
    ARC下,__block修饰对象会被强引用,无法避免循环引用,需要手动街环

  • 使用__unsafe_unretained
    修饰对象不会增加引用计数,避免了循环引用
    如果修饰对象在某一时机被释放了,会产出悬垂指针


CADisplayLink、NSTimer的循环引用问题:

CADisplayLink、NSTimer会对target产生强引用,如果target也对他们进行了强引用,就会出现循环引用问题

解决方案1:使用中间人
1751468-22c121ae113f1444.png
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是以栈为节点,以双向链表形式合成的一个数据结构

猜你喜欢

转载自blog.csdn.net/weixin_34087503/article/details/87375160