iOS内存管理

简介: iOS内存管理

一、内存管理


1.什么是内存泄漏?什么是安全释放


内存泄漏指动态分配内存的对象在使用完后没有被系统回收内存,导致该对象始终占用内存,又无法通过代码访问,属于内存管理出错,如果出现大量内存泄漏,那么会导致系统内存不足的问题。由于内存泄漏的检测是程序开发中不可避免的问题,所以对于程序员而言,一方面要深入理解内存管理原则,养成良好的编程习惯以减少内存泄漏情况的发生;另一方面可以通过各种方法,例如使用Xcode提供的检测调试工具Instruments,检测可能导致内存泄漏的代码,并及时进行优化。

安全释放指释放掉不再使用的对象的同时不会造成内存泄漏或指针悬挂问题的操作。为了保证安全释放,在对象dealloc后要将其指针置为nil。另外,要严格遵守内存管理原则,保证对象的引用计数正确,同时要注意避免引用循环的出现。


2.僵尸对象、野指针、空指针分别指什么?它们有什么区别


(1)僵尸对象


一个引用计数为0的Objective-C对象被释放后就变成僵尸对象。僵尸对象的内存已经被系统回收,虽然该对象可能还存在,数据依然在内存中,但僵尸对象已经是不稳定对象了,不可以再访问或者使用,它的内存是随时可能被别的对象申请而占用的。需要注意的是,僵尸对象所占的内存是正常的,不会造成内存泄漏。


(2)野指针


野指针又叫“悬挂指针”,野指针出现的原因是指针没有赋值,或者指针指向的对象已经被释放掉了。野指针指向一块随机的垃圾内存,向它们发送消息会报EXC_BAD_ACCESS错误导致程序崩溃。


(3)空指针


空指针不同于野指针,它是一个没有指向任何内容的指针。空指针是有效指针,值为nil、NULL、Nil或0等,给空指针发送消息不会报错,只是不响应消息而已,应该给野指针及时赋予零值使其变成有效的空指针,避免内存报错。


3.Objective-C有GC垃圾回收机制吗


垃圾回收(Garbage Collection,GC)简单地说就是程序中及时处理废弃不用的内存对象的机制,防止内存中废弃对象堆积过多造成内存泄漏。

Objective-C语言本身是支持垃圾回收机制的,但有平台局限性,仅限于Mac桌面系统开发中,而在iPhone和iPad等苹果移动终端设备中是不支持垃圾回收机制的。在移动设备开发中的内存管理是采用手动引用计数(Manual Reference Counting,MRC)以及iOS5以后的自动引用计数(Automatic Reference Counting,ARC),本质都是引用计数(RC),通过引用计数的方式来管理内存的分配与释放,从而防止内存泄漏。


另外,引用计数和垃圾回收是有区别的。垃圾回收是宏观的,对整体进行内存管理,虽然不同平台垃圾回收机制不同,但基本原理都是一样的:将所有对象看作一个集合,然后在GC循环中定时检测活动对象和非活动对象,及时将用不到的非活动对象释放掉以避免内存泄漏,也就是说用不到的垃圾对象是交给GC来管理释放的,而无需开发者关心,典型的是Java语言中的垃圾回收机制;相比于GC,引用计数是局部性的,开发者要管理控制每个对象的引用计数,单个对象引用计数为0后会马上被释放掉。ARC是一种改进,由编译器帮助开发者自动管理控制引用计数(自动在合适的时机发送release和retain消息)。另外,自动释放池(autorelease pool)像是一个局部的垃圾回收,将部分垃圾对象集中释放,相对于单个释放会有一定延迟。


4.在Objective-C中,与alloc语义相反的方法是dealloc还是release


alloc与dealloc语意相反,alloc是创建变量,dealloc是释放变量。

retain与release语义相反,retain保留一个对象,调用后使变量的引用计数加1,而release释放一个对象,调用后使变量的引用计数减1。

虽然alloc对应dealloc,retain对应release,但是与alloc配对使用的方法是release,而不是dealloc。为什么呢?这要从它们的实际效果来看。事实上alloc和release配对使用只是表象,本质上还是retain和release的配对使用。alloc用来创建对象,刚创建的对象默认引用计数为1,相当于调用alloc创建对象过程中同时会调用一次retain使对象引用计数加1,自然要有对应的release的一次调用,使对象不再被用时能够被释放掉防止内存泄漏。

此外,dealloc是在对象引用计数为0以后系统自动调用的,dealloc没有使对象引用计数减1的作用,只是在对象引用计数为0后被系统调用进行内存回收的收尾工作。


二、内存管理机制


1.当使用block时,什么情况会发生引用循环?如何解决


