上篇文章講述了iOS內(nèi)存管理的基本概念,這里是一些內(nèi)存優(yōu)化的小技巧
Strong Weak Dance
經(jīng)常使用閉包的話,應(yīng)該會(huì)特別注意這一條吧。
先將強(qiáng)引用的對(duì)象轉(zhuǎn)為弱引用指針,防止了Block和對(duì)象之間的循環(huán)引用。再在Block的中,將weakSelf的弱引用轉(zhuǎn)換成strongSelf這樣的強(qiáng)引用指針,防止了多線程和ARC環(huán)境下弱引用隨時(shí)被釋放的問題
__weak MyViewController *wself = self;
self.completionHandler = ^(NSInteger result) {
__strong __typeof(wself) sself = wself; // 強(qiáng)引用一次
[sself.property removeObserver: sself forKeyPath:@"pathName"];
};
降低內(nèi)存峰值
Lazy Allocation
延時(shí)加載是很常用的一種優(yōu)化方法,如果有些情況我們不會(huì)立即使用某一對(duì)象和某些資源,我們完全可以在使用的時(shí)候再進(jìn)行加載,這些就可以避免初次運(yùn)行程序的時(shí)候內(nèi)存消耗嚴(yán)重。
lazy var goodsImageView: UIImageView = {
let goodsImageView = UIImageView()
return goodsImageView
}()
在喵神的博客中,也發(fā)現(xiàn)了很好玩的地方,Swift標(biāo)準(zhǔn)庫中,定義了一些Lazy方法,可以配合像 map或是 filter 這類接受閉包并進(jìn)行運(yùn)行的方法一起,讓整個(gè)行為變成延時(shí)進(jìn)行。
let data = 1...3
let result = data.lazy.map {
(i: Int) -> Int in
print("正在處理 \(i)")
return i * 2
}
print("準(zhǔn)備訪問結(jié)果")
for i in result {
print("操作后結(jié)果為 \(i)")
}
print("操作完畢")
運(yùn)行結(jié)果為:
// 準(zhǔn)備訪問結(jié)果
// 正在處理 1
// 操作后結(jié)果為 2
// 正在處理 2
// 操作后結(jié)果為 4
// 正在處理 3
// 操作后結(jié)果為 6
// 操作完畢
圖片的讀取
相信寫代碼的時(shí)候都會(huì)涉及到圖片的讀取,我們經(jīng)常使用的方法
imageView?.image = UIImage(named: name)
imageView?.image = UIImage(contentsOfFile: path)
- 對(duì)于第一種,是帶緩存機(jī)制的,如果頻繁讀取小文件,用它就只需要讀取一次就好,但是缺點(diǎn)就是如果使用大圖片會(huì)常駐內(nèi)存,對(duì)于降低內(nèi)存峰值是不利的。
- 對(duì)于第二種方法,不帶緩存機(jī)制,適合使用大圖片,使用完就釋放
NSData & 內(nèi)存映射文件
在我們經(jīng)常使用NSData時(shí),出鏡率很高的兩個(gè)方法
public init?(contentsOfFile path: String)
public init(contentsOfFile path: String, options readOptionsMask: NSDataReadingOptions) throws
第二種比第一個(gè)多一個(gè)Options,第二種方式是創(chuàng)建了一個(gè)內(nèi)存映射文件,把內(nèi)容放在虛擬內(nèi)存中,只有讀取操作的時(shí)候才會(huì)讀到相對(duì)應(yīng)頁的物理內(nèi)存頁中。所以后者,對(duì)于大文件是很劃算的,推薦使用第二種。對(duì)于可選的方式如下:

其他方式
在莊延軍老師的分享中,還有其他一些技巧,但是對(duì)于只懂Swift的我,實(shí)在沒接觸過哪些方案,等我接觸之后,再來寫第三個(gè)分享。下面簡單列舉一下:
-
callocVSmalloc+memset - 棧內(nèi)存分配
NSAutoReleasePool
從MRC時(shí)代說起,當(dāng)需要我們手動(dòng)來管理對(duì)象引用計(jì)數(shù)的時(shí)候,我們需要Retain和Release方法,那么可能一個(gè)線程中有大量的Retain和Release方法,這個(gè)時(shí)候使用一個(gè)池一起管理他們是不是方便很多,將線程中要執(zhí)行的任務(wù)都放在自動(dòng)釋放池中,自動(dòng)釋放池會(huì)捕獲所有任務(wù)中的對(duì)象,在任務(wù)結(jié)束或線程關(guān)閉之時(shí)自動(dòng)釋放這些對(duì)象。
當(dāng)然自動(dòng)釋放池都會(huì)使用NSAutoreleasePool類創(chuàng)建,并在自動(dòng)釋放池收到 drain消息時(shí)將這些對(duì)象的引用計(jì)數(shù)減一,然后將它們從池子中移除 (這一過程形象地稱為“抽干池子”)。
在ARC時(shí)代,不再使用NSAutoreleasePool來創(chuàng)建自動(dòng)釋放池,而是使用@autoreleasepool代碼塊,新建Objective-C工程的時(shí)候,會(huì)幫我們創(chuàng)建一個(gè)main.m文件,已經(jīng)幫我們?cè)谥骶€程創(chuàng)建了一個(gè)自動(dòng)釋放池。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
更進(jìn)一步,其實(shí)@autoreleasepool 在編譯時(shí)會(huì)被展開為 NSAutoreleasePool,并且在每個(gè)主Runloop結(jié)束時(shí)進(jìn)行drain操作。
那么到了Swift時(shí)代,又具有了新的變化,不需要手動(dòng)地調(diào)用autorelease 這樣的方法來管理引用計(jì)數(shù),但是這些方法還是都會(huì)被調(diào)用的,只不過是編譯器在編譯時(shí)在合適的地方幫我們加入了而已。
在Swift標(biāo)準(zhǔn)庫中可以發(fā)現(xiàn):
public func autoreleasepool(@noescape code: () -> ())
那么我們就可以利用閉包的特點(diǎn),在需要的時(shí)候非常簡便的使用它
autoreleasepool {
// 線程執(zhí)行任務(wù)的邏輯代碼
}
AutoReleasePool
為什么在ARC時(shí)代還需要使用自動(dòng)釋放池呢?其原因就是為了避免內(nèi)存峰值,那比如說我有一個(gè)很大的For循環(huán),里面不斷生成autorelease
對(duì)象(比如:dataWithContentsOfFile返回的是 autorelease 的對(duì)象)。其實(shí)每迭代一次,資源都已經(jīng)用完了(就是說我用好了,還你),不需要再用了,這個(gè)時(shí)候就可以釋放了,但是程序需要等到Runloop結(jié)束的時(shí)候才可以釋放,這就增大了內(nèi)存的峰值。
上面的話有點(diǎn)繞,但是真心非常重要。
那么怎么做,可以減小內(nèi)存峰值呢?我們可以在For循環(huán)中添加autoreleasepool,這樣就可以保證每次迭代完畢一次就可以釋放點(diǎn)內(nèi)存。
func loadBigData() {
for i in 1...10000 {
autoreleasepool {
let data = NSData.dataWithContentsOfFile(
path, options: nil, error: nil)
}
}
}
簡單總結(jié)
上面的闡述了多種方法,其中對(duì)于降低內(nèi)存峰值有作用的是:
-
LazyAllocation - 圖片的讀取的正確方式
-
NSData& 內(nèi)存映射文件 -
callocVSmalloc+memset - 棧內(nèi)存分配
autoReleasePool
內(nèi)存警告處理
到收到內(nèi)存警告的時(shí)候,所要做的:
- 盡可能多的釋放資源,尤其是圖片等占用內(nèi)存多的資源,等需要的時(shí)候再進(jìn)行重建
- 單例模式的濫用,會(huì)導(dǎo)致單例對(duì)象一直持有資源,在內(nèi)存緊張的時(shí)候要進(jìn)行釋放,當(dāng)然我也在其他博客中看到一些單例模式的替換方案