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

693 阅读5分钟

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地址请点击这里