你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 關於內存洩漏,還有哪些是你不知道的?

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

編輯:IOS開發基礎

前言

好久沒寫東西了,因為最近懶了些,且找不到什麼好的題材,所以准備對內存洩漏的問題做一篇整理。內存洩漏問題一直是項目開發中的一大問題,本文力求幫助從事過一段時間工作的iOS開發者快速尋找App中的內存洩漏問題。部分內容比較基礎,大神可無視勿噴。

一、從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,破掉這個循環。畫圖表示為:

1767950-e4fea03eee29eba3.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即可

@property (nonatomic, weak) id delegate;

下圖比較形象的說明了使用weak修飾就是為了防止ViewController和UITableView相互強引用內存無法釋放的問題:

1767950-2d4403294c5b1d4a.jpg

delegate循環引用

四、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];
    });

最後的執行結果為

1767950-62b8e5e44447f4d7.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工具(選取真機測試)

1767950-dd1d8aad0c2e0e2d.png

Instruments

2、簡單暴力的重寫dealloc方法,加入斷點或打印判斷某類是否正常釋放。

1767950-e4b71f21cd68c854.png

dealloc

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

暫時寫到這裡,未完待續

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved