你必定要知道的iOS 內存管理|乾貨滿滿

iPhone 做爲一個移動設備,其計算和內存資源一般是很是有限的,而許多用戶對應用的性能卻很敏感,卡頓、應用回到前臺丟失狀態、甚至 OOM 閃退,這就給了 iOS 工程師一個很大的挑戰。html

網上的絕大多數關於 iOS 內存管理的文章,大可能是圍繞 ARC/MRC、循環引用的原理或者是如何找尋內存泄漏來展開的,而這些內容更準確的說應該是 ObjC 或者 Swift 的內存管理,是語言層面帶來的特性,而不是操做系統自己的內存管理。git

若是咱們須要聊聊」管理「內存,那麼就須要先了解一些基礎知識。github

內存基礎概念複習

物理內存

一個設備的 RAM 大小。如下是維基百科上的資料:web

簡單來講,iPhone 8(不包括 plus) 和 iPhone 7(不包括 plus)及以前都是 2G 內存,iPhone 6 和 6 plus 及以前都是 1G 內存。面試

虛擬內存(VM for Virtual Memory)

每一個進程都有一個本身私有的虛擬內存空間。對於32位設備來講是 4GB,而64位設備(5s之後的設備)是 18EB(1EB = 1000PB, 1PB = 1000TB),映射到物理內存空間。緩存

內存管理、映射中的基本單位是頁,一頁的大小是 4kb(早期設備)或者 16kb(A7 芯片及之後)網絡

由於有頁的存在,每次申請內存都必須以頁爲單位。然而這樣一來,若是隻是申請幾個 byte,卻不得不分配一頁(16kb),是很是大的浪費。所以在用戶態咱們有 「heap」 的概念。session

Page In/Out

因爲虛擬內存的空間遠遠大於物理內存,在任意一個時間點,虛擬內存中的一個頁並不必定老是在物理內存中,而是可能被暫時存到了磁盤上,這樣物理內存即可以暫時釋放這部分空間,供優先級更高的任務使用,所以磁盤能夠做爲 backing store 以擴展物理內存(MacOS 中有,iOS 沒有)。另外一種多是加載一個比較大的文件/動態庫,每次使用咱們可能只須要加載其中的一部分,那麼就可使用 mmap 映射這個文件到虛擬內存空間,這樣當咱們訪問其中一部分時,系統會自動把這一部分從磁盤加載到內存,而不加載其他部分。數據結構

這樣把磁盤中的數據寫到內存/從內存中寫回磁盤成爲 page in/out。架構

Wired memory

沒法被 page out 的內存,主要爲系統層所用,開發者不須要考慮這些。

VM Region

一個 VM Region 是指一段連續的內存頁(在虛擬地址空間裏),這些頁擁有相同的屬性(如讀寫權限、是不是 wired,也就是是否能被 page out)。舉幾個例子:

  • mapped file,即映射到磁盤的一個文件
  • __TEXT,r-x,多數爲二進制
  • __DATA,rw-,爲可讀寫數據
  • MALLOC_(SIZE),顧名思義是 malloc 申請的內存

VM Object

每一個 VM Region 對應一個數據結構,名爲 VM Object。Object 會記錄這個 Region 內存的屬性

Resident Page

當前正在物理內存中的頁(沒有被交換出去)

與其餘App共存的狀況

  • app 內存消耗較低,同時其餘 app 也很是「自律」,不會大手大腳地消耗內存,那麼即便切換到其餘應用,咱們本身的 app 依然是「活着」的,保留了用戶的使用狀態,體驗較好
  • app 內存消耗較低,可是其餘 app 很是消耗內存(多是使用不當,也多是自己就很是消耗內存,好比大型遊戲),那除了當前在前臺的進程,其餘 app 都會被系統回收,用來給活躍進程提供內存資源。這種狀況咱們沒法控制
  • app 內存消耗比較大,那切換到其餘 app 之後,即便其餘 app 向系統申請不是特別大的內存,系統也會由於資源緊張,優先把消耗內存較多的 app 回收掉。用戶會發現只要 app 一旦退到後臺,過會再打開時就會從新加載
  • app 內存消耗特別大,在前臺運行時就有可能被系統 kill 掉,引發閃退

在 iOS 上管理殺進程釋放資源策略模塊叫作 Jetsam,這裏推薦五子棋的文章,其中有詳細的介紹。

