iOS內存二三事

簡單總結下iOS內存方面的一些知識html

1、內存基礎概念

一、物理內存 & 虛擬內存

  • 物理內存(Physical Memory): 指經過物理內存條而得到的內存空間,和虛擬內存對應;主要做用是:設備運行時爲操做系統和各類程序提供臨時儲存空間;iPhone 6 和 6 Plus 及以前都是1G 內存,目前比較新的iPhone XS和XS Max是4GB內存;
  • 虛擬內存(Virtual Memory):是計算機系統內存管理的一種技術,爲每個進程提供了一個一致的、私有的地址空間;其主要做用是:保護了每一個進程的地址空間不會被其餘進程破壞,下降內存管理的複雜性;32位設備虛擬內存大小是4GB,64位設備(5s之後的設備)是 4GB * 4GB;
  • 虛擬內存是進程運行時全部內存空間的總和,而且可能有一部分不在物理內存中;

二、段頁式存儲

  • 目前,大部分通用的計算機的內存管理使用段頁式存儲結構;用戶程序先分段,每一個段內再分頁;而頁是存儲的最基本單位,iOS設備的arm64架構後,頁大小是16KB;
  • 利用邏輯地址(段號 + 段內頁號 + 頁內地址) 進行地址變化,得到物理地址;這樣的話,在段頁式結構中,須三次訪問內存才能獲取數據或指令;
  • 當進程訪問一個虛擬內存的頁時,而對應的物理內存卻不存在時,會觸發一次Page Fault缺頁中斷),將須要的數據 or 指令從磁盤加載到物理內存頁中,創建映射關係,而後再恢復現場,程序自己是無感知的;

三、Swap In/Out & Page In/Out

  • 磁盤內部有一個區域叫作交換空間(Swap Space),MMU(內存管理單元) 會將暫時不用的內存塊內容寫在交互空間上(硬盤),這就是Swap Out;當須要時候再從Swap Space中讀取到內存中,這就是Swap In;Swap in和swap out的操做都是比較耗時的, 頻繁的Swap in和Swap out操做很是影響系統性能;python

  • Page In/OutSwap In/Out 概念相似,只不過Page In/Out是將某些頁的數據寫到內存/從內存寫回磁盤交互區;而Swap In/Out是將整個地址空間的數據寫到內存/從內存寫回磁盤交互區;本質都是交互機制。git

  • macOS支持這類交換機制,可是iOS不支持;主要有兩方面考慮吧:程序員

    • 移動設備的閃存讀寫次數有限,頻繁寫會下降壽命;
    • 相比PC機,移動設備閃存空間有限(15年6s最小存儲空間16GB、最大128GB;19年XS Max最小64GB,最大521GB)

四、to be continued

  • 通用的計算機(大型機和專用計算機不在此範圍)的存儲器通常設置爲多層,從最靠近CPU的Cache一直到磁盤,速度愈來愈慢,價格也愈來愈便宜。
  • 爲了充分利用好硬件資源,Cache和Swap機制應運而生,Cache機制是一種用空間換時間的機制;而Swap機制是使用時間換空間的;
  • 可是,iOS系統捨棄交換機制,取而代之的是壓縮內存(Compressed memory)機制

2、iOS內存管理基礎概念

一、iOS的內存分區

從高地址到低地址各區域以下:github

  • 棧區(stack): 由編譯器⾃動分配,存放函數的參數值,局部變量的值等,做用域執行完畢以後,就會被系統收回;(棧區的地址從高到低分配)
  • 堆區(heap):通常由程序員分配和釋放,用於存放程序運行中被動態分配的內存段;iOS中的Objective-C對象存放在這裏,由ARC管理;(堆區的地址是從低到高分配)
  • 全局區靜態區:由編譯器分配,主要是存放全局變量 和 靜態變量,程序結束後由系統釋放;主要分兩個區:
    • BSS區未初始化的全局變量 和 靜態變量;
    • 數據區已初始化的全局變量 和 靜態變量;
  • 常量區: 存放的是常量,如常量字符串,程序結束後由系統釋放;
  • 代碼區:存放函數體的二進制代碼,程序結束後由系統釋放;

內存分區示意

二、內存頁類型:Clean和Dirty

  • 內存頁按照各自的分配和使用狀態,分爲 CleanDirty 兩類。其中Clean Page是能夠被回收的,Dirty Page不能;web

    int *array = malloc(20000 * sizeof(int)); // 第1步
    array[0] = 32                             // 第2步
    array[19999] = 64                         // 第3步
    複製代碼
  • 第一步,申請一塊長度爲80000 字節的內存空間,按照一頁 16KB 來計算,就須要 6 頁內存來存儲。當這些內存頁開闢出來的時候,它們都是 Clean 的;objective-c

  • 第二步,向處於第一頁的內存寫入數據時,第一頁內存會變成 Dirty;緩存

  • 第三步,當向處於最後一頁的內存寫入數據時,這一頁也會變成 Dirty;bash

三、VM Region

  • iOS進程中全部內存就是由許許多多的VM Region組成的;服務器

  • VM Region一段連續的內存頁(在虛擬地址空間裏)VM Region的結構以下

    struct vm_region_submap_info_64 {
    	vm_prot_t		protection;     /* present access protection */
    	vm_prot_t		max_protection; /* max avail through vm_prot */
    	vm_inherit_t		inheritance;/* behavior of map/obj on fork */
    	memory_object_offset_t	offset;		/* offset into object/map */
      unsigned int            user_tag;	/* user tag on map entry */
      unsigned int            pages_resident;	/* only valid for objects */
      unsigned int            pages_shared_now_private; /* only for objects */
      unsigned int            pages_swapped_out; /* only for objects */
      unsigned int            pages_dirtied;   /* only for objects */
      unsigned int            ref_count;	 /* obj/map mappers, etc */
      unsigned short          shadow_depth; 	/* only for obj */
      unsigned char           external_pager;  /* only for obj */
      unsigned char           share_mode;	/* see enumeration */
    	boolean_t		is_submap;	/* submap vs obj */
    	vm_behavior_t		behavior;	/* access behavior hint */
    	vm_offset_t		object_id;	/* obj/map name, not a handle */
    	unsigned short		user_wired_count; 
    };
    複製代碼
  • VM Region包含的重要信息有:

    • pages_resident:在用的物理內存頁數
    • pages_dirtied:Dirty的內存頁數
    • pages_swapped_out:Swapped的內存頁數(實際指的是被Compressed Memory頁數
  • 能夠經過了解pages_dirtiedpages_swapped_out來了解VM Region的真實物理內存使用。

3、iOS內存管理機制

一、OC的內存管理

  • Objective-C提供兩種方式的內存管理方式:MRC(手動管理引用計數)和 ARC(自動引用計數)
  • Objective-C內存管理的基本原則:誰建立,誰釋放,誰引用,誰管理;
  • iOS 5以後提出的ARC被普遍接收,畢竟不須要管理引用計數是個很爽的事情;有了ARC,開開心心寫OC;可是ARC只管理Objective-C對象的內存,CoreFoundation對象、CoreGraphics對象、還有C/C++的內存分配仍是須要開發者本身管理。

OC的內存管理圖

二、系統內存分類

​ 從 iOS7 開始,系統開始採用Compressed Memory機制優化內存使用,內存類型能夠分爲三類:

  • Clean Memory:能夠被釋放或重建的,主要包括:

    • Code
    • framework,每一個 framework都有 _DATA_CONST 段,當 App 在運行時使用到了某個 framework,它所對應的 _DATA_CONST 的內存就會由 Clean 變爲 Dirty。
    • memory-mapped files(已被加載到內存中的文件)
  • Dirty Memory:指那些被寫入過數據的內存,主要包括:

    • 全部堆區中的對象(Heap allocations)
    • 圖像解碼緩衝區(Decoded image buffers)
    • frameworks(framework中 _DATA 段和 _DATA_DIRTY 段)
    在使用 framework 的過程當中會產生Dirty Memory,使用單例或者全局初始化方法是減小Dirty Memory;這是由於單例一旦建立就不會銷燬,全局初始化方法會在 class 加載時執行。
    複製代碼
  • Compressed Memory

    • 在內存吃緊時,系統會將不使用的內存進行壓縮(Compresses unaccessed pages)

    • 在須要的時候,進行解壓 (Decompresses pages upon access)

    • 優點:減小了不活躍內存佔用;減小磁盤IO帶來的損耗;壓縮/解壓十分迅速,可以儘量減小 CPU 的時間開銷;支持多核操做。

    • 舉例:當咱們使用 NSDictionary 去緩存數據的時候,假設如今已經使用了 3 頁內存,當不訪問的時候可能會被壓縮爲 1 頁,再次使用到時候又會解壓成 3 頁。

  • 介紹Clear MemoryDirty Memory 的Code以下:

    //堆分配的內存 Dirty Memory
    NSString *str1 = [NSString stringWithString:@"Welcome!"]; 
    //常量字符串, 存放在一個只讀數據段裏面,這段內存釋放後,還能夠在讀取重建 Clear Memory
    NSString *str2 = @"Welcome!"; 
    //分配100M虛擬內存,當沒有用時沒有創建映射,Clear Memory
    char *buf = malloc(100 * 1024 *1024); 關係
    for (int i = 0; i < 3 * 1024 * 1024; ++i) {
        //寫入數據了,Dirty Memory
    		buf[i] = rand();									
    }
    複製代碼
  • 說明:在內存吃緊的狀況下,釋放Clean Memory,不能釋放Dirty Memory ,因此Dirty Memory 的內存越多,App的穩定性越差。

三、Jetsam機制

  • Jetsam機制是操做系統爲了控制內存資源過分使用而採用的一種管理機制;Jetsam是一個獨立運行的進程,會把一些優先級不高或者佔用內存過大的App殺掉;在殺掉App後會記錄一些數據信息並保存到日誌。

  • App優先級能夠這麼簡單理解:前臺App > 後臺App; 佔用內存少 > 佔用內存多;

  • Jetsam產生的這些日誌能夠在手機設置->隱私->分析中找到,日誌是以JetsamEvent開頭,日誌中有內存頁大小(pageSize),CPU時間(cpuTime)等字段。

  • 查看設置->隱私->分析中以JetsamEvent開頭的系統日誌,關注兩個重要的信息;

    "pageSize" : 16384,
    //內存頁達到上限
    "rpages" : 948,      //App 佔用的內存頁數量
    "reason" : "per-process-limit",  //App 佔用的內存超過了系統對單個 App 的內存限制。
    複製代碼
    • 說明:該內存「上限」計算:pageSize rpages = 16384 948 /1024/1014 = 14.8MB,這個App應該是因優先級不高而被強殺,畢竟App內存使用上限不可能不到15MB。

四、內存警告

  • 內存警告(Memory Warning)三種通知方式:
    • UIApplicationDelegateapplicationDidReceiveMemoryWarning:
    • 視圖控制器UIViewControllerdidReceiveMemoryWarning
    • UIApplicationDidReceiveMemoryWarningNotification通知
  • 出現OOM前不必定出現內存警告;有多是瞬間申請了大量內存,而剛好此時主線程在忙於其餘事情,致使可能沒有經歷過Memory Warning就發生了OOM;也可能即使出現了屢次Memory Warning後,也不見得會在最後一次Memory Warning的幾秒鐘後出現OOM(多是1-2分鐘後);
  • 並不是全部內存警告都是由 App 形成的,例如在內存較小的設備上,當你接聽電話的時候也有可能發生內存警告。

4、常見的內存類問題

一、FOOM

  • FOOM(Foreground Out Of Memory),是指App在前臺因消耗內存過多引發系統強殺。對用戶而言,表現跟Crash同樣。

  • Facebook早在2015年8月提出FOOM檢測辦法,大體原理是排除各類狀況後,剩餘的狀況是FOOM;Facebook如何斷定上一次啓動是否出現FOOM方法:

    • 1.App沒有升級
    • 2.App沒有調用exit()或abort()退出
    • 3.App沒有出現Crash (依賴於自身CrashReport組件的Crash回調)
    • 4.用戶沒有強退App
    • 5.系統沒有升級/重啓
    • 6.App當時沒有後臺運行(依賴於ApplicationState和先後臺切換通知)
    • 7.App出現FOOM (依賴於ApplicationState和先後臺切換通知)
  • 排查法有誤報的可能,由於有些被系統強殺case,可是咱們捕獲不到信息,也可能被歸類到OOM;已知被系統強殺的case是:OOM和watchdog(Code 0x8badf00d)。

  • 發現FOOM問題的關鍵:監控App使用內存增加,在收到內存警告通知時,dump 內存信息,獲取對象名稱、對象個數、各對象的內存值,並在合適的時機上報到服務器;增強對大內存的分配監控。

二、內存泄露

  • 內存泄露(Memory Leak):指申請的內存空間使用完畢以後未回收,內存泄露問題多的話,對App質量影響很大;

  • 目前引發內存泄露的主要緣由是循環引用(堆內存中對象相互引用,彼此都得不到釋放的機會),目前,調試階段使用Instrument的Leaks工具發現,線上利用MLeaksFinder發現後上報;

  • 如何避免內存泄露在後面有介紹

三、WKWebview白屏

  • WKWebview白屏問題,嚴格來講,是一種內存方面的問題;以前的UIWebview由於內存使用過大會Crash,而WKWebview不會Crash,會白屏;

  • WKWebView是一個多進程組件,Network Loading以及UI Rendering在其它進程中執行,當WKWebView整體的內存佔用比較大時,WebContent Process會Crash,從而出現白屏現象。

  • 解決辦法

    • KVO監聽URL, 當URL爲nil,從新reload

    • 在進程被終止回調中,從新reload

      // 此方法適用iOS9.0以上 
      - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0){
      		//reload
      }
      複製代碼

四、野指針問題

  • 野指針:指向一個已刪除的對象 或 受限內存區域的指針;目前此類問題不多了,主要來自兩個方面

    • MRC時代 到 ARC時代,OC對象管理極大方便了,ARC解決大部分野指針問題
    • iOS 9開始,系統庫中部分遺留的被assign(unsafe_unretain)修飾的 delegatetarget-action修改爲了weak,內存被回收的時候,這些指針設爲nil,這也大幅度減小了野指針的出現。
  • 目前絕大部分App都是iOS 9起步,野指針少了不少,可是工程中依然會有野指針問題,本質仍是內存使用不當;

  • Mach Exception大多數都是野指針的問題,崩潰日誌裏最常見objc_msgSendunrecognized selector sent to等等。

  • 對於野指針問題,最好能復現,使用Zombie Object 幫助調試,Zombie Object 實現原理就是 hook 住了對象的dealloc方法,經過調用本身的__dealloc_zombie方法來把對象進行殭屍化,當這個對象再次收到消息,objc_msgsend的時候,調用abort()崩潰並打印出調用的方法。。

五、iOS 10 nano_free Crash

  • 在iOS 10.0 - 10.1,蘋果bug引入nano_free Crash問題,這些Crash發生libsystem_malloc.dylib中的 nano zone內的;

  • libsystem_malloc.dylib中,對內存的管理有兩個實現:nano zonescalable zone。他們分別管理不一樣大小的內存塊:

    • 內存分配函數malloc和calloc等默認使用的是nano_zone,nao_zone是 256B 如下小內存的分配,
    • 大於 256B 的時候會使用 scalable_zone 來分配。
  • 當時微信團隊提出的幾種解決思路,最後給的解決方案是不使用nano zone,具體描述

    • 建立一個本身的zone,命名爲guard zone
    • 修改nano zone的函數指針,重定向到guard zone(經過malloc_zone_create建立的)。
      • 對於沒有傳入指針的函數,直接重定向到guard zone
      • 對於有傳入指針的函數,先用size判斷所屬的zone,再進行分發。
  • 更多見:

