iOS开发系列之内存泄漏分析(上)

iOS自从引入ARC机制后,一般的内存管理就可以不用我们码农来负责了,但是一些操作如果不注意,还是会引起内存泄漏。

本文主要介绍一下内存泄漏的原理、常规的检测方法以及出现的常用场景和修改方法。

1、 内存泄漏原理

内存泄漏的在百度上的解释就是“程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果”。

在我的理解里就是,公司给一个入职的员工分配了一个工位,但是这个员工离职后,这个工位却不能分配给下一位入职的员工使用,造成了大量的资源浪费。

2、 常规的检测方法

2.1、Analyze静态分析 (command + shift + b)。

2.2、动态分析方法(Instrument工具库里的Leaks),product->profile ->leaks 打开可以工具主窗口,具体使用方法可以参考这篇文章

3、 内存泄漏的场景和分析:

3.1、代理的属性关键字设置为strong造成的内存泄漏

请看下面这段代码:

@protocol MFMemoryLeakViewDelegate <NSObject>

@end

@interface MFMemoryLeakView : UIView

@property (nonatomic, strong) id<MFMemoryLeakViewDelegate> delegate;

@end
    MFMemoryLeakView *view = [[MFMemoryLeakView alloc] initWithFrame:self.view.bounds];
    view.delegate = self;
    [self.view addSubview:view];

造成的后果就是控制器得不到释放,原因是控制器对视图进行了强引用,而控制器又是视图的代理,视图对代理进行了强引用,导致了控制器和视图的循环引用。
解决方法也很简单,strong改成weak就行:

@property (nonatomic, weak) id<MFMemoryLeakViewDelegate> delegate;

3.2、CoreGraphics框架里申请的内存忘记释放

请看下面这段代码:

- (UIImage *)coreGraphicsMemoryLeak{
    CGRect myImageRect = self.view.bounds;
    CGImageRef imageRef = [UIImage imageNamed:@"MemoryLeakTip.jpeg"].CGImage;
    CGImageRef subImageRef = CGImageCreateWithImageInRect(imageRef, myImageRect);
    UIGraphicsBeginImageContext(myImageRect.size);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextDrawImage(context, myImageRect, subImageRef);
    UIImage *newImage = [UIImage imageWithCGImage:subImageRef];
    CGImageRelease(subImageRef);
//    CGImageRelease(imageRef);
    UIGraphicsEndImageContext();
    return newImage;
}

如果"CGImageRelease(subImageRef)"这行代码缺失,就会引起内存泄漏,使用静态分析可以轻易发现。

需要注意的是:只有当CGImageRef使用create或retain后才要手动release,没有就不需要手动处理了,系统会进行自动的释放。上面的imageRef对象就是这样,如果进行了手动release,会引起不确定性的崩溃。

为什么是不确定性的崩溃呢,目前我支持的一种说法是:CFRelease的对象不能是NULL,若是NULL的话,会引起runtime的错误并且程序要崩溃,本来imageRef的管理者是会在某个时刻调用release的,但是因为这里已经release过了,已经成了NULL,所以当这个调用时期到来的时候就crash掉了。

关于这个问题,大家可以使用我的demo进行尝试,打开后图中注释的代码后运行,先进入内存泄漏的页面,然后返回上级,再进入这个页面,程序崩溃,demo地址见底部。

3.3、 CoreFoundation框架里申请的内存忘记释放

请看下面这段代码:

- (NSString *)coreFoundationMemoryLeak{
    CFUUIDRef uuid_ref = CFUUIDCreate(NULL);
    CFStringRef uuid_string_ref= CFUUIDCreateString(NULL, uuid_ref);
//    NSString *uuid = (__bridge NSString *)uuid_string_ref;
    NSString *uuid = (__bridge_transfer NSString *)uuid_string_ref;
    CFRelease(uuid_ref);
//    CFRelease(uuid_string_ref);
    return uuid;
}

如果"CFRelease(uuid_ref)"这行代码缺失,就会引起内存泄漏,使用静态分析可以轻易发现。

需要注意的是:“ __bridge”是将CoreFoundation框架的对象所有权交给Foundation框架来使用,但是Foundation框架中的对象并不能管理该对象的内存。“ __bridge_transfer”是将CoreFoundation框架的对象所有权交给Foundation来管理,如果Foundation中对象销毁,那么我们之前的对象(CoreFoundation)会一起销毁。