OOM的斷定

蘋果官方關於 OOM 的文檔和接口很是少,以致於 facebook 在判斷應用是否上次由於 OOM 而閃退時,須要通過一個漫長的邏輯判斷,當不知足全部條件時才能斷定爲 OOM(想象一下若是系統能提供一個接口,告訴開發者上次的退出緣由,會方便多少!)

咱們會好奇,當一個普通 app 啓動時,內存消耗究竟有多少?

如何查看內存佔用量

咱們剛討論到內存的不一樣類別,那麼應該選用哪一個值做爲內存佔用量的標準呢?

Memory Footprint

在 WWDC13 704 中,蘋果推薦用 footprint 命令來查看一個進程的內存佔用。

關於什麼是 footprint,在官方文檔 Minimizing your app’s Memory Footprint 裏有說明:

Refers to the total current amount of system memory that is allocated to your app.

因爲該命令只能在 MacOS 上運行,而且 iOS 上也沒有 Activity Monitor,咱們新建一個 Mac app,而後用不一樣手段測量內存佔用

  • Instruments 中的 All Heap & Anonymous VM: 8.32MB
  • Xcode: 47.4MB
  • 系統 Activity Monitor: 47.4MB
  • 使用 footprint 命令(能夠經過 man footprint 查看文檔): 47MB
  • task_vm_info.phys_footprint: 47.4MB
  • task_info.resident_size: 80MB

能夠看到,Xcode、系統、footprint 工具和 phys_footprint 獲得的數據是一致的,而既然官方推薦了 footprint,所以咱們以這幾個方法獲得的結果做爲標準。猜想 footprint 比 Instruments 數據更大的緣由是存在一些」非代碼執行開銷「,如把系統和應用二進制加載進內存。iOS 中雖然不能使用系統 Activity Monitor 和 footprint 命令,也能在 Xcode 中和 phys_footprint 獲得一樣的結果。

至此咱們能夠獲得一個結論: Instruments 中顯示的部分,其實也只是整個應用進程裏內存的一部分。可是因爲咱們可以控制的只有這一部分,所以應該把精力投入到 Instruments 的分析中去。

使用 Instruments 分析

應用的詳細性能分析老是須要依賴 Instruments 的強大功能。從 Allocations 角度來看,總的內存佔用 = All Heap Allocations + All Anonymous VM:

  • All Heap Allocations,幾乎全部類實例,包括 UIViewController、UIView、UIImage、Foundation 和咱們代碼裏的各類類/結構實例。通常和咱們的代碼直接相關。
  • All Anonymous VM,能夠看到都是由」VM:」開頭的

主要包含一些系統模塊的內存佔用。有些部分雖然看起來離咱們的業務邏輯比較遠,但實際上是保證咱們代碼正常運行不可或缺的部分,也是咱們經常忽視的部分。通常包括:

  • CG raster data(光柵化數據,也就是像素數據。注意不必定是圖片,一塊顯示緩存裏也多是文字或者其餘內容。一般每像素消耗 4 個字節)
  • Image IO(圖片編解碼緩存)
  • Stack(每一個線程都會須要500KB左右的棧空間)
  • CoreAnimation
  • SQLite
  • network
  • 等等

咱們平時最常常會作的 debug 之一,就是查找循環引用。而循環引用形成的 leak 數據一般是 UIKit 或咱們本身的一些數據結構,會被歸類到 heap。這些是咱們相對熟悉的,網上也有很是多的文章,這裏再也不討論。而就 VM 這塊來講,由於不受咱們直接控制,文檔也較少,因此相對神祕一些,每每容易被忽視。

對於 VM 中的線程棧開銷、網絡 buffer 等,咱們其實沒有太大的控制能力,一般這些也不會是內存開銷的主要緣由(除非有成百上千的線程和頻繁大量的網絡請求)。而對於即刻和絕大多數 app 來講,尤爲是採用了 AsyncDisplayKit(用空間換時間)的狀況下,渲染開銷是絕對不可忽視的一塊。

我一直認爲,移動設備上無論是 CPU、GPU 仍是內存,最大的性能殺手必定是佈局和渲染。佈局數據和通常數據結構相似,單個內存開銷最多以 KB 計,而渲染緩存很容易就用「兆」來計算,更容易影響到總體開銷。

