iOS开发——MRC(手动内存管理)

iOS开发——MRC(手动内存管理)

  • 内存分配区域
    • 栈区
    • 堆区
      • 总结
    • 常量区
      • 总结
    • 代码区
      • 总结
    • 全局区
    • 关于如何查看一个对象在堆区 / 栈区
  • 需要知道的知识
  • 手动引用计数MRC
    • 四个法则
    • 非自己生成的对象,且该对象存在,但自己不持有
    • dealloc
    • retainCount
      • 不要使用retainCount

我们要想了解内存管理的知识前,必须先搞明白计算机的内存分配以及计算机是如何处理内存的。

内存分配区域

内存指的就是RAM(random access memory),内存分配区域主要分为五个区:栈区(系统管理的地方)、堆区(程序员控制的地方)、静态区(全局区)、常量区、代码区。

栈区

栈区(stack)由编译器自动分配并释放,存放的是函数的参数值局部变量基本类型的变量和对象引用类型方法调用的实参也是保存在栈区。所以我们可以把栈看做一个临时寄存交换的内存区
栈是系统数据结构,对应线程/进程是唯一的。优点是快速高效,缺点是有限制,数据操作不灵活。

堆区

由程序员分配(malloc、new)和释放(delete),主要存在new构造的对象和数组如果不释放,可能造成内存泄露,程序结束时可能会由操作系统回收。
堆向高地址扩展的数据结构,是不连续的内存区域。 程序员负责在何时释放内存,在ARC程序中,计数器为0的时候,在当次的runloop结束后,释放掉内存。堆中的所有东西都是匿名的,这样不能按名字访问,而只能通过指针访问
对于堆来讲,频繁的new/delete势必会造成内存空间的不连续性,从而造成大量的碎片 ,使程序效率降低。

总结

  1. 灵活方便,数据适应面广泛,但是效率有一定降低。[顺序随意]
  2. 堆是函数库内部数据结构,不一定唯一。
  3. 不同堆分配的内存无法互相操作。
  4. 堆空间的分配总是动态的
  5. 虽然程序结束时所有的数据空间都会被释放回系统,但是精确的申请内存,释放内存匹配是良好程序的基本要素。

常量区

文字常量区存放常量字符,程序结束后自动释放,不允许修改。

总结

  1. 该区是编译时分配的内存空间,在程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放。
  2. 存放常量:整型、字符型、浮点、字符串等。

代码区

存放函数体的二进制代码

总结

  1. 它是可执行程序在内存中的镜像。
  2. 代码段需要防止在运行时被非法修改,所以只允许读取操作,而不允许写入操作。

全局区

全局变量和静态变量都被分配到同一块内存。

全局区(静态区) (static): 全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量存放在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域,程序结束后由系统释放。

注意:全局区又可分为未初始化全局区(BSS段)和初始化全局区(DATA段)。

关于如何查看一个对象在堆区 / 栈区

  1. 你初始化方法以 new, alloc, retain,copy 开头都是在堆区,也包括被引用计数管理的对象。常量这些就会在栈区。简单一点,除去 NSString *aString = @“aaa” 这种编译时会转换为常量其它的 Objective-C 对象,理论上来说,都是在堆区。此外,block 也是个例外,具体的可以去了解内存管理相关的知识点。
  2. 如果是在方法执行过程当中,定义在本地的原生类型(或者说值类型)。那么肯定它是在栈上。当函数执行结束时直接销毁。而其它的引用类型(或者oc中的interface)都是在堆上创建的,由ARC负责清理
  3. 一般情况下你可以打印出地址,判断他是在堆还是栈内存中,栈是高地址往下,堆是低地址往上,一般看到0x7fff…这种的肯定是是在栈中的,要注意一点的是TEXT段,像NSString这种基本放在TEXT段中,这种地址比堆地址更低,也很好区分;你可以去看一下内存是怎么划分的;

需要知道的知识

在学习MRC之前除了要了解计算机关于内存的分配区域之外,还需要了解一些知识,具体如下:

  1. 什么样的数据需要进行内存管理?任何继承了NSObject的对象都需要进行内存管理,而那些非对象类型的数据比如:int、char、double、struct等等就不需要进行内存管理。
  2. 继承了NSObject的对象存储在操作系统的堆里,堆一般由程序员分配释放,程序结束时可能有OS回收。
  3. 非OC对象一般放在操作系统的栈里,栈由操作系统自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈(先进后出)。

另外现在我们的XCode默认为ARC环境,如果需要MRC我们需要手动设置:

手动引用计数MRC

我们要想手动进行内存管理就需要知道哪些情况会引起计数的变化:
iOS开发——MRC(手动内存管理)_第1张图片
对象被废弃对象所占内存解除分配 并放回“可用内存池中”。

四个法则

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

/*
 * 自己生成并持有该对象
 */
 id obj0 = [[NSObeject alloc] init];
 id obj1 = [NSObeject new];

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