六、to be continued

  • 內存類問題排查難度大,複雜。須要對內存有更深刻的理解
  • 爲了不內存問題:根本上是優化內存使用,主要表如今:減小大塊內存使用,下降內存峯值,避免內存泄露,處理內存警告;

5、優化內存使用

一、下降圖片解碼和渲染開銷

  • 將圖片渲染顯示在屏幕上,須要先將其解碼成位圖;而位圖大小:像素高 * 像素寬 * 4字節(4字節對應一個像素點,4個通道的大小);圖片解碼會形成內存使用上升,尤爲是高分辨率圖解碼可能致使內存暴漲;
  • 代碼優化建議
    • 善待"大"圖(位圖大小大於60MB)解碼:將原圖裁剪成多個小圖,而後依次繪製到目標位圖context中,具體可見SDWebImage中和關於**decodedAndScaledDownImageWithImage:**的實現;

    • 限制併發解碼圖片的個數;

      • 圖片大小調整和顯示大小同樣,以此避免重採樣(重採樣也很消耗資源,放大圖像稱爲上採樣/插值(upsamping),縮小圖像爲小採樣(downsampling));
      • 使用 ImageIO直接讀取圖像大小和元數據信息,減小內存開銷。
      • iOS10後使用 UIGraphicsImageRenderer 建立 image 上下文,而不是UIGraphicsBeginImageContextWithOptions,由於前者的性能更好、更高效,而且支持廣色域;

