内存

内存

[toc]

必看文章:

什么是内存管理?是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。

内存管理管理的是堆上的内存,栈上的内存并不需要我们管理。

说说内存管理

我们知道,当不再使用一个对象时应该将其释放,但是在某些情况下,我们很难理清一个对象什么时候不再使用(比如self.name不止在一个方法里会被调用到的时候),这可怎么办?ObjC提供autorelease方法来解决这个问题。

说到内存管理,我们就不得不提引用计数。当对象的引用计数为0,对象的内存就会被释放。

引用计数是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术可以实现自动资源管理的目的。

当我们在创建一个对象的实例并在堆上申请内存时,或者在其他对象中需要持有这个对象时,该对象的引用计数就会加1;
当代码结束使用该对象/释放对象时,则将对象的引用计数减1;而想要释放一个对象的内存,我们将该对象的引用计数减到0,即进行release到0。
在ARC的情况下,为了方便管理内存,它会有一个autorelease的东西,它省去了我们还要自己去执行release方法的操作。同时与autorelease相关的还有一个叫AutoreleasePool自动释放池。

通过autorelease,当给一个对象发送autorelease消息(类方法创建的对象系统会自动添加autorelease)时,对象在接收到autorelease消息后,它会被添加到了当前的自动释放池autoreleasepool中。在未来某个时间,当自动释放池被销毁时,会给池里所有的对象发送release消息将其释放(释放≠销毁)。如果自动释放池向对象发送release消息后对象的引用计数变为了0,则改对象就会被销毁,内存就会被回收。在释放前这个时间段内,对象还是可以使用的(注意1:autorelease不会改变对象的引用计数,release才改变引用计数)(注意2:自动释放池实质上只是在释放的时候给池中所有对象对象发送release消息,不保证对象一定会销毁,如果自动释放池向对象发送release消息后对象的引用计数仍大于1,对象就无法销毁。

1
2
3
id array = [NSMutableArray arrayWithCapacity:1];
此源代码等同于以下源码。
id array = [[[NSMutableArray alloc] initWithCapacity:1] autorelease];

所以AutoreleasePool的释放有如下两种情况。

  • 一是Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。(一定要回答到runloop)。那runloop什么时候结束呢?

    一个RunLoop包含若干个Mode,每个Mode包含若干个Source/Timer/Observer/Port。当启动一个RunLoop时会先指定一个Mode,检查指定Mode是否存在以及Mode中是否含有SourceTimer,如果Mode不存在或者Mode中无SourceTimer,认为该Mode是一个空的ModeRunLoop就直接退出。

  • 二是手动调用AutoreleasePool的释放方法(drain方法)来销毁AutoreleasePool

AutoreleasePool(自动释放池)

1、AutoreleasePool(自动释放池)介绍

AutoreleasePool(自动释放池)是OC中的一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机。即当我们创建了一个对象,并把他加入到了自动释放池中时,他不会立即被释放,会等到一次runloop结束或者作用域超出autoreleasepool{}之后再被释放。在正常情况下,创建的变量会在超出其作用域的时候release,但是如果将变量加入AutoreleasePool,那么release将延迟执行。

我们把main.m文件通过Xcode自带的xcrun命令,来编译成main.cpp文件

命令行如下

1
xcrun -sdk iphonesimulator clang -rewrite-objc ./main.m

可以在编译出来的cpp文件中,看到如下自动释放池的结构体如下。

1
2
3
4
5
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} // 构造函数
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);} // 析构函数
void * atautoreleasepoolobj;
};

objc4是我们通常说的Runtime源码,我们遇到的libobjc.A.dylib就是用它编译出来的。

源码查看,进入http://opensource.apple.com/source/搜索objc4,

image-20211103173107899

NSThread、NSRunLoop 和 NSAutoreleasePool三者之间的关系

  • NSThread 和 NSRunLoop是一一对应的关系
  • 在NSRunLoop对象的每个运行循环(event loop)开始前,系统会自动创建一个autoreleasepool,并在运行循环(event loop)结束时drain掉这个pool,同时释放所有autorelease对象
  • autoreleasepool只会对应一个线程,每个线程可能会对应多个autoreleasepool,比如autoreleasepool嵌套的情况
  • autorelease本质上就是延迟调用release方法
  • MRC环境,通过调用[obj autorelease]延迟内存的释放
  • ARC环境,甚至可以完全不知道autorelease也能管理好内存

看到这里有人可能会问,那到底延迟到什么时候执行呢?看完本文后,各位心中自然会有答案。

(3)、autorelease、autoreleasepool(自动释放池)
(4)、autoreleasepool(自动释放池)  

  这里说到的自动释放池,顾名思义,就是一个池,这个池可以容纳对象,而且可以自动释放,这就大大增加了我们处理对象的灵活性。   

