本文以 iOS Memory 的相關內容做爲主題,主要從通常操做系統的內存管理、iOS 系統內存、app 內存管理等三個層面進行了介紹,主要內容的目錄以下:css
iOS 是基於 BSD 發展而來,因此先理解通常的桌面操做系統的內存機制是很是有必要的。在此基礎之上,本文會進一步在 iOS 系統層面進行分析,包括 iOS 總體的內存機制,以及 iOS 系統運行時的內存佔用的狀況。最後會將粒度縮小到 iOS 中的單個 app,講到單個 app 的內存管理策略。html
爲了從根本上更好地理解和分析 iOS 系統上的內存特性,咱們首先須要正確理解通常操做系統通用的內存機制。git
馮·諾依曼結構(Von Neumann architecture)在 1945 年就已經被提出了, 這個概念當時十分新穎,它第一次將存儲器和運算器分離,致使了以存儲器爲核心的現代計算機的誕生。程序員
在馮·諾依曼結構中,存儲器有着重要地位,它存放着程序的指令以及數據,在程序運行時,根據須要提供給 CPU 使用。能夠想象,一個理想的存儲器,應該是兼顧讀寫速度快、容量大、價格便宜等特色的,可是魚和熊掌不可兼得,讀寫速度越快的存儲器也更貴、容量更小。github
但馮·諾依曼結構存在一個難以克服的問題,被稱爲馮·諾依曼瓶頸 —— 在目前的科技水平之下,CPU 與存儲器之間的讀寫速率遠遠小於 CPU 的工做效率。簡單來講就是 CPU 太快了,存儲器讀寫速度不夠快,形成了 CPU 性能的浪費。面試
既然如今咱們沒辦法得到完美的存儲器,那咱們如何儘可能突破馮·諾依曼結構的瓶頸呢?現行的解決方式就是採用多級存儲,來平衡存儲器的讀寫速率、容量、價格。算法
存儲器主要分爲兩類:易失性存儲器速度更快,斷電以後數據會丟失;非易失性存儲器容量更大、價格更低,斷電也不會丟失數據。隨機訪問存儲器 RAM 也分爲兩類,其中 SRAM 速度更快,因此用做高速緩存,DRAM 用做主存。只讀存儲器 ROM 實際上只有最開始的時候是隻讀的,後來隨着發展也可以進行讀寫了,只是沿用了以前的名字。swift
上圖就是多層存儲器的具體狀況,咱們平時常說的內存,實際上就是指的 L4 主存。而 L1-L3 高速緩存和主存相比,速度更快,而且它們都已經集成在 CPU 芯片內部了。其中 L0 寄存器自己就是 CPU 的組成部分之一,讀寫速度最快,操做耗費 0 個時鐘週期。api
簡單來講,存儲器的分級實際上就是一種緩存思想。金字塔底部的部分容量大,更便宜,主要是爲了發揮其存儲屬性;而金字塔尖的高速緩存部分讀寫速度快,負責將高頻使用的部分緩存起來,必定程度上優化總體的讀寫效率。數組
爲何採用緩存就可以提升效率呢?邏輯上理解起來其實很簡單,具體來講就是由於存在局部性原理(Principle of locality) —— 被使用過的存儲器內容在將來可能會被屢次使用,以及它附近的內容也大機率被使用。當咱們把這些內容放在高速緩存中,那麼就能夠在部分狀況下節約訪問存儲器的時間。
那麼,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 或者頁表之中,提升翻譯的效率。
根據官方文檔 Memory Usage Performance Guidelines(如今已經不更新了)咱們能知道 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 應該去清理一些沒必要要的內存,來釋放必定的空間。
若是 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 內存機制的特色以後,咱們可以意識到合理控制 app 使用的內存是很是重要的一件事。那麼具體來講,咱們須要減小的是哪些部分呢?實際上這就是所謂的 iOS 內存佔用(Memory Footprint)的部分。
上文講到內存分頁,實際上內存頁也有分類,通常來講分爲 clean memory 和 dirty memory 兩種,iOS 中也有 compressed 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複製代碼複製代碼
全部不屬於 clean memory 的內存都是 dirty memory。這部份內存並不能被系統從新建立,因此 dirty 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 系統層面上的內存機制,在系統層面上的內存管理大多數狀況下都已經由操做系統自動完成了。iOS 中一個 app 就是一個進程,因此開發者平時常常討論的內存管理,好比 MRC、ARC 等等,實際上屬於進程內部的內存管理,或者說是語言層面上的內存管理。這部份內存管理語言自己、操做系統均會有一些管理策略,可是做爲開發者來講,不少時候仍是須要從語言層面直接進行操做的。
前文咱們說過,每一個進程都有獨立的虛擬內存地址空間,也就是所謂的進程地址空間。如今咱們稍微簡化一下,一個 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!)
}
}複製代碼複製代碼
上面這段代碼中,viewController
會持有 someClosure
,而 someClosure
也由於須要使用 self.a + self.b
而持有了 viewController
,這就致使了循環引用。注意,閉包和類類似,都是引用類型,當把閉包賦值給類的屬性時,其實是把閉包的引用賦值給了這個屬性。
解決方法也很簡單,利用 Swift 提供的閉包捕獲列表,將循環引用中的一個強引用關係改成弱引用就行了。實際上,Swift 要求在閉包中使用到了 self
的成員都必須不能省略 self.
的關鍵詞,就是爲了提醒這種狀況下可能發生循環引用問題。
someClosure = { [weak self] inguard let self = self else { return 0 }return self.a + self.b
}複製代碼複製代碼
weak
關鍵字能將循環引用中的一個強引用替換爲弱引用,以此來破解循環引用。而還有另外一個關鍵字 unowned
,經過將強引用替換爲無主引用,也能破解循環引用,不過兩者有什麼區別呢?弱引用對象能夠爲 nil
,而無主引用對象不能,會發生運行時錯誤。
好比上面的例子咱們使用了 weak
,那麼就須要額外使用 guard let
進行一步解包。而若是使用 unowned
,就能夠省略解包的一步:
someClosure = { [unowned self] inreturn self.a + self.b
}複製代碼複製代碼
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
若是簡單類比,使用 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.
一種狀況,若是兩個互相持有的對象,一個可能爲 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") }
}複製代碼複製代碼
而另外一種狀況,對於閉包,在閉包和捕獲的實例老是相互引用而且同時銷燬時,能夠將閉包的捕獲定義爲 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.
好比下面這個例子中的閉包,首先 asHTML
被聲明爲 lazy
,那麼必定是 self
先被初始化;同時內部也沒有使用 asHTML
屬性,因此 self
一旦被銷燬,閉包也不存在了。這種狀況下就應該使用 unowned
:
class HTMLElement {let name: Stringlet text: String?
lazy var asHTML: () -> String = {
[unowned self] inif let text = self.text {return "\(text)"} else {return ""}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
}複製代碼複製代碼
總的來講,最關鍵的點在於 weak
比 unowned
更加安全,可以避免意外的 crash,這對於工程來講是很是有益的。因此大多數時候,就像咱們經過 if let
以及 guard let
來避免使用 !
強制解析同樣,咱們也一般直接使用 weak
。
因爲閉包常常產生循環引用的問題,並且加上 weak
以及 guard let
以後也不會出現錯誤,因此不少時候咱們遇到閉包就直接無腦使用 weak
,這實際上就太過粗糙了。
好比,若是在 viewController
中使用了相似下面的閉包,就不會發生循環引用,由於 DispatchQueue
並不會被持有:
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.execute()
}複製代碼複製代碼
更典型的好比使用 static functions 的時候:
class APIClass {
// static 函數
static func getData(params: String, completion:@escaping (String) -> Void) {
request(method: .get, parameters: params) { (response) incompletion(response)
}
}
}
class viewController {
var params = "something"
var value = ""override func viewDidLoad() {
super.viewDidLoad()
getData(params: self.params) { (value) inself.value = value
}
}
}複製代碼複製代碼
此時並不會產生循環引用,由於 self
並不會持有 static class,所以也不會產生內存泄漏:
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 ("?");
}複製代碼複製代碼
而如何監控內存警告,以及處理 Jetsam 事件呢?首先,內核會調起一個內核優先級最高(95 /* MAXPRI_KERNEL */
已是內核能給線程分配的最高優先級了)的線程:
// 一樣在 bsd/kern/kern_memorystatus.c 文件中
result = kernel_thread_start_priority(memorystatus_thread, NULL, 95 /* MAXPRI_KERNEL */, &jetsam_threads[i].thread);複製代碼複製代碼
這個線程會維護兩個列表,一個是基於優先級的進程列表,另外一個是每一個進程消耗的內存頁的列表。與此同時,它會監聽內核 pageout
線程對總體內存使用狀況的通知,在內存告急時向每一個進程轉發內存警告,也就是觸發 didReceiveMemoryWarning
方法。
而殺掉應用,觸發 OOM,主要是經過 memorystatus_kill_on_VM_page_shortage
,有同步和異步兩種方式。同步方式會馬上殺掉進程,先根據優先級,殺掉優先級低的進程;同一優先級再根據內存大小,殺掉內存佔用大的進程。而異步方式只會標記當前進程,經過專門的內存管理線程去殺死。
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微信內存監控。
內存泄漏
最多見的緣由之一就是內存泄漏。
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:有內存分頁的具體信息,具體能夠參考 WWDC 2016 - Syetem Trace in Depth。
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交流羣,能夠加小編的QQ3268829585,無論你是小白仍是大牛歡迎入駐 ,分享BAT,阿里面試題、面試經驗,討論技術, 你們一塊兒交流學習成長!但願幫助開發者少走彎路。
來源:本文爲第三方轉載,若有侵權請聯繫小編刪除。