iOS之内存管理

作者: 芝麻酱的简书 | 来源:发表于2019-01-16 14:22 被阅读67次

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

    循环引用分类:

    • 自循环引用
    • 相互循环引用
    • 多循环引用
    自循环引用

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

    相互循环引用
    多循环引用
    如何破除循环引用?
    • 使用__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是以栈为节点,以双向链表形式合成的一个数据结构

    相关文章

      网友评论

        本文标题:iOS之内存管理

        本文链接:https://www.haomeiwen.com/subject/rdcmdqtx.html