iOS開發系列之內存泄漏分析(上)


iOS自從引入ARC機制后,一般的內存管理就可以不用我們碼農來負責了,但是一些操作如果不注意,還是會引起內存泄漏。

本文主要介紹一下內存泄漏的原理、常規的檢測方法以及出現的常用場景和修改方法。

1、 內存泄漏原理

內存泄漏的在百度上的解釋就是“程序中已動態分配的堆內存由於某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重后果”。

在我的理解里就是,公司給一個入職的員工分配了一個工位,但是這個員工離職后,這個工位卻不能分配給下一位入職的員工使用,造成了大量的資源浪費。

2、 常規的檢測方法

2.1、Analyze靜態分析 (command + shift + b)。

2.2、動態分析方法(Instrument工具庫里的Leaks),product->profile ->leaks 打開可以工具主窗口,具體使用方法可以參考這篇文章:https://www.jianshu.com/p/9fc2132d09c7。

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地址請點擊這里:https://github.com/zmfflying/ZMFBlogProject


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM