iOS 内存管理

​ 前言:随着手机市场日新月异的更新,目前无论安卓手机还是iPhone手机的内存都越来越大,但是手机系统和App也越来越大,越来越复杂,这时内存管理仍然是必要的,不然再大的内存也会消耗殆尽,从而手机卡死,App闪退崩溃。

一、基本概念

1、内存管理概念

​ 移动设备的内存有限,每个app所能占用的内存也是有限制的,当app所占用的内存较多时,这时得回收一些不需要再使用的内存空间。iOS使用引用计数来管理OC对象的内存,有两种内存管理方案:

​ 1)ARC:Automatic Reference Counting,自动引用计数,系统自动帮你管理引用计数,从iOS 5开始以后使用,主要是对OC的对象类型有用(因为对象类型存储在堆区,需要自己手动管理),对于基本数据类型比如int、float等无效(因为基本数据类型在栈区,由系统自动管理),ARC目前是主流;说白了,就是编译器自动帮你在合适的位置插入retain/release方法

​ 2)MRC:Manual Reference Counting,手动引用计数,需要手动管理引用计数,iOS 5之前的方案,目前基本不使用了,但是在CoreGraphics、CoreFoundation框架里,还是要经常手动释放对象的。

// 画一条直线
CGMutablePathRef path = CGPathCreateMutable(); //创建path
CGPathMoveToPoint(path, nil, 100, 100);
CGPathAddLineToPoint(path, nil, 150, 100);
CGPathRelease(path); //需要手动Release对象
// CFRelease(path); // 等效
2、引用计数

​ 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间。调用retain或strong会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1。

​ 简单总结:当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease(引用计数不会立刻减一)来释放它;想拥有某个对象,就让它的引用计数+1,不想再拥有某个对象,就让它的引用计数-1。

3、AutoreleasePool

官方文档-NSAutoreleasePool,Autorelease延迟了对象的销毁的时间。

// 在MRC
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Code here
[pool release];

// 在ARC
@autoreleasepool {
    // Code here
}

1)AutoreleasePool概念:自动释放池,OC中的一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机。

2)AutoreleasePool原理:ARC下,我们使用@autoreleasepool{}来使用一个AutoreleasePool,随后编译器将其改写成下面的样子:

// atautoreleasepoolobj:哨兵对象
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
// {}中的代码
objc_autoreleasePoolPop(atautoreleasepoolobj);

而这两个函数都是对AutoreleasePoolPage的简单封装,所以自动释放机制的核心就在于这个类。

3)AutoreleasePoolPage:AutoreleasePoolPage是一个C++实现的类,结构代码为:

class AutoreleasePoolPage {
    ...
    id *next; //指向当前可插入对象的地址
    pthread_t const thread; //当前线程 
    AutoreleasePoolPage * const parent; //前驱指针
    AutoreleasePoolPage *child; //后继指针

    static void * operator new(size_t size) { //创建PoolPage,SIZE为4096
        return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
    }
    
    /**
    begin()和end()方法标记了被自动管理对象的范围
    */
    id * begin() { //最低地址
        return (id *) ((uint8_t *)this+sizeof(*this));
    }
    id * end() { //最高地址
        return (id *) ((uint8_t *)this+SIZE);
    } 
    ...
}
  • AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针,即前驱和后继指针)。
  • AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),一部分存自己的实例变量,大部分存autorelease对象的地址。
  • 结构中的thread指针指向当前线程。
  • 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入。

向一个对象发送- autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置

4)Autorelease对象什么时候释放:每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop()作为入参,那么:

1、根据传入的哨兵对象地址找到哨兵对象所处的page;

2、在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并移动next指针到正确位置;

3、从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page。

总结:在objc_autoreleasePoolPop的时候对Autorelease对象进行释放

5)嵌套的AutoreleasePool:知道了上面的原理,嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象而已,就像剥洋葱一样,每次一层,互不影响。

6)AutoreleasePool本身什么时候释放:每个线程(包括主线程)拥有NSAutoreleasePool的栈。如果新的pool被创建(调用objc_autoreleasePoolPush),它就会被添加到栈顶;当pool被销毁(调用objc_autoreleasePoolPop),它就从栈顶被移除;最新发送autorelease消息的对象,会被添加到最近的自动释放池(即栈顶的释放池);当线程终止时,会把栈内所有的释放池移除。

