iOS学习笔记--内存管理

191 阅读9分钟

本文为学习笔记,主要是以下两篇参考文章内容的简化版,加深理解,复习使用。

参考文章

一、iOS内存管理

二、iOS Memory 内存详解 (长文)


一、堆(Heap)和栈(stack)的概念

C5AE57F0-5F5B-41D6-B11B-F40177F951F8.png

  • 栈存放值类型

  • 堆存放对象类型,对象的引用计数是在堆内存中操作的

堆是什么

堆是计算机科学中一类特殊数据结构的统称。通常是一个可以被看做一棵树的数组对象。

在队列中,调度程序反复提取队列中第一个作业并运行,因为实际情况中某些时间较短的任务将等待很长时间才能结束,或者某些时间不短,但具有重要作用的作业,同样应当具有优先权。堆即为解决此类问题设计的一种数据结构。

堆又被称为优先队列,但是堆并不是队列。在队列中只能按照进入的先后顺序来取出元素,但是在堆中是按照元素的优先级取出元素

栈是什么

栈是计算机科学中一种特殊的串列形式的抽象资料型别。

只允许在链接串列或阵列的一端进出数据(top: 顶端元素,push: 加入元素, pop: 取出元素)。

按照后进先出的原理运作

栈也可以用一维数组或连结串列的形式来完成。

内存分配

堆栈空间分配

  • 栈(操作系统):由操作系统自动分配释放。存放函数的参数值,局部变量的值等。操作方式类似于数据结构中的栈。

  • 堆(操作系统):一般由程序员分配释放,否则程序结束时可能由OS回收,分配方式类似于链表。

堆栈缓存方式

  • 栈使用的一级缓存,他们通常都是被调用时处于存储空间中,调用完毕立即释放。

  • 堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对慢一些。


二、引用计数

引用计数是一种内存管理技术,指将资源(可以是对象、内存或磁盘空间等)的被引用次数保存起来,当被引用次数变为0时就将其释放的过程。

1.MRC操作对象

NSObject中提供了有关引用计数的如下方法:

  • retain: 引用计数器 +1

  • release: 引用计数器 -1

  • autorelease: 不改变该对象的引用计数器的值,只是将对象添加到自动释放池中

  • retainCount: 返回该对象的引用计数的值

对象操作Objective-C方法
生成并持有对象alloc/new/copy/mutableCopy等方法
持有对象retain方法
释放对象release方法
废弃对象dealloc方法

自己生成的对象,自己持有

id obj = [[NSObject alloc] init];

[obj release]; // 释放对象

非自己生成的对象,自己也能持有

id obj = [NSMutableArray array]; // 非自己生成的对象,暂时没有持有

[obj retain]; // 通过retain持有对象

[obj release]; // 通过release释放对象

autorelease pool

每条线程都包含一个与其对应的自动释放池,当线程被终止的时候,对应的自动释放池会被销毁。同时,处于该自动释放池内的对象将会进行一次release操作

2.ARC操作对象的修饰符
2.1 __strong修饰符

在ARC模式下,id和OC对象的所有权修饰符默认都是__strong。在变量超出作用域后,该变量就会被废弃,同时赋值给该变量的对象也会被释放,例如:

{
    // 变量p持有Person对象的强引用

    Person *p = [Person person];

    // __strong修饰符可以省略

    // Person __strong *p = [Person person];
}
    // 变量p超出作用域,释放对Person对象的强引用

    // Person对象持有者不存在,该对象被释放

上述例子对应的MRC代码如下:

{
    Person *p = [Person person];

    [p retain];

    [p release];
}
strong与属性

如果一个属性的修饰符是strong,对于其实例变量所持有的对象,编译器会在该实例变量所属类的dealloc方法为其添加释放对象的方法。在MRC下,dealloc如下:

- (void)dealloc {
    [p release];

    [super dealloc];
}
strong的实现

在ARC中,除了会自动调用“保留”和释放方法外,还进行了优化。比如某个对象执行了多次“保留”和释放方法,那么ARC针对特殊情况有可能会将该对象的“保留”和释放成对地移除:

