iOS 內存優(yōu)化

簡述:

本應釋放的內存沒有釋放,導致可用空間減少的現(xiàn)象。
舉個例子:你dismiss了一個視圖控制器,但是最終卻沒有執(zhí)行這個視圖控制器的dealloc方法,就會導致內存泄露。
目前遇到的導致內存泄漏比較嚴重的有這幾個地方:

1. Timer

NSTimer經(jīng)常會被作為某個類的成員變量,而NSTimer初始化時要指定self為target,容易造成循環(huán)引用。 另一方面,若timer一直處于validate的狀態(tài),則其引用計數(shù)將始終大于0。

- (instancetype)init {
    self = [super init];
    if (self) {
        _timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"%@ called!", [self class]);
        }];
        [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
    }
    return self;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor yellowColor];
    
    [_timer fire];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    // 控制器視圖將要消失的時候清除 timer 不失為一個好時機。
    [self cleanTimer];
}

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

- (void)dealloc {
    // 應該在更合適的地方釋放掉timer,否則會造成循環(huán)引用,導致控制器無法釋放
//    [self cleanTimer];
    NSLog(@"%@ dealloc!!!", [self class]);
}

這個例子中控制器無法釋放,造成內存泄漏,原因如下:
從timer的角度,timer認為調用方(控制器)被析構時會進入 dealloc,在 dealloc 可以順便將 timer 的計時停掉并且釋放內存;
但是從控制器的角度,他認為 timer 不停止計時不析構,那我永遠沒機會進入 dealloc。循環(huán)引用,互相等待,子子孫孫無窮盡也。
問題的癥結在于-(void)cleanTimer函數(shù)的調用時機不對,顯然不能想當然地放在調用者的 dealloc 中。一個比較好的解決方法是開放這個函數(shù),在更合適的位置(比如在- (void)viewWillDisappear:(BOOL)animated;中)調用來清理現(xiàn)場。

2. Delegate

開發(fā)過程中使用retain修飾符或無修飾符(無修飾符默認strong),導致很多應該釋放的視圖控制器都沒釋放。這個修改很簡單:將修飾符改成weak即可。
注:為什么不用assign, 如果用assign聲明的變量在棧中可能不會自動賦值為nil,就會造成野指針錯誤!
weak聲明的變量在棧中就會自動清空,賦值為nil。

// 如果此處用 retain 修飾,則添加這個代理方法的控制器就會由于 delegate 沒有清空而無法釋放,造成內存泄露。
//@property (retain, nonatomic) DelegateViewDelegate delegate;
@property (weak, nonatomic) DelegateViewDelegate delegate;

3. Block

block容易出現(xiàn)內存泄露,根本原因是存在對象間的循環(huán)引用問題(對象a強引用對象b,對象b強引用對象a)。

舉例說明:
創(chuàng)建一個對象并為對象添加一個block屬性

@interface BlockObject : NSObject

@property (copy, nonatomic) dispatch_block_t block;

@end

為控制器添加三個屬性,其中包括新創(chuàng)建的對象屬性

@interface BlockViewController ()

// self 對 object 對象進行強引用
@property (strong, nonatomic) BlockObject *object;
@property (assign, nonatomic) NSInteger index;
@property (copy, nonatomic) dispatch_block_t block;

@end

造成內存泄露寫法一:

_object = [[BlockObject alloc] init];

[_object setBlock:^{
    // object 對象對 self (成員變量或屬性)進行強引用,就會造成循環(huán)引用
    self.index = 1; // _index = 1;
}];

解決方式:

_object = [[BlockObject alloc] init];

// 先將 self 轉成 weak,之后在 block 內部轉成 strong 使用,是常見的解決方案。
__weak typeof(self)weakSelf = self;
[_object setBlock:^{
    __strong typeof(self)strongSelf = weakSelf;
    strongSelf.index = 1;
}];

用全局變量的寫法也會造成內存泄露:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 此處會發(fā)生內存泄露,因為 self 添加了全局 block,self 對此 block 存在強引用。
    [self executeBlock2:^{
        self.index = 1;
    }];
}

- (void)executeBlock2:(dispatch_block_t)block {
    // 這個 _block 全局變量就是內存泄露的原因,如果 block 內部使用weakSelf就會打破這個循環(huán)了。
    _block = block;
    if (block) {
        block();
    }
}

4. Image

關于圖片加載占用內存問題:
imageNamed: 方法會在內存中緩存圖片,用于常用的圖片。
imageWithContentsOfFile: 方法在視圖銷毀的時候會釋放圖片占用的內存,適合不常用的大圖等。

#pragma mark - 圖片加載內存占用問題 -
// 初始化時內存占用為 42M
// 加載之后為 56M,控制器dealloc 之后內存并沒有明顯減少
cell.imageView.image = [UIImage imageNamed:imageName];
    
// 加載之后為 56M,控制器dealloc 之后內存明顯減少,回到之前水平 44M 左右
NSString *file = [[NSBundle mainBundle] pathForResource:imageName ofType:nil];
cell.imageView.image = [UIImage imageWithContentsOfFile:file];

所以需要時刻注意圖片操作是否合理,避免大量占用內存。
注意:

  1. imageWithContentsOfFile: 方法無法讀取.xcassets里的圖片。
  2. imageWithContentsOfFile: 方法讀取圖片需要加文件后綴名如png,jpg等。

5. Table View

Table view需要有很好的滾動性能,不然用戶會在滾動過程中發(fā)現(xiàn)動畫的瑕疵。
為了保證table view平滑滾動,確保你采取了以下的措施:

1.正確使用reuseIdentifier來重用cells。
2.將所有不需要透明的視圖 opaque(不透明)設置為YES,包括cell自身。
3.緩存行高。
4.如果cell內現(xiàn)實的內容來自web,使用異步加載,緩存請求結果。
5.使用shadowPath來畫陰影。
6.減少subviews的數(shù)量。
7.盡量不適用cellForRowAtIndexPath:,如果你需要用到它,只用一次然后緩存結果。
8.使用正確的數(shù)據(jù)結構來存儲數(shù)據(jù)。
9.使用rowHeight, sectionFooterHeightsectionHeaderHeight來設定固定的高,不要請求delegate。

6. 不要阻塞主線程

永遠不要使主線程承擔過多。因為UIKit在主線程上做所有工作,渲染,管理觸摸反應,回應輸入等都需要在它上面完成。
一直使用主線程的風險就是如果你的代碼真的block了主線程,你的app會失去反應。
大部分阻礙主進程的情形是你的app在做一些牽涉到讀寫外部資源的I/O操作,比如存儲或者網(wǎng)絡。

7. 選擇正確的Collection

學會選擇對業(yè)務場景最合適的類或者對象是寫出能效高的代碼的基礎。當處理collections時這句話尤其正確。
一些常見collection的總結:

  • Arrays: 有序的一組值。使用index來lookup很快,使用value lookup很慢,插入/刪除很慢。
  • Dictionaries: 存儲鍵值對。用鍵來查找比較快。
  • Sets: 無序的一組值。用值來查找很快,插入/刪除很快。

8. 打開gzip壓縮

大量app依賴于遠端資源和第三方API,你可能會開發(fā)一個需要從遠端下載XML, JSON, HTML或者其它格式的app。
問題是我們的目標是移動設備,因此你就不能指望網(wǎng)絡狀況有多好。一個用戶現(xiàn)在還在edge網(wǎng)絡,下一分鐘可能就切換到了3G。不論什么場景,你肯定不想讓你的用戶等太長時間。
減小文檔的一個方式就是在服務端和你的app中打開gzip。這對于文字這種能有更高壓縮率的數(shù)據(jù)來說會有更顯著的效用。
好消息是,iOS已經(jīng)在NSURLConnection中默認支持了gzip壓縮,當然AFNetworking這些基于它的框架亦然。像Google App Engine這些云服務提供者也已經(jīng)支持了壓縮輸出。

9. 重用和延遲加載(lazy load) Views