常见的使用block引起引用循环的情况为:在一个对象中强引用了一个block,在该block中又强引用了该对象,此时就出现了该对象和该block的循环引用。示例代码如下:

/*Test.h*/
#import <Foundation/Foundation.h>
/* 声明一个名为MYBlock的block,参数为空,返回值为void*/
typedef void(^MYBlock)();
@interface Test :NSObject
/* 定义并强引用一个MYBlock*/
@property(nonatomic, strong) MYBlock block;
/*对象属性*/
@property (nonatomic, copy) NSString *name;
- (void)print;
@end
/*Test.m*/
#import "Test.h"
@implementation Test
- (void)print {
    self.block= ^{
        NSLog(@"%@",self.name);
    };
    self.block();
}
@end


解决上面的引用循环的方法有以下两种:一是强制将一方置nil,破坏引用循环;二是将对象使用__weak或者__block修饰符修饰之后再在block中使用(注意是在自动引用计数下),代码如下:

- (void)print {
    __weak typeof(self) weakSelf = self;
    self.block() = ^{
        NSLog(@"%@",weakSelf.name);
    };
    self.block();
}


2.CAAnimation的delegate是强引用还是弱引用


CAAnimation的delegate(代理)是强引用,是内存管理中一个罕见的特例。为了避免循环引用问题,delegate通常都使用assign或weak修饰表示弱引用,而CAAnimation动画是异步的,如果动画的代理是弱引用而不是强引用,那么会导致其随时都可能被释放掉。在使用动画时要注意采取措施避免循环引用,例如及时在视图移除之前的合适时机移除动画。

CAAnimation的代理定义如下,明确说明动画的代理在动画对象整个生命周期间是被强引用的,默认为nil。

/*The delegate of the animation.This object is retained for the
 *lifetime of the animation object.Defaults to nil.See below for the
 *supported delegate methods.*/
@property (nullable,strong)id <CAAnimationDelegate>delegate;


3. 按照默认法则,哪些关键字生成的对象需要手动释放


起初在MRC(手动引用计数)中开发者要自己手动管理内存,基本原则是:谁创建,谁释放,谁引用,谁管理。其中,创建主要始于关键词new、alloc和copy的使用,创建并持有开始引用计数为1,如果引用要通过发送retain消息增加引用计数,通过发送release消息减少引用计数,那么引用计数变为0后对象会被系统清理释放。现在有了ARC(自动引用计数)后编译器会自动管理引用计数,开发者不再需也不可以手动管理引用计数。

使用new、alloc、copy关键字生成的对象和retain了的对象需要手动释放。被设置为autorelease的对象不需要手动释放,会直接进入自动释放池。

常见真题:下面代码的输出是什么?

NSMutableArray *ary = [[NSMutableArray array] retain];
NSString *str = [NSString stringWithFormat:@"test"];
[str retain];
[ary addObject:str];
NSLog(@"%@%d",str,[str retainCount]);
[str retain];
[str release];
[str release];
NSLog(@"%@%d",str,[str retainCount]);
[ary removeAllObjects];
NSLog(@"%@%d",str,[str retainCount]);


A.2,3,1 B.3,2,1 C.1,2,3 D.2,1,3

答案:B。

本题考查的是非MRC下引用计数的使用(只有在MRC下才可以通过retain和release关键字手动管理内存对象,才可以向对象发送retainCount消息获取当前引用计数的值),开始使用类方法stringWithFormat在堆上新创建了一个字符串对象str,str创建并持有该字符串对象默认引用计数为1,之后retain使引用计数加1变为2,然后又动态添加到数组中且该过程同样会让其引用计数加1变为3(数组的add操作是添加对成员对象的强引用),此时打印结果引用计数为3;之后的3次操作使引用计数加1后又减2,变为2,此时打印引用计数结果为2;最后数组清空成员对象,数组的remove操作会让移除的对象引用计数减1,所以str的引用计数变为了1,打印结果为1。因此,先后引用计数的打印结果为:3,2,1。本题答案 选B。

这里要特别注意上面为何说stringWithFormat方法是在堆上创建的字符串对象,这里涉及NSString的内存管理,下面单独对其进行扩展和分析。Objective-C中常用的创建NSString字符串对象的方法主要有以下5种:

/*字面量直接创建*/
NSString *str1 = @"string";
/*类方法创建*/
NSString *str2 = [NSString stringWithFormat:@"string"];
/*编译器优化后弃用,效果等同于str1的字面量创建方式*/
NSString *str3 = [NSString stringWithString:@"string"];
/*实例方法创建*/
NSString *str4 = [[NSString alloc]initWithFormat:@"string"];
/*编译器优化后弃用,效果等同于str1的字面量创建方式*/
NSString *str5 = [[NSString alloc]initWithString:@"string"];


