簡述:
本應釋放的內存沒有釋放,導致可用空間減少的現(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];
所以需要時刻注意圖片操作是否合理,避免大量占用內存。
注意:
imageWithContentsOfFile:方法無法讀取.xcassets里的圖片。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,sectionFooterHeight和sectionHeaderHeight來設定固定的高,不要請求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更是如此。
這里我們用到的技巧就是模仿UITableView和UICollectionView的操作:不要一次創(chuàng)建所有的subview,而是當需要時才創(chuàng)建,當它們完成了使命,把他們放進一個可重用的隊列中。
這樣的話你就只需要在滾動發(fā)生時創(chuàng)建你的views,避免了不劃算的內存分配。
創(chuàng)建views的能效問題也適用于你app的其它方面。想象一下一個用戶點擊一個按鈕的時候需要呈現(xiàn)一個view的場景。有兩種實現(xiàn)方法:
- 創(chuàng)建并隱藏這個view當這個screen加載的時候,當需要時顯示它;
- 當需要時才創(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)化