關于內存泄漏,還有哪些是你不知道的?

一、從AFNet說起

對于iOS開發者,網絡請求類AFNetWorking是再熟悉不過了,對于AFNetWorking的使用我們通常會對通用參數、網址環境切換、網絡狀態監測、請求錯誤信息等進行封裝。在封裝網絡請求類時需注意的是需要將請求隊列管理者AFHTTPSessionManager聲明為單例創建形式。對于該問題,AFNetWorking的作者在gitHub上也指出建議使用者在相同配置下保證AFHTTPSessionManager只有一個,進行全局管理,因此我們可以通過單例形式進行解決。下方展示部分核心代碼:

  • (AFHTTPSessionManager*)defaultNetManager {
    static AFHTTPSessionManager *manager;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    manager = [[AFHTTPSessionManager alloc]init];
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    });
    return manager;
    }
  • (void)GET:(NSString)url parameters:(NSDictionary)parameter returnData:(void (^)(NSData * resultData,NSError * error))returnBlock{
    //請求隊列管理者 單例創建形式 防止內存泄漏
    AFHTTPSessionManager * manager = [HttpRequest defaultNetManager];
    [manager GET:url parameters:parameter progress:^(NSProgress * _Nonnull downloadProgress) {
    } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
    returnBlock(responseObject,nil);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
    returnBlock(nil,error);
    }];
    }

二、Block循環引用
Block循環引用的問題已是老經常談了,至今已有多篇文章詳細解釋其原理及造成循環引用的原因等,不泛畫圖或實例列舉,這里不一一贅述。總結一句話防止Block循環引用就是要防止對象之間引用的閉環出現。舉個開發中的實際例子,就拿很多人在用的MJRefresh說起

self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
self.page = 1;
[self.dataArr removeAllObjects];
[self loadData];
}];
若在MJRefresh的執行Block中調用當前self或其所屬屬性,一定要注意循環引用問題。我們簡單分析下MJRefresh為什么會造成循環引用問題:

點擊進入headerWithRefreshingBlock對應方法即可

pragma mark - 構造方法

  • (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
    {
    MJRefreshHeader *cmp = [[self alloc] init];
    cmp.refreshingBlock = refreshingBlock;
    return cmp;
    }
    這里僅有三行代碼,無非就是創建了下拉刷新部分View然后返回,這里比較重要的是cmp.refreshingBlock = refreshingBlock;這一句,這里的refreshingBlock是屬于MJRefreshHeader的強引用屬性,最后header會成為我們自己tableView的強引用屬性mj_header,也就是說self.tableView強引用header, header強引用refreshingBlock,如果refreshingBlock里面強引用self,就成了循環引用,所以必須使用weakSelf,破掉這個循環。畫圖表示為:
1493199112592302.png

循環引用示意圖

閉環為:

self--->self.tableView--->self.tableView.mj_header---

self.tableView.mj_header.refreshingBlock--->self

解決方案大家應該也不陌生

__weak typeof(self) weakself = self;
self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
__strong typeof(self) strongself = weakself;
strongself.page = 1;
[strongself.dataArr removeAllObjects];
[strongself loadData];
}];
【??strongself是為了防止內存提前釋放,有興趣的童鞋可深入了解,這里不做過多解釋了。當然也可借助libextobjc庫進行解決,書寫為@weakify和@strongify會更方便些。】

相應的對于自定義View中的一些Block傳值問題同樣需要注意,與上述類似。
三、delegate循環引用問題

delegate循環引用問題比較基礎,只需注意將代理屬性修飾為weak即可

1
@property (nonatomic, weak) id delegate;
下圖比較形象的說明了使用weak修飾就是為了防止ViewController和UITableView相互強引用內存無法釋放的問題:

1493199269633542.jpg

四、NSTimer循環引用

對于定時器NSTimer,使用不正確也會造成內存泄漏問題。這里簡單舉個例子,我們聲明了一個類TestNSTimer,在其init方法中創建定時器執行操作。

import "TestNSTimer.h"

@interface TestNSTimer ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation TestNSTimer

  • (instancetype)init {
    if (self = [super init]) {
    _timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeRefresh:) userInfo:nil repeats:YES];
    }
    return self;
    }

  • (void)timeRefresh:(NSTimer*)timer {
    NSLog(@"TimeRefresh...");
    }

  • (void)cleanTimer {
    [_timer invalidate];
    _timer = nil;
    }

  • (void)dealloc {
    [super dealloc];
    NSLog(@"銷毀");
    [self cleanTimer];
    }

@end
在外部調用時,將其創建后5秒銷毀。

