内存管理系列—OC的内存管理模式

2020/3/25 23:01:48

本文主要是介绍内存管理系列—OC的内存管理模式,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

1. 引言

常常面试的时候,会被问到“谈谈你对OC中内存管理的理解”,个人觉得应该从以下三个部分来回答,才比较全面

本文主要介绍OC的内存管理的模式(机制)来分析。

2.为什么需要对OC进行内存管理

  • 程序在运行的过程中通常通过以下行为,来增加程序的的内存占用
    1. 创建一个OC对象
    2. 定义一个变量
    3. 调用一个函数或者方法
  • 移动设备分配给每个App的内存有限,App运行中会创建大量对象, OC对象存储在堆中,系统不会自动释放堆中的内存,对象没有及时释放,就会占用大量内存,系统会发出内存警告,对应用运行造成影响
  • 如果程序占用内存过大,系统可能会强制关闭程序,造成程序崩溃、闪退现象,影响用户体验

所以,我们需要对内存进行合理的分配内存、清除内存,回收那些不需要再使用的对象。从而保证程序的稳定性。

3. 哪些对象才需要我们进行内存管理

任何继承了NSObject的对象需要进行内存管理,而其他非对象类型(int、char、float、double、struct、enum等) 不需要进行内存管理

这是因为:

  • 继承了NSObject的对象的存储在操作系统的堆里边。一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式类似于链表
  • 非OC对象一般放在操作系统的栈里面,由操作系统自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈(先进后出)

4. 内存管理机制

在OC中没有垃圾回收机制,OC提供了一套机制来管理内存,即“引用计数”,每个OC对象都有自己的引用计数器

4.1 引用计数

  1. 【简单定义】:引用计数简单来说就是统计一块内存的所有权, 当这块内存被创建出来的时候(alloc、new或copy),它的引用计数从0增加到1,表示有一个对象或指针持有这块内存,拥有这块内存的所有权
  2. 【引用计数的增加】:如果这时候有另外一个对象或指针指向这块内存,那么为了表示这个后来的对象或指针对这块内存的所有权,引用计数加1变为2
  3. 【引用计数的较少】:之后若有一个对象或指针不再指向这块内存时,引用计数减1,表示这个对象或指针不再拥有这块内存的所有权
  4. 【内存的释放】:当一块内存的引用计数变为0,表示没有任何对象或指针持有这块内存,系统便会立刻释放掉这块内存

5. 内存管理原则

《iOS与OS X多线程和内存管理》这本书说的是

  • 自己生成的对象,自己持有
  • 非自己生成的对象,自己也能持有
  • 不在需要自己持有的对象时释放
  • 非自己持有的对象无法释放

但是个人觉得太绕了,简单来说: 对于所有的对象而言,你只要记住Apple的官网上的内存管理三定律就可以:

  • 一个对象可以有一个或多个拥有者
  • 当它一个拥有者都没有时,它就会被回收
  • 如果想保留一个对象不被回收,你就必需成为它的拥有者(ownership)

6. 内存管理的3种模式

在开发时引用计数又分为ARC(自动引用计数)和MRC(手动引用计数)。ARC的本质其实就是MRC,只不过是系统帮助开发者管理已创建的对象或内存空间,自动在系统认为合适的时间和地点释放掉已经失去作用的内存空间,原理是一样的。

而对于自动释放池(Autorelease Pool可以算是半自动的机制,所以这里我单独归为一类,不和MRC一起分析。

6.1 MRC手动引用计数

遵循谁申请、谁添加、谁释放的原则。需要手动处理内存技术的增加和修改。将从以下几个方面来深入了解MRC种的内存管理。

6.1.1 内存管理原则

  1. 谁创建谁release:如果你通过alloc、new、copy或mutableCopy来创建一个对象,那么必须调用release或autorelease
  2. 谁retain谁release: 只要你调用了retain,就必须调用一次release 上个demo感受下
    运行环境:MRC模式
-(void)test{
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        NSLog(@"retainCount = %lu", [p retainCount]); // 1
        [p retain];// 只要给对象发送一个retain消息, 对象的引用计数器就会+1
        NSLog(@"retainCount = %lu", [p retainCount]); // 2
        // 通过指针变量p,给p指向的对象发送一条release消息
        // 只要对象接收到release消息, 引用计数器就会-1
        [p release];
        // 需要注意的是: release并不代表销毁\回收对象, 仅仅是计数器-1
        NSLog(@"retainCount = %lu", [p retainCount]); // 1
        [p release]; // 0
        
    }
    NSLog(@"----自动释放池已释放------");
}
// 打印结果
2020-03-25 14:58:10.251949+0800 02-内存管理-MRC开发[13803:235592] retainCount = 1
2020-03-25 14:58:10.252081+0800 02-内存管理-MRC开发[13803:235592] retainCount = 2
2020-03-25 14:58:10.252147+0800 02-内存管理-MRC开发[13803:235592] retainCount = 1
2020-03-25 14:58:10.252239+0800 02-内存管理-MRC开发[13803:235592] Person被释放了
2020-03-25 14:58:10.252332+0800 02-内存管理-MRC开发[13803:235592] ----自动释放池已释放------

复制代码

当引用计数为0的时候,person对象就被释放了,Peson中得dealloc方法就会打印“Person被释放了”****

6.1.2 引用计数关键字

在MRC中会引起引用计数变化的关键字有:alloc,retain,copy,release,autorelease。(strong关键字只用于ARC,作用等同于retain)

  • alloc:它的作用是开辟一块新的内存空间,并使这块内存的引用计数从0增加到1,
  • retain:只能由对象调用,它的作用是使这个对象的内存空间的引用计数加1,并不会新开辟一块内存空间
  • copy:这个部分单独抽出来讲,涉及内容比较多。
  • release:它的作用是使对象的内存空间的引用计数减1,若引用计数变为0则系统会立刻释放掉这块内存。如果引用计数为0的基础上再调用release,便会造成过度释放,使内存崩溃;
  • autorelease:它的作用于release类似,但不是立刻减1,相当于一个延迟的release,通常用于方法返回值的释放

6.1.3 属性关键字

MRC中常用的属性关键自主要是:assign、reatin、copy

  • assign
    如果在property后边加上assign,系统就不会帮我们生成set方法内存管理的代码,仅仅只会生成普通的getter/setter方法,默认什么都不写就是assign
@property (nonatomic,assign) int val;
复制代码
  • copy关键字
    系统会自动帮我们生成getter/setter方法内存管理的代码
@property (nonatomic,copy) NSString *name;
// copy修饰的属性,内部setter方法的实现大概酱紫:
- (void)setName:(NSString *)name {
    if (_name != name) {
        [_name release];
        _name = [name copy];
    }
}
复制代码
  • retain关键字
    系统会自动帮我们生成getter/setter方法内存管理的代码
@property (nonatomic,retain) NSString *name;
// retain修饰的属性,内部setter方法的实现大概酱紫:
- (void)setName:(NSString *)name {
    if (_name != name) {
        [_name release];
        _name = [name retain];
    }
}
复制代码

6.1.4 copy关键字重点解析

我门先来弄清楚一些概念:

  1. iOS提供了两个拷贝方法:

    • copy 不可变拷贝,产生【不可变】副本(即便本来是可变的,拷贝出来的都是不可变的)
    • mutableCopy 可变拷贝,产生【可变】副本(即便本来是不可变的,拷贝出来的都是可变的)
  2. 拷贝的目的

    • 产生一个副本对象,跟源对象互不影响
    • 修改了源对象,不会影响副本对象
    • 修改了副本对象,不会影响源对象
  3. 浅拷贝和深拷贝

    • 浅拷贝:指针拷贝,【没有】产生新对象,引用计数会加1(retainCount += 1)
    • 深拷贝:内容拷贝,【有】产生新对象,引用计数初始1(retainCount = 1)

思考🤔:对于copy,引用计数是否可能小于0?

下面来看几个列子:

  • TaggedPointer的浅拷贝和深拷贝
