logo头像
Snippet 博客主题

iOS内存管理

开发中,内存的分配及引用计数的增减,每个开发者都知道new、copy、alloc,retain都会有与之对应的release,如果没有new、copy、alloc,retain而产生的新对象,生成的代码都是autorelease,在ARC模式下开发,编译器会自动帮助开发者添加对应的代码。本文将从内存的分配、内存的释放、循环引用及内存释放检测原理方面分享内存管理的小知识。

  • 内存分配
  • 内存的释放
  • 循环引用
  • 内存释放检测原理

内存分配

当App运行时,系统会一块内存地址,并把这块内存划分为5个区域:

  • 栈区:编译器或系统自动分配并释放;存放函数的参数值、局部变量等
  • 堆区:开发者分配和释放,如果开发者没有释放,则程序结束时,由操作系统统一回收
  • 全局区:程序结束时,由操作系统统一回收;存放全局变量和静态变量
  • 常量区:程序结束时,由操作系统统一回收;存放常量字符串
  • 代码区:存放app的二进制代码

内存的释放

当每次调用release时,会自动判断引用计数是否为0,然后再调用dealloc;可以从Runtime的源码中发现调用流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
- (void)dealloc {
_objc_rootDealloc(self);
}

void_objc_rootDealloc(id obj) {
assert(obj);
obj->rootDealloc();
}

inline void objc_object::rootDealloc() {
if (isTaggedPointer()) return;
if (fastpath(isa.nonpointer &&
!isa. weakly_referenced &&
!isa.has_assoc &&
!isa.has_CXX_dtor &&
!isa.has_sidetable_rc)) {
assert(!sidetable_present());
free(this) ;
} else {
object_dispose((id)this) ;
}
}


id object_dispose(id obj) {
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}

void *objc_ destructInstance(id obj) {
if(obj) {
Class isa = obj->getIsa();
if (isa->hasCxxDtor()) {
object_cxxDestruct(obj);
}
if (isa->instancesHaveAssociated0bjects()) {
_object_remove_assocations(obj);
}
objc_clear_deallocating(obj);
}
return obj;
}

object_cxxDestruct函数最终调用objc_storeStrong函数来释放ivars,实际上是对ivars变量的引用计数-1.

1
2
3
4
5
6
7
8
9
10
11
12
void __cdecl -[NSobject .cxx_destruct] (NSObject *self, SEL a2) {
objc_storeStrong((__int64)&self->_obj, 0LL);
}

void objc_storeStrong(id *location, id obj) {
id prev = *location;
if (obj == prev) { return; }

objc_retain(obj);
*location = obj;
objc_release(prev);
}

循环引用

在ARC时代,我们使用Strong指针来强引用一个对象(引用计数加1),避免该对象被释放。使用Weak指针来弱引用一个对象(引用计数减1),当这对象没有St rong指针指向该对象时(引用计数为0),该对象也就会被释放。

那么,当一个Strong对象A在使用完毕后,同时还有St rong指针B指向它。而且指针B同时也直接或间接的被A对象持有。恭喜你,这就是循环引用

还有另一种情况,这并不属于循环引用的范畴,但也放在这一起说了吧。就是一个单例对象持有一个对象B,因为单例对象是在栈里的,只有APP销毁时,该单例才会被释放,这也就导致了对象B会一直存在于内存中.

常见的几种循环引用

1
2
3
4
5
1、objA -> objB -> block ->objA

2、objA -> objB -> delegate -> objA

3、Timer -> objA(只是没释放,不属于循环引用)

内存释放检测原理

当一个UIViewController被pop或dismiss后,该UIViewController 包括它的view,view的subviews 等等将很快被释放(除非你把它设计成单例,或者持有它的强引用,但一般很少这样做)。于是,我们只需在一一个ViewController被pop或dismiss一小段时间后,看看该UIViewController,它的view, view 的subviews 等等是否还存在。具体的方法是,为基类NSObject添加一个方法-willDealloc方法,该方法的作用是,先用一个弱指针指向self, 并在一小段时间(3秒)后,通过这个弱指针调用-assertNotDealloc, 而-assertNotDealloc主要作用是直接中断言。

1
2
3
4
5
6
7
8
9
10
11
- (BOOL)willDealloc {
__weak typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[weakSelf assertNotDealloc] ;
});
return YES;
}

- (void)assertNotDealloc {
NSAssert(NO, @"");
}

这样,当我们认为某个对象应该要被释放了,在释放前调用这个方法,如果3秒后它被释放成功,weakSelf 就指向nil,不会调用到-assertNotDealloc方法,也就不会中断言,如果它没被释放(泄露了) ,-assertNotDealloc 就会被调用中断言。这样,当一个UIViewController被pop或dismiss时(我们认为它应该要被释放了),我们遍历该UIViewController上的所有view,依次调-willDealloc,若3秒后没被释放,就会中断言。