文章目录
再了解内存管理这块知识,我认为有必要先了解一下计算机是如何处理内存的
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来结束自己的生命
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,这个对象是不能被释放的。
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占用的内存,也可以说取决于“运气”,因为这个时间是不确定的。
还有如果给其一个自动释放池的销毁那加上断点 其输出的结果可能不同应该也是这个原因