iOS之深入解析Memory內存
一、前言
iOS 是基于 BSD 發展而來,理解一般的桌面操作系統的內存機制是非常有必要的。在此基礎之上,進一步在 iOS 系統層面進行分析,包括 iOS 整體的內存機制,以及 iOS 系統運行時的內存占用的情況。最后會將粒度縮小到 iOS 中的單個 App,到單個 App 的內存管理策略。
那么:
什么是馮·諾依曼結構?
什么是馮·諾依曼結構的瓶頸,以及如何突破瓶頸?
存儲器分哪兩類,分別有什么特點?
為什么使用緩存能提高效率?
什么是物理尋址?什么是虛擬尋址?
虛擬地址翻譯過程由誰負責?具體流程是怎樣的?
虛擬內存有哪些意義?
什么是內存交換機制?
內存分頁有什么意義?
iOS 的內存機制有什么特點?
clean memory、dirty memory、compressed memory 分別是什么?
引起循環引用的本質原因是什么?
weak 和 unowned 的區別是什么?
列舉一些不會導致循環引用的閉包場景。
什么是 OOM 崩潰?
檢測 OOM 崩潰有哪些常見方法?
OOM 崩潰有哪些常見原因?
現以 iOS Memory 的相關內容作為主題,主要從一般操作系統的內存管理、iOS 系統內存、App 內存管理等三個層面進行了分析,主要內容的目錄如下:
二、操作系統的內存機制
① 馮·諾依曼結構
馮·諾依曼結構(Von Neumann architecture)在 1945 年就已經被提出,它第一次將存儲器和運算器分離,導致了以存儲器為核心的現代計算機的誕生:
在馮·諾依曼結構中,存儲器有著重要地位,它存放著程序的指令以及數據,在程序運行時,根據需要提供給 CPU 使用。可以想象,一個理想的存儲器,應該是兼顧讀寫速度快、容量大、價格便宜等特點的,但是魚和熊掌不可兼得,讀寫速度越快的存儲器也更貴、容量更小。
但馮·諾依曼結構存在一個難以克服的問題,被稱為馮·諾依曼瓶頸。在目前的科技水平之下,CPU 與存儲器之間的讀寫速率遠遠小于 CPU 的工作效率。簡單來說,就是 CPU 太快了,存儲器讀寫速度不夠快,造成了 CPU 性能的浪費。
既然現在沒辦法獲得完美的存儲器,那么如何盡量突破馮·諾依曼結構的瓶頸呢?現行的解決方式就是采用多級存儲,來平衡存儲器的讀寫速率、容量、價格。
② 存儲器的層次結構
存儲器主要分為兩類:易失性存儲器速度更快,斷電之后數據會丟失;非易失性存儲器容量更大、價格更低,斷電也不會丟失數據,如下所示:
隨機訪問存儲器 RAM 也分為兩類,其中 SRAM 速度更快,所以用作高速緩存,DRAM 用作主存。只讀存儲器 ROM 實際上只有最開始的時候是只讀的,后來隨著發展也能夠進行讀寫了,只是沿用了之前的名字。
如下所示,是多層存儲器的具體情況,我們平時常說的內存,實際上就是指的 L4 主存。而 L1-L3 高速緩存和主存相比,速度更快,并且它們都已經集成在 CPU 芯片內部。其中 L0 寄存器本身就是 CPU 的組成部分之一,讀寫速度最快,操作耗費 0 個時鐘周期:
簡單來說,存儲器的分級實際上就是一種緩存思想。金字塔底部的部分容量大,更便宜,主要是為了發揮其存儲屬性;而金字塔尖的高速緩存部分讀寫速度快,負責將高頻使用的部分緩存起來,一定程度上優化整體的讀寫效率。
為什么采用緩存就能夠提高效率呢?邏輯上理解起來其實很簡單,具體來說就是因為存在局部性原理(Principle of locality),被使用過的存儲器內容在未來可能會被多次使用,以及它附近的內容也大概率被使用。當把這些內容放在高速緩存中,那么就可以在部分情況下節約訪問存儲器的時間。
③ CPU 尋址方式
那么,CPU 是如何訪問內存的呢?內存可以被看作一個數組,數組元素是一個字節大小的空間,而數組索引則是所謂的物理地址(Physical Address)。最簡單最直接的方式,就是 CPU 直接通過物理地址去訪問對應的內存,這樣也被叫做物理尋址。
物理尋址后來也擴展支持了分段機制,通過在 CPU 中增加段寄存器,將物理地址變成了 “段地址”:“段內偏移量” 的形式,增加了物理尋址的尋址范圍。
不過支持了分段機制的物理尋址,仍然有一些問題,最嚴重的問題之一就是地址空間缺乏保護。簡單來說,因為直接暴露的是物理地址,所以進程可以訪問到任何物理地址,用戶進程想干什么就干什么,這是非常危險的。
現代處理器使用的是虛擬尋址的方式,CPU 通過訪問虛擬地址(Virtual Address),經過翻譯獲得物理地址,才能訪問內存,這個翻譯過程由 CPU 中的內存管理單元(Memory Management Unit,縮寫為 MMU)完成。
具體流程如下所示:
首先會在 TLB(Translation Lookaside Buffer)中進行查詢,它表位于 CPU 內部,查詢速度最快;
如果沒有命中,那么接下來會在頁表(Page Table)中進行查詢,頁表位于物理內存中,所以查詢速度較慢;
最后如果發現目標頁并不在物理內存中,稱為缺頁,此時會去磁盤中找,當然,如果頁表中還找不到,那就是出錯了。
翻譯過程實際上和前文講到的存儲器分級類似,都體現了緩存思想:TLB 的速度最快,但是容量也最小,之后是頁表,最慢的是硬盤。
④ 虛擬內存
剛才提到,直接使用物理尋址,會有地址空間缺乏保護的嚴重問題,那么如何解決呢?實際上在使用了虛擬尋址之后,由于每次都會進行一個翻譯過程,所以可以在翻譯中增加一些額外的權限判定,對地址空間進行保護。所以,對于每個進程來說,操作系統可以為其提供一個獨立的、私有的、連續的地址空間,這就是所謂的虛擬內存。
虛擬內存最大的意義就是保護了進程的地址空間,使得進程之間不能夠越權進行互相地干擾。對于每個進程來說,操作系統通過虛擬內存進行"欺騙",進程只能夠操作被分配的虛擬內存的部分。與此同時,進程可見的虛擬內存是一個連續的地址空間,這樣也方便了程序員對內存進行管理。
對于進程來說,它的可見部分只有分配給它的虛擬內存,而虛擬內存實際上可能映射到物理內存以及硬盤的任何區域。由于硬盤讀寫速度并不如內存快,所以操作系統會優先使用物理內存空間,但是當物理內存空間不夠時,就會將部分內存數據交換到硬盤上去存儲,這就是所謂的 Swap 內存交換機制。有了內存交換機制以后,相比起物理尋址,虛擬內存實際上利用硬盤空間拓展了內存空間。
總結起來,虛擬內存有下面幾個意義:保護了每個進程的地址空間、簡化了內存管理、利用硬盤空間拓展了內存空間。
⑤ 內存分頁
基于前文的思路,虛擬內存和物理內存建立了映射的關系。為了方便映射和管理,虛擬內存和物理內存都被分割成相同大小的單位,物理內存的最小單位被稱為幀(Frame),而虛擬內存的最小單位被稱為頁(Page)(注意頁和幀大小相同,有著類似函數的映射關系,前文提到的借助 TLB、頁表進行的翻譯過程,實際上和函數的映射非常類似)。
內存分頁最大的意義在于,支持了物理內存的離散使用。由于存在映射過程,所以虛擬內存對應的物理內存可以任意存放,這樣就方便了操作系統對物理內存的管理,也能夠可以最大化利用物理內存。同時,也可以采用一些頁面調度(Paging)算法,利用翻譯過程中也存在的局部性原理,將大概率被使用的幀地址加入到 TLB 或者頁表之中,提高翻譯的效率。
三、iOS 的內存機制
① 使用虛擬內存
iOS 和大多數桌面操作系統一樣,使用了虛擬內存機制。
② 內存有限,但單應用可用內存大
對于移動設備來說,受限于客觀條件,物理內存容量本身就小,而 iPhone 的 RAM 本身也是偏小的,最新的 iPhone XS Max 也才有 4GB,橫向對比小米 9 可達 8GB,華為 P30 也是 8GB。根據 List of iPhones 可以查看歷代 iPhone 的內存大小。
但是與其他手機不同的是,iOS 系統給每個進程分配的虛擬內存空間非常大。據官方文檔的說法,iOS 為每個 32 位的進程都會提供高達 4GB 的可尋址空間,這已經算非常大的了。
③ 沒有內存交換機制
虛擬內存遠大于物理內存,那如果物理內存不夠用了該怎么辦呢?之前說到,其他桌面操作系統(比如 OS X)有內存交換機制,在需要時能將物理內存中的一部分內容交換到硬盤上去,利用硬盤空間拓展內存空間,這也是使用虛擬內存帶來的優勢之一。
然而 iOS 并不支持內存交換機制,大多數移動設備都不支持內存交換機制。移動設備上的大容量存儲器通常是閃存(Flash),它的讀寫速度遠遠小于電腦所使用的硬盤,這就導致了在移動設備就算使用內存交換機制,也并不能提升性能。其次,移動設備的容量本身就經常短缺、閃存的讀寫壽命也是有限的,所以這種情況下還拿閃存來做內存交換,就有點太過奢侈了。
需要注意的是,網上有少數文章說 iOS 沒有虛擬內存機制,實際上應該指的是 iOS 沒有內存交換機制,因為在 Windows 系統下,虛擬內存有時指的是硬盤提供給內存交換的大小。
④ 內存警告
那么當內存不夠用時,iOS 的處理是會發出內存警告,告知進程去清理自己的內存。
iOS 上一個進程就對應一個 App,代碼中的 didReceiveMemoryWarning() 方法就是在內存警告發生時被觸發,app 應該去清理一些不必要的內存,來釋放一定的空間。
⑤ OOM 崩潰
如果 App 在發生了內存警告,并進行了清理之后,物理內存還是不夠用了,那么就會發生 OOM 崩潰,也就是 Out of Memory Crash。
在 stack overflow 上,有人對單個 App 能夠使用的最大內存做了統計:iOS app max memory budget。以 iPhone XS Max 為例,總共的可用內存是 3735 MB(比硬件大小小一些,因為系統本身也會消耗一部分內存),而單個 App 可用內存達到 2039 MB,達到了 55%。當 App 使用的內存超過這個臨界值,就會發生 OOM 崩潰。可以看出,單個 App 的可用物理內存實際上還是很大的,要發生 OOM 崩潰,絕大多數情況下都是程序本身出了問題。
四、iOS 系統內存占用
了解了 iOS 內存機制的特點之后,能夠意識到合理控制 App 使用的內存是非常重要的一件事。那么具體來說,我們需要減少的是哪些部分呢?實際上這就是所謂的 iOS 內存占用(Memory Footprint)的部分。
上文說到內存分頁,實際上內存頁也有分類,一般來說分為 clean memory 和 dirty memory 兩種,iOS 中也有 compressed memory 的概念。
① Clean memory & dirty memory
對于一般的桌面操作系統,clean memory 可以認為是能夠進行 Page Out 的部分,Page Out 指的是將優先級低的內存數據交換到磁盤上的操作,但 iOS 并沒有內存交換機制,所以對 iOS 這樣的定義是不嚴謹的。那么對于 iOS 來說,clean memory 指的是能被重新創建的內存,它主要包含下面幾類:
App 的二進制可執行文件;
framework 中的 _DATA_CONST 段;
文件映射的內存;
未寫入數據的內存。
內存映射的文件指的是當 App 訪問一個文件時,系統會將文件映射加載到內存中,如果文件只讀,那么這部分內存就屬于 clean memory。另外需要注意的是,鏈接的 framework 中 _DATA_CONST 并不絕對屬于 clean memory,當 App 使用到 framework 時,就會變成 dirty memory。
未寫入數據的內存也屬于 clean memory,如下面這段代碼,只有寫入了的部分才屬于 dirty memory:
int *array = malloc(20000 * sizeof(int)); array[0] = 32 array[19999] = 64
1
2
3
所有不屬于 clean memory 的內存都是 dirty memory,這部分內存并不能被系統重新創建,所以 dirty memory 會始終占據物理內存,直到物理內存不夠用之后,系統便會開始清理:
② Compressed memory
當物理內存不夠用時,iOS 會將部分物理內存壓縮,在需要讀寫時再解壓,以達到節約內存的目的。而壓縮之后的內存,就是所謂的 compressed memory。蘋果最開始只是在 OS X 上使用這項技術,后來也在 iOS 系統上使用。
實際上,隨著虛擬內存技術的發展,很多桌面操作系統早已經應用了內存壓縮技術,比如 Windows 中的 memory combining 技術。這本質上來說和內存交換機制類似,都是是一種用 CPU 時間換內存空間的方式,只不過內存壓縮技術消耗的時間更少,但占用 CPU 更高。不過在文章最開始,我們就已經談到由于 CPU 算力過剩,在大多數場景下,物理內存的空間相比起 CPU 算力來說顯然更為重要,所以內存壓縮技術非常有用。
根據 OS X Mavericks Core Technology Overview 官方文檔來看,使用 compressed memory 能在內存緊張時,將目標內存壓縮至原有的一半以下,同時壓縮和解壓消耗的時間都非常小。對于 OS X,compressed memory 也能和內存交換技術共用,提高內存交換的效率,畢竟壓縮后再進行交換效率明顯更高,只是 iOS 沒有內存交換,也就不存在這方面的好處了。
本質上來講,compressed memory 也屬于 dirty memory。
③ 內存占用組成
對于 App 來說,主要關心的內存是 dirty memory,當然其中也包含 compressed memory。而對于 clean memory,作為開發者通常可以不必關心。
當內存占用的部分過大,就會發生前文所說的內存警告以及 OOM 崩潰等情況,所以我們應該盡可能的減少內存占用,并對內存警告以及 OOM 崩潰做好防范。減少內存占用也能側面提升啟動速度,要加載的內存少了,自然啟動速度會變快。
按照正常的思路,App 監聽到內存警告時應該主動清理釋放掉一些優先級低的內存,這本質上是沒錯的。不過由于 compressed memory 的特殊性,所以導致內存占用的實際大小考慮起來會有些復雜。
如下所示,當收到內存警告時,我們嘗試將 Dictionary 中的部分內容釋放掉,但由于之前的 Dictionary 由于未使用,所以正處于被壓縮狀態;而解壓、釋放部分內容之后,Dictionary 處于未壓縮狀態,可能并沒有減少物理內存,甚至可能反而讓物理內存更大:
所以,進行緩存更推薦使用 NSCache 而不是 NSDictionary,就是因為 NSCache 不僅線程安全,而且對存在 compressed memory 情況下的內存警告也做了優化,可以由系統自動釋放內存。
五、iOS App 內存管理
-理解了 iOS 系統層面上的內存機制,在系統層面上的內存管理大多數情況下都已經由操作系統自動完成了。iOS 中一個 App 就是一個進程,所以開發者平時經常討論的內存管理,比如 MRC、ARC 等等,實際上屬于進程內部的內存管理,或者說是語言層面上的內存管理。這部分內存管理語言本身、操作系統均會有一些管理策略,但是作為開發者來說,很多時候還是需要從語言層面直接進行操作的。
① iOS App 地址空間
每個進程都有獨立的虛擬內存地址空間,也就是所謂的進程地址空間。現在稍微簡化一下,一個 iOS App 對應的進程地址空間大概如下圖所示:
每個區域實際上都存儲相應的內容,其中代碼區、常量區、靜態區這三個區域都是自動加載,并且在進程結束之后被系統釋放,開發者并不需要進行關注。
棧區一般存放局部變量、臨時變量,由編譯器自動分配和釋放,每個線程運行時都對應一個棧。而堆區用于動態內存的申請,由程序員分配和釋放。一般來說,棧區由于被系統自動管理,速度更快,但是使用起來并不如堆區靈活。
對于 Swift 來說,值類型存于棧區,引用類型存于堆區。值類型典型的有 struct、enum 以及 tuple 都是值類型。而比如 Int、Double、Array,Dictionary 等其實都是用結構體實現的,也是值類型。而 class、closure 都是引用類型,也就是說 Swift 中我們如果遇到類和閉包,就要留個心眼,考慮一下他們的引用情況。
② 引用計數
堆區需要程序員進行管理,如何管理、記錄、回收就是一個很值得思考的問題。iOS 采用的是引用計數(Reference Counting)的方式,將資源被引用的次數保存起來,當被引用次數變為零時就將其空間釋放回收。
對于早期 iOS 來說,使用的是 MRC(Mannul Reference Counting)手動管理引用計數,通過插入 retain、release 等方法來管理對象的生命周期。但由于 MRC 維護起來實在是太麻煩了,2011 年的 WWDC 大會上提出了 ARC(Automatic Reference Counting)自動管理引用計數,通過編譯器的靜態分析,自動插入引入計數的管理邏輯,從而避免繁雜的手動管理。
引用計數只是垃圾回收中的一種,除此之外還有標記-清除算法(Mark Sweep GC)、可達性算法(Tracing GC)等。相比之下,引用計數由于只記錄了對象的被引用次數,實際上只是一個局部的信息,而缺乏全局信息,因此可能產生循環引用的問題,于是在代碼層面就需要格外注意。
那么,為什么 iOS 還要采用引用計數呢?首先使用引用計數,對象生命周期結束時,可以立刻被回收,而不需要等到全局遍歷之后再回首。其次,在內存不充裕的情況下,tracing GC 算法的延遲更大,效率反而更低,由于 iPhone 整體內存偏小,所以引用計數算是一種更為合理的選擇。
③ 循環引用
內存泄漏指的是沒能釋放不能使用的內存,會浪費大量內存,很可能導致應用崩潰。ARC 可能導致的循環引用就是其中一種,并且也是 iOS 上最常發生的。什么情況下會發生循環引用,大家可能都比較熟悉了,Swift 中比較典型的是在使用閉包的時候:
class viewController: UIViewController { var a = 10 var b = 20 var someClosure: (() -> Int)? func anotherFunction(closure: @escaping () -> Int) { DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { print(closure) } } override func viewDidLoad() { super.viewDidLoad() someClosure = { return self.a + self.b } anotherFunction(closure: someClosure!) } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
上面這段代碼中,viewController 會持有 someClosure,而 someClosure 也因為需要使用 self.a + self.b 而持有了 viewController,這就導致了循環引用。注意,閉包和類相似,都是引用類型,當把閉包賦值給類的屬性時,實際上是把閉包的引用賦值給了這個屬性。
解決方法也很簡單,利用 Swift 提供的閉包捕獲列表,將循環引用中的一個強引用關系改為弱引用就好了。實際上,Swift 要求在閉包中使用到了 self 的成員都必須不能省略 self. 的關鍵詞,就是為了提醒這種情況下可能發生循環引用問題。
someClosure = { [weak self] in guard let self = self else { return 0 } return self.a + self.b }
1
2
3
4
④ weak 和 unowned
weak 關鍵字能將循環引用中的一個強引用替換為弱引用,以此來破解循環引用。而還有另一個關鍵字 unowned,通過將強引用替換為無主引用,也能破解循環引用,不過二者有什么區別呢?弱引用對象可以為 nil,而無主引用對象不能,會發生運行時錯誤。
比如上面的例子使用了 weak,那么就需要額外使用 guard let 進行一步解包,而如果使用 unowned,就可以省略解包的一步:
someClosure = { [unowned self] in return self.a + self.b }
1
2
3
weak 在底層添加了附加層,間接地把 unowned 引用包裹到了一個可選容器里面,雖然這樣做會更加清晰,但是在性能方面帶來了一些影響,所以 unowned 會更快一些。
但是無主引用有可能導致 crash,就是無主引用的對象為 nil 時,比如上面這個例子中,anotherFunction 我們會延遲 5s 調用 someClosure,但是如果 5s 內我們已經 pop 了這個 viewController,那么 unowned self 在調用時就會發現 self 已經被釋放了,此時就會發生崩潰:
Fatal error: Attempted to read an unowned reference but the object was already deallocated
1
如果簡單類比,使用 weak 的引用對象就類似于一個可選類型,使用時需要考慮解包;而使用 unowned 的引用對象就類似于已經進行強制解包了,不需要再解包,但是如果對象是 nil,那么就會直接 crash:
到底什么情況下可以使用 unowned 呢?根據官方文檔 Automatic Reference Counting 所說,無主引用在其他實例有相同或者更長的生命周期時使用:
Unlike a weak reference, however, an unowned reference is used when the other instance has the same lifetime or a longer lifetime.
1
一種情況,如果兩個互相持有的對象,一個可能為 nil 而另一個不會為 nil,那么就可以使用 unowned。比如官方文檔中的這個例子,每張信用卡必然有它的主人,CreditCard 必然對應一個 Customer,所以這里使用了 unowned:
class Customer { let name: String var card: CreditCard? init(name: String) { self.name = name } deinit { print("\(name) is being deinitialized") } } class CreditCard { let number: UInt64 unowned let customer: Customer init(number: UInt64, customer: Customer) { self.number = number self.customer = customer } deinit { print("Card #\(number) is being deinitialized") } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
而另一種情況,對于閉包,在閉包和捕獲的實例總是相互引用并且同時銷毀時,可以將閉包的捕獲定義為 unowned。如果被捕獲的引用絕對不會變為 nil,應該使用 unowned,而不是 weak:
If the captured reference will never become nil, it should always be captured as an unowned reference, rather than a weak reference.
1
比如下面這個例子中的閉包,首先 asHTML 被聲明為 lazy,那么一定是 self 先被初始化;同時內部也沒有使用 asHTML 屬性,所以 self 一旦被銷毀,閉包也不存在了。這種情況下就應該使用 unowned:
class HTMLElement { let name: String let text: String? lazy var asHTML: () -> String = { [unowned self] in if let text = self.text { return "<\(self.name)>\(text)\(self.name)>" } else { return "<\(self.name) />" } } init(name: String, text: String? = nil) { self.name = name self.text = text } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
總的來說,最關鍵的點在于 weak 比 unowned 更加安全,能夠避免意外的 crash,這對于工程來說是非常有益的。所以大多數時候,就像通過 if let 以及 guard let 來避免使用 ! 強制解析一樣,我們也通常直接使用 weak。
⑤ 不會導致循環引用的情形
由于閉包經常產生循環引用的問題,而且加上 weak 以及 guard let 之后也不會出現錯誤,所以很多時候我們遇到閉包就直接無腦使用 weak,這實際上就太過粗糙。
比如,如果在 viewController 中使用了類似下面的閉包,就不會發生循環引用,因為 DispatchQueue 并不會被持有:
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.execute() }
1
2
3
更典型的比如使用 static functions 的時候:
class APIClass { // static 函數 static func getData(params: String, completion:@escaping (String) -> Void) { request(method: .get, parameters: params) { (response) in completion(response) } } } class viewController { var params = "something" var value = "" override func viewDidLoad() { super.viewDidLoad() getData(params: self.params) { (value) in self.value = value } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
此時并不會產生循環引用,因為 self 并不會持有 static class,因此也不會產生內存泄漏:
六、OOM 崩潰
① Jetsam 機制
iOS 是一個從 BSD 衍生而來的系統,其內核是 Mach。其中內存警告,以及 OOM 崩潰的處理機制就是 Jetsam 機制,也被稱為 Memorystatus。Jetsam 會始終監控內存整體使用情況,當內存不足時會根據優先級、內存占用大小殺掉一些進程,并記錄成 JetsamEvent。
根據 apple 開源的內核代碼 apple/darwin-xnu,我們可以看到,Jetsam 維護了一個優先級隊列,具體的優先級內容可以在 bsd/kern/kern_memorystatus.c 文件中找到:
static const char * memorystatus_priority_band_name(int32_t priority) { switch (priority) { case JETSAM_PRIORITY_FOREGROUND: return "FOREGROUND"; case JETSAM_PRIORITY_AUDIO_AND_ACCESSORY: return "AUDIO_AND_ACCESSORY"; case JETSAM_PRIORITY_CONDUCTOR: return "CONDUCTOR"; case JETSAM_PRIORITY_HOME: return "HOME"; case JETSAM_PRIORITY_EXECUTIVE: return "EXECUTIVE"; case JETSAM_PRIORITY_IMPORTANT: return "IMPORTANT"; case JETSAM_PRIORITY_CRITICAL: return "CRITICAL"; } return ("?"); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
而如何監控內存警告,以及處理 Jetsam 事件呢?首先,內核會調起一個內核優先級最高(95 /* MAXPRI_KERNEL */ 已經是內核能給線程分配的最高優先級了)的線程:
// 同樣在 bsd/kern/kern_memorystatus.c 文件中 result = kernel_thread_start_priority(memorystatus_thread, NULL, 95 /* MAXPRI_KERNEL */, &jetsam_threads[i].thread);
1
2
這個線程會維護兩個列表,一個是基于優先級的進程列表,另一個是每個進程消耗的內存頁的列表。與此同時,它會監聽內核 pageout 線程對整體內存使用情況的通知,在內存告急時向每個進程轉發內存警告,也就是觸發 didReceiveMemoryWarning 方法。
而殺掉應用,觸發 OOM,主要是通過 memorystatus_kill_on_VM_page_shortage,有同步和異步兩種方式。同步方式會立刻殺掉進程,先根據優先級,殺掉優先級低的進程;同一優先級再根據內存大小,殺掉內存占用大的進程。而異步方式只會標記當前進程,通過專門的內存管理線程去殺死。
② 如何檢測 OOM
OOM 分為兩大類,Foreground OOM / Background OOM,簡寫為 FOOM 以及 BOOM。而其中 FOOM 是指 app 在前臺時由于消耗內存過大,而被系統殺死,直接表現為 crash。
而 Facebook 開源的 FBAllocationTracker,原理是 hook 了 malloc/free 等方法,以此在運行時記錄所有實例的分配信息,從而發現一些實例的內存異常情況,有點類似于在 app 內運行、性能更好的 Allocation。但是這個庫只能監控 Objective-C 對象,所以局限性非常大,同時因為沒辦法拿到對象的堆棧信息,所以更難定位 OOM 的具體原因。
而騰訊開源的 OOMDetector,通過 malloc/free 的更底層接口 malloc_logger_t 記錄當前存活對象的內存分配信息,同時也根據系統的 backtrace_symbols 回溯了堆棧信息。之后再根據伸展樹(Splay Tree)等做數據存儲分析,具體方式參看這篇文章:iOS微信內存監控。
③ OOM 常見原因
內存泄漏:最常見的原因之一就是內存泄漏。
UIWebview 缺陷:無論是打開網頁,還是執行一段簡單的 js 代碼,UIWebView 都會占用大量內存,同時舊版本的 css 動畫也會導致大量問題,所以最好使用 WKWebView。
大圖片、大視圖:縮放、繪制分辨率高的大圖片,播放 gif 圖,以及渲染本身 size 過大的視圖(例如超長的 TextView)等,都會占用大量內存,輕則造成卡頓,重則可能在解析、渲染的過程中發生 OOM。
七、內存分析
關于內存占用情況、內存泄漏,我們都有一系列方法進行分析檢測:
Xcode memory gauge:在 Xcode 的 Debug navigator 中,可以粗略查看內存占用的情況;
Instrument - Allocations:可以查看虛擬內存占用、堆信息、對象信息、調用棧信息,VM Regions 信息等。可以利用這個工具分析內存,并針對地進行優化;
Instrument - Leaks:用于檢測內存泄漏;
MLeaksFinder:通過判斷 UIViewController 被銷毀后其子 view 是否也都被銷毀,可以在不入侵代碼的情況下檢測內存泄漏;
Instrument - VM Tracker:可以查看內存占用信息,查看各類型內存的占用情況,比如 dirty memory 的大小等等,可以輔助分析內存過大、內存泄漏等原因;
Instrument - Virtual Memory Trace:有內存分頁的具體信息;
Memory Resource Exceptions:從 Xcode 10 開始,內存占用過大時,調試器能捕獲到 EXC_RESOURCE RESOURCE_TYPE_MEMORY 異常,并斷點在觸發異常拋出的地方;
Xcode Memory Debugger:Xcode 中可以直接查看所有對象間的相互依賴關系,可以非常方便的查找循環引用的問題,同時,還可以將這些信息導出為 memgraph 文件;
memgraph + 命令行指令:結合上一步輸出的 memgraph 文件,可以通過一些指令來分析內存情況。vmmap 可以打印出進程信息,以及 VMRegions 的信息等,結合 grep 可以查看指定 VMRegion 的信息。leaks 可追蹤堆中的對象,從而查看內存泄漏、堆棧信息等。heap 會打印出堆中所有信息,方便追蹤內存占用較大的對象。malloc_history 可以查看 heap 指令得到的對象的堆棧信息,從而方便地發現問題。總結:malloc_history ===> Creation;leaks ===> Reference;heap & vmmap ===> Size。
iOS 虛擬化
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。