+ (id)person {

    Person *tmp = [[Person alloc] init]; // 引用计数1

    [tmp autorelease]; // 注册到自动释放池(ARC下无效)

    return tmp;
}

{

    // ARC代码

    _p = [Person person];

    // MRC实现展示

    Person *p = [Person person]; // Person类对象引用计数为1

    _p = [p retain]; // Person类对象引用计数为2

    [_p release]; // Person类对象引用计数为1
}

    // 清空自动释放池, Person类对象引用计数为0,释放对象

上述代码中,+(id)person方法中的autorelease方法延迟了该对象的生命周期,在稍后自动释放池会进行release操作。

而_p通过retain来持有该对象,使用完立马执行release操作。

可以看出retain和autorelease是多余的。可以简化成如下代码:

+ (id)person {

    Person *tmp = [[Person alloc] init]; // 引用计数1

    return tmp;

}

{

    // ARC代码

    _p = [Person person];

    // MRC实现展示

    _p = [Person person]; // Person类对象引用计数为1

    [_p release]; // Person类对象引用计数为0,释放对象
}

那么ARC是如何判断是否移除这种成对的操作呢?其实在ARC中,并不是直接执行retain和autorelease操作的,而是通过以下两个方法:

objc_autoreleaseReturnValue(obj);//对应autorelease

objc_retainAutoreleasedReturnValue(obj);//对应retain

以下为两个方法对应的伪代码:

id objc_autoreleaseReturnValue(id obj) {

    if ("返回对象obj后面的那段代码是否执行retain") {

    // 是

    set_flag(obj); // 设置标志位

    return obj;

    } else {
    
        return [obj autorelease];
    }
}

id objc_retainAutoreleasedReturnValue(id obj) {

    if (get_flag(obj)) {

    // 有标志位

    return obj;

    } else {

        return [obj retain];
    }
}

通过以上两段伪代码,重新梳理代码如下:

+ (id)person {

    Person *tmp = [[Person alloc] init]; // 引用计数1

    return objc_autoreleaseReturnValue(id tmp);
}

{

    // ARC代码

    _p = [Person person];

    // MRC实现展示
 
    Person *p = [Person person];

    _p = objc_retainAutoreleasedReturnValue(p); // Person类对象引用计数为1

    [_p release]; // Person类对象引用计数为0,释放对象
}

在ARC中,通过设置和检测标志位可以移除多余的成对(“保留”&“释放”)操作,优化程序的性能。

2.2 __weak修饰符

__weak修饰符一般用于解决开发中遇到的循环引用问题。

基于运行时库,如果变量或属性使用weak来修饰,当其所指向的对象被回收,会自动为该变量或属性赋值为nil,有效地避免野指针奔溃。

weak和变量

如果一个变量被__weak修饰,代表该变量对所指向的对象具有弱引用。例如以下代码:

    Person __weak weakPerson = nil;

    if (1) {

    Person *person = [[Person alloc] init];

    weakPerson = person;

    NSLog(@"%@", weakPerson);// 输出: <Person: 0x600000018120>
}

NSLog(@"%@", weakPerson);// 输出:(null)

从上述输出结果可以看出,当超出作用域后,person变量对Person对象的强引用失效。Person对象持有者不存在,所以该对象被回收。同时,weakPerson变量对Person的弱引用失效,weakPerson变量被赋值为nil。

另外需要注意,如果一个变量被weak修饰,那这个变量不能持有对象实例,编译器会发出警告。如下代码:

Person __weak *weakPerson = [[Person alloc] init];

因为weakPerson被__weak修饰,不能持有生成的Person对象。所以Person对象创建完立即被释放,编译器会给出相应的警告:

Assigning retained object to weak variable; object will be released after assignment
weak的实现
// ARC下

Person *person = [[Person alloc] init];

Person __weak *p = person;

// MRC对应的模拟代码

Person *person = [[Person alloc] init];

Person *p;

objc_initWeak(&p, person);

objc_destroyWeak(&p, 0);

上述MRC对应的模拟代码用objc_initWeak和objc_destroyWeak函数对p进行初始化和释放。这两个函数都调用同一个函数。如下:

