内存管理
内存管理,是指软件运行时对计算机内存资源的分配和使用的技术。其主要目的是如何高效、快速的分配。
大纲
- 堆和栈
- 引用计数
- MRC
深浅拷贝
autorelease - ARC
所有权修饰符
常见的几种内存泄漏
使用instruments检测内存泄漏
堆和栈
堆
一种特别的树状的数据结构。
在队列中,调度程序反复提取队列中的第一个作业并运行,因为实际情况中某些较短的任务要等待很长时间才能结束,或某些不短小,但具有重要性的作业,同样具有优先权。堆即为解决此类问题设计的一种数据结构。(按照元素的优先级取出元素)
栈
一种特殊的串列形式的抽象数据结构,其特殊之处在于只能允许在链表或数组的一端进行加入数据或输出数据的运算。
后进先出LIFO。
内存分配中的堆和栈
![](https://img.haomeiwen.com/i6644067/01560d1d4f3784c2.png)
堆栈空间分配
栈,是一块连续的内存区域,从高地址往低地址扩展。由操作系统自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
堆,是一块不连续的内存区域,从低地址往低高址扩展。一般由程序员释放,若程序员不释放,程序结束后可能由OS释放,分配方式类似于链表。
堆都是动态分配的,没有静态分配的堆。栈有两种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配,动态分配是alloca函数进行分配。栈的动态分配和堆的动态分配不同,是由编译器释放的。
堆栈缓存方式
栈,一级缓存,被调用时处于存储空间中,调用完毕立即释放。
堆,二级缓存,生命周期由虚拟机的垃圾回收算法来决定。调用速度相当对慢
引用计数
概念
一种内存管理技术,将资源的被引用计数保存起来,当引用计数变为零时就将其释放的过程。
MRC(Manual Reference Counting)
自己生成的对象,自己持有。(使用alloc,new,copy,mutablecopy或以这些单词开头的方法)
非自己生成的对象自己也能持有。(retain,非自己生成的对象内部一般是用的autorelease实现的,可以做到本身不持有对象例如:[NSArray array]操作)
不再需要自己持有的对象时释放。(release)
非自己持有的对象不能释放。(倘若在程序中释放了非自己所持有的对象就会造成崩溃)
深浅拷贝
copy,mutablecopy。一般系统自带的类实现了NSCopying和NSMutableCopying协议,自定义的类需要自己实现NSCopying和NSMutableCopying协议。
- 浅拷贝 指针拷贝,内存地址相同
- 深拷贝 值拷贝,内存地址不同
NSMutableArray | NSArray | NSString | NSMutableString | |
---|---|---|---|---|
copy | 深拷贝 | 浅拷贝 | 浅拷贝 | 深拷贝 |
mutableCopy | 深拷贝 | 深拷贝 | 深拷贝 | 深拷贝 |
总结:可变对象不论执行copy操作还是mutablecopy操作,都是深拷贝,不可变对象执行mutablecopy操作是深拷贝。深拷贝的数组里的元素是浅拷贝(内存地址相同)
autorelease
当对一个对象发送autorelease消息,会将对象添加到NSAutoreleasePool中去,当自动释放池执行drain操作时,会自动给里面的对象发送release消息,来释放对象。
ARC(Automatic Reference Counting)
编译器在编译时会帮我们自动插入
retain,release,copy,autorelease,autoreleasepool。
-fno-objc-arc,可以指定某个文件不使用ARC.
autoreleasepool
autorelease对象到底是何时被析构的?
一个常见的误区就是代码块执行完毕会释放,但其实并不是这样的。autoreleasepool的drain操作是跟runloop有关的,一个runloop结束以后才会执行drain操作,所以在代码块执行完毕后autorelease对象不一定会会析构。
- 每个线程并没有默认的autoleasepool,需要手动创建,避免内存泄漏。(main函数里创建了autoleasepool,当主线程运行循环结束时,释放所有对象.iOS main 函数中为何要包着 @autoreleasepool ?)
for (int i =0;i<1000000;i++){
NSString *str = [NSString stringWithString:@"hahahha"];
}
for (int i =0;i<1000000;i++){
@autoreleasepool{
NSString *str = [NSString stringWithString:@"hahahha"];
}
}
- 结论:
第二段代码会对内存进行优化,释放速度块
第一段代码造成内存大量堆积,释放速度慢
使用场景:当程序有大量中间临时变量产生时,避免内存峰值过高,及时释放内存的场景
所有权修饰符
- __strong
__strong修饰符表示对象的“强引用”,是id类型和对象类型默认的所有权修饰符。__strong修饰的变量在超出其作用域时,会释放其被赋予的对象。
- __weak
__weak弱引用,不能持有对象实例,当该对象被废弃时,此弱引用会自动失效且处于nil赋值的状态。 - __unsafe_unretained
__unsafe_unretained是不安全的修饰符,有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象,不持有对象。 - __autoreleasing
不用显示的附加__autoreleasing修饰符,这是由于编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回值的对象注册到autoreleasepool.
属性与所有权修饰符的对应关系
属性的修饰符 | 所有权修饰符 |
---|---|
assign | __unsafe_unretained |
copy | __strong |
retain | __strong |
strong | __strong |
unsafe_unretained | __unsafe_unretained |
weak | __weak |
给属性赋值是就相当于赋值给附加各属性对应的所有权修饰符的变量中。只有copy不是简单的赋值操作,是通过NSCoping接口中的copyWithZone:方法赋值源所生成的对象。
常见的几种内存泄漏
内存泄漏就是当废弃的对象在超出其生命周期后继续存在。
- 对象类型变量作为C语言结构体成员
struct Data{
NSMutableArray __unsafe_unretained *array
}
注意,这里要加上__unsafe_unretained修饰符,否则会报错,对象类型的变量不能直接作为结构体的成员变量。(可以用__bridge和c语言变量进行值转换)
- 循环引用
- 两个对象相互持有对方的强引用,可以用弱引用(weak)解决。
- block持有self对象,需要在block块外面设置弱引用,里面设置强引用解决。
__weak __typeof(self) weakSelf = self;
self.block = ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
[strongSelf doSomething];
};
这里block里面用强引用,为了保证在block里面访问self时能保证self不被释放。
- NSTimer
示例代码
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(onTimerTimeOut) userInfo:nil repeats:YES];
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
}
![](https://img.haomeiwen.com/i6644067/e05c48edcdf9c976.png)
当前类被timer强引用,dealloc方法并不会执行。
有一种解决办法是像blockskit一样,用block实现,解除循环引用(不过也需要注意block的循环引用)
+ (id)bk_scheduledTimerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)(NSTimer *timer))block repeats:(BOOL)inRepeats
{
NSParameterAssert(block != nil);
return [self scheduledTimerWithTimeInterval:inTimeInterval target:self selector:@selector(bk_executeBlockFromTimer:) userInfo:[block copy] repeats:inRepeats];
}
还有一种方法,利用中间类,来破除循环引用,这里就不多说啦。
使用instruments检测内存泄漏
有几种方式打卡instruments调试工具
1.Xcode->Open Develop Tool->Instruments
2.Product->Profile
3.长按运行按钮->Profile
一. 使用Leaks检测
-
强引用
无循环引用:
15483201379515.jpg
有循环引用:
15483175101215.jpg
-
block循环引用
无循环引用:
15483202064026.jpg
有循环引用:
![](https://img.haomeiwen.com/i6644067/1424fa7069c2de5b.jpg)
-
NSTimer
无循环引用:
15483204816830.jpg
有循环引用:
15483177310618.jpg
通过上面的图可以分析出来,使用Leaks只能检测出block的循环引用(红色x标记)。但其实以上几种情况发生循环引用时的内存都是只升不减的。
具体的查看方法
![](https://img.haomeiwen.com/i6644067/53e00dd66fdac0f9.jpg)
选中红色✘这个范围,在下方选择Call Tree,并勾选invert call tree和Hide system Library就可以看到具体发生内存泄漏调用的函数,双击则会定位到具体的代码。如下图:
![](https://img.haomeiwen.com/i6644067/537e58a3a845c62d.jpg)
Leaks还可以选择Cycles&Roots查看发生循环引用的地方
![](https://img.haomeiwen.com/i6644067/71df69041786fdd9.jpg)
二.使用Allocation检测
![](https://img.haomeiwen.com/i6644067/62397f436532813a.jpg)
对AViewController不断地进行push、pop操作,用Allocations可以查看到AViewController的Totol数不为0,然后就可以针对这个类的使用情况再深入排查可能会出现内存泄漏的地方
![](https://img.haomeiwen.com/i6644067/21e25aaf6711c8ae.jpg)
总结:虽然使用ARC后能够大大避免内存泄漏的出现,但还是有一些场景会导致内存泄漏,上述例子的场景还是比较容易避免的,在调用链长的时候一些循环引用就比较难发现了,因此我们在开发完一个功能模块后使用Instruments来检测一遍是比较好的习惯。
最后附上Demo
TestInstruments
网友评论