7)main函数的autoreleasepool作用:从技术角度看,不是非要有个自动释放池。因为块的末尾恰好是应用程序的终止处,而此时操作系统会将引用程序所占的全部内存都释放掉。虽说如此,但是如果不写这个块的话,那么由UIApplicationMain函数所自动释放的那些对象,就没有自动释放池可用,于是系统发出了警告,所以说,这个池子可以理解成最外围捕捉自动释放对象用的。

8)Autoreleasepool 与 Runloop 的关系:主线程默认为我们开启 Runloop,Runloop 会自动帮我们创建Autoreleasepool,并进行Push、Pop 等操作来进行内存管理。准确的说,在kCFRunLoopEntry即将进入的时候进行push,创建pool;在kCFRunLoopBeforeWaiting即将休眠的时候,进行pop和push,即释放旧的池并创建新池,在kCFRunLoopExit即将退出RunLoop的时候进行pop,释放旧的池。更多详情-iOS RunLoop

9)ARC 下什么样的对象由 Autoreleasepool 管理:所有的对象都归AutoreleasePool管理吗?非也,在ARC环境下,对于普通的对象(通过alloc、new、copy、mutableCopy创建)是由编译器在合适的地方为我们 Realease,只有收到Autorelease消息的对象才归AutoreleasePool管理。

那到底什么情况才归AutoreleasePool管理呢?

  • 系统自带的方法中,如果不包含alloc new copy mutableCopy,则这些方法返回的对象都是autorelease的。比如[NSDate date],[NSString stringWithFormat:@"%ld", i]等。
  • 开发者自己通过方法创建并返回一个对象,比如自己创建的类方法返回的对象就是autorelease的,因为需要延迟调用。

10)子线程默认不会开启 Runloop,那出现 Autorelease 对象如何处理?不手动处理会内存泄漏吗?:如果在子线程你创建了 Pool 的话,产生的 Autorelease 对象就会交给 pool 去管理;如果你没有创建 Pool ,但是产生了 Autorelease 对象,就会调用 autoreleaseNoPage 方法,在这个方法中,会自动帮你创建一个 hotpage(hotPage 可以理解为当前正在使用的 AutoreleasePoolPage),并把Autorelease对象放进该Pool中。也就是说你不进行手动的内存管理,也不会内存泄漏啦。参考-各个线程 Autorelease 对象的内存管理

4、内存分区

​ 内存分区大体分为5个区:

1、代码区:存放App代码,App程序会拷贝到这里。

2、常量区:常量字符串就是放在这里的,还有const常量。

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

4、堆区(heap):需要我们自己管理内存,alloc申请内存release释放内存。创建的对象也都放在这里, 地址是从低到高分配。堆是所有程序共享的内存,当N个这样的内存得不到释放,堆区会被挤爆,程序立马瘫痪。

5、栈区(stack):由系统去管理内存,地址从高到低分配,First In Last Out先进后出原则。会存一些局部变量,函数跳转时现场保护(寄存器值保存于恢复),这些系统都会帮我们自动实现,无需我们干预。所以大量的局部变量,深递归,函数循环调用都可能耗尽栈内存而造成程序崩溃 。

来一张高清大图表示各个分区的地址高低:

iOS内存地址的分配
5、浅拷贝和深拷贝

​ 1)浅拷贝:指针拷贝,并没有创建新的对象,比如NSString、NSArray、NSDictionary调用copy方法;

​ 2)深拷贝:创建一个新的对象,比如NSMutableString、NSMutableArray、NSMutableDictionary调用copy方法,生成一个不可变的新对象,或者NSString、NSArray、NSDictionary调用mutableCopy生成一个新的可变的对象;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 浅拷贝:NSString、NSArray、NSDictionary调用copy方法
    NSString *str1 = @"Hello";
    NSString *str2 = [str1 copy]; //浅拷贝
    
    // 深拷贝:NSMutableString、NSMutableArray、NSMutableDictionary调用copy方法
    NSMutableString *str3 = [[NSMutableString alloc] initWithString:@"World"];
    NSString *str4 = [str3 copy]; //深拷贝
    // 深拷贝:NSString、NSArray、NSDictionary调用mutableCopy方法
    NSMutableString *str5 = [str1 mutableCopy]; //深拷贝
    
    NSLog(@"str1 = %p", str1);
    NSLog(@"str2 = %p", str2);
    NSLog(@"str3 = %p", str3);
    NSLog(@"str4 = %p", str4);
    NSLog(@"str5 = %p", str5);
}

运行结果:

str1 = 0x10e0bd440
str2 = 0x10e0bd440
str3 = 0x600002d7f240
str4 = 0xe641bba2b7d9af9f
str5 = 0x600002d7c750

二、循环引用

​ 既然现在都是ARC的时代了,系统帮你管理引用计数,帮你管理内存分配,你就可以安枕无忧吗?非也,如果代码使用不当,可能造成对象之间的循环引用,导致引用计数大于0,系统没法回收内存,导致内存泄漏;如果反复出现内存泄漏,当使用的内存超过系统限制时,App被系统kill,App程序闪退。

​ 下面介绍三种常见的循环引用模式:

1、三种循环引用模式

​ 1)自循环引用:对象的强持有变量指向自身,比如ViewController强持有一个block,在block里又捕获持有ViewController,造成自循环引用;

​ 2)相互循环引用:比如定义一个A类和B类,A类有一个B类的属性,B类有一个A类的属性,修饰词都是strong类型,在A类里面访问B类属性和B类里边访问A类属性,那么就会出现相互持有,不会走dealloc方法;

​ 3)多循环引用:比如类似于三角恋关系,A持有B,B持有C,C又持有A,造成循环引用;

2、如何解决循环引用

​ 1)_ _weak:弱引用,项目中使用最多的方式,无论是使用weak修饰self,还是修饰delegate等,使用广泛。weak指针指向的对象在被废弃之后会被自动置为nil:

​ 当weak指针指向的对象被废弃之后,dealloc的内部实现当中会调用清除弱引用的一个方法。然后在清除弱引用的方法当中,会通过哈希算法来查找被废弃对象在弱引用表当中的位置,来提取所对应的弱引用指针的列表数组,然后进行for循环遍历,把每一个weak指针都置为nil。

​ 2)_ _unsafe_unretained:不安全引用,修饰对象不会增加引用计数,当指针指向的对象被废弃后,指针不会置为nil,成为悬垂指针

​ 3)_ _block:在MRC模式下,_ _block修饰对象不会增加引用计数,避免了循环引用;在ARC模式下, _ _block一般用来在block内部修改外部的局部变量。如果对于block有兴趣,移步-iOS Block

@property(nonatomic, copy) void (^testBlock)(void);

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) wself = self;
//    __unsafe_unretained typeof(self) wself = self;
    self.testBlock = ^{
            //当然,如果不在block内使用self,就不会捕获self,自然就没有循环引用了
        NSLog(@"testBlock = %@", wself); 
    };
    self.testBlock();
}

- (void)dealloc {
    NSLog(@"ViewController dealloc");
}

三、解决NSTimer、CADisplayLink内存泄漏问题

​ 由于NSTimer在项目中经常使用,并且就算使用weak修饰,还是会存在内存泄漏问题,所以单独拿出来用代码解释说明,并提供优雅的解决方案。CADisplayLink也是定时器,想详细了解-iOS RunLoop

@interface BViewController ()
@property(nonatomic, assign) NSInteger num;
@property(nonatomic, weak) NSTimer *timer; //使用weak修饰也不能解决内存泄漏
@end

@implementation BViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.num = 0;
    // 默认自动添加到RunLoop,也就是RunLoop持有timer,直到调用invalidate,才移除timer
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFire:) userInfo:nil repeats:YES];
}

- (void)dealloc {
    NSLog(@"BViewController dealloc");
    // invalidate:停止定时器,并把timer从RunLoop中移除,并把timer的target强引用去除
    [self.timer invalidate];
    self.timer = nil;
}

- (void)timerFire:(NSTimer *)timer {
    self.num ++;
    NSLog(@"num = %ld", self.num);
}