任意打開一個 app,能夠看到渲染無非就是兩大部分:圖片和文字。

圖片渲染開銷

咱們知道,解壓後的圖片是由無數像素數據組成。每一個像素點一般包括紅、綠、藍和 alpha 數據,每一個值都是 8 位(0–255),所以一個像素一般會佔用 4 個字節(32 bit per pixel。少數專業的 app 可能會用更大的空間來表示色深,消耗的內存會相應線性增長)。

下面咱們來計算一些一般的圖片開銷:

  • 普通圖片大小,如 500 600 32bpp = 1MB
  • 跟 iPhone X 屏幕同樣大的:1125 2436 32bpp = 10MB
  • 即刻中容許最大的圖片,總像素不超過1500w:15000000 * 32bpp = 57MB

有了大體的概念,之後看到一張圖能簡單預估,大概會吃掉多少內存。

縮放

  • 內存開銷多少與圖片文件的大小(解壓前的大小)沒有直接關係,而是跟圖片分辨率有關。舉個例子:一樣是 100 * 100,jpeg 和 png 兩張圖,文件大小可能差幾倍,可是渲染後的內存開銷是徹底同樣的——解壓後的圖片 buffer 大小與圖片尺寸成正比,有多少像素就有多少數據。
  • 一般咱們下載的圖片和最終展現在界面上的尺寸是不一樣的,有時可能須要將一張巨型圖片展現在一個很小的 view 裏。若是不作縮放,那麼原圖就會被整個解壓,消耗大量內存,而不少像素點會在展現前被壓縮掉,徹底浪費了。因此把圖片縮放到實際顯示大小很是重要,並且解碼量變少的話,速度也會提升不少。
  • 若是在網上搜索圖片縮放方案的話,通常都會找到相似「新建一個 context ,把圖畫在裏面,再從 context 裏讀取圖片」的答案。此時若是原圖很大,那麼即便縮放後的圖片很小,解壓原圖的過程仍然須要佔用大量內存資源,一不當心就會 OOM。可是若是換用 ImageIO 狀況就會好不少,整個過程最多隻會佔用縮放後的圖片所需的內存(一般只有原圖的幾分之一),大大減小了內存壓力。

解碼

圖片解碼是每一個開發者都繞不過去的話題。圖片從壓縮的格式化數據變成像素數據須要通過解碼,而解碼對 CPU 和內存的開銷都比較大,同時解碼後的數據如何管理,如何顯示都是須要咱們注意的。

  • 一般咱們把一張圖片設置到 UIImageView 上,系統會自動處理解碼過程,但這樣會在主線程上佔用必定 CPU 資源,引發卡頓。使用 ImageIO 解碼 + 後臺線程執行是 WWDC(18 session 219) 推薦的作法。
  • ImageIO 功能很強大,可是不支持 webp
  • AsyncDisplayKit 的一大思想是拿空間換時間,換取流暢的性能,可是內存開銷會比 UIKit 高。一樣用一個全屏的 UIImageView 測試,直接用UIImage(named:)來設置圖片,雖然不可避免要在主線程上作解壓,可是消耗的內存反而較小,只有4MB(正常須要10MB)。猜想神祕的 IOSurface 對圖片數據作了某些優化。蘋果有這麼一段話描述 IOSurface:
Share hardware-accelerated buffer data (framebuffers and textures) across multiple processes. Manage image memory more efficiently.

渲染

網上關於渲染的資料不少,可是不少都是人云亦云,咱們來講一些比較少討論的點:

  • 咱們常常會須要預先渲染文字/圖片以提升性能,此時須要儘量保證這塊 context 的大小與屏幕上的實際尺寸一致,避免浪費內存。能夠經過 View Hierarchy 調試工具,打印一個 layer 的 contents 屬性來查看其中的 CGImage(backing image)以及其大小