开发中推荐的是前两种str1和str2的创建方式,分别用来创建不可变字符串和格式化字符串。最新的编译器优化后弃用了str3的stringWithString和str5的initWithString创建方式,现在这样创建会出现警告,说这样创建是多余的,因为实际效果和直接用字面量创建相同,也都是在常量内存区创建一个不可变字符串,由系统自动管理内存和优化内存,它们创建后可以认为已经被autorelease了。另外,此处由于字符串的内容都是“string”,使用str1、str3和str5创建的字符串对象实际在常量内存区只有一个备份,这是编译器的优化效果,而str2和str4由于是在堆上创建所以各自有自己的备份。

此外,最重要的是这5种方法创建的字符串对象所处的内存类型,str1、str3和str5都是创建的不可变字符串,是位于常量内存区的,由系统管理内存;stringWithFormat和initWithFormat创建的都是格式化的动态字符串对象,在堆上创建,需要手动管理内存。


4.Objective-C是如何实现内存管理的


Objective-C的内存管理本质上是通过引用计数实现的,每次RunLoop都会检查对象的引用计数,如果引用计数为0,那么说明该对象已经没有再被使用了,此时可以对其进行释放了。其中,引用计数可以大体分为3种:MRC、ARC和内存池。

那么引用计数是如何操作的呢?其实不论哪种引用计数方式,它们本质上都是在合适的时机将对象的引用计数加1或者减1。

所以对于引用计数可以总结如下:

1)使对象引用计数加1的常见操作有alloc、copy、retain。

2)使对象引用计数减1的常见操作有release、autorealease。

自动释放池是一个统一来释放一组对象的容器,在向对象发送autorelease消息时,对象并没有立即释放,而是将对象加入到最新的自动释放池(即将该对象的引用交给自动释放池,之后统一调用release),自动释放池会在程序执行到作用域结束的位置时进行drain释放操作,这个时候会对池中的每一个对象都发送release消息来释放所有对象。这样其实就实现了这些对象的延迟释放。

自动释放池释放的时机指自动释放池内的所有对象是在什么时候释放的,这里要提到程序的运行周期RunLoop。对于每一个新的RunLoop,系统都会隐式地创建一个autorelease pool,RunLoop结束时自动释放池便会进行对象释放操作。autorelease和release的区别主要是引用计数减1的时机不同,autorelease是在对象的使用真正结束的时候才做引用计数减1,而不是收到消息立马释放。

retain、release和autorelease的内部实现代码如下:

- (id)retain {
    /* 对象引用计数加1*/
    NSIncrementExtraRefCount(self);
    return self;
}
- (void)release {
    /*对象引用计数减1,之后如果引用计数为0,那么释放*/
    if(NSDecrementExtraRefCountWasZero(self)) {
        NSDeallocateObject(self);
    }
}
- (id)autorelease {
    /* 添加对象到自动释放池*/
    [NSAutoreleasePool addObject:self];
    return self;
}


5.如何实现autorealeasepool


autorealeasepool(自动释放池)其实并没有其自身的结构,它是基于多个Autorelease PoolPage(一个C++类)以双向链表组合起来的结构,其基本操作都是简单封装了AutoreleasePoolPage的操作方法。例如,可以通过push操作添加对象,或者通过pop操作弹出对象,以及通过release操作释放销毁对象,对应的3个封装后的操作函数为:objc_autoreleasepoolPush、objc_autoreleasepoolPop和objc_autorelease。自动释放池将用完的对象集中起来,统一释放,起到延迟释放对象的作用。

自动释放池存储于内存中的栈上,释放池之间遵循“先进后出”原则。例如下面代码所示的释放池嵌套。

/* 释放池 1*/
@autoreleasepool {
    People *person1 = [[[Person alloc] init] autorelease];
    /* 释放池 2*/
    @autoreleasepool {
        People *person2 = [[[Person alloc] init] autorelease];
    }
    People *person3 = [[[Person alloc] init] autorelease];
}


代码中释放池1和释放池2在内存中的结构如图所示,释放池1先入栈,后出栈;释放池2后入栈,先出栈。person2对象在释放池2中,会被先释放;person1和person3在释放池1中,会后被释放。


1684484032294.png


常见真题:下面这段代码有什么问题?如何修改?

for (int i = 0; i < someLargeNumber; i++) {
    NSString *string = @"Abc";
    string = [string lowercaseString];
    string = [string stringByAppendingString:@"xyz"];
    NSLog(@"%@",string);
}


