[iOS开发]引用计数与MRC

本文深入探讨了Objective-C中的内存区域,包括栈、堆、全局区、常量区和代码区,强调了堆区的内存管理。介绍了全局变量、静态变量、extern变量和const常量的使用及区别。讲解了编译和链接过程,并详细阐述了引用计数(MRC)的四个基本法则,包括alloc/init、retain、release和autorelease的工作原理。最后,讨论了使用自动释放池的情况和注意事项,以及release非自己持有对象的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


再了解内存管理这块知识,我认为有必要先了解一下计算机是如何处理内存的

1. 内存分配区域

我们可以简单的将内存区域分为内区和外区

1.1 内区

1.1.1 栈

临时变量由编译器自动分配,在不需要时自动清除的变量存储区,通常是局部变量和函数参数。在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用。用户栈在程序执行期间可以动态地扩展和收缩。

1.1.2 堆

由new\alloc创建的对象所分配的内存块,它们的释放系统不会主动去管,而是由我们的开发者去告诉系统什么时候释放这块内存(一个对象引用计数为0时系统就会回销毁该内存区域对象)。一般一个 new 就要对应一个 release。在ARC下编译器会自动在合适位置为OC对象添加release操作。会在当前线程Runloop退出或休眠时销毁这些对象,MRC则需程序员手动释放。
堆可以动态地扩展和收缩。
我们讨论的内存管理就是针对堆区进行讨论

1.1.3 全局区

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

1.1.3.1 static静态变量
  • 只能在本文件中访问,修改全局变量的作用域
  • 避免重复定义全局变量

全局静态变量

  • 优点 :不管对象方法还是类方法都可以访问和修改全局静态变量,并且外部类无法调用静态变量,定义后只会指向固定的指针地址,供所有对象使用,节省空间。 并且外部类无法调用静态变量,定义后只会指向固定的指针地址,供所有对象使用,节省空间。
  • 缺点存在的生命周期长,从定义直到程序结束。所以从内存优化和程序编译的角度来说,尽量少用全局静态变量。程序运行时会单独加载一次全局静态变量,过多的全局静态变量会造成程序启动慢。

静态局部变量

  • 优点 :定义后只会存在一份值,每次调用都是使用的同一个对象内存地址的值,并没有重新创建,节省空间,只能在该局部代码块中使用。
  • 缺点 :存在的生命周期长,从定义直到程序结束,只能在该局部代码块中使用。

所以局部和全局静态变量从根本上来说没有什么区别,只是作用域不同。如果仅仅一个类中的对象方法和类方法使用并且值可变,我们就可以定义全局静态变量,如果是多个类使用并可变,建议定义在model作为成员变量使用。如果是不可变值,宏定义即可。

1.1.3.2 extern全局变量

只是用来获取全局变量(包括静态全局变量)的值,不能用于定义变量。现在当前文件查找有没有全局变量,没有找到,才会去其他文件查找。

全局静态变量与全局变量 其实本质上是没有区别的,只是存在修饰区别,一个static让其只能内部使用,一个extern让其可以外部使用

当某个全局变量,没有用static修饰时,其作用域为整个项目文件,若在其他类想引用该变量,则用extern关键字。
例如:想引用其他类的全局变量则在当前类中实现extern int age。如果该作用域不想被外界修改,则用static修饰该变量,则其作用域只限于该文件。
如下:

#import <Foundation/Foundation.h>
#import "Apple.h"
extern int a;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Apple *apple = [[Apple alloc]init];
//        [apple print];
        a = 5;
        NSLog(@"%d",a);
        [apple print];
        
    }
    return 0;
}

在需要引用其他类的全局变量的当前类中实现extern int age,然后把被引用的那个类的static变量删除

#import "Apple.h"
@implementation Apple
int a = 10;
- (void)print {
    NSLog(@"a = %d",a);
}
@end

此时打印结果就为两个都是5.

1.1.3.3 const常量

被const修饰的变量是只读的

  • const的用法:
    const用来修饰右边的值

主要会产生问题的是 * 是指针指向符,我们主要要看 * 与const的关系

  1. const在前,const修饰str这个整体,所以整体不能改变,这个整体是str指向内存中的值。
  2. const在 * 后 表示str指向的地址不能改变
    在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • const与宏有什么区别呢
    在这里插入图片描述
    所以如果使用大量宏容易造成编译时间久