二、其餘下降內存峯值辦法

  • 合理使用autorealsepool,下降內存峯值,避免 OOM

    • 基於引用計數,Pool執行drain方法會release全部該Pool中的autorelease對象
    • 能夠嵌套多個AutoReleasePool
    • 每一個線程並無設置默認的AutoReleasePool,須要手動建立,避免內存泄露
    • 在一段內存分配頻繁的代碼中嵌套AutoReleasePool有利於下降總體內存峯值
  • 複用大內存對象,如UITableViewCell對象;懶加載大的內存對象

  • imageNamed 和 imageWithContentOfFile 的選擇

    • imageNamed使用系統緩存,適用於頻繁使用的小圖片
    • imageWithContentOfFile不帶緩存機制,適用於大圖片,使用完就釋放
  • 建議NSData讀取文件方式

    • 建議使用[NSData dataWithContentsOfFile:path options:NSDataReadingMappedIfSafe error:&error];
    • 該API映射文件到虛擬內存, 只有讀取操做的時候纔會讀取相應頁的內容到物理內存頁中;
  • NSCache 代替 NSMutableDictionary, 由於 NSCache 能夠自動清理內存,在內存吃緊的時候會更加合理。

    • NSCache有2種界限條件:totalCostLimt & countLimit,超過這兩種界限後系統會去釋放一些舊的資源.
    • 監聽到內存警告消息後移除全部Cache
  • 使用 NSPurgableData 代替NSData,主要緣由以下:

    • 當系統處於低內存的時候會自動移除
    • 適用於大數據
  • 棧內存分配alloca(size_t)

    • 棧分配僅僅修改棧指針寄存器,比malloc遍歷並修改空閒列表要快得多
    • 棧內存通常都已經在物理內存中,不用擔憂頁錯誤
    • 函數返回的時候棧分配的空間都會自動釋放
    • 但僅適合小空間的分配,而且函數嵌套不宜過深
  • 堆內存分配calloc VS malloc + memset

    • calloc(size_t num,size_t size)分配內存時是虛擬內存,只有在訪問的時候纔會發生物理頁的映射關係;
    • malloc + memset 會產生Dirty Memory
    • calloc函數獲得的內存空間是通過初始化的,其內容全爲0,而malloc函數獲得的內存空間是未初始化的,必須使用memset函數來初始化;

三、避免循環引用,減小內存泄露

  • 代碼優化建議
    • 申明代理(delegate)爲weak;
    • 使用 weak strong dance 來解決 block 中的循環引用問題;
    • 實現NSProxy(虛擬類)的子類,而後在子類中定義weak修飾的target,而後實現消息轉發方法,使target處理業務邏輯;通常用於解決NSTimer、CADisplayLink的循環引用;
    • CoreFoundation對象、CoreGraphics對象、還有C/C++的內存分配須要管理好,有malloc就要有free
  • 善用工具:MLeaksFinder + InstrumentFBMemoryProfiler + Instrument組合使用。經過MLeaksFinder/FBMemoryProfiler發現後。而後使用Instrument再驗證;

四、處理內存警告

  • Memory Warning時,儘量釋放多資源,尤爲圖片等佔內存多的資源,等須要用的時候再重建;
  • Memory Warning時,部分單例對象可考慮釋放掉;
  • Memory Warning時,不建議self.view = nil:iOS 6以後,系統發出 Memory Warning 時,系統會自動回收CALayer的CABackingStore對象(bitmap 內容);雖然沒有回收 UIView 和 CALayer 類,可是也回收了大部份內存,在須要 bitmap 類時,經過調用 UIView 的 drawRect: 方法重建。
  • 最佳的實踐仍是減小大塊內存使用,下降內存峯值,避免內存泄露

歷史文章

以前對內存問題一些整理

以前對圖片解碼和圖片優化一些總結

參考文章

深刻了解iOS中的OOM(低內存崩潰)

WWDC 2018:iOS 內存深刻研究

page fault帶來的性能問題

iOS微信內存監控

iOS野指針定位總結

iOS中的內存管理

iOS 內存管理研究

Linux對內存的管理, 以及page fault的概念

iOS內存管理和malloc源碼解讀

libmalloc源碼分析之初始化

相關文章
相關標籤/搜索