iOS内存管理基础篇

iOS进阶
文章目录
  1. 1. 内存管理基础结构
    1. 1.1. 程序可执行文件的结构
    2. 1.2. data 和 bss 区
    3. 1.3.
    4. 1.4.
    5. 1.5. 全局区 / 静态区
    6. 1.6. 文字常量区
    7. 1.7. 程序代码区
    8. 1.8. 结构体(Struct)
      1. 1.8.1. 1、定义
      2. 1.8.2. 2、成员访问
        1. 1.8.2.1. 3、成员存储
    9. 1.9. 内存分配
    10. 1.10. 页面置换算法
      1. 1.10.1. FIFO算法
      2. 1.10.2. OPT(MIN)算法
      3. 1.10.3. LRU(Least-Recently-Used)算法
    11. 1.11. MRC于ARC 环境设置
  2. 2. Reference Counting
    1. 2.1. alloc/retain/release/dealloc 实现
      1. 2.1.1. alloc
      2. 2.1.2. retainCount
      3. 2.1.3. retain
      4. 2.1.4. release
    2. 2.2. 所有的修饰符
      1. 2.2.1. _strong 修饰符
      2. 2.2.2. _weak修饰符
      3. 2.2.3. _unsafe_unretained修饰符
      4. 2.2.4. _autoreleasing 修饰符
  3. 3. MRC(Manual Reference Counting)
    1. 3.0.1. dealloc
    2. 3.0.2. 野指针 & 空指针
    3. 3.0.3. MRC @property参数
    4. 3.0.4. MRC 循环引用
    5. 3.0.5. autoreleasepool
    6. 3.0.6. autoreleasepool 注意
    7. 3.0.7. autoreleasepool 循环
    8. 3.0.8. autoreleasepool 错误用法
  • 4. ARC(Automatic Reference Counting)
    1. 4.1. ARC @property
    2. 4.2. ARC 注意
    3. 4.3. NSThread & NSRunLoop & NSAutoreleasePool
    4. 4.4. 需要手动添加autoreleasepool的情况
  • 5. 内存管理的实际应用
    1. 5.1. 项目
    2. 5.2. ARC 实例
    3. 5.3. MRC 实例
      1. 5.3.1. NSString的引用计数是随机值,NSMutableString的引用计数是正常值
      2. 5.3.2. 对于字符串常量、NSNumber做常量时?
      3. 5.3.3. stringWithFormat创建的string?
      4. 5.3.4. stringWithString创建的string?
      5. 5.3.5. 除了alloc new copy mutableCopy retain显示增加retainCount以外还有哪些看不到的能够增加引用计数的操作?
      6. 5.3.6. 苹果不推荐使用retainCount方法,因为他对程序本身没有作用,retainCount可能永远不会反回0,有时候系统会优化对象的释放行为,在保留计数还是1的时候就释放了。
      7. 5.3.7. retainCount 关于NSString和NSMutableString的例子
  • 6. 参考文献
  • 本文是iOS 内存管理的基础篇,从最基本的堆栈开始一步步的了解iOS的内存管理。

    内存管理基础结构

    程序可执行文件的结构

    一个程序的可执行文件在内存中的结果,从大的角度可以分为两个部分:只读部分和可读写部分。只读部分包括程序代码(.text)和程序中的常量(.rodata)。可读写部分(也就是变量)大致可以分成下面几个部分:

    • .data: 初始化了的全局变量和静态变量
    • .bss: 即 Block Started by Symbol, 未初始化的全局变量和静态变量
    • heap: 堆,使用 malloc, realloc, 和 free 函数控制的变量,堆在所有的线程,共享库,和动态加载的模块中被共享使用
    • stack: 栈,函数调用时使用栈来保存函数现场,自动变量(即生命周期限制在某个 scope 的变量)也存放在栈中

    架构图

    下面来具体解释一下:

    data 和 bss 区

    这两个都是存放全局变量的 他们之间的区别是:
    data区存放的是初始化了的全局变量和静态变量,而bss区存放的是未初始化过得

    1
    2
    3
    4
    5
    6
    //初始化
    int val = 3;
    char string[] = "Hello World";

    //未初始化
    static int i;

    已经初始化的变量最开始会被放在.text中 因为值是卸载代码中的,程序运行起来之后就会被拷贝到.data区或者bss区。

    答疑一: 静态变量和全局变量

    全局变量:在一个代码文件(具体说应该一个 translation unit/compilation unit))当中,一个变量要么定义在函数中,要么定义在在函数外面。当定义在函数外面时,这个变量就有了全局作用域,成为了全局变量。全局变量不光意味着这个变量可以在整个文件中使用,也意味着这个变量可以在其他文件中使用(这种叫做 external linkage)。当有如下两个文件时

    a.c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <stdio.h>
    int a;
    int compute(void);
    int main()
    {
    a = 1;
    printf("%d %d\n", a, compute());
    return 0;
    }

    b.c

    1
    2
    3
    4
    5
    6
    int a;
    int compute(void)
    {
    a = 0;
    return a;
    }

    在 Link 过程中会产生重复定义错误,因为有两个全局的 a 变量,Linker 不知道应该使用哪一个。为了避免这种情况,就需要引入 static

    静态变量: 指使用 static 关键字修饰的变量,static 关键字对变量的作用域进行了限制,具体的

    限制如下:

    • 在函数外定义:全局变量,但是只在当前文件中可见(叫做 internal linkage)
    • 在函数内定义:全局变量,但是只在此函数内可见(同时,在多次函数调用中,变量的值不会丢失)
      (C++)在类中定义:全局变量,但是只在此类中可见

    对于全局变量来说,为了避免上面提到的重复定义错误,我们可以在一个文件中使用 static,另一个不使用。这样使用 static 的就会使用自己的 a 变量,而没有用 static 的会使用全局的 a 变量。当然,最好两个都使用 static,避免更多可能的命名冲突。

    注意:’静态’这个中文翻译实在是有些莫名其妙,给人的感觉像是不可改变的,而实际上 static 跟不可改变没有关系,不可改变的变量使用 const 关键字修饰,注意不要混淆。

    Bonus 部分 —— extern: extern 是 C 语言中另一个关键字,用来指示变量或函数的定义在别的文件中,使用 extern 可以在多个源文件中共享某个变量,例如这里的例子。 extern 跟 static 在含义上是“水火不容”的,一个表示不能在别的地方用,一个表示要去别的地方找。如果同时使用的话,有两种情况,一种是先使用 static,后使用 extern ,即:

    1
    2
    static int m;
    extern int m;

    这种情况,后面的 m 实际上就是前面的 m 。如果反过来:

    1
    2
    extern int m;
    static int m;

    这种情况的行为是未定义的,编译器也会给出警告。

    答疑二 程序在内存和硬盘上不同的存在形式(不懂!!!)

    这里我们提到的几个区,是指程序在内存中的存在形式。和程序在硬盘上存储的格式不是完全对应的。程序在硬盘上存储的格式更加复杂,而且是和操作系统有关的,具体可以参考这里。一个比较明显的例子可以帮你区分这个差别:之前我们提到过未定义的全局变量存储在 .bss 区,这个区域不会占用可执行文件的空间(一般只存储这个区域的长度),但是却会占用内存空间。这些变量没有定义,因此可执行文件中不需要存储(也不知道)它们的值,在程序启动过程中,它们的值会被初始化成 0 ,存储在内存中

    栈是用于存放本地变量,内部临时变量以及有关上下文的内存区域。程序在调用函数时,操作系统会自动通过压栈和弹栈完成保存函数现场等操作,不需要程序员手动干预。

    栈是一块连续的内存区域,栈顶的地址和栈的最大容量是系统预先规定好的。能从栈获得的空间较小。如果申请的空间超过栈的剩余空间时,例如递归深度过深,将提示stackoverflow。

    栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高

    堆是用于存放除了栈里的东西之外所有其他东西的内存区域,当使用mallocfree时就是在操作堆中的内存。对于堆来说·释放工作由程序员控制,容易产生memory leak

    堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大

    对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,永远都不可能有一个内存块从栈中间弹出。

    堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现

    计算机底层并没有对堆的支持,堆则是C/C++函数库提供的,同时由于上面提到的碎片问题,都会导致堆的效率比栈要低。

    全局区 / 静态区

    存储全局变量和静态变量,程序结束后由系统释放

    初始化区 非初始化区分开存放

    文字常量区

    存储字符串常量,程序结束后由系统释放

    程序代码区

    存储函数体的二进制代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //main.cpp
    int a = 0; // 全局初始化区
    char *p1; // 全局未初始化区
    main {
    int b; // 栈
    char s[] = "abc"; // 栈
    char *p2; // 栈
    char *p3 = "123456"; // 123456\0在常量区,p3在栈上
    static int c =0// 全局静态初始化区
    p1 = (char *)malloc(10);
    p2 = (char *)malloc(20); // 分配得来的10和20字节的区域就在堆区
    strcpy(p1, "123456"); // 123456\0在常量区,这个函数的作用是将"123456" 这串字符串复制一份放在p1申请的10个字节的堆区域中。
    // p3指向的"123456"与这里的"123456"可能会被编译器优化成一个地址。
    }

    结构体(Struct)

    1、定义

    struct tag { member-list } variable-list;

    2、成员访问

    直接访问: 变量名.成员名
    间接访问: 结构体指针名->成员名

    3、成员存储

    获得EXAMPLE类型结构体所占内存大小: int size_example = sizeof( struct EXAMPLE );
    获得成员b相对于EXAMPLE储存地址的偏移量: int offset_b = offsetof( struct EXAMPLE, b );

    内存分配

    • 虚拟地址:用户编程时将代码(或数据)分成若干个段,每条代码或每个数据的地址由段名称 + 段内相对地址构成,这样的程序地址称为虚拟地址

    • 逻辑地址:虚拟地址中,段内相对地址部分称为逻辑地址

    • 物理地址:实际物理内存中所看到的存储地址称为物理地址

    • 逻辑地址空间:在实际应用中,将虚拟地址和逻辑地址经常不加区分,通称为逻辑地址。逻辑地址的集合称为逻辑地址空间

    • 线性地址空间:CPU地址总线可以访问的所有地址集合称为线性地址空间

    • 物理地址空间:实际存在的可访问的物理内存地址集合称为物理地址空间

    • MMU(Memery Management Unit内存管理单元):实现将用户程序的虚拟地址(逻辑地址) → 物理地址映射的CPU中的硬件电路

    • 基地址:在进行地址映射时,经常以段或页为单位并以其最小地址(即起始地址)为基值来进行计算

    • 偏移量:在以段或页为单位进行地址映射时,相对于基地址的地址值
      虚拟地址先经过分段机制映射到线性地址,然后线性地址通过分页机制映射到物理地址。

    页面置换算法

    FIFO算法

    先入先出,即淘汰最早调入的页面。

    OPT(MIN)算法

    选未来最远将使用的页淘汰,是一种最优的方案,可以证明缺页数最小。
    可惜,MIN需要知道将来发生的事,只能在理论中存在,实际不可应用。

    LRU(Least-Recently-Used)算法

    用过去的历史预测将来,选最近最长时间没有使用的页淘汰(也称最近最少使用)。
    LRU准确实现:计数器法,页码栈法。
    由于代价较高,通常不使用准确实现,而是采用近似实现,例如Clock算法。

    内存抖动现象:页面的频繁更换,导致整个系统效率急剧下降,这个现象称为内存抖动(或颠簸)。抖动一般是内存分配算法不好,内存太小引或者程序的算法不佳引起的

    Belady现象:对有的页面置换算法,页错误率可能会随着分配帧数增加而增加。
    FIFO会产生Belady异常。
    栈式算法无Belady异常,LRU,LFU(最不经常使用),OPT都属于栈式算法。

    OC的内存管理

    MRC于ARC 环境设置

    参数配置

    Reference Counting

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

    alloc/retain/release/dealloc 实现

    在 Xcode 中 设置 Debug -> Debug Workflow -> Always Show Disassenbly 打开。这样在打断点后,可以看到更详细的方法调用。

    alloc

    通过设置断点追踪程序的执行,下面列出了执行所调用的方法和函数:

    1
    2
    3
    4
    +alloc
    +allocWithZone:
    class_createInstance
    calloc

    下面我们来看这几个跟retainCount相关的方法到底都做了什么!

    retainCount

    1
    2
    __CFdoExternRefOperation
    CFBasicHashGetCountOfKey

    retain

    1
    2
    __CFdoExternRefOperation
    CFBasicHashAddValue

    release

    1
    2
    __CFdoExternRefOperation
    CFBasicHashRemoveValue

    很明显 这几个方法都调用了__CFdoExternRefOperation这个方法,下面我们来看一下这个方法的实现:

    CFRuntime.c __CFDoExternRefOperation:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    int __CFDoExternRefOperation(uintptr_t op, id obj) {
    CFBasicHashRef table = 取得对象的散列表(obj);
    int count;

    switch (op) {
    case OPERATION_retainCount:
    count = CFBasicHashGetCountOfKey(table, obj);
    return count;
    break;
    case OPERATION_retain:
    count = CFBasicHashAddValue(table, obj);
    return obj;
    case OPERATION_release:
    count = CFBasicHashRemoveValue(table, obj);
    return 0 == count;
    }
    }

    BasicHash这样的方法名可以看出,其实引用计数表就是散列表。key 为 hash(对象的地址) value为引用计数

    所有的修饰符

    _strong 修饰符

    是id类型和对象类型默认的所有权修饰符

    1
    id obj = [[NSObject alloc] init];

    上面的源码与下面的相同

    1
    id _strong obj = [[NSObject alloc] init];

    _weak修饰符

    _weak修饰符出现是为了避免发生循环引用,循环引用容易发生内存泄漏.所为内存泄漏就是应当废弃的对象在超出其生存周期后继续存在。

    即使只有一个对象也有可能发生循环引用

    1
    2
    id test = [[Test alloc] init];
    [test setObject:test];

    __weak修饰符还有一个优点:在持有某对象的弱引用时,若该对象被废弃则弱引用将自动失效且处于nil被赋值的状态(空弱引用)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    id __weak obj1 = nil;
    {
    id __strong obj0 = [[NSObject alloc] init];

    obj1 = obj0;

    NSLog(@"A:%@",obj1);
    }
    NSLog(@"A:%@",obj1);

    输出结果:

    1
    2
    A:<NSObject:ox753e180>
    B:null

    _unsafe_unretained修饰符

    是不安全的所有权修饰符,因此:

    1
    id __unsafe_unretained obj = [[NSObject alloc] init];

    仍然会提示

    1
    Assigning retained object to unsafe_unretained variable; object will be released after assignment

    这一点跟__weak是一样 因为自己无法持有自己创建的对象 创建完成之后就会被销毁

    _autoreleasing 修饰符

    ARC有效时,要通过将对象赋值给附加了__autoreleaseing修饰符的变量来替代调用autorelease方法,对象赋值给附有__autoreleaseing修饰符的变量等价于在ARC无效时调用对象的autorelease方法,即对象被注册到autoreleasepool中

    1
    2
    3
    4
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [obj autorelease];

    [pool drain];

    等价于

    1
    2
    3
    4
    @autoreleasepool{
    id __autoreleasing obj2;
    obj2 = obj;
    }

    MRC(Manual Reference Counting)

    dealloc

    [super dealloc];一定在最后一行
    不能直接调用
    一旦对象被回收,继续使用会野指针

    野指针 & 空指针

    • 野指针即一个指针指向了“僵尸对象(不能再使用的对象)”
    • 给野指针发消息报错:EXC_BAD_ACCESS
    • 避免野指针发消息报错,对象释放后,将指针置为空指针
      空指针即没有指向任何存储空间(存的nil)
      向空指针发送消息没有任何反应

    MRC @property参数

    成员变量前加上@property,自动生成基本的setter/getter
    property加上retain,自动生成有内存管理的setter/getter
    property加上assign,自动生成基本的setter/getter,默认什么都不加就是assign

    MRC 循环引用

    当两端互相引用时,应该一端用retain,一端用assign

    autoreleasepool

    • [p autorelease] 给p发送一条autorelease消息,将p放到autoreleasepool,在autoreleasepool释放时做一次release操作
    • autorelease方法返回对象本身,引用计数不会变化

    autoreleasepool 注意

    并不是放到autoreleasepool代码中,都会自动加入到自动释放池

    1
    2
    3
    4
    5
    6
    @autoreleasepool {
    // 因为没有调用 autorelease 方法,所以对象没有加入到自动释放池
    Person *p = [[Person alloc] init];
    [p run];
    }

    autorelease是一个方法, 只有在autoreleasepool中调用才有效

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    @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];
    }

    autoreleasepool 循环

    • 尽量避免对大内存使用autorelease
    • 不要把for循环放在@autoreleasepool之间,会造成内存峰值上升

    autoreleasepool 错误用法

    不能连续调用autorelease
    调用autorelease后又调用release

    ARC(Automatic Reference Counting)

    ARC自动引用计数内存管理,通过编译器(Clang Complier),本质上还是会使用到retain、release等关键字方法,只是不是开发者手动添加,而是编译器在编译过程中添加retain、release等关键字方法到相应的代码行。

    ARC

    ARC @property

    • strong : 用于OC对象,相当于MRC中的retain
    • weak : 用于OC对象,相当于MRC中的assign
    • assign : 用于基本数据类型,跟MRC中的assign一样

    ARC 注意

    不能调用release
    不能调用autorelease
    不能调用[super dealloc]

    NSThread & NSRunLoop & NSAutoreleasePool

    • 1、每个线程(包括主线程)都拥有一个专属的NSRunLoop,并在需要时自动创建
    • 2、主线程的NSRunLoop对象(包括系统级别的其它线程)的每个event loop开始前,自动创建一个autoreleasepool,并在event loop结束时drain
    • 3、每个autoreleasepool对应且只对应一个线程

    需要手动添加autoreleasepool的情况

    • 编写的程序不是基于UI框架的,比如命令行工具
    • 编写的循环中创建了大量的临时对象
    • 创建了一个辅助线程

    内存管理的实际应用

    项目

    • 1、YYKit :解决循环中创建的大量临时对象
    • 2、AFNetworking: 创建了辅助线程
    • 3、XX会 混编时, 标注MRC文件:-fno-objc-arc

    ARC 实例

    • 1、ARC想要主动释放,最好是提前置为nil
    • 2、ARC下获取引用计数
      KVC [obj valueForKey:@”retainCount”]
      私有API _objc_rootRetainCount(obj)
      CFGetRetainCount((__bridge CFTypeRef)(obj))

    MRC 实例

    NSString的引用计数是随机值,NSMutableString的引用计数是正常值

    NSString的class是__NSCFConstantString,字符串常量
    NSMutableString的class是__NSCFString,有引用计数

    对于字符串常量、NSNumber做常量时?

    retain 和 release都不会有影响,因为系统不会回收,也不会对其做引用计数

    stringWithFormat创建的string?

    为变量,所以会有引用计数
    现在返回的已经是常量,见后面的例子

    stringWithString创建的string?

    取决于它后面的string对象,如果是常量则不做计数,如果是变量则做计数

    除了alloc new copy mutableCopy retain显示增加retainCount以外还有哪些看不到的能够增加引用计数的操作?

    容器类array、dic addObject;release时,里面的成员都会release一次,和autorelease pool一致
    addsubview, 因为view有栈(subviews),加入栈中retainCount+1
    navcontroller的push, 因为nav有栈(viewcontrollers),加入栈中retainCount+1
    performSelector 调用时target和info都会加1,结束时减1

    苹果不推荐使用retainCount方法,因为他对程序本身没有作用,retainCount可能永远不会反回0,有时候系统会优化对象的释放行为,在保留计数还是1的时候就释放了。

    retainCount 关于NSString和NSMutableString的例子

    1
    2
    3
    4
    5
    6
    7
    // 1
    NSMutableString *str = [[NSMutableString alloc] init];
    NSMutableString *str2 = [[NSMutableString alloc] init];
    NSLog(@"%ld, %ld", [str retainCount], [str2 retainCount]);//1,1

    str2 = [str copy]; //copy返回一个不可变对象属于常量
    NSLog(@"%ld, %ld", [str retainCount], [str2 retainCount]);//1,-1
    1
    2
    3
    4
    5
    6
    7
    // 2
    NSString *str = [[NSString alloc] init];
    NSString *str2 = [[NSString alloc] init];
    NSLog(@"%ld, %ld", [str retainCount], [str2 retainCount]); //-1,-1

    str2 = [str copy];
    NSLog(@"%ld, %ld", [str retainCount], [str2 retainCount]);//-1,-1
    1
    2
    3
    4
    5
    6
    7
    // 3
    NSString *str = [[NSString alloc] initWithFormat:@"abc%@", @"hehe"];
    NSString *str2 = [[NSString alloc] initWithFormat:@"bbc%@", @"hehe"];
    NSLog(@"%ld, %ld", [str retainCount], [str2 retainCount]); //-1,-1

    str2 = [str mutableCopy];
    NSLog(@"%ld, %ld", [str retainCount], [str2 retainCount]);//-1,1

    参考文献

    阿里面试基础

    内存管理基础到进阶