文章目录
再了解内存管理这块知识,我认为有必要先了解一下计算机是如何处理内存的
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的关系
- const在前,const修饰str这个整体,所以整体不能改变,这个整体是str指向内存中的值。
- 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 自己生成的对象,自己持有
- alloc\new\copy\mutableCopy 方法名开头来创建的对象意味着自己生成的对象只有自己持有
- 持有的本质其实就是强引用
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
那么什么时候需要使用自动释放池呢?
- If you write a loop that creates many temporary object. 循环中创建了许多临时对象,在循环里使用自动释放池,用来减少高内存占用。
- 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占用的内存,也可以说取决于“运气”,因为这个时间是不确定的。
还有如果给其一个自动释放池的销毁那加上断点 其输出的结果可能不同应该也是这个原因