儘管在進入後臺之後,程式的工作受到大幅度的限制,但是我們總是不會希望應用突然被作業系統殺死,中斷了重要的後臺工作。後臺應用被殺死,影響的不止是使用者體驗,比如正在播放的音樂戛然而止,正在導航的語音意外中斷。由於作業系統殺死後臺應用並不是任何一種異常、或者執行錯誤,應用失去響應意外的能力,極有可能破壞了單次執行中積累的重要資料。因此如何減少應用在後臺執行被殺死,是一個值得思考的問題
後臺應用如何被殺
拋開因為應用在後臺停留太久被殺死之外,因為記憶體問題被殺死是最重要的原因。記憶體和其他硬體資源有很大的不同,區別在於:
當資源需求量多於資源所能供應的時,
CPU
表現為效能下降,而作業系統會為了回收記憶體殺死後臺應用
我們可以把裝置記憶體分成四塊區域,包括system
、background
、foreground
和free
四部分。
system
在裝置開機後是固定佔用的,幾乎不可能被我們的應用訪問background
屬於被後臺執行的應用使用,記憶體不足時會被回收foreground
屬於當前執行應用使用的記憶體free
屬於可分配記憶體,系統會優先分配這部分記憶體給應用使用
假設裝置剛開機,使用者直接開啟你的應用,這時候裝置的記憶體不包括background
部分:
應用在這時退出前臺,由於此時還有足夠的free
記憶體可分配,系統暫時不會收回記憶體:
然後使用者開啟了一個新的應用,但是新的應用需要大量記憶體,此時超出了裝置的可用記憶體量:
於是系統向你的應用發出memory warning
通知,系統回收了暫時不用的記憶體,然後有足夠的記憶體給新應用使用:
又或者根本沒發出warning
,你的應用直接被殺死:
記憶體警告
記憶體警告memory warning
會在系統察覺到可能沒有更多的free
記憶體使用時發出,包括通知和代理兩種方式的調起。通常來說,開發者和系統都會針對warning
做應對措施,根據處理方的角色不同,我們可以將其分為主動策略
和被動策略
記憶體分類
在瞭解兩種應對策略的差異之前,我們需要了解對於每一個執行的應用來說,可被系統自動清除的記憶體分為兩類:(此處記憶體限制為主動分配,可隨時釋放的記憶體)
-
Dirty
左側正在展示的內容會被我們使用
array
或者dictionary
儲存起來。多數時候,為了更流暢的效果,我們可能會快取頁面外的資料,比如預載入單元格、預計算高度等。這些資料決定了當前應用的狀態、內容等,是活躍資料
。這部分記憶體不會被系統回收,會一直儲存直到應用終止 -
Purgeable
右側儲存了可能使用過的資料,比如使用者進入推薦功能後請求了大量的資料,使用
NSCache
快取在記憶體中,以便重新進入推薦頁時快速的展示內容,這部分資料是明顯的未使用資料
。這部分的記憶體會被系統直接清除後很快的重新分配使用
從記憶體的分類來看,減少Dirty
資料的數量,增加Purgeable
可以減少記憶體使用的壓力
被動策略
之所以要先說被動策略,是因為作業系統在發出warning
之前,已經做了一些工作來嘗試解決問題,只有瞭解這些才能更好的幫助我們解決問題
-
NSPurgeableData
前面說了被標記為
purgeable
的記憶體會被系統直接清理資料然後重新分配使用,這個清理的過程發生在發出警告之前。如果在清理purgeable
之後就有足夠的記憶體可用了,就不會發出警告。NSPurgeableData
是NSData
的子類,它提供了類似dispatch_group_t
的介面,使用一個整型變數儲存使用狀態,當值為0
時,表示資料處於未使用
狀態NSPurgeableData *purgeableData = [[NSPurgeableData alloc] initWithBytes: fileData.bytes length: fileData.length]; [purgeableData beginContentAccess]; /// 值+1,開始使用資料 ...... [purgeableData endContentAccess]; /// 值-1,結束使用資料 ...... /// 過了很長時間,如果無法訪問資料,說明被回收,需要重新建立 if (![purgeableData beginContentAccess]) { purgeableData = [[NSPurgeableData alloc] initWithBytes: fileData.bytes length: fileData.length]; } [purgeableData beginContentAccess]; 複製程式碼
-
NSCache
NSCache
提供了一套類似dictionary
的簡單介面讓我們存取資料,同時允許我們設定一個快取最大值。NSCache
的資料被認為是purgeable
型別,這些資料會在快取達到最大值時以LRU
演算法淘汰使用最少者,也會被系統在發出警告前回收 -
memory warning
清理
purgeable
資料後,如果記憶體還是不夠用,這時候系統就會向所有活躍應用發出記憶體警告,此時進入開發者的主動應對環節
主動策略
從操作上來說,purgeable
的資料都需要開發者維護,更像是一種主動性的機制。但這個機制在記憶體不夠用時,會被系統自動收回記憶體,而無需開發者進行更多額外的工作,因此被我歸類到被動策略
。主動策略更應該是接收到memory warning
後,開發者進行的工作:
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
/// release data cannot keep the application running
[self.database close];
}
- (void)didReceiveMemoryWarningNotification: (NSNotification *)notification {
/// release data cannot keep the application running
[self.dataSource removeAllObjects];
[self.images removeAllObjects];
[self cleanAllAnimatorViews];
}
複製程式碼
處理warning
幾乎是開發者的唯一有效手段,但是這種手段也並不完全可靠,如果依賴於系統發出的記憶體警告,存在幾個風險點:
memory warning
是針對所有執行應用發出的,這意味著同一時間,CPU
可能忙不過來處理你的工作- 如果恰好屬於執行應用佇列的後列,在
purgeable
資料被回收、且其他應用響應完警告後,依舊沒有足夠記憶體,有可能直接被殺死而沒有處理的機會
因此,即便處理警告幾乎是唯一手段,依舊還得思考其他方式。假如我們從將一部分資料對映到磁碟上,應用可以以最小的記憶體佔用執行
檔案對映
檔案對映不是萬能藥,每次修改資料都會同樣更新到磁碟相同位置,即便SSD
的存取能力已經很強大了,但依舊是不小的開銷。你的資料應該總是滿足這些要求,才考慮使用檔案對映:
- 應用執行的必要資料。比如音樂應用的播放音樂資訊、播放器類
- 構造或者還原的成本很高。如果成本不高,那麼重新構建並沒有什麼損失
- 資料便於計算。如果資料總是變化的,會對檔案大小的計算造成嚴重干擾
- 資料幾乎不發生改變。變化越少,儲存價值越高
這些條件決定了檔案對映這種方式並不是適用於所有資料,基本適用於配置檔案、影象資料、文字資料等,而這些資料也往往是佔用記憶體的大頭
另外如果資料量很大時,將資料整合成一個大資料段。同樣資料大小的情況下,系統處理多塊檔案對映的記憶體效率要低的多。另外太多的虛擬記憶體塊,也會導致系統強行殺死應用。NSData
允許我們使用options
提供檔案對映的方式:
typedef NS_OPTIONS(NSUInteger, NSDataReadingOptions) {
NSDataReadingMappedIfSafe = 1UL << 0,
NSDataReadingUncached = 1UL << 1,
NSDataReadingMappedAlways API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0)) = 1UL << 3,
};
複製程式碼
其他
由於蘋果的沙盒機制,我們過去總是更多的關注於應用內對於記憶體的使用情況,比較少關注應用在後臺執行時可能遇到的記憶體問題,引用WWDC
視訊上蘋果工程師的一句話:
你應該總是認為:在你應用之外的其他東西,會對你保持敵意並且有毀滅你的可能
因此,我們要學習如何合理的運用記憶體使用策略,來抵抗應用外的進攻。當然,最重要的是,我們可以理直氣壯的告訴QA
:哼,應用的記憶體使用降下來了!