做爲 backing image 的 CGImage

  • 一旦涉及到 offscreen rendering,就可能會須要多消耗一塊內存/顯存。那到底什麼是離屏渲染?無論是 CPU 仍是 GPU,只要不能直接在 frame buffer 上畫,都屬於offscreen rendering。在 Core Animation: Advanced Techniques 書裏有 offscreen rendering 的一段說明:

    Offscreen rendering is invoked whenever the combination of layer properties that have been specified mean that the layer cannot be drawn directly to the screen without pre- compositing. Offscreen rendering does not necessarily imply software drawing, but it means that the layer must first be rendered (either by the CPU or GPU) into an offscreen context before being displayed.

  • layer mask 會形成離屏渲染,猜測多是因爲涉及到」根據 mask 去掉一些像素「,沒法直接在 frame buffer 中作
  • 圓角要慎用,但不是說徹底不能用— — 只有圓角和 clipsToBounds 結合的時候,纔會形成離屏渲染。猜測這二者結合起來也會形成相似 mask 的效果,用來切除圓角之外的部分
  • backgroundColor 能夠直接在 frame buffer 上畫,所以並不須要額外內存

文字渲染的CPU和內存開銷

關於文字渲染的文檔資料並非不少,所以咱們須要作一些實驗來判斷。 新建一個項目,添加一個全屏的 label,不停切換文字,獲得 cpu 佔用率穩定在 15%,gpu佔用率 0%。而且 Time Profiling 顯示

排名第一的方法主要是在調用 render_glyphs,說明主要是 CPU 參與了文字渲染。

  • 文字渲染中,主要內存開銷調用棧:

  • 雖然文字比較多,可是隻佔用了 2.75MB(2883584 byte,能夠看到這邊蘋果仍然是用1024KB = 1MB的換算)的內存。那麼問題來了,咱們上面提到一塊跟屏幕同樣大的顯示區域佔用空間大約是 10MB,爲何文字佔用這麼少呢?

理論上 iPhone X 全屏有 1125 * 2436 = 2740500 個像素,距離實際佔用內存很是接近,只多了 143084 byte(139.73kb),說明差很少正好是一個像素對應一個字節。這印證了 WWDC(WWDC18 219和416)上的結論,即黑白的位圖只佔用 1 個字節,比 4 字節節省 75% 的空間。固然實際使用過程當中很難限制文字只採用黑白兩種顏色,可是仍是應該瞭解蘋果的優化過程。

在以上測試基礎上,若是咱們嘗試把第一個字符加上紅色屬性,或者添加 emoji,那麼渲染結果就再也不是黑白的了,而是一張彩色圖片,相似普通圖片那樣每一個像素須要 4 個字節來描述。所以理論上所消耗的內存會變成 2.75MB * 4 = 10MB 多一點。測試獲得:

結果佔用了 11468800 bytes,是原來 2740500 的 3.97 倍,與理論值 4 很是接近(可能內存中還存在一些附屬的其餘元數據,而這些不會如同像素數據同樣線性放大,所以不徹底是精確 4 倍關係)。比較好的印證了以前的結論。

整整一屏的文字,在 3x 設備上,只佔用了 2MB 多一點的內存,能夠說是很是省了。

總結

iOS 的內存管理有如下幾個特色:

  • 文檔較少,系統提供的接口也較少,所以你們本身生產的輪子較多,須要多作實驗才能獲得可靠的結論。多利用 Instruments 也會發現一些以前忽略的點
  • 內存問題的暴露有必定延時性,OOM 在本地很難復現,須要投入大量時間測試,同時配套相應的監控系統
  • 技術變化較慢,操做系統這一層的知識在過去和將來的很長一段時間都不太會改變,或只是微調,值得花時間來研究
  • 經典的時空取捨問題,在資源有限的設備上,如何平衡 CPU/GPU 和內存的開銷,來達到性能最大化
  • 可以幫助咱們瞭解一些文字和圖片渲染的本質,更好的了渲染系統的工做原理,畢竟這是客戶端工程師不可替代的職責之一

推薦?:

  • 020 持續更新,精品小圈子每日都有新內容,乾貨濃度極高。
  • 結實人脈、討論技術 你想要的這裏都有!
  • 搶先入羣,跑贏同齡人!(入羣無需任何費用)
  • (直接搜索羣號:789143298,快速入羣)
  • 點擊此處,與iOS開發大牛一塊兒交流學習

申請即送:

  • BAT大廠面試題、獨家面試工具包,
  • 資料免費領取,包括 數據結構、底層進階、圖形視覺、音視頻、架構設計、逆向安防、RxSwift、flutter,

做者:即刻技術團隊
連接:https://juejin.im/post/5bec0e...

相關文章
相關標籤/搜索