(5)、autoreleasepool里面对象的内存什么时候释放?

  在runloop sleep的时候当前autoreleasePool drain(objc_autoreleasePoolPop) 掉,向里面的对象都发送release消息,建立一个新的autoreleasePool(objc_autoreleasePoolPush)。或者简单的说就是当@autoreleasepool结束时,里面的内存就会回收;

ARC时代,系统自动管理自己的autoreleasepool,runloop就是iOS中的消息循环机制,当一个runloop结束时系统才会一次性清理掉被autorelease处理过的对象,其实本质上说是在本次runloop迭代结束时清理掉被本次迭代期间被放到autorelease pool中的对象的。至于何时runloop结束并没有固定的duration。

(6)、runloop、autorelease pool以及线程之间的关系

每个线程(包含主线程)都有一个Runloop。对于每一个Runloop,系统会隐式创建一个Autorelease pool,这样所有的release pool会构成一个像callstack一样的一个栈式结构,在每一个Runloop结束时,当前栈顶的Autorelease pool会被销毁,这样这个pool里的每个Object会被release。

##### (7)、自动释放池怎样创建

ObjC提供两种方法创建自动释放池:

方法一:使用NSAutoreleasePool来创建

1
2
3
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc]init];
//这里写代码
[pool release];

方法二:使用@autoreleasepool创建

1
2
3
@autoreleasepool {
//这里写代码
}

自动释放池创建后,就会成为活动的池子,释放池子后,池子将释放其所包含的所有对象。

以上两种方法推荐第一种,因为将内存交给ObjC管理更高效。

(8)、自动释放池使用注意

1)自动释放池实质上只是在释放的时候给池中所有对象对象发送release消息,不保证对象一定会销毁,如果自动释放池向对象发送release消息后对象的引用计数仍大于1,对象就无法销毁。

2)自动释放池中的对象会集中同一时间释放,如果操作需要生成的对象较多占用内存空间大,可以使用多个释放池来进行优化。比如在一个循环中需要创建大量的临时变量,可以创建内部的池子来降低内存占用峰值。

3)autorelease不会改变对象的引用计数

(9)、自动释放池的应用/什么时候要用@autoreleasepool

有些情况下,我们还是需要手动创建自动释放池,那么,什么时候呢?

苹果文档中的翻译如下:

  1. 如果你正在编写不基于UI 框架的程序,比如命令行工具。
  2. 如果你编写的循环创建了很多临时对象。那么你可以在循环中使用自动释放池block,在下次迭代前处理这些对象。在循环中使用自动释放池block,有助于减少应用程序的内存占用。
  3. 你生成了一个辅助线程。 一旦线程开始执行你必须自己创建自动释放池。否则,应用将泄漏对象。

按我的理解,最重要的使用场景,应该是有大量中间临时变量产生时,避免内存使用峰值过高,及时释放内存的场景。

如在一个循环事件中,如果循环次数较大或者事件处理占用内存较大,就会导致内存占用不断增长,可能会导致不希望看到的后果。

举个例子,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NSArray *urls = <# An array of file URLs #>;
for (NSURL *url in urls) {
@autoreleasepool {
NSError *error;
NSString *fileContents = [NSString stringWithContentsOfURL:url
encoding:NSUTF8StringEncoding
error:&error];
}
}

//或
for (int i = 0; i < 100000; i ++) {
@autoreleasepool {
NSString * log = [NSString stringWithFormat:@"%d", i];
NSLog(@"%@", log);
}
}

如果这个for循环里不使用@autoreleasepool,虽然每个循环中生成的字符串对象都会放在自动释放池子中(假设是1号自动释放池),但是这个1号自动释放池是需要等到循环事件结束时释放的。这时候由于循环太大,势必会造成在循环期间内存不增长。所以,这里我们需要使用@autoreleasepool,额外创建一个2号自动释放池,来使得在每个@autoreleasepool结束时,里面的临时变量都会回收,内存使用更加合理。

例子2:假如有2000张图片,每张1M左右,现在需要获取所有图片的尺寸,你会怎么做?
  如果这样做

1
2
3
4
for (int i = 0; i < 2000; i ++) {
CGSize size = [UIImage imageNamed:[NSString stringWithFormat:@"%d.jpg",i]].size;
//add size to array
}

  用imageNamed方法加载图片占用Cache的内存,autoReleasePool也不能释放,对此问题需要另外的解决方法,当然保险的当然是双管齐下了

1
2
3
4
5
6
   for (int i = 0; i < 2000; i ++) {
@autoreleasepool {
CGSize size = [UIImage imageWithContentsOfFile:filePath].size;
//add siez to array
}
}

常见笔试/面试题

< 返回目录

自动释放池底层怎么实现?

答:自动释放池以栈的形式实现:当你创建一个新的自动释放池时,它将被添加到栈顶.当一个对象收到发送autorelease消息时,他被添加到当前线程的处于栈顶的自动释放池中,当自动释放池被回收时,他们从栈中被删除,并且会给池子里面所有的对象都会做一次release操作