更多的view意味著更多的渲染,也就是更多的CPU和內存消耗,對于那種嵌套了很多view在UIScrollView里邊的app更是如此。
這里我們用到的技巧就是模仿UITableViewUICollectionView的操作:不要一次創(chuàng)建所有的subview,而是當需要時才創(chuàng)建,當它們完成了使命,把他們放進一個可重用的隊列中。
這樣的話你就只需要在滾動發(fā)生時創(chuàng)建你的views,避免了不劃算的內存分配。
創(chuàng)建views的能效問題也適用于你app的其它方面。想象一下一個用戶點擊一個按鈕的時候需要呈現(xiàn)一個view的場景。有兩種實現(xiàn)方法:

  1. 創(chuàng)建并隱藏這個view當這個screen加載的時候,當需要時顯示它;
  2. 當需要時才創(chuàng)建并展示。
    每個方案都有其優(yōu)缺點。用第一種方案的話因為你需要一開始就創(chuàng)建一個view并保持它直到不再使用,這就會更加消耗內存。然而這也會使你的app操作更敏感因為當用戶點擊按鈕的時候它只需要改變一下這個view的可見性。
    第二種方案則相反-消耗更少內存,但是會在點擊按鈕的時候比第一種稍顯卡頓。

10. 處理內存警告

一旦系統(tǒng)內存過低,iOS會通知所有運行中app。在官方文檔中是這樣記述:
如果你的app收到了內存警告,它就需要盡可能釋放更多的內存。最佳方式是移除對緩存,圖片object和其他一些可以重創(chuàng)建的objects的strong references.

幸運的是,UIKit提供了幾種收集低內存警告的方法:

  • 在app delegate中使用applicationDidReceiveMemoryWarning:的方法
  • 在你的自定義UIViewController的子類(subclass)中覆蓋didReceiveMemoryWarning
  • 注冊并接收 UIApplicationDidReceiveMemoryWarningNotification的通知
    一旦收到這類通知,你就需要釋放任何不必要的內存使用。

例如,UIViewController的默認行為是移除一些不可見的view,它的一些子類則可以補充這個方法,刪掉一些額外的數(shù)據(jù)結構。一個有圖片緩存的app可以移除不在屏幕上顯示的圖片。
這樣對內存警報的處理是很必要的,若不重視,你的app就可能被系統(tǒng)殺掉。
然而,當你一定要確認你所選擇的object是可以被重現(xiàn)創(chuàng)建的來釋放內存。一定要在開發(fā)中用模擬器中的內存提醒模擬去測試一下。

11. 重用大開銷對象

一些objects的初始化很慢,比如NSDateFormatter和NSCalendar。然而,你又不可避免地需要使用它們,比如從JSON或者XML中解析數(shù)據(jù)。
想要避免使用這個對象的瓶頸你就需要重用他們,可以通過添加屬性到你的class里或者創(chuàng)建靜態(tài)變量來實現(xiàn)。
注意如果你要選擇第二種方法,對象會在你的app運行時一直存在于內存中,和單例(singleton)很相似。

Demo地址:iOS 內存優(yōu)化

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 一. 視圖控制對象通過alloc和init來創(chuàng)建,但是視圖控制對象不會在創(chuàng)建的那一刻就馬上創(chuàng)建相應的視圖,而是等到...
    iOS菜鳥攻城獅閱讀 671評論 0 7
  • 1. 用ARC管理內存 ARC(Automatic ReferenceCounting, 自動引用計數(shù)),它避免了...
    anyurchao閱讀 2,890評論 0 16
  • 1、運行MemoryProblems后,運行崩潰出現(xiàn)EXC_BAD_ACCESS,啟動NSZombieEnable...
    雒琰湦閱讀 1,233評論 0 1
  • 引起內存泄漏的原因 引起內存泄漏的原因主要有三類,如下 循環(huán)引用 強引用 非OC對象 1、循環(huán)引用。最簡單的循環(huán)引...
    荒漠現(xiàn)甘泉閱讀 197評論 0 2
  • 1. 避免內存泄漏 ① 避免對象之間循環(huán)引用(代理一定要弱引用)② block 中對象的循環(huán)引用、添加的通知在銷毀...
    Install_be閱讀 216評論 0 0

友情鏈接更多精彩內容