所以__bridge_transfer这种桥接方式,以后就不用再自己手动管理内存了。如果上面代码里的“CFRelease(uuid_string_ref)”的注释,uuid就会被销毁,程序运行到reurn 就崩溃。

3.4、NSTimer 不正确使用造成的内存泄漏

3.4.1、NSTimer重复设置为NO的时候,不会引起内存泄漏

3.4.2、NSTimer重复设置为YES的时候,有执行invalidate就不会内存泄漏,没有执行invalidate就会内存泄漏,在 timer的执行方法里调用invalidate也可以。

3.4.3、中间target:控制器无法释放,是因为timer对控制器进行了强引用,使用类方法创建的timer默认加入了runloop,所以,timer只要不持有控制器,控制器就能释放了。

[NSTimer scheduledTimerWithTimeInterval:1 target:[MFTarget target:self] selector:@selector(timerActionOtherTarget:) userInfo:nil repeats:YES];
#import "MFTarget.h"

@implementation MFTarget

- (instancetype)initWithTarget:(id)target {
    _target = target;
    return self;
}

+ (instancetype)target:(id)target {
    return [[MFTarget alloc] initWithTarget:target];
}

//这里将selector 转发给_target 去响应
- (id)forwardingTargetForSelector:(SEL)selector {
    if ([_target respondsToSelector:selector]) {
        return _target;
    }
    return nil;
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    void *null = NULL;
    [invocation setReturnValue:&null];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}

这样控制器的确是释放了,但是timer的方法还是会在不断的调用,如果对性能要求不那么严谨的,可以使用这种方法,具体代码见demo。

3.4.4、重写NSTimer:结合上面中间target的思路,在timer内部进行invalidate操作,请看一下代码。

@interface MFTimer : NSObject

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

@end
#import "MFTimer.h"

@interface MFTimer ()

@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer *timer;

@end

@implementation MFTimer

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {
    MFTimer *mfTimer = [[MFTimer alloc] init];
    mfTimer.timer = [NSTimer timerWithTimeInterval:ti target:mfTimer selector:@selector(timerAction:) userInfo:userInfo repeats:yesOrNo];
    mfTimer.target = aTarget;
    mfTimer.selector = aSelector;
    return mfTimer.timer;
}

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo {
    MFTimer *mfTimer = [[MFTimer alloc] init];
    mfTimer.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:mfTimer selector:@selector(timerAction:) userInfo:userInfo repeats:yesOrNo];
    mfTimer.target = aTarget;
    mfTimer.selector = aSelector;
    return mfTimer.timer;
}

- (void)timerAction:(NSTimer *)timer {
    if (self.target) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        //不判断是否响应,是为了不实现定时器的方法就报错
        [self.target performSelector:self.selector withObject:timer];
#pragma clang diagnostic pop
    }else {
        [self.timer invalidate];
        self.timer = nil;
    }
}

@end

3.4.5、使用block创建定时器,需要正确使用block,要执行invalidate,否则也会内存泄漏。这里涉及到block的内存泄漏问题,我会在下篇中一起讲解。

其他内存泄漏如通知和KVO、block循环引用 、NSThread造成的内存泄漏请见下篇。

demo地址请点击这里

欢迎大家来我的小窝做客啊,里面记录下了我进步的点点滴滴,一切逆境只是前进的理由,与君共勉。

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

推荐阅读更多精彩内容

  • 一、NSTimer简介 二、NSTimer与RunLoop 三、NSTimer内存泄露分析1.NSTimer引用分...
    浮游lb阅读 5,671评论 2 23
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,101评论 1 32
  • 再一次面试中被问到nstimer的争取使用方法,原理,我当时就说了[_timer invalidate],time...
    iOS开发小平哥阅读 4,059评论 1 13
  • # 前言 反复地复习iOS基础知识和原理,打磨知识体系是非常重要的,本篇就是重新温习iOS的内存管理。 内存管理是...
    Vein_阅读 796评论 0 2
  • 今日体验:晚上快下班的时候来了一辆奥迪A4L做保养。该车这次需要做6万公里的大保了,在拉完项目之后和客户报价经过客...
    其实_1d17阅读 133评论 0 0