Notes and thoughts from Tony

iOS内存管理机制

Comments

前言

最近重温了一遍C语言的内存机制,对内存四区模型的理解又稍微深刻了那么一点。回过头来再看iOS的内存管理,其实是有想通之处的。想深入理解iOS的内存管理机制,就得先了解C的内存机制。

C语言内存

内存四区模型

在程序执行之前的过程大概是:

  1. 操作系统把物理硬盘代码load到内存
  2. 操作系统把c代码分成四个区
  3. 操作系统找到main函数入口执行

内存四区

  • 栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。后进先出(LIFO)。

  • 堆区(heap): 一般由程序员分配释放,若程序员不释放,程序结束时可能由操作系统回收。注意它与数据结构中的堆是两回事,分配方式类似于链表。先进先出(FIFO)。

  • 数据区:主要包括静态全局区和常量区,如果要站在汇编角度细分的话还可以分为很多小的区。

全局区/静态区(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束后有系统释放。

常量区:常量字符串就是放在这里的。程序结束后由系统释放。

  • 代码区:存放函数体的二进制代码。

堆栈的生长方向

  • 栈是逆向生长,先进栈的所分配的内存空间地址更大。

  • 堆是顺序生长,先进栈的所分配的内存空间地址更小。

注意:对于指针指向的所分配的某一块内存(无论是堆还是栈)的首地址永远是这块内存中最小的。

堆栈的简化模型 堆栈的简化模型

iOS内存分区

iOS的内存分区跟C语言类似:

  • 栈区(stack):存放的局部变量、先进后出、一旦出了作用域就会被销毁;函数跳转地址,现场保护等;程序猿不需要管理栈区变量的内存;栈区地址从高到低分配。

  • 堆区(heap):堆区的内存分配使用的是alloc;需要程序猿管理内存;ARC的内存的管理,是编译器再编译的时候自动添加retain、release、autorelease;堆区的地址是从低到高分配。

  • 全局区/静态区(static):包括两个部分:未初始化过 、初始化过;也就是说,(全局区/静态区)在内存中是放在一起的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域;eg:int a;未初始化的。int a = 10;已初始化的。

  • 常量区:常量字符串就是放在这里。

  • 代码区:存放App二进制代码。

其他事项:

在iOS中,堆区的内存是应用程序共享的,堆中的内存分配是系统负责的。

系统使用一个链表来维护所有已经分配的内存空间(系统仅仅纪录,并不管理具体的内容)。

变量使用结束后,需要释放内存,OC中是根据引用计数是否为0时,为0就说明没有任何变量使用该空间,那么系统将直接收回。

当一个app启动后,代码区,常量区,全局区大小已固定,因此指向这些区的指针不会产生崩溃性的错误。而堆区和栈区是时时刻刻变化的(堆的创建销毁,栈的弹入弹出),所以当使用一个指针指向这两个区里面的内存时,一定要注意内存是否已经被释放,否则会产生程序崩溃(也即是野指针报错)。

操作系统使用stack段中的指针值访问heap段中的对象。如果stack对象的指针没有了,则heap中的对象就不能访问。这也是内存泄露的原因。

iOS内存管理

iOS开发中,内存中的对象主要有两类,一类是值类型,比如int、float、struct等基本数据类型,另一类是引用类型,也就是继承自NSObject类的所有的OC对象。

值类型会被放入栈中,依次紧密排列,在内存中占有一块连续的内存空间,遵循先进后出的原则。

引用类型会被放到堆中,当给对象分配内存空间时,会随机的从内存当中开辟空间,对象与对象之间可能会留有不确定大小的空白空间,因此会产生很多内存碎片,需要我们管理。

栈内存与堆内存从性能上比较,栈内存要优于堆内存,这是因为栈遵循先进后出的原则,因此当数据量过大时,存入栈会明显的降低性能。因此,我们会把大量的数据存入堆中,然后栈中存放堆的地址,当需要调用数据时,就可以快速的通过栈内的地址找到堆中的数据。

ARC

Objective-C中提供了两种内存管理机制MRC(Mannul Reference Counting)ARC(Automatic Reference Counting)

这里主要介绍下ARC的机制。ARC是IOS5推出的新功能,通过ARC,可以自动的管理内存。在ARC模式下,只要没有强指针(强引用)指向对象,对象就会被释放。在ARC模式下,不允许使用retain、release、retainCount等方法。并且,如果使用dealloc方法时,不允许调用[super dealloc]方法。ARC模式下的property变量修饰词为strong、weak,相当于MRC模式下的retain、assign。strong :代替retain,缺省关键词,代表强引用。**weak:代替assign,声明了一个可以自动设置nil的弱引用,但是比assign多一个功能,指针指向的地址被释放之后,指针本身也会自动被释放。**

与内存有关的修饰符

  • strong :强引用,ARC中使用,与MRC中retain类似,使用之后,计数器+1。
  • weak :弱引用 ,ARC中使用,如果只想的对象被释放了,其指向nil,可以有效的避免野指针,其引用计数为1。
  • readwrite : 可读可写特性,需要生成getter方法和setter方法时使用。
  • readonly : 只读特性,只会生成getter方法 不会生成setter方法,不希望属性在类外改变。
  • assign :赋值特性,不涉及引用计数,弱引用,setter方法将传入参数赋值给实例变量,仅设置变量时使用。
  • retain :表示持有特性,setter方法将传入参数先保留,再赋值,传入参数的retaincount会+1。
  • copy :表示拷贝特性,setter方法将传入对象复制一份,需要完全一份新的变量时。
  • nonatomic :非原子操作,不加同步,多线程访问可提高性能,但是线程不安全的。决定编译器生成的setter getter是否是原子操作。
  • atomic :原子操作,同步的,表示多线程安全,与nonatomic相反。

MRC与ARC混编

在ARC的项目中,对MRC的文件可以添加编译选项-fno-objc-arc的标识;在MRC的项目中,对ARC的文件可以添加编译选项-fobjc-arc的标识。

ARC内存管理

即便有了ARC,也是有可能会内存泄露的。

Block

有这样一个场景:

在网络工具类NetworkFetch中有一个网络请求的回调Block:

//.h
//定义Block
typedef void (^NetworkCompletionHandler)(NSData *data);

//网络请求方法
- (void)startWithCompletionHandler:(NetworkCompletionHandler)completion;

//.m
//Block属性
@property (nonatomic, copy) (NetworkCompletionHandler)completionHandler;

某个类中使用网络工具类发送请求并处理回调:

- (void)fetchData {
    NSURL *url = [NSURL alloc] initWithString:@"/* some url string */";
    _networkFetcher = [[NetworkFetch alloc] initWithURL:url];
    [_networkFetcher startWithCompletionHandler:^(NSData *data) {
        NSLog(@"request url %@ finished.", _networkFetcher);
        _fetcherData = data;
    }]
}

很明显在使用block的过程中形成了循环引用:self持有 networkFetecher;networkFetecher持有block;block持有 self。三者形成循环引用,内存泄露。

下面的例子也会造成内存泄露:

- (void)fetchData {
    NSURL *url = [NSURL alloc] initWithString:@"/* some url string */";
    NetworkFetecher *networkFetcher = [[NetworkFetch alloc] initWithURL:url];
    [networkFetcher startWithCompletionHandler:^(NSData *data) {
        NSLog(@"request url %@ finished.", networkFetcher);
    }]
}

networkFetecher持有block,block持有networkFetecher,形成内存孤岛,无法释放。

解决方案有两种:

  • 将对象置为nil,消除引用,打破循环引用。这种方法容易漏掉某个该置nil的属性。
// 代码中任意地方
_networkFetecher = nil;
  • 将强引用转换成弱引用,打破循环引用。
__weak __typeof(self) weakSelf = self;

//如果想防止 weakSelf 被释放,可以再次强引用
__typeof(&*weakSelf) strongSelf = weakSelf;
if (strongSelf) {
	//do something with strongSelf
}

&*weakSelf中的&*主要是为了兼容早期的LLVM。也可以使用RAC比较优雅的@weakify(self)@strongify(self)

另外方法名带有usingBlockCocoaFramework方法或GCD的API本身会对传入的block做一个复制的操作,也需要注意循环引用的问题。关于Block的内存分配在iOS的Block的内存分配中有深入的阐述。

performSelector

performSelector就是在运行时执行一个selector。

- (id)performSelector:(SEL)selector;

[object methodName];
[object performSelector:@selector(methodName)];

如果有以下的代码:

SEL selector;
if (/* some condition */) {
    selector = @selector(newObject);
} else if (/* some other condition */) {
    selector = @selector(copy);
} else {
    selector = @selector(someProperty);
}
id ret = [object performSelector:selector];

这段代码就相当于在动态之上再动态绑定。正是由于动态,编译器不知道即将调用的selector是什么,不了解方法签名和返回值,甚至是否有返回值都不懂,所以编译器无法用ARC的内存管理规则来判断返回值是否应该释放。因此,ARC采用了比较谨慎的做法,不添加释放操作,即在方法返回对象时就可能将其持有,从而可能导致内存泄露。

以本段代码为例,前两种情况(newObject, copy)都需要再次释放,而第三种情况不需要。这种泄露隐藏得如此之深,以至于使用static analyzer都很难检测到。如果把代码的最后一行改成:

[object performSelector:selector];

不创建一个返回值变量测试分析,简直难以想象这里居然会出现内存问题。所以如果你使用的selector有返回值,一定要处理掉。

NSNotificationcenter

这个比较常见,记得removeObserver就可以了,也可以用RAC的对应方法。

NSTimer

在使用NSTimer addtarget时,为了防止target被释放而导致的程序异常,timer会持有target,所以这也是一处内存泄露的隐患。

@property (nonatomic, strong) NSTimer *timer;

- (void)someMethod{
    timer = [NSTimer scheduledTimerWithTimeInterval:0.1  
                                             target:self  
                                           selector:@selector(handleTimer:)  
                                           userInfo:nil  
                                            repeats:YES];  
}

异常处理

JAVA开发的同事一直不理解为什么iOS开发没有try...catch。Apple 提供了错误处理(NSError)和异常处理(NSException)两种机制,而 try...catch就是使用exception捕获异常。NSError应用在在绝大部分的场景下,并且这也是Apple所推荐。那什么时候用NSException 呢?在极其严重的直接导致程序崩溃情况下才使用,并且无需考虑恢复问题。

NSArray *array = @[@"a", @"b", @"c"];
@try {
    // 可能抛出异常的代码
    [array objectAtIndex:3];
}
@catch (NSException *exception) {
    // 处理异常
    NSLog(@"throw an exception: %@", exception.reason);
}
@finally {
    NSLog(@"finally execution");
}

MRC下的try...catch:

// 注意:在 @try @catch @finally 块内定义的变量都是局部变量
@try {
    EOCSomeClass *object = [[EOCSomeClass alloc] init];
    [object doSomethingMayThrowException];
    [object release];
}
@catch (NSException *exception) {
    NSLog(@"throw an exception: %@", exception.reason);
}

如果doSomethingMayThrowException方法抛出了异常,那么object对象就无法释放。如果object对象持有了重要且稀缺的资源,就可能会造成严重后果。

不过,OC中大部分crash如:内存溢出、野指针等都是无法捕获的,而能捕获的只是像数组越界之类(这在代码洁癖的眼中难道不应该提前判断么)。

参考:
C语言之内存四区模型和函数调用模型
ARC 下内存泄露的那些点