id p;

p = 0;

objc_storeWeak(&p, person);

objc_storeWeak(&p, 0);

根据上述代码分析:

1.初始化变量

使用变量弱引用指向一个对象时,通过传入变量的地址和赋值对象两个参数来调用objc_store方法。该方法内部会将对象的地址(&person)作为键值,把变量p的地址(&p)注册到weak表中。

2.释放变量

调用objc_storeWeak(&p,0)把变量的地址从weak表中删除。变量地址从weak表删除前,利用被回收对象的地址作为键值进行检索,把对应的变量地址赋值为nil。

weak和访问

访问被__weak修饰过的变量所指向的对象时,对应的模拟代码如下:

Person *person = [[Person alloc] init];

Person __weak *p = person;

NSLog(@"%@",p);

// MRC下模拟代码

Person *person = [[Person alloc] init];

Person *p;

objc_initWeak(&p, person);

id tmp = objc_loadWeakRetained(&p);

objc_autorelease(tmp);

NSLog(@"%@",tmp);

objc_destroyWeak(&p);

通过上述代码可以看出,访问一个被__weak修饰的变量,相对于赋值多了两个步骤:

  1. objc_loadWeakRetained, 通过该函数去除对应变量锁引用的对象并retain;

  2. 把变量指向的对象注册到自动释放池。在自动释放池结束前都能安全使用该对象。

需要注意的是,如果大量访问weak变量,会导致变量所引用的对象被多次加入自动释放池,从而影响性能。如果需要大量访问,可以通过__strong修饰的变量来解决,如下:

Person __weak *p = obj;

Person *tmp = p; // p引用的对象被注册到自动释放池

NSLog(@"%@", tmp);

NSLog(@"%@", tmp);

NSLog(@"%@", tmp);

NSLog(@"%@", tmp);

NSLog(@"%@", tmp);

通过利用__strong中间变量的使用,p引用的对象仅注册 1 次到自动释放池中,有效地减少了自动释放池中的对象。

2.3 __unsafe_unretained

如果一个变量被__unsafe_unretained修饰,那么该变量不属于编辑器的内存管理对象。该修饰符表明不保留值,即对其所指向的对象既不强引用,也不弱引用。例如以下代码:

Person __unsafe_unretained unretainedPerson;

if (1) {

Person *p = [[Person alloc] init];

unretainedPerson = p;

NSLog(@"%@", unretainedPerson);// 输出: <Person: 0x600000018120>

}

NSLog(@"%@", unretainedPerson);// 奔溃

当超出作用域,变量p对Person类对象的强引用失效。Person类对象的引用计数为0,该对象被回收。当再次使用unretainedPerson变量的时候,因为其指向的对象已经被回收,所以会发生野指针奔溃。而使用__weak修饰的话,变量所指向的对象被回收,会将变量赋值为nil。

被__unsafe_unretained修饰的变量跟__weak一样,不能持有对象实例。对象创建完立马被回收,编译器会给出相应警告。

当我们给被__unsafe_unretained修饰的变量赋值时,必须保证赋值对象确实存在,否则程序会发生奔溃。

2.4 __autoreleasing

在ARC和MRC中,autorelease作用一致,只是两者的表现方式有所不同。

1.如果返回的对象归调用者所有,如下:

@autoreleasepool {

    Person __autoreleasing *p = [[Person alloc] init];

}

模拟代码如下:

id pool = objc_autoreleasePoolPush();

Person *p = [[Person alloc] init]; // 持有

objc_autorelease(p);

objc_autoreleasePoolPop(pool);

2.如果返回的对象 归调用者所有,如下:

@autoreleasepool {

Person __autoreleasing *p = [Person person];

}

模拟代码如下:

id pool = objc_autoreleasePoolPush();

Person *p = [Person person];

objc_retainAutoreleasedReturnValue(p); // 持有(优化后的retain方法)

objc_autorelease(p);

objc_autoreleasePoolPop(pool);

从以上两个例子可以看出,__autoreleasing的实现都是通过objc_autorelease()函数,只不过是持有方法有所不同而已。