void copyTest1) {   
    NSString *str1 = [[NSString alloc] initWithFormat:@"abc"]; // TaggedPointer
    NSString *str2 = str1.copy; // 浅拷贝 TaggedPointer
    NSMutableString *str3 = str1.mutableCopy; // 深拷贝 对象
    
    NSLog(@"str1 %@ --- %zd --- %p", str1, str1.retainCount, str1);
    NSLog(@"str2 %@ --- %zd --- %p", str2, str2.retainCount, str2);
    NSLog(@"str3 %@ --- %zd --- %p", str3, str3.retainCount, str3);
}
// 打印结果
2020-03-25 16:00:37.509103+0800 03-内存管理-copy[14479:285375] str1 abc --- -1 --- 0x8093262be428885
2020-03-25 16:00:37.509190+0800 03-内存管理-copy[14479:285375] str2 abc --- -1 --- 0x8093262be428885
2020-03-25 16:00:37.509270+0800 03-内存管理-copy[14479:285375] str3 abc --- 1 --- 0x1007029d0
复制代码
void copyTest2() {
   NSString *str1 = @"ABC"; // 直接写出来的,不是通过方法创建的字符串,编译时会生成为【字符串常量】
    NSLog(@"str1 %@ --- %zd --- %p", str1, str1.retainCount, str1);
    
    NSString *str2 = str1.copy; // 浅拷贝 字符串常量
    NSLog(@"str2 %@ --- %zd --- %p", str2, str2.retainCount, str2);
    
    NSString *str3 = [[NSString alloc] initWithFormat:@"efg"]; // TaggedPointer
    NSLog(@"str3 %@ --- %zd --- %p", str3, str3.retainCount, str3);
    
    NSMutableString *str4 = str3.mutableCopy; // 深拷贝 对象
    NSString *str5 = str4.copy; // 深拷贝 对象
    
    NSLog(@"str4 %@ --- %zd --- %p", str4, str4.retainCount, str4);
    NSLog(@"str5 %@ --- %zd --- %p", str5, str5.retainCount, str5);
    

    //打印结果
2020-03-25 16:12:07.188709+0800 03-内存管理-copy[14642:295736] str1 ABC --- -1 --- 0x1000030b0
2020-03-25 16:12:07.188866+0800 03-内存管理-copy[14642:295736] str2 ABC --- -1 --- 0x1000030b0
2020-03-25 16:12:07.189023+0800 03-内存管理-copy[14642:295736] str3 efg --- -1 --- 0xd9de73a142fc7bc1
2020-03-25 16:12:07.189245+0800 03-内存管理-copy[14642:295736] str4 efg --- 1 --- 0x10077d8d0
2020-03-25 16:12:07.189316+0800 03-内存管理-copy[14642:295736] str5 efg --- -1 --- 0xd9de73a142fc7bc1
}
复制代码

总结

对于常量区的数据(字符串常量),TaggedPointer的引用计数一直都为【-1】,TaggedPointer不是对象,是个指针。
复制代码

思考🤔:对于copy,引用计数是否可能大于1,网上很多文章说copy不会改变引用计数?

 void copyTest3() {
    NSString *str1 = [[NSString alloc] initWithFormat:@"老郑的技术杂货铺"]; // 对象 str1.retainCount = 1
    NSString *str2 = str1.copy; // 浅拷贝 对象 str1.retainCount = 2
    NSMutableString *str3 = str1.mutableCopy; // 深拷贝 对象 str3.retainCount = 1
    
    NSLog(@"str1 %@ --- %zd --- %p", str1, str1.retainCount, str1);
    NSLog(@"str2 %@ --- %zd --- %p", str2, str2.retainCount, str2);
    NSLog(@"str3 %@ --- %zd --- %p", str3, str3.retainCount, str3);
}
//打印结果:
2020-03-25 16:18:45.498256+0800 03-内存管理-copy[14728:302195] str1 老郑的技术杂货铺 --- 2 --- 0x1006059b0
2020-03-25 16:18:45.498313+0800 03-内存管理-copy[14728:302195] str2 老郑的技术杂货铺 --- 2 --- 0x1006059b0
2020-03-25 16:18:45.498349+0800 03-内存管理-copy[14728:302195] str3 老郑的技术杂货铺 --- 1 --- 0x100605820
复制代码

总结

可见对不可变对象进行copy操作,引用计数会+1
复制代码

6.2 ARC自动引用计数管理

在App编译阶段,由Xcode添加了内存管理的代码,自动加入了 retain 、 release 后的代码

6.2.1 内存管理原则

只要没有强指针指向(没有被强引用),对象就会被释放。

  • 强指针
    • 默认所有对象的指针变量都是强指针
    • 被__strong修饰的指针
 Person *person1 = [[Person alloc] init];
 __strong  Person *person2 = [[Person alloc] init];
复制代码

相应的也有弱指针(弱引用),但不影响对象的释放

  • 弱指针:被__weak修饰的指针
__weak  Person *p = [[Person alloc] init];
复制代码

6.2.2 属性关键字

  • strong : 用于OC对象,相当于MRC中的retain,是强引用
  • weak : 用于OC对象,相当于MRC中的assign,比如修饰delegate
  • assign : 用于基本数据类型,跟MRC中的assign一样,也可修饰对象,但会存在野指针
  • copy : 主要用于修饰block、字符串、数组

6.2.3 ARC注意事项

  • 不允许调用release、retain、retainCount、autorelease方法
  • 重写父类的dealloc方法时,不能再调用 [super dealloc];

6.2.4 ARC中对象的释放

简单看几种情况:

  • 局部变量释放对象随之被释放
#import "Person.h"

@implementation Person
-(void)dealloc{
    NSLog(@"Person已释放-----dealloc");
}
@end