1.1.4 常量区

这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。一般值都是放在这个地方的。常量字符串就是放在这里的。 程序结束后由系统释放。

1.1.5 代码区

存放函数的二进制代码

1.2 外区–自由存储区

由malloc等分配的内存快,与堆相似,不过其实用free来结束自己的生命

1.3 iOS中的内存分布

在这里插入图片描述

在这里插入图片描述

1.4 编译、链接的过程

Xcode去运行一个程序,从源码到程序这个过程中隐藏了预处理、编译、汇编和链接4个过程:
请添加图片描述

预处理

处理源代码文件中以#开头的预编译指令。

clang -E main.m -o main.i

编译

编译就是把上面得到的.i文件进行:词法分析、语法分析、静态分析、优化生成相应的汇编代码,得到.s文件。

clang -S main.i -o main.s
  • 词法分析:源代码的字符序列分割成一个个token(关键字、标识符、字面量、特殊符号),比如把标识符放到符号表(静态链接那篇,重点讲符号表)。
  • 语法分析:生成抽象语法树 AST,此时运算符号的优先级确定了;有些符号具有多重含义也确定了,比如“*”是乘号还是对指针取内容;表达式不合法、括号不匹配等,都会报错。
  • 静态分析:分析类型声明和匹配问题。比如整型和字符串相加,肯定会报错。
  • 中间语言生成:CodeGen根据AST自顶向下遍历逐步翻译成 LLVM IR,并且在编译期就可以确定的表达式进行优化,比如代码里t1=2+6,可以优化t1=8。(假如开启了bitcode,)
  • 目标代码生成与优化:根据中间语言生成依赖具体机器的汇编语言。并优化汇编语言。这个过程中,假如有变量且定义在同一个编译单元里,那给这个变量分配空间,确定变量的地址。假如变量或者函数不定义在这个编译单元,得链接时候,才能确定地址。

汇编

汇编就是把上面得到的.s文件里的汇编指令一一翻译成机器指令。得到.o文件,也就是目标文件,后面会重点讲的Mach-O文件。

clang -c main.s -o main.o

链接

clang main.o -o main

链接就是把目标文件(一个或多个)和需要的库(静态库/动态库)链接成可执行文件。后面会分别讲静态链接和动态链接。

2.引用计数与MRC部分

2.1 基础的表格

对象方法引用计数
生成对象并自己持有alloc/copy+1(从0变为1)
持有对象retain+1
释放对象release-1
废弃对象dealloc对象所占内存解除分配 并放回“可用内存池中”

2.2 内存管理的思考方式(四个基本法则)

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

  1. alloc\new\copy\mutableCopy 方法名开头来创建的对象意味着自己生成的对象只有自己持有
  2. 持有的本质其实就是强引用

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

用 alloc / new / copy / mutableCopy 以外的方法取得的对象,因为非自己生成并持有,所以自己不是该对象的持有者。(比如 NSMutableArray 类的 array方法),但是我们可以通过retain来手动持有对象。

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

通过release进行释放

书上还介绍了这样一种用法

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

我们不使用release释放,而是使用autorelease进行释放

autorelease释放与简单的release释放有什么区别?

首先说说什么是自动释放池:自动释放池是OC的一种内存自动回收机制,可以将一些临时变量通过自动释放池来回收统一释放。自动释放池销毁的时候,池子里面所有的对象都会做一次release操作

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

在这里插入图片描述
其实也就是autorelease 方法不会改变调用者的引用计数,它只是改变了对象释放时机,不再让程序员负责释放这个对象,而是交给自动释放池去处理

autorelease 方法相当于把调用者注册到 autoreleasepool 中,ARC环境下不能显式地调用 autorelease 方法和显式地创建 NSAutoreleasePool 对象,但可以使用@autoreleasepool { }块代替(并不代表块中所有内容都被注册到了自动释放池中)。

我们清楚ARC中我们的块在作用域结束后会自己进行release操作,那么在MRC中呢?
自动释放,听起来和像ARC,但实际上其实更类似于C语言中的局部变量。autorelease会像C语言的自动变量那样来对待对象实例。当超出对象实例的作用域时,对象实例的release方法会被调用。不同于C语言我们也可以自己设定变量的作用域,类似如下
在这里插入图片描述
对于所有调用过autorelease实例方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。

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 1 0
在这里插入图片描述