/*
 * 持有非自己生成的对象
 */
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有
[obj retain]; // 自己持有对象

不在需要自己持有对象的时候,释放。

/*
 * 不在需要自己持有的对象的时候,释放
 */
id obj = [[NSObeject alloc] init]; // 此时持有对象

[obj release]; // 释放对象
/*
 * 指向对象的指针仍就被保留在obj这个变量中
 * 但对象已经释放,不可访问
 */

非自己持有的对象无需释放。

/*
 * 非自己持有的对象无法释放
 */
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有

[obj release]; // ~~~此时将运行时crash 或编译器报error~~~ 非 ARC 下,调用该方法会导致编译器报 issues。此操作的行为是未定义的,可能会导致运行时 crash 或者其它未知行为

非自己生成的对象,且该对象存在,但自己不持有

这句话听着很绕口,但其实很好理解,但是要了解这句话首先我们要知道一个叫autorelease的东西。
所谓autorelease就是自动释放,注意:autorelease和ARC 是完全不同的两个东西,没有任何联系。
类似于C语言的局部变量,超出变量作用域,局部变量就会被废弃,不可再访问。
autorelease会在超出作用域时,调用对象的release实例方法,与C语言不同的是,编程人员可以设定变量的作用域,类似下图:
iOS开发——MRC(手动内存管理)_第2张图片

我们来看一下代码:

- (id) getAObjNotRetain {
    id obj = [[NSObject alloc] init]; // 自己持有对象
    
    [obj autorelease]; // 取得的对象存在,但自己不持有该对象
    
    return obj;
}

取得的对象存在,但自己不持有该对象,那么该对象被谁持有了呢?
调用 autorelease 方法,就会把该对象放到离自己最近的自动释放池中(栈顶的释放池,多重自动释放池嵌套是以栈的形式存取的),即:使对象的持有权转移给了自动释放池(即注册到了自动释放池中),调用方拿到了对象,但这个对象还不被调用方所持有。

那么问题来了,autorelease和release有什么区别?
Autorelease实际上只是把对release的调用延迟了,对于每一个Autorelease,系统只是把该对象放入了当前的Autorelease pool中,当该pool被释放时,该pool中的所有对象会被调用Release

autorelease pool的作用就是来避免频繁申请/释放内存(就是pool的作用了)。
[NSMutableArray array][NSArray array]都可以取得谁都不持有的对象,这些方法都是通过 autorelease 实现的。

iOS开发——MRC(手动内存管理)_第3张图片

autorelease 方法不会改变调用者的引用计数,它只是改变了对象释放时机,不再让程序员负责释放这个对象,而是交给自动释放池去处理。等到自动释放池销毁时,所有对象统一都会被释放。

看一下下面的代码:

int main(int argc, const char * argv[]) {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
        id obj = [[NSObject alloc]init];
        NSLog(@"%lu",(unsigned long)[obj retainCount]);
        [obj autorelease];
        NSLog(@"%lu",(unsigned long)[obj retainCount]);
        [pool drain];
        NSLog(@"%lu",(unsigned long)[obj retainCount]);
    return 0;
}

它的打印结果是什么呢?
在这里插入图片描述

首先生成对象引用计数+1,接着将持有权转移给了自动释放池引用计数不变,然后销毁池子,池子中的所用对象调用release,引用计数-1,变为0。
假如我将代码改成如下:

int main(int argc, const char * argv[]) {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
        id obj = [[NSObject alloc]init];
        NSLog(@"%lu",(unsigned long)[obj retainCount]);
        [obj autorelease];
        NSLog(@"%lu",(unsigned long)[obj retainCount]);
        [pool drain];
        NSLog(@"%lu",(unsigned long)[obj retainCount]);
        [obj autorelease];
        NSLog(@"%lu",(unsigned long)[obj retainCount]);
    return 0;
}

这样会发生什么呢?这样的话程序会崩溃,因为池子销毁的时候,池子里的所有对象 都被释放了,调用一个不存在的对象,程序当然会崩溃。

dealloc

NSObject 协议中定义的内存管理方法与遵守这些方法命名约定的自定义方法的组合提供了用于引用计数环境中的内存管理的基本模型。NSObject 类还定义了一个dealloc 方法,该方法在对象被销毁时自动调用,也就是在引用计数为0时。

retainCount

不要使用retainCount

看一段代码:

    NSNumber *number = [NSNumber numberWithInt:1];
    NSLog(@"retainCount = %lu",[number retainCount]);

我们看一下打印结果:
在这里插入图片描述
结果大的离谱啊,查阅了一下官方的文档,第一句就是“Do not use this method.”,后面给出了说明,因为Autorelease pool的存在,对于内存的管理会相当复杂,retainCount就不能用作调试内存时的依据了。

你可能感兴趣的:(objective-c,ios)