@end

先从一个AViewController跳转至BViewController,点击返回按钮,发现并没有调用dealloc析构函数,为什么?首先BViewController弱持有timer,timer强持有BViewController,当点击返回按钮时,NavigationController不再持有BViewController,当前的RunLoop仍然强持有timer,而timer强持有BViewController,所以BViewController引用计数不为0,自然得不到释放。

NSTimer强引用ViewController

其实不管BViewController用strong还是用weak修饰timer,最终timer都会强持有BViewController,造成内存泄漏。现在列出三种解决方案:

1、使用block回调

​ 使用block回调处理事件,而不是使用target,这样就不会强持有target,但是是iOS 10之后才有的方法。

@interface BViewController ()
@property(nonatomic, assign) NSInteger num;
@property(nonatomic, strong) NSTimer *timer;
@end

@implementation BViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.num = 0;
    __weak typeof(self) wself = self;
    // 注意:这个方法是iOS 10之后的
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [wself timerFire:timer];
    }];
}

- (void)dealloc {
    NSLog(@"BViewController dealloc");
    // invalidate:停止定时器,并把timer从RunLoop中移除,并把timer的target强引用去除
    [self.timer invalidate];
    self.timer = nil;
}

- (void)timerFire:(NSTimer *)timer {
    self.num ++;
    NSLog(@"num = %ld", self.num);
}

@end
2、使用动态消息解析

​ RunLoop会对timer强引用,timer对BViewController强引用,那么我们可以创建一个中间类来弱引用BViewController,让timer对中间类强引用,方法2和下面的方法3都是基于这个思想做的。当然这里还牵扯到OC的消息转发机制,如果有兴趣请看-iOS Runtime

给NSTimer添加中间件

创建一个中间类TimerMiddleware:

// .h文件
@interface TimerMiddleware : NSObject
+ (instancetype)middlewareWithTarget:(id)target;
@end

// .m文件
@interface TimerMiddleware ()
/**
 这里使用weak,弱引用,才能对BViewController弱引用,才能解决内存泄漏
 如果使用strong,那么还是会内存泄漏
 */
@property(nonatomic, weak) id target;
@end

@implementation TimerMiddleware
+ (instancetype)middlewareWithTarget:(id)target {
    TimerMiddleware *middleware = [TimerMiddleware new];
    middleware.target = target;
    return middleware;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.target;
}

@end

在BViewController中:

// 其他代码和上面一样
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.num = 0;
    TimerMiddleware *middleware = [TimerMiddleware middlewareWithTarget:self];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:middleware selector:@selector(timerFire:) userInfo:nil repeats:YES];
}

运行结果:BViewController会走dealloc方法,说明解决了内存泄漏问题。

3、使用NSProxy转发消息

1、NSProxy是跟NSObject一个级别的基类,用来设计做消息转发的;

2、NSProxy是抽象类,使用时候我们需要使用其子类;

3、NSProxy和NSObject都遵循NSObject协议;

4、NSProxy不会跟NSObject类一样去父类搜索方法实现,会直接进入消息转发流程,所以效率更高。

先创建一个TimerProxy,继承自NSProxy:

// .h文件
@interface TimerProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@end

// .m文件
@interface TimerProxy ()
@property(nonatomic, weak) id target;
@end

@implementation TimerProxy

+ (instancetype)proxyWithTarget:(id)target {
    TimerProxy *proxy = [TimerProxy alloc]; //注意:没有init方法
    proxy.target = target;
    return proxy;
}

// NSProxy接收到消息会自动进入到调用这个方法 进入消息转发流程
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}
@end

在BViewController中:

// 其他代码和上面一样
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.num = 0;
    TimerProxy *proxy = [TimerProxy proxyWithTarget:self];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:proxy selector:@selector(timerFire:) userInfo:nil repeats:YES];
}

总结:如果不管iOS系统版本,那么使用block方式简单;如果需要兼容系统版本,那么使用NSProxy更加高效,一次封装,终生受用😄。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,565评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,021评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,003评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,015评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,020评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,856评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,178评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,824评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,264评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,788评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,913评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,535评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,130评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,102评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,334评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,298评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,622评论 2 343