复制代码
//Test.m
-(void)test{
    @autoreleasepool {
        int a = 10; // 栈
        int b = 20; // 栈
        //p在栈上 Person对象(计数器==1) : 堆
        Person *p = [[Person alloc] init];
    }// 执行到这一行局部变量p释放
    // 由于没有强指针指向对象, 所以对象也释放
    NSLog(@"----自动释放池已释放------");
}
// 打印结果
2020-03-25 16:32:39.073845+0800 Test[14845:312200] Person已释放-----dealloc
2020-03-25 16:32:39.073965+0800 Test[14845:312200] ----自动释放池已释放------
复制代码
  • 清空指针对象随之被释放
-(void)test{
    @autoreleasepool {
       Person *p = [[Person alloc] init];
       p = nil; // 执行到这一行, 由于没有强指针指向对象, 所以对象被释放
        NSLog(@"----当前还在函数作用域内------");
    }
    NSLog(@"----自动释放池已释放------");
}
// 打印结果
2020-03-25 16:39:12.924164+0800 Test[14915:317802] Person已释放-----dealloc
2020-03-25 16:39:12.924311+0800 Test[14915:317802] ----当前还在函数作用域内------
2020-03-25 16:39:12.924446+0800 Test[14915:317802] ----自动释放池已释放------

复制代码

6.3 自动释放池(Autorelease Pool)

它不会像ARC或者MRC那样在对象不再会被使用时马上被释放,而是等到一个时机去释放它,内存池的释放操作分为自动和手动。自动释放受runloop机制影响

6.3.1 手动释放

  • ARC下使用方式:

比如for循环,可能是内存飙升,这个时候我门可以手动释放内存(@autoreleasepool)

// ARC
 NSMutableArray * arr = [NSMutableArray array];
    for (int i = 0; i < largeCount; i++) {
        @autoreleasepool {//开始代表创建自动释放池     
            NSNumber * numTep = [NSNumber numberWithInt:i];
            [arr addObject:numTep];
        }//结束代表销毁自动释放池
    }
复制代码
  • MRC下使用方式

方式一:

NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init];
Person *p = [[[Person alloc] init] autorelease];
[autoreleasePool drain];
复制代码

方式二:

@autoreleasepool
{ // 创建一个自动释放池
        Person *p = [[Person new] autorelease];
        // 将代码写到这里就放入了自动释放池
} // 销毁自动释放池(会给池子中所有对象发送一条release消息)
复制代码

6.3.2 自动释放

这里涉及到runloop,不做详细的描述,会在runloop相关文章中在做深入讲解,有个大概了解即可

苹果在主线程 RunLoop 里注册了两个 Observer:

  1. 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。优先级最高,保证创建释放池发生在其他所有回调之前。
  2. 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入睡眠) 和 Exit(即将退出Loop),
    • BeforeWaiting(准备进入睡眠)时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;
    • Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后

6.3.3 autorelease关键字和释放池(MRC)

当我们不再使用一个对象的时候应该将其空间释放,但是有时候我们不知道何时应该将其释放。为了解决这个问题,Objective-C提供了autorelease方法,在MRC中才能使用。

  1. autorelease注意事项
    • autorelease是一种支持引用计数的内存管理方式,只要给对象发送一条autorelease消息,会将对象放到一个自动释放池中,当自动释放池被销毁时,会对池子里面的所有对象做一次release操作
    • autorelease方法会返回对象本身,且调用完autorelease方法后,对象的计数器不变
    • 并不是放到自动释放池代码中,都会自动加入到自动释放池
    • 在自动释放池的外部发送autorelease 不会被加入到自动释放池中
  • 错误写法❌
//错误的写法
@autoreleasepool {
}
// 没有与之对应的自动释放池, 只有在自动释放池中调用autorelease才会放到释放池
Person *p = [[[Person alloc] init] autorelease];
[p run];

复制代码
  • 正确写法
// 正确写法
@autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
 }

// 正确写法
Person *p = [[Person alloc] init];
@autoreleasepool {
    [p autorelease];
}
复制代码
  1. autorelease本质原理
    autorelease实际上只是把对release的调用延迟了,对于每一个autorelease,系统只是把该对象放入了当前的autorelease pool中,当该pool被释放时,该pool中的所有对象会被调用release。

6.3.4 小结

  • autorelease方法不会改变对象的引用计数器(销毁时影响),只是将这个对象放到自动释放池中;
  • OC中类库的类方法一般都不需要手动释放,因为内部已经调用了autorelease方法,如[NSDate date]
  • 自动释放池是以栈的形式存在
  • 自动释放池中不适宜放占用内存比较大的对象

参考文章:



这篇关于内存管理系列—OC的内存管理模式的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程