那么什么时候需要使用自动释放池呢?
  1. If you write a loop that creates many temporary object. 循环中创建了许多临时对象,在循环里使用自动释放池,用来减少高内存占用。
  2. If you spawn a secondary thread. 开启子线程的时候要自己创建自己的释放池,否则可能会发生内存泄漏。

2.2.4 释放非自己持有的对象会导致程序崩溃

释放非自己持有的对象会导致程序崩溃
事实测试并不会… 可能修复了不会崩溃

3. 其他事项

3.1 Effective Objective-C 2.0中提到:不要使用retainCount!

我们在MRC中,有时可能会想要打印引用计数,但retainCount方法并不是很有用,由于对象可能会处于自动释放池中,这会导致打印的引用计数并不精准,而且其他程序库也很有可能自行保留或释放对象,这都会扰乱引用计数的具体值。

3.2 retainCount很大

int main(int argc, const char * argv[]) {
    NSString *firstString = @"你好";
    NSString *secondString = [NSString stringWithFormat:@"hello"];
    NSString *thirdString = [NSString stringWithFormat:@"helloWorld"];
    NSLog(@"%lu,%lu,%lu",(long)[firstString retainCount],(long)[secondString retainCount],(long)[thirdString retainCount]);
    return 0;
}

在这里插入图片描述

可以看到是2的64次方减一
在这里插入图片描述

编译器会把单例对象所表示的数据放在应用程序的二进制文件里,这样的话,运行程序时就可以直接用了,无需再创建NSString对象。

测试证明,即便对其进行 release 操作,retainCount 也不会产生任何变化。这个值意味着无限的retainCount,这个对象是不能被释放的。

如果我们单纯的使用NSString或者NSArray 的alloc init或new方法 这两个也都是无限大,但是当我们换成可变数组或者可变字符串时候其又会变成1。如果使用initWith…字符串就和下面的一样,数组也正常。类似情况的还有NSNumber,单纯使用alloc int\new方法,其引用计数为0,但是如果附上初值,就会变成无限大。

个人理解就是NSNumber不赋值单纯new一个新的出来,那么NSNumber这个对象其实还是没有生成,他必须需要一个初值才能生成对象,如果带了那么底层对其处理是将其视为标签指针类型,单例对象,方便处理。

NSString和NSArray应该是如果不附初值系统就会直接视为单例对象进行处理。
NSMutableString和NSMutableArray就正常了。
在这里插入图片描述
lldb打一下地址,确实是视为单例对象进行处理

3.3 三种类型字符串的copy/mutableCopy/retainCount情况

int main(int argc, const char * argv[]) {
    NSString *firstString = @"你好";
    NSString *secondString = [NSString stringWithFormat:@"hello"];
    NSString *thirdString = [NSString stringWithFormat:@"helloWorld"];
    
    NSString *test1 = [firstString copy];
    NSString *test2 = [firstString mutableCopy];
    NSString *test3 = [secondString copy];
    NSString *test4 = [secondString mutableCopy];
    NSString *test5 = [thirdString copy];
    NSString *test6 = [thirdString mutableCopy];
    return 0;
}

用lldb进行调试可以看到
在这里插入图片描述
经过测试

总结就是

无论原来的三个的类型是NSString还是NSMutableString类型

copy 会使原来的对象引用计数加一(当然仅有正常类型的字符串,而不是单例创建的,毕竟那两个引用计数是无限的),并拷贝对象地址给新的指针,所以类型与原类型一致。
mutableCopy 不会改变引用计数,会拷贝内容到堆上,生成一个 __NSCFString 对象,新对象的引用计数为1.

3.4 release自己不持有的对象并没有导致崩溃

在这里插入图片描述
在这里插入图片描述

并不是加了一句NSLog之后就一定会造成程序crash的,如果那句新加的NSLog没有占用原来array的内存,那下一句NSLog依旧能够响应发送给array的消息,结果会类似第一种代码所产生的结果。

所以说,两种情况都是有可能发生的,至于到底发生哪种情况,完全取决于何时系统会清理掉array占用的内存,也可以说取决于“运气”,因为这个时间是不确定的。

还有如果给其一个自动释放池的销毁那加上断点 其输出的结果可能不同应该也是这个原因

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值