簡(jiǎn)述:
本應(yīng)釋放的內(nèi)存沒(méi)有釋放,導(dǎo)致可用空間減少的現(xiàn)象。
舉個(gè)例子:你dismiss了一個(gè)視圖控制器庄拇,但是最終卻沒(méi)有執(zhí)行這個(gè)視圖控制器的dealloc方法,就會(huì)導(dǎo)致內(nèi)存泄露韭邓。
目前遇到的導(dǎo)致內(nèi)存泄漏比較嚴(yán)重的有這幾個(gè)地方:
1. Timer
NSTimer經(jīng)常會(huì)被作為某個(gè)類的成員變量措近,而NSTimer初始化時(shí)要指定self為target,容易造成循環(huán)引用女淑。 另一方面瞭郑,若timer一直處于validate的狀態(tài),則其引用計(jì)數(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];
// 控制器視圖將要消失的時(shí)候清除 timer 不失為一個(gè)好時(shí)機(jī)屈张。
[self cleanTimer];
}
- (void)cleanTimer {
[_timer invalidate];
_timer = nil;
}
- (void)dealloc {
// 應(yīng)該在更合適的地方釋放掉timer,否則會(huì)造成循環(huán)引用袱巨,導(dǎo)致控制器無(wú)法釋放
// [self cleanTimer];
NSLog(@"%@ dealloc!!!", [self class]);
}
這個(gè)例子中控制器無(wú)法釋放阁谆,造成內(nèi)存泄漏,原因如下:
從timer的角度愉老,timer認(rèn)為調(diào)用方(控制器)被析構(gòu)時(shí)會(huì)進(jìn)入 dealloc场绿,在 dealloc 可以順便將 timer 的計(jì)時(shí)停掉并且釋放內(nèi)存;
但是從控制器的角度嫉入,他認(rèn)為 timer 不停止計(jì)時(shí)不析構(gòu)焰盗,那我永遠(yuǎn)沒(méi)機(jī)會(huì)進(jìn)入 dealloc璧尸。循環(huán)引用,互相等待熬拒,子子孫孫無(wú)窮盡也爷光。
問(wèn)題的癥結(jié)在于-(void)cleanTimer
函數(shù)的調(diào)用時(shí)機(jī)不對(duì),顯然不能想當(dāng)然地放在調(diào)用者的 dealloc 中澎粟。一個(gè)比較好的解決方法是開(kāi)放這個(gè)函數(shù)蛀序,在更合適的位置(比如在- (void)viewWillDisappear:(BOOL)animated;
中)調(diào)用來(lái)清理現(xiàn)場(chǎng)。
2. Delegate
開(kāi)發(fā)過(guò)程中使用retain修飾符或無(wú)修飾符(無(wú)修飾符默認(rèn)strong)活烙,導(dǎo)致很多應(yīng)該釋放的視圖控制器都沒(méi)釋放哼拔。這個(gè)修改很簡(jiǎn)單:將修飾符改成weak即可。
注:為什么不用assign瓣颅, 如果用assign聲明的變量在棧中可能不會(huì)自動(dòng)賦值為nil倦逐,就會(huì)造成野指針錯(cuò)誤!
weak聲明的變量在棧中就會(huì)自動(dòng)清空宫补,賦值為nil檬姥。
// 如果此處用 retain 修飾,則添加這個(gè)代理方法的控制器就會(huì)由于 delegate 沒(méi)有清空而無(wú)法釋放粉怕,造成內(nèi)存泄露健民。
//@property (retain, nonatomic) DelegateViewDelegate delegate;
@property (weak, nonatomic) DelegateViewDelegate delegate;
3. Block
block容易出現(xiàn)內(nèi)存泄露,根本原因是存在對(duì)象間的循環(huán)引用問(wèn)題(對(duì)象a強(qiáng)引用對(duì)象b贫贝,對(duì)象b強(qiáng)引用對(duì)象a)秉犹。
舉例說(shuō)明:
創(chuàng)建一個(gè)對(duì)象并為對(duì)象添加一個(gè)block屬性
@interface BlockObject : NSObject
@property (copy, nonatomic) dispatch_block_t block;
@end
為控制器添加三個(gè)屬性,其中包括新創(chuàng)建的對(duì)象屬性
@interface BlockViewController ()
// self 對(duì) object 對(duì)象進(jìn)行強(qiáng)引用
@property (strong, nonatomic) BlockObject *object;
@property (assign, nonatomic) NSInteger index;
@property (copy, nonatomic) dispatch_block_t block;
@end
造成內(nèi)存泄露寫(xiě)法一:
_object = [[BlockObject alloc] init];
[_object setBlock:^{
// object 對(duì)象對(duì) self (成員變量或?qū)傩裕┻M(jìn)行強(qiáng)引用稚晚,就會(huì)造成循環(huán)引用
self.index = 1; // _index = 1;
}];
解決方式:
_object = [[BlockObject alloc] init];
// 先將 self 轉(zhuǎn)成 weak崇堵,之后在 block 內(nèi)部轉(zhuǎn)成 strong 使用,是常見(jiàn)的解決方案客燕。
__weak typeof(self)weakSelf = self;
[_object setBlock:^{
__strong typeof(self)strongSelf = weakSelf;
strongSelf.index = 1;
}];
用全局變量的寫(xiě)法也會(huì)造成內(nèi)存泄露:
- (void)viewDidLoad {
[super viewDidLoad];
// 此處會(huì)發(fā)生內(nèi)存泄露鸳劳,因?yàn)?self 添加了全局 block,self 對(duì)此 block 存在強(qiáng)引用也搓。
[self executeBlock2:^{
self.index = 1;
}];
}
- (void)executeBlock2:(dispatch_block_t)block {
// 這個(gè) _block 全局變量就是內(nèi)存泄露的原因赏廓,如果 block 內(nèi)部使用weakSelf就會(huì)打破這個(gè)循環(huán)了。
_block = block;
if (block) {
block();
}
}
4. Image
關(guān)于圖片加載占用內(nèi)存問(wèn)題:
imageNamed:
方法會(huì)在內(nèi)存中緩存圖片傍妒,用于常用的圖片幔摸。
imageWithContentsOfFile:
方法在視圖銷毀的時(shí)候會(huì)釋放圖片占用的內(nèi)存,適合不常用的大圖等颤练。
#pragma mark - 圖片加載內(nèi)存占用問(wèn)題 -
// 初始化時(shí)內(nèi)存占用為 42M
// 加載之后為 56M既忆,控制器dealloc 之后內(nèi)存并沒(méi)有明顯減少
cell.imageView.image = [UIImage imageNamed:imageName];
// 加載之后為 56M,控制器dealloc 之后內(nèi)存明顯減少,回到之前水平 44M 左右
NSString *file = [[NSBundle mainBundle] pathForResource:imageName ofType:nil];
cell.imageView.image = [UIImage imageWithContentsOfFile:file];
所以需要時(shí)刻注意圖片操作是否合理尿贫,避免大量占用內(nèi)存。
注意:
imageWithContentsOfFile:
方法無(wú)法讀取.xcassets里的圖片踏揣。imageWithContentsOfFile:
方法讀取圖片需要加文件后綴名如png庆亡,jpg等。
5. Table View
Table view需要有很好的滾動(dòng)性能捞稿,不然用戶會(huì)在滾動(dòng)過(guò)程中發(fā)現(xiàn)動(dòng)畫(huà)的瑕疵又谋。
為了保證table view平滑滾動(dòng),確保你采取了以下的措施:
1.正確使用
reuseIdentifier
來(lái)重用cells娱局。
2.將所有不需要透明的視圖 opaque(不透明)設(shè)置為YES彰亥,包括cell自身。
3.緩存行高衰齐。
4.如果cell內(nèi)現(xiàn)實(shí)的內(nèi)容來(lái)自web任斋,使用異步加載,緩存請(qǐng)求結(jié)果耻涛。
5.使用shadowPath
來(lái)畫(huà)陰影废酷。
6.減少subviews的數(shù)量。
7.盡量不適用cellForRowAtIndexPath:
抹缕,如果你需要用到它澈蟆,只用一次然后緩存結(jié)果。
8.使用正確的數(shù)據(jù)結(jié)構(gòu)來(lái)存儲(chǔ)數(shù)據(jù)卓研。
9.使用rowHeight
,sectionFooterHeight
和sectionHeaderHeight
來(lái)設(shè)定固定的高趴俘,不要請(qǐng)求delegate。
6. 不要阻塞主線程
永遠(yuǎn)不要使主線程承擔(dān)過(guò)多奏赘。因?yàn)閁IKit在主線程上做所有工作寥闪,渲染,管理觸摸反應(yīng)磨淌,回應(yīng)輸入等都需要在它上面完成橙垢。
一直使用主線程的風(fēng)險(xiǎn)就是如果你的代碼真的block了主線程,你的app會(huì)失去反應(yīng)伦糯。
大部分阻礙主進(jìn)程的情形是你的app在做一些牽涉到讀寫(xiě)外部資源的I/O操作柜某,比如存儲(chǔ)或者網(wǎng)絡(luò)。
7. 選擇正確的Collection
學(xué)會(huì)選擇對(duì)業(yè)務(wù)場(chǎng)景最合適的類或者對(duì)象是寫(xiě)出能效高的代碼的基礎(chǔ)敛纲。當(dāng)處理collections時(shí)這句話尤其正確喂击。
一些常見(jiàn)collection的總結(jié):
- Arrays: 有序的一組值。使用index來(lái)lookup很快淤翔,使用value lookup很慢翰绊,插入/刪除很慢。
- Dictionaries: 存儲(chǔ)鍵值對(duì)。用鍵來(lái)查找比較快监嗜。
- Sets: 無(wú)序的一組值谐檀。用值來(lái)查找很快,插入/刪除很快裁奇。
8. 打開(kāi)gzip壓縮
大量app依賴于遠(yuǎn)端資源和第三方API桐猬,你可能會(huì)開(kāi)發(fā)一個(gè)需要從遠(yuǎn)端下載XML, JSON, HTML或者其它格式的app。
問(wèn)題是我們的目標(biāo)是移動(dòng)設(shè)備刽肠,因此你就不能指望網(wǎng)絡(luò)狀況有多好溃肪。一個(gè)用戶現(xiàn)在還在edge網(wǎng)絡(luò),下一分鐘可能就切換到了3G音五。不論什么場(chǎng)景惫撰,你肯定不想讓你的用戶等太長(zhǎng)時(shí)間。
減小文檔的一個(gè)方式就是在服務(wù)端和你的app中打開(kāi)gzip躺涝。這對(duì)于文字這種能有更高壓縮率的數(shù)據(jù)來(lái)說(shuō)會(huì)有更顯著的效用厨钻。
好消息是,iOS已經(jīng)在NSURLConnection中默認(rèn)支持了gzip壓縮坚嗜,當(dāng)然AFNetworking這些基于它的框架亦然莉撇。像Google App Engine這些云服務(wù)提供者也已經(jīng)支持了壓縮輸出。
9. 重用和延遲加載(lazy load) Views
更多的view意味著更多的渲染惶傻,也就是更多的CPU和內(nèi)存消耗棍郎,對(duì)于那種嵌套了很多view在UIScrollView里邊的app更是如此。
這里我們用到的技巧就是模仿UITableView
和UICollectionView
的操作:不要一次創(chuàng)建所有的subview银室,而是當(dāng)需要時(shí)才創(chuàng)建涂佃,當(dāng)它們完成了使命,把他們放進(jìn)一個(gè)可重用的隊(duì)列中蜈敢。
這樣的話你就只需要在滾動(dòng)發(fā)生時(shí)創(chuàng)建你的views辜荠,避免了不劃算的內(nèi)存分配。
創(chuàng)建views的能效問(wèn)題也適用于你app的其它方面抓狭。想象一下一個(gè)用戶點(diǎn)擊一個(gè)按鈕的時(shí)候需要呈現(xiàn)一個(gè)view的場(chǎng)景伯病。有兩種實(shí)現(xiàn)方法:
- 創(chuàng)建并隱藏這個(gè)view當(dāng)這個(gè)screen加載的時(shí)候,當(dāng)需要時(shí)顯示它否过;
- 當(dāng)需要時(shí)才創(chuàng)建并展示午笛。
每個(gè)方案都有其優(yōu)缺點(diǎn)。用第一種方案的話因?yàn)槟阈枰婚_(kāi)始就創(chuàng)建一個(gè)view并保持它直到不再使用苗桂,這就會(huì)更加消耗內(nèi)存药磺。然而這也會(huì)使你的app操作更敏感因?yàn)楫?dāng)用戶點(diǎn)擊按鈕的時(shí)候它只需要改變一下這個(gè)view的可見(jiàn)性。
第二種方案則相反-消耗更少內(nèi)存煤伟,但是會(huì)在點(diǎn)擊按鈕的時(shí)候比第一種稍顯卡頓癌佩。
10. 處理內(nèi)存警告
一旦系統(tǒng)內(nèi)存過(guò)低木缝,iOS會(huì)通知所有運(yùn)行中app。在官方文檔中是這樣記述:
如果你的app收到了內(nèi)存警告围辙,它就需要盡可能釋放更多的內(nèi)存我碟。最佳方式是移除對(duì)緩存,圖片object和其他一些可以重創(chuàng)建的objects的strong references.
幸運(yùn)的是姚建,UIKit提供了幾種收集低內(nèi)存警告的方法:
- 在app delegate中使用
applicationDidReceiveMemoryWarning:
的方法 - 在你的自定義UIViewController的子類(subclass)中覆蓋
didReceiveMemoryWarning
- 注冊(cè)并接收 UIApplicationDidReceiveMemoryWarningNotification的通知
一旦收到這類通知矫俺,你就需要釋放任何不必要的內(nèi)存使用。
例如桥胞,UIViewController的默認(rèn)行為是移除一些不可見(jiàn)的view恳守,它的一些子類則可以補(bǔ)充這個(gè)方法考婴,刪掉一些額外的數(shù)據(jù)結(jié)構(gòu)贩虾。一個(gè)有圖片緩存的app可以移除不在屏幕上顯示的圖片。
這樣對(duì)內(nèi)存警報(bào)的處理是很必要的沥阱,若不重視缎罢,你的app就可能被系統(tǒng)殺掉。
然而考杉,當(dāng)你一定要確認(rèn)你所選擇的object是可以被重現(xiàn)創(chuàng)建的來(lái)釋放內(nèi)存策精。一定要在開(kāi)發(fā)中用模擬器中的內(nèi)存提醒模擬去測(cè)試一下。
11. 重用大開(kāi)銷對(duì)象
一些objects的初始化很慢崇棠,比如NSDateFormatter和NSCalendar咽袜。然而,你又不可避免地需要使用它們枕稀,比如從JSON或者XML中解析數(shù)據(jù)询刹。
想要避免使用這個(gè)對(duì)象的瓶頸你就需要重用他們,可以通過(guò)添加屬性到你的class里或者創(chuàng)建靜態(tài)變量來(lái)實(shí)現(xiàn)萎坷。
注意如果你要選擇第二種方法凹联,對(duì)象會(huì)在你的app運(yùn)行時(shí)一直存在于內(nèi)存中,和單例(singleton)很相似哆档。
Demo地址:iOS 內(nèi)存優(yōu)化