答案:代码通过循环短时间内创建了大量的NSString对象,在默认的自动释放池释放之前这些对象无法被立即释放,会占用大量内存,造成内存高峰以致内存不足。

为了防止大量对象堆积应该在循环内手动添加自动释放池,这样在每一次循环结束,循环内的自动释放池都会被自动释放及时腾出内存,从而大大缩短了循环内对象的生命周期,避免内存占用高峰。

代码改进方法是在循环内部嵌套一个自动释放池:

for (int i = 0; i < 1000000; i++) {
    @autoreleasepool {
        NSString *string = @"Abc";
        string = [string lowercaseString];
        string = [string stringByAppendingString:@"xyz"];
        NSLog(@"%@",string);
    }
}


6.如果一个对象释放前被加到了NotificationCenter中,不在NotificationCenter中,那么remove对象可能会怎样


前面已经讲到对于NotificationCenter的使用,只要添加对象到消息中心进行通知注册,之后就一定要对其remove进行通知注销。将对象添加到消息中心后,消息中心只是保存该对象的地址,消息中心到时候会根据地址发送通知给该对象,但并没有取得该对象的强引用,对象的引用计数不会加1。如果对象释放后没有从消息中心remove,也就是通知中心还保存着那个指针,而那个指针指的对象可能已经被释放销毁了,那么那个指针就成为一个野指针,当通知发生时,会向这个野指针发送消息导致程序崩溃。


7.NSArray和NSMutableArray在Copy和MutableCopy下的内存情况是怎样的


有两种情况:浅拷贝和深拷贝。浅拷贝只是复制了内存地址,也就是对内存空间的引用;深拷贝是开辟新的空间并复制原空间相同的内容,新指针指向新空间内容。

除了NSArray在Copy下是浅复制,其他都是深复制,测试代码如下:

/* 不可变数组 */
NSArray *oldArray = @[@1, @2, @3];
NSArray *newArray;
/* 可变数组 */
NSMutableArray *oldMulArray = [[NSMutableArray alloc]initWithArray:oldArray];
NSMutableArray *newMulArray;
newArray = [oldArray copy];
newArray = [oldArray mutableCopy];
newMulArray = [oldMulArray copy];
newMulArray = [oldMulArray mutableCopy];


目录
相关文章
|
存储 算法 Java
iOS开发 - 穿针引线之内存管理(二)
iOS开发 - 穿针引线之内存管理
225 0
|
存储 程序员 C语言
iOS开发 - 穿针引线之内存管理(一)
iOS开发 - 穿针引线之内存管理
187 0
|
存储 安全 API
iOS-底层原理 33:内存管理(三)AutoReleasePool & NSRunLoop 底层分析
iOS-底层原理 33:内存管理(三)AutoReleasePool & NSRunLoop 底层分析
212 0
iOS-底层原理 33:内存管理(三)AutoReleasePool & NSRunLoop 底层分析
|
iOS开发
iOS-底层原理 33:内存管理(二)强引用分析
iOS-底层原理 33:内存管理(二)强引用分析
196 0
iOS-底层原理 33:内存管理(二)强引用分析
|
存储 编译器 Serverless
iOS-底层原理 33:内存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底层分析
iOS-底层原理 33:内存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底层分析
255 0
iOS-底层原理 33:内存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底层分析
|
编译器 Swift iOS开发
iOS开发篇-内存管理(下)
现在iOS开发已经是ARC甚至是swift的时代,但是内存管理仍是一个重点关注的问题,如果只知盲目开发而不知个中原理,踩坑就跳不出来了,理解好内存管理,能让我们写出更有质量的代码。
|
Java 编译器 Swift
iOS开发篇-内存管理(中)
现在iOS开发已经是ARC甚至是swift的时代,但是内存管理仍是一个重点关注的问题,如果只知盲目开发而不知个中原理,踩坑就跳不出来了,理解好内存管理,能让我们写出更有质量的代码。
|
Swift iOS开发
iOS开发篇-内存管理(上)
现在iOS开发已经是ARC甚至是swift的时代,但是内存管理仍是一个重点关注的问题,如果只知盲目开发而不知个中原理,踩坑就跳不出来了,理解好内存管理,能让我们写出更有质量的代码。
|
安全 Java 程序员
iOS有关内存管理的二三事
iOS有关内存管理的二三事
116 0
iOS有关内存管理的二三事
|
Java C语言 iOS开发
理解 iOS 和 macOS 的内存管理
本文将会介绍 iOS 和 macOS 应用开发过程中,如何进行内存管理,以及介绍一些内存管理使用的场景,帮助大家理解内存方面的问题
7522 0