TestNSTimer *timer = [[TestNSTimer alloc]init];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [timer release];
});

最后的執行結果為

1493199379916619.png

NSTimer打印結果

可見TestNSTimer對象并沒有正常釋放,定時器仍然在無限的執行下去。

我們都知道定時器使用完畢時需要將其停止并滯空,但cleanTimer方法到底何時調用呢?在當前類的dealloc方法中嗎?并不是,若將cleanTimer方法調用在dealloc方法中會產生如下問題,當前類銷毀執行dealloc的前提是定時器需要停止并滯空,而定時器停止并滯空的時機在當前類調用dealloc方法時,這樣就造成了互相等待的場景,從而內存一直無法釋放。因此需要注意cleanTimer的調用時機從而避免內存無法釋放,如上的解決方案為將cleanTimer方法外漏,在外部調用即可。

TestNSTimer *timer = [[TestNSTimer alloc]init];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[timer cleanTimer];
[timer release];
});
打印結果

五、非OC對象內存處理

對于iOS開發,ARC模式已發揚光大多年,可能很多人早已忘記當年retain、release的年代,但ARC的出現并不是說我們完全可以忽視內存泄漏的問題。對于一些非OC對象,使用完畢后其內存仍需要我們手動釋放。

舉個例子,比如常用的濾鏡操作調節圖片亮度
CIImage *beginImage = [[CIImage alloc]initWithImage:[UIImage imageNamed:@"yourname.jpg"]];
CIFilter *filter = [CIFilter filterWithName:@"CIColorControls"];
[filter setValue:beginImage forKey:kCIInputImageKey];
[filter setValue:[NSNumber numberWithFloat:.5] forKey:@"inputBrightness"];//亮度-1~1
CIImage *outputImage = [filter outputImage];
//GPU優化
EAGLContext * eaglContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
eaglContext.multiThreaded = YES;
CIContext *context = [CIContext contextWithEAGLContext:eaglContext];
[EAGLContext setCurrentContext:eaglContext];

CGImageRef ref = [context createCGImage:outputImage fromRect:outputImage.extent];
UIImage *endImg = [UIImage imageWithCGImage:ref];
_imageView.image = endImg;
CGImageRelease(ref);//非OC對象需要手動內存釋放
在如上代碼中的CGImageRef類型變量非OC對象,其需要手動執行釋放操作CGImageRelease(ref),否則會造成大量的內存泄漏導致程序崩潰。其他的對于CoreFoundation框架下的某些對象或變量需要手動釋放、C語言代碼中的malloc等需要對應free等都需要注意。

五、地圖類處理

若項目中使用地圖相關類,一定要檢測內存情況,因為地圖是比較耗費App內存的,因此在根據文檔實現某地圖相關功能的同時,我們需要注意內存的正確釋放,大體需要注意的有需在使用完畢時將地圖、代理等滯空為nil,注意地圖中標注(大頭針)的復用,并且在使用完畢時清空標注數組等。

  • (void)clearMapView{
    self.mapView = nil;
    self.mapView.delegate =nil;
    self.mapView.showsUserLocation = NO;
    [self.mapView removeAnnotations:self.annotations];
    [self.mapView removeOverlays:self.overlays];
    [self.mapView setCompassImage:nil];
    }
    六、大次數循環內存暴漲問題

記得有道比較經典的面試題,查看如下代碼有何問題:

for (int i = 0; i < 100000; i++) {
NSString *string = @"Abc";
string = [string lowercaseString];
string = [string stringByAppendingString:@"xyz"];
NSLog(@"%@", string);
}
該循環內產生大量的臨時對象,直至循環結束才釋放,可能導致內存泄漏,解決方法為在循環中創建自己的autoReleasePool,及時釋放占用內存大的臨時變量,減少內存占用峰值。

for (int i = 0; i < 100000; i++) {
@autoreleasepool {
NSString *string = @"Abc";
string = [string lowercaseString];
string = [string stringByAppendingString:@"xyz"];
NSLog(@"%@", string);
}
}
若對autoReleasePool陌生,可查閱相關資料,畢竟不是一兩句即可說明的。
附、如何檢測App的內存泄漏問題

1、借助Xcode自帶的Instruments工具(選取真機測試)
2、簡單暴力的重寫dealloc方法,加入斷點或打印判斷某類是否正常釋放。

1767950-e4b71f21cd68c854.png

dealloc

3、通過Facebook出品的FBMemoryProfiler工具類進行檢測,感興趣的童鞋可進行了解。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,082評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,891評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,076評論 2 375

推薦閱讀更多精彩內容