從 OOM 到 iOS 內存管理 | 創做者訓練營

點贊評論,感受有用的朋友能夠關注筆者公衆號 iOS 成長指北,持續更新好文章。html

萬字長文,建議收藏閱讀node

從 OOM 崩潰出發,涉獵 iOS Jetsam 機制的相關內容,介紹如何得到設備內存閾值。介紹內存分配的基本概念,瞭解 iOS APP 的內存分佈,以及如何分析 iOS 內存佔用。引入一些實際的方法來在 iOS 開發過程當中規避內存問題。ios

一切的一切,都從一個 OOM 崩潰出發。git

前言

《iOS Crash Dump Analysis》 一書的翻譯工做,對筆者來講意義重大。讓筆者系統的學習了一下如何進行崩潰分析,以及崩潰分析的緣由。程序員

內存問題一直是致使系統崩潰的重要緣由,絕大部分的緣由多是由於開發者在開發過程當中每每會忽視內存問題,咱們常常專一於使用而忘了深究。github

因爲內存問題致使的 iOS 應用程序發生的崩潰大體分爲如下兩種: 錯誤的內存訪問和超出內存限制。在進行深刻以前咱們先了解一下。objective-c

錯誤的內存訪問

咱們的崩潰報告收集工具會收集崩潰報告,並將其符號化。macos

而不管自建仍是使用自三方崩潰工具都會將崩潰報告中的 Exception Type ,也就是異常類型,放置在顯眼位置。swift

若是是使用xcode

Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note:  EXC_CORPSE_NOTIFY
Triggered by Thread:  0
複製代碼

在 iOS 崩潰報告中,關於 應用自己 的內存異常類型有兩種

EXC_BAD_ACCESS (SIGSEGV)EXC_BAD_ACCESS (SIGBUS)

代表咱們的程序極可能試圖訪問錯誤的或者是咱們沒有權限訪問的內存地址。或因爲內存壓力,該內存已被釋放,即訪問錯誤已經不存在內存位置(常見的如野指針訪問)。

SIGSEGV(段衝突):表示存儲器地址甚至沒有映射到進程地址區間。

  • Pointer Authentication(指針驗證機制):使用 Apple A12 芯片或更高版本的設備將稱爲 Pointer Authentication 的安全功能做爲 ARMv8.3-A 體系結構的一部分。若是因爲錯誤或惡意操做而要更改指針地址,則該指針將被認爲是無效的,而且若是將其用於更改程序的控制流,則最終將致使 SIGSEGV。

SIGBUS(總線錯誤):內存地址已正確映射到進程的地址區間,但不容許進程訪問內存。

筆者在 [譯]《iOS Crash Dump Analysis》- 崩潰報告 中介紹了詳細介紹瞭如何分析已有的崩潰報告,如何根據崩潰報告快速定位崩潰問題。

超出內存限制(Memory Limit)

有些內存崩潰問題並不能直接提如今咱們的崩潰報告中。意味着並無特定的異常類型來告知咱們這種錯誤屬於超出內存限制的崩潰。

與 macOS 相比,激進(積極)的內存管理是 iOS 的一個特色,macOS 對內存使用有很是寬鬆的限制。通俗來講,移動設備是內存受限的設備。Jetsam 嚴格的內存管理系統爲咱們提供了良好的服務,在給定的 RAM 量下保證最佳的用戶體驗。

iOS 存在 Foreground Out-Of-Memory (FOOM)Background Out-Of-Memory (BOOM) 兩種超出內存限制的 OOM 崩潰現象。從名稱上能夠看出來,一種是因爲使用應用時自己超出內存限制致使的崩潰,另外一種因爲當前設備在後臺中,而用戶正在使用拍照功能進行大量的拍照和圖像特效時,此時內存使用量大幅度增長,爲了保證正在進行的進程有足夠的內存可供使用。

若是在 iOS 崩潰報告中出現異常類型爲 EXC_CRASH (SIGQUIT) 時,這意味着應用的某個拓展程序花費了太長的時間或者消耗了太多的內存。

OOM 崩潰

什麼是 OOM 崩潰?

那麼當內存不夠用時,iOS 會發出內存警告,告知進程去清理本身的內存。iOS 上一個進程就對應一個 app。若是 app 在發生了內存警告,並進行了清理以後,物理內存仍是不夠用了,那麼就會發生 OOM 崩潰,也就是 Out of Memory Crash。咱們主要關注正在使用的應用程序發生的 OOM 崩潰,也就是前文提到的 Foreground Out-Of-Memory (FOOM)

iOS 經過 Jetsam 機制來實現上述功能。

Jetsam 機制

Jetsam 機制能夠理解爲操做系統爲控制內存資源過分使用而採用的一種管理機制。Jetsam是一個獨立運行的進程,每一個進程都有一個內存閾值,一旦超過這個閾值,Jetsam將當即殺死該進程。

在前文咱們提到,OOM 崩潰並無體如今崩潰報告中,而是出如今 Apple 自己的 Jetsam 報告中。在 iOS 中,Jetsam 是將當前應用從內存中彈出以知足當前最重要應用需求的系統。

Jetsam 一詞最初是一個航海術語,指船隻將不想要的東西扔進海里,以減輕船的重量。

當咱們的應用被 Jetsam 機制殺死時,手機會生成系統日誌。在手機系統設置隱私分析中,找到以 JetSamEvent. 的開頭的系統日誌。在這些日誌中,你能夠獲取一些關於應用程序的內存信息。能夠在日誌的開頭,看到了pageSize,並找到了 perprocesslimit 項(不是全部日誌都有,可是能夠找到它)。經過使用項目的 rpages * pageSize 能夠獲得 OOM 的閾值。

一個 Jetsam 日誌大概像下面同樣:

{"bug_type":"298","timestamp":"2020-10-15 17:29:58.79
 +0100","os_version":"iPhone OS 14.2
 (18B5061e)","incident_id":"B04A36B1-19EC-4895-B203-6AE21BE52B02"
}
{
  "crashReporterKey" :
 "d3e622273dd1296e8599964c99f70e07d25c8ddc",
  "kernel" : "Darwin Kernel Version 20.1.0: Mon Sep 21 00:09:01
 PDT 2020; root:xnu-7195.40.113.0.2~22\/RELEASE_ARM64_T8030",
  "product" : "iPhone12,1",
  "incident" : "B04A36B1-19EC-4895-B203-6AE21BE52B02",
  "date" : "2020-10-15 17:29:58.79 +0100",
  "build" : "iPhone OS 14.2 (18B5061e)",
  "timeDelta" : 7,
  "memoryStatus" : {
  "compressorSize" : 96635,
  "compressions" : 3009015,
  "decompressions" : 2533158,
  "zoneMapCap" : 1472872448,
  "largestZone" : "APFS_4K_OBJS",
  "largestZoneSize" : 41271296,
  "pageSize" : 16384,
  "uncompressed" : 257255,
  "zoneMapSize" : 193200128,
  "memoryPages" : {
    "active" : 45459,
    "throttled" : 0,
    "fileBacked" : 34023,
    "wired" : 49236,
    "anonymous" : 55900,
    "purgeable" : 12,
    "inactive" : 40671,
    "free" : 5142,
    "speculative" : 3793
  }
},
  "largestProcess" : "AppStore",
  "genCounter" : 1,
  "processes" : [
  {
    "uuid" : "7607487f-d2b1-3251-a2a6-562c8c4be18c",
    "states" : [
      "daemon",
      "idle"
    ],
    "age" : 3724485992920,
    "purgeable" : 0,
    "fds" : 25,
    "coalition" : 68,
    "rpages" : 229,
    "priority" : 0,
    "physicalPages" : {
      "internal" : [
        6,
        183
      ]
    },
    "pid" : 350,
    "cpuTime" : 0.066796999999999995,
    "name" : "SBRendererService",
    "lifetimeMax" : 976
  },
.
.
{
  "uuid" : "f71f1e2b-a7ca-332d-bf87-42193c153ef8",
  "states" : [
    "daemon",
    "idle"
  ],
  "lifetimeMax" : 385,
  "killDelta" : 13595,
  "age" : 94337735133,
  "purgeable" : 0,
  "fds" : 50,
  "genCount" : 0,
  "coalition" : 320,
  "rpages" : 382,
  "priority" : 1,
  "reason" : "highwater",
  "physicalPages" : {
    "internal" : [
      327,
      41
    ]
  },
  "pid" : 2527,
  "idleDelta" : 41601646,
  "name" : "wifianalyticsd",
  "cpuTime" : 0.634077
},
.
.
複製代碼

這裏能夠看一下筆者的 [譯]《iOS Crash Dump Analysis 2》- 系統診斷 學習如何獲取設備的系統診斷報告以及對於獲取的 Jetsam 報告如何解讀。

固然也能夠閱讀一下 Identifying High-Memory Use with Jetsam Event ReportsMonitoring Basic Memory Statistics 等官方文檔。

肯定內存閾值

Apple 並無準確的文檔說明每一個設備的內存限制。對於設備的內存 OOM 閾值大概有如下幾個方法獲取。這裏獲取的限制最好是在重啓 iPhone 之後,使得設備清空 RAM 緩存。

方法一: Jetsam 日誌

在前文介紹瞭如何從 Jetsam 日誌中經過使用項目的 rpages * pageSize 能夠獲得 OOM 的閾值。這裏就再也不贅述了。

方法二: 互聯網數據

互聯網上有不少關於OOM 閾值的文章並列舉了不一樣設備的 OOM閾值,筆者感受比較精確的是這兩個

StackOverflow

咱們能夠在 StackOverflow post 大概瞭解不一樣設備的內存限制。有問題,StackOverflow 一下。

Split 工具

基於 Split 工具 獲取的 Jaspers 列表

設備 RAM 閾值範圍(百分制)
256MB 49% - 51%
512MB 53% - 63%
1024MB 57% - 68%
2048MB 68% - 69%
3072MB 63% - 66%
4096MB 77%
6144MB 81%

特別的案例:

設備 RAM 閾值範圍(百分制)
iPhone X (3072MB) 50%
iPhone XS/XS Max (4096MB) 55%
iPhone XR (3072MB) 63%
iPhone 11/11 Pro Max (4096MB) 54% - 55%

根據筆者的經驗,1GB設備 安全閾值 45%,2-3GB 設備安全閾值 50% 4GB設備安全閾值 55%。 macOS 的百分比可能更大。

利用下面的方法獲取當前設備的 RAM 值

[NSProcessInfo processInfo].physicalMemory
複製代碼

方法三: 主動觸發 didReceiveMemoryWarning

當內存不夠用時,iOS 會發出內存警告,告知進程去清理本身的內存, 在當前頁面(Controller)中,這個方法是 - (void)didReceiveMemoryWarning。能夠經過不停地增長內存,來獲取當前設備的 OOM 閾值。

咱們能夠根據如下方法獲取 設備的 OOM 閾值

#import "ViewController.h"
#import <mach/mach.h>

#define kOneMB  2014 * 1024

@interface ViewController ()
{
    NSTimer *timer;

    int allocatedMB;
    Byte *p[10000];
    
    int physicalMemorySizeMB;
    int memoryWarningSizeMB;
    int memoryLimitSizeMB;
    BOOL firstMemoryWarningReceived;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    physicalMemorySizeMB = (int)([[NSProcessInfo processInfo] physicalMemory] / kOneMB);
    firstMemoryWarningReceived = YES;
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
        
    if (firstMemoryWarningReceived == NO) {
        return ;
    }
    memoryWarningSizeMB = [self usedSizeOfMemory];
    firstMemoryWarningReceived = NO;
}

- (IBAction)startTest:(UIButton *)button {
    [timer invalidate];
    timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(allocateMemory) userInfo:nil repeats:YES];
}

- (void)allocateMemory {
    
    p[allocatedMB] = malloc(1048576);
    memset(p[allocatedMB], 0, 1048576);
    allocatedMB += 1;
    
    memoryLimitSizeMB = [self usedSizeOfMemory];
    if (memoryWarningSizeMB && memoryLimitSizeMB) {
        NSLog(@"----- memory warnning:%dMB, memory limit:%dMB", memoryWarningSizeMB, memoryLimitSizeMB);
    }
}

- (int)usedSizeOfMemory {
    task_vm_info_data_t taskInfo;
    mach_msg_type_number_t infoCount = TASK_VM_INFO_COUNT;
    kern_return_t kernReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&taskInfo, &infoCount);

    if (kernReturn != KERN_SUCCESS) {
        return 0;
    }
    return (int)(taskInfo.phys_footprint / kOneMB);
}

@end
複製代碼

在 iOS 13 以上的設備中,咱們可使用系統 os/proc.h 所提供的一個新的 API

__BEGIN_DECLS

/*!
 * @function os_proc_available_memory
 * ... 爲了篇幅進行截斷
 * @result
 * The remaining bytes. 0 is returned if the calling process is not an app, or
 * the calling process exceeds its memory limit.
 */

    API_UNAVAILABLE(macos) API_AVAILABLE(ios(13.0), tvos(13.0), watchos(6.0))
extern
size_t os_proc_available_memory(void);

__END_DECLS
複製代碼

來獲取當前設備的內存閾值

#import <mach/mach.h>
#import <os/proc.h>
...

- (int)limitSizeOfMemory {
    if (@available(iOS 13.0, *)) {
        task_vm_info_data_t taskInfo;
        mach_msg_type_number_t infoCount = TASK_VM_INFO_COUNT;
        kern_return_t kernReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&taskInfo, &infoCount);

        if (kernReturn != KERN_SUCCESS) {
            return 0;
        }
        return (int)((taskInfo.phys_footprint + os_proc_available_memory()) / 1024.0 / 1024.0);
    }
    return 0;
}
複製代碼

你也能夠定義一個方法,在使用大量內存以前,先獲取一下當前的可用內存

#import <os/proc.h>

+ (CGFloat)availableSizeOfMemory {
    if (@available(iOS 13.0, *)) {
        return os_proc_available_memory() / 1024.0 / 1024.0;
    }
    // ...
}

複製代碼

請用真機測試,不要使用模擬器測試!

源碼探究

咱們知道 iOS/macOS 的內核是 XNU,XNU 是開源的。咱們能夠在開源的 XNU 內核源代碼中探索 Apple Jetsam 的具體實現。

XNU 內核的內層是 Mach 層。做爲一個微內核,mach 是一個只提供基本服務的薄層,好比處理器管理和調度以及IPC(進程間通訊)。XNU 的第二個主要部分是 BSD 層。咱們能夠把它當作是 Mach 的外層。BSD 爲最終用戶的應用程序提供了一個接口。其職責包括進程管理、文件系統和網絡。

內存管理中常見的拋棄時間也是由 BSD 生成的,所以讓咱們能夠從 BSD init 做爲切入點來探討其原理。

BSD init 初始化各類子系統,好比虛擬內存管理等等。

...

是什麼致使 FOOM 崩潰?

有多種緣由可能會致使堆內存增加過分並致使 FOOM 崩潰:

循環引用

通常來講致使 OOM 的主要緣由就是代碼中會出現循環引用。也就是咱們常說的內存泄露

內存泄漏是指在某一時刻分配的內存,但從未被釋放,也再也不被應用程序引用。因爲沒有對它的引用,如今就沒有方法訪問和釋放它,內存不能再被使用。

內存泄漏無可避免地增長了應用程序的內存佔用,這部分 RAM 將永遠不會釋放,直到應用程序中止運行。

緩存

對於正在處理須要大量內存或計算時間的頻繁訪問對象的開發人員而言,緩存多是相當重要的。儘管在性能方面提供了巨大的好處,可是緩存可能會佔用大量內存。緩存如此多的對象,可能會你或其餘應用程序沒有可用的RAM,從而有可能迫使系統終止它們。

常見的緩存示例是緩存圖像。

圖像渲染

就內存而言,圖像渲染是很昂貴的。該過程分爲兩個階段:解碼和渲染。

解碼階段是將圖像數據(數據緩衝區)轉換爲可由顯示硬件(圖像緩衝區)解釋的信息。這包括每一個像素的顏色和透明度。

渲染階段是硬件消耗圖像緩衝區並將其實際 繪製 在屏幕上。

在 iOS 中渲染的圖片實際上所佔的內存其大小是經過將每一個像素的字節數乘以圖像的寬度和高度來計算的。渲染一份像素爲 3024 x 4032,顏色空間爲 RGB,帶 DisplayP3 色彩配置文件。該顏色配置文件每一個像素佔用16位大小。所以,常規方法渲染這樣一個照片須要的內存大概須要 3024 x 4032 x 8 / 1024 / 1024 ≈ 93.02 MB的大小。

經過在設置圖像屬性以前簡單地將圖像調整爲圖像視圖的大小,能夠減小RAM數量級。能夠查看 Mattt 大神的 Image Resizing Techniques 瞭解在 iOS 中咱們如何調整獲取的圖片大小。

iOS 內存詳解

在瞭解 iOS/macOS 內存以前,咱們先了解一下基本概念。

全部進程(執行的程序)都必須佔用必定數量的內存,它或是用來存放從磁盤載入的程序代碼,或是存放取自用戶輸入的數據等等。不過進程對這些內存的管理方式因內存用途不一而不盡相同,有些內存是事先靜態分配和統一回收的,而有些倒是按須要動態分配和回收的。

基本概念

在操做系統中,管理內存的方法是首先將連續的內存排序爲內存頁,而後將頁面排序爲段。這容許將元數據屬性分配給應用於該段內的全部頁面的段。這容許咱們的程序代碼(程序 TEXT )被設置爲只讀但可執行。提升了性能和安全性。

RAMROM

RAM(random access memory)即隨機存儲內存,這種存儲器在斷電時將丟失其存儲內容,故主要用於存儲短期使用的程序。

ROM(Read-Only Memory)即只讀內存,是一種只能讀出事先所存數據的固態半導體存儲器。

App 程序啓動時,系統會將 App 程序從 Flash 或 ROM 中拷貝到內存(RAM),而後從 RAM 裏面執行代碼。CPU 不能直接從 ROM 中讀取並運行程序。

關於 iOS 的啓動過程,能夠參照筆者以前的文章 深刻理解 iOS 啓動流程和優化技巧 文章,獲取 iOS 的啓動流程。

虛擬內存

macOS 和 iOS都包含一個徹底集成的 虛擬內存 系統。這兩個系統爲每一個 32 位進程提供了多達 4 GB的可尋址空間。

虛擬內存容許操做系統擺脫物理 RAM 的限制。虛擬內存管理器爲每一個進程建立一個邏輯地址空間(或虛擬地址空間),並將其劃分爲稱爲頁面的大小相同的內存塊。處理器和它的內存管理單元(MMU)維護一個頁表來將程序的邏輯地址空間中的頁面映射到計算機 RAM 中的硬件地址。當程序代碼訪問內存中的地址時,MMU 使用頁表將指定的邏輯地址轉換爲實際的硬件內存地址。這種轉換是自動發生的,而且對正在運行的應用程序是透明的。

內存分頁

就程序而言,其邏輯地址空間中的地址老是可用的。可是,若是應用程序訪問當前不在物理 RAM 中的內存頁上的地址,則會發生頁面錯誤。當發生這種狀況時,虛擬內存系統調用一個特殊的頁面錯誤處理程序來當即響應錯誤。頁面錯誤處理程序中止當前執行的代碼,在物理內存中找到一個空閒的頁面,從磁盤加載包含所需數據的頁面,更新頁表,而後將控制權返回給程序代碼,而後程序代碼能夠正常訪問內存地址。這個過程稱爲分頁。

內存交換

若是物理內存中沒有可用的可用頁面,則處理程序必須首先釋放現有頁面覺得新頁面騰出空間。系統發佈頁面的方式取決於平臺。在 OSX 中,虛擬內存系統一般將頁寫入備份存儲。備份存儲是一個基於磁盤的存儲庫,其中包含給定進程使用的內存頁的副本。將數據從物理內存移動到備份存儲稱爲 paging out(或 swapping out);將數據從備份存儲移回物理內存稱爲paging in(或 swapping in)。

iOS 不支持內存交換

可是,iOS 不支持交換空間,而且大多數移動設備都不支持交換空間。移動設備的大容量內存一般是閃存,它的讀寫速度遠遠小於計算機使用的硬盤,這致使即便移動設備上使用了交換空間,也沒法提升性能。其次,移動設備自己容量每每不足,內存的讀寫壽命也有限,在這種狀況下,使用閃存進行內存交換有點奢侈。

頁面永遠不會被調出到磁盤,可是隻讀頁面仍然能夠根據須要從磁盤調出。

分頁大小

在早期的 iOS 設備上, 分頁的大小爲 4 KB,而在 A7 和 A8 芯片上,對 64 位機器其分頁大小爲 16 KB 而 32 爲機器分頁大小依舊爲 4 KB。對於 A9 及以上芯片,其分頁大小都爲 16 KB。

針對 iOS 設備來講,其32位機器內存分頁大小爲 4 KB 而 64 位機器內存分頁大小爲 16 KB

VM 對象

進程的邏輯地址空間由內存的映射區域組成。每一個映射的內存區域包含已知數量的虛擬內存頁。每一個區域都有特定的屬性來控制諸如繼承(區域的一部分能夠從區域映射)、寫保護以及它是否被鏈接(即,它不能被調出)。由於區域包含已知數量的頁面,因此它們是頁面對齊的,這意味着區域的起始地址也是頁面的起始地址,而結束地址也定義了頁面的結尾。

內核將 VM 對象與邏輯地址空間的每一個區域相關聯。內核使用VM對象來跟蹤和管理相關區域的駐留頁和非駐留頁。區域能夠映射到備份存儲的一部分或文件系統中的內存映射文件。每一個 VM 對象都包含一個映射,該映射將區域與默認 pagervnode pager 相關聯。默認的尋呼機是一個系統管理器,它管理後臺存儲中的非駐留虛擬內存頁,並在請求時獲取這些頁。vnode pager 實現內存映射文件訪問。vnode pager 使用分頁機制直接向文件提供一個窗口。這種機制容許讀寫文件的某些部分,就像它們位於內存中同樣

VM 對象對咱們分析常駐內存具備頗有效的。後面咱們會用點時間來分析一下如何進行 iOS 內存分析。

iOS 內存佔用

當咱們在討論 iOS 內存佔用及內存管理時,咱們提到的都是虛擬內存

應用程序的內存使用取決於頁數及內存頁的大小。

上文講到內存分頁,實際上內存頁也有分類,通常來講分爲 Clean MemoryDirty MemoryCompressed Memory 的概念。

Clean Memory

  • 內存映射文件是磁盤中存在的已加載到內存中的文件。
  • 若是內存映射文件是隻讀的,那麼它們將始終做爲 Clean 頁面。(例如,image.jpgblob.data,,Training.modelFrameworks
  • 內核管理文件什麼時候進入和離開 RAM。

Dirty Memory

  • Dirty Memory 指的是應用程序鎖寫入的任何內存。
  • 全部堆分配對象(例如 mallocArrayNSCacheUIViewsString圖像解碼緩衝區,例如CGRasterDataImageIOFrameworks)都會是 Dirty Memory
  • 在使用Frameworks的過程當中會產生Dirty Memory。使用單個實例或全局初始化方法能夠減小 Dirty Memory,由於一旦建立了單個實例,它就不會被銷燬,而且全局初始化方法將在類加載時執行。

Compressed Memory

因爲閃存容量和讀寫壽命的限制,iOS 上沒有交換空間機制,所以改用壓縮內存。壓縮內存將壓縮和存儲未訪問的頁面。內存壓縮器用於存儲和檢索壓縮內存。 內存壓縮主要執行兩個操做

  • 壓縮未訪問的頁面
  • 訪問時解壓縮頁面

壓縮內存可以在內存緊張時將最近使用的內存使用率壓縮到原始大小的一半如下,並在須要時能夠解壓縮和從新使用。它不只節省了內存,並且提升了系統的響應速度。

具備如下優點:

  • 減小非活動內存使用
  • 優化用電使用,經過壓縮減小磁盤 IO 損耗
  • 快速壓縮/解壓縮,最大程度地減小 CPU 時間開銷
  • 充分利用多核優點

例如,當咱們使用 NSDictionary 來緩存數據時,假設如今咱們已經使用了 3 頁內存,當咱們不訪問它時,它可能被壓縮爲 1 頁,而當咱們再次使用它時,它將被解壓縮爲 3 頁。

本質上來說,Dirty Memory 也屬於 Compressed Memory

macOS 也存在內存壓縮,經過內存壓縮能夠提升內存交換的效率。

內存佔用限制

Dirty MemoryCompressed Memory 會增長內存佔用量。

內存壓縮技術使得內存的釋放變得複雜。內存壓縮技術是在操做系統級實現的,它對進程不敏感。有趣的是,若是當前進程收到內存警告,則該進程此時準備釋放大量誤用的內存。若是訪問了太多的壓縮內存,當內存被解壓縮時,內存壓力會更大,而後出現 OOM,當前進程被系統殺死。

緩存數據的目的是減輕 CPU 的壓力,可是過多的緩存將佔用過多的內存。在某些須要緩存數據的狀況下,可使用 NSCache 代替 NSDictionary。 NSCache 分配的內存其實是可清除內存,能夠由系統自動釋放。還建議將 NSCache 和 NSPurgeableData 結合使用不只可使系統根據狀況回收內存,並且還能夠在清理內存的同時刪除相關對象。

iOS 內存佔用分析(稍微提一下)

iOS 內存佔用量能夠經過 Xcode 內存量規進行測量,Instruments 提供了多種工具來分析應用程序的內存佔用狀況。

VM Tracker

VM Tracker 提供 Dirty Memory 大小 、交換(預壓縮)大小和駐留大小的內存分配信息。對肯定 Dirty Memory 大小有顯著做用。

  • 首先打開 Xcode 選擇 Product 中的 Profile

  • 而後打開Instruments 的 Allocations 工具

當你有了 Snapshots 以後,你能夠看到 Dirty Memory 狀態隨着時間的推移。你還能夠看到哪些對象佔用了你大部分的 Dirty Memory 。若是您想更深刻地研究,可使用 VMMap,這對於高級內存調試很是有用。

命令行工具 - VMMap

與 Heap 和 Leaks 同樣,VMMap是一個很好的命令行工具,用於在虛擬內存環境中調試內存對象。

在使用 VMMap 以前,首先應該準備當前應用程序的的 Memory Graph。

  • 在運行 APP 過程當中,打開 Memory Graph

選擇 View Memory Graph Hierarchy

  • 生成完 Memory Graph 之後,點擊 File -> Export Memory graph 導出 一個 .memgraph 文件

  • 使用 VMMAP -sumary test.memgraph 命令進行分析

使用 -summary 提供了虛擬內存區域中的大小,例如虛擬內存大小,常駐內存大小,Dirty Memory 大小 ,交換大小等。Dirty Memory 和交換大小是增長內存大小的主要方面。

iOS APP 內存分配

前文咱們說過,每一個進程都有獨立的虛擬內存地址空間,也就是所謂的進程地址空間。如今咱們稍微簡化一下,一個 iOS app 對應的進程地址空間大概以下圖所示:

iOS 中的內存大體能夠分爲代碼區,全局/靜態區,常量區,堆區,棧區。

代碼區

代碼段是用來存放可執行文件的操做指令(存放函數的二進制代碼),也就是說是它是可執行程序在內存種的鏡像。代碼段須要防止在運行時被非法修改,因此只准許讀取操做,而不容許寫入(修改)操做——它是不可寫的。

全局/靜態區

全局/靜態區存放的是靜態變量,靜態全局變量,以及全局變量。初始化的全局變量,靜態變量,靜態全局變量存放在同一區域,未初始化的變量存放在相鄰的區域。程序結束後由系統釋放。

常量區

常量區存放的就是字符串常量,int常量等這些常量。

棧區

這塊區域是由編譯器自動分配並釋放的,棧區存放的是函數的參數及自動變量。棧是向低地址擴展的一塊連續的內存區域。分配在棧上的變量,當函數的做用域結束,系統就會自動銷燬變量。

堆區

堆區內存通常是由程序員本身分配並釋放的。當咱們使用 alloc 來分配內存時分配的內存就是在堆上。因爲咱們如今大部分都是使用 ARC,因此如今堆區的分配和釋放也基本不須要咱們來管理。堆區是向高地址擴展的一塊非連續區域。

棧區和堆區的比較

分配方式不一樣

棧區是由編譯器自動分配和釋放,可是堆區是由程序員來分配和釋放。

申請後系統的響應

棧區:棧區內存由編譯器分配和釋放,在函數執行時分配,在函數結束時收回。只要棧區剩餘內存大於所申請的內存,那麼系統將爲程序提供內存。 堆區:系統有一個存放空閒內存地址的鏈表,當程序員申請堆內存的時候,系統會遍歷這個鏈表,找到第一個內存大於所申請內存的堆節點,並把這個堆節點從鏈表中移除。因爲這塊內存的大小不少時候不是剛恰好所申請的同樣大,因此剩餘的那一部分還會回到這個空閒鏈表中。

申請大小的限制

棧區:棧區是向低地址擴展的數據結構,也就是說棧頂的地址和棧的容量大小是由系統決定的。棧的容量大小通常是 2M,當申請的棧內存大於 2M 時就會出現棧溢出。所以棧可分配的空間比較小。 堆區:堆是向高地址擴展的數據結構,是不連續的。堆的大小受限於計算機系統中有效的虛擬空間,所以堆可分配的空間比較大。

申請效率的比較

棧:棧由系統自動分配,速度較快,可是不受程序員控制。 堆:堆是由 alloc 分配的內存,速度較慢,而且容易產生內存碎片。

棧的限制

應用中新建立的每一個線程都有專用的棧空間,該空間由保留的內存和初始提交的內存組成。棧能夠在線程存在期間自由使用。線程的最大棧空間很小,這就決定了如下的限制。

  • 可被遞歸調用的最大方法數。

    每一個方法都有其本身的棧幀,並會消耗總體的棧空間。

  • 一個方法中最多可使用的變量個數。

    全部的變量都會載入方法的棧幀中,並消耗必定的棧空間。

  • 視圖層級中能夠嵌入的最大視圖深度。

    渲染複合視圖將在整個視圖層級樹中遞歸地調用 layoutSubViews 和 drawRect 方法。如 果層級過深,可能會致使棧溢出。

總結

知道 iOS APP 內存分配對咱們是有好處的。衆所周知的 sunnyxx 大神的 神經病院objc runtime入院考試 的最後一題:

@interface Sark : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Sark
- (void)speak {
    NSLog(@"my name's %@", self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [Sark class];
    void *obj = &cls;
    [(__bridge id)obj speak];
}
@end
複製代碼

在理解 iOS 中內存分配的入棧順序,以及棧地址如何進行偏移,才能更好地理解爲何最終答案打印的是當前的 self

內存管理

內存管理模型基於 持有關係 的概念。若是一個對象正處於被持有狀態,那它佔用的內存就不能被回收。當一個對象建立於某個方法的內部時,那該方法就持有這個對象了。若是這個對象從方法 返回,則調用者聲稱創建了持有關係。這個值能夠賦值給其餘變量,對應的變量一樣會聲稱創建了持有關係。

一旦與某個對象相關的任務所有完成,那麼就是放棄了 持有關係 。這一過程沒有轉移 持有關係 ,而是分別增長或減小了持有者的數量。當持有者的數量降爲零時,對象會被釋放相關的內存也會被回收。

這種持有關係 持有關係 被稱做 引用計數(Retain Count)。

Apple 提供了兩種內存管理的方法

MRC or MRR

咱們通常將手動管理引用計數的方法稱爲(manual reference counting,MRC)手動引用計數管理。官方文檔上則將這種方式稱爲(manual retain-release, MRR)手動持有釋放,能夠經過跟蹤本身擁有的對象顯式地管理內存。

ARC

ARC 背後的原理是依賴編譯器的靜態分析能力,經過在編譯時找出合理的插入引用計數管理代碼,從而完全解放程序員。ARC 的工做原理是在編譯時添加代碼,以確保對象的生存期儘量長,但不會更長。從概念上講,它遵循與手動引用計數相同的內存管理約定,爲開發者添加了適當的內存管理調用

手動的內存管理方法,已經淹沒在 iOS 開發的歷史長河中。對從 ARC 時代成長起來的 iOS 開發者來講,雖然 ARC 幫咱們解決了引用計數的大部分問題,可是一旦開發者須要與底層 Core Foundation 對象交互的話,就須要本身來考慮管理這些對象的引用計數。

內存管理策略

NSObject protocol 中定義的方法和命名慣例一塊兒提供了一個引用計數環境,內存管理的基本模式處於這種環境中。NSObject 類還定義了一個方法 dealloc,該方法在釋放對象時自動調用。NSObject 類做爲 Foundation 框架的根類,幾乎主要的類都繼承於 NSObject 類。

內存管理模型基於對象全部權。任何對象均可以有一個或多個全部者。只要一個對象至少有一個全部者,它就繼續存在。若是一個對象沒有全部者,運行時系統會自動銷燬它。爲了確保清楚地知道您什麼時候擁有一個對象,何時沒有,Cocoa 設置瞭如下策略:

  • 本身生成的對象,本身持有。
  • 非本身生成的對象,本身也能持有。
  • 不在須要本身持有對象的時候,釋放。
  • 非本身持有的對象無需釋放。

要想避免內存泄漏和應用崩潰,你應當在編寫 Objective-C 代碼時牢記這些規則。

錯誤的內存管理執行會致使兩種問題:

  • 釋放或覆蓋仍在使用的數據 這會致使內存損壞,一般會致使應用程序崩潰,或者更糟的是,損壞的用戶數據。
  • 不釋放再也不使用的數據會致使內存泄漏 內存泄漏是指分配的內存沒有被釋放,即便它再也不被使用。泄漏會致使應用程序使用愈來愈多的內存,而這又可能致使系統性能降低或應用程序被終止。

內存泄露

內存泄漏不等於發生了循環引用,內存泄漏是指內存在不須要訪問的時候沒有釋放,或者說任何致使對象在內存中長時間活動的緣由均可能會致使內存泄漏。

例如,不合理的使用單例,不合理的使用全局變量(主要指非全局常量), 以及不合理的存儲一些內容。

若是一個對象存活的時間足夠長,而且這個對象附帶了大量的對象屬性的話,那麼這也會致使內存泄露。

17年,網易在其公衆號上發佈了其稱爲 大白 的 iOS APP運行時Crash自動修復系統,幾乎涵蓋了當前 iOS 崩潰的主要緣由。在其 野指針防禦 一節中,當對已經釋放的內存進行訪問時,提供一個臨時對象用來響應消息傳遞。而且說明了須要控制其內存大小。

當咱們須要實現一個全局變量或單例時,或者是當咱們存儲一些信息用做信息收集時,咱們須要考慮,在何時咱們應該釋放掉內存。

當咱們使用 runtime(獲取方法,屬性、關聯對象等) 或者是進行繪製時,不要忘記釋放內存。

objc_property_t *props = class_copyPropertyList(class, &propsCount);
...
free(props);
複製代碼

例如 FXBlurView 裏,僅僅爲了展現一些 Core Foundation 繪製時須要

- (UIImage *)blurredImageWithRadius:(CGFloat)radius iterations:(NSUInteger)iterations tintColor:(UIColor *)tintColor
{
   ...
    vImage_Buffer buffer1, buffer2;
    buffer1.width = buffer2.width = CGImageGetWidth(imageRef);
    buffer1.height = buffer2.height = CGImageGetHeight(imageRef);
    buffer1.rowBytes = buffer2.rowBytes = CGImageGetBytesPerRow(imageRef);
    size_t bytes = buffer1.rowBytes * buffer1.height;
    buffer1.data = malloc(bytes);
    buffer2.data = malloc(bytes);
    ...
    //copy image data
    CFDataRef dataSource = CGDataProviderCopyData(CGImageGetDataProvider(imageRef));
    memcpy(buffer1.data, CFDataGetBytePtr(dataSource), bytes);
    CFRelease(dataSource);
    ...
    //free buffers
    free(buffer2.data);
    free(tempBuffer);
    ...
    CGImageRelease(imageRef);
    CGContextRelease(ctx);
    free(buffer1.data);
    return image;
}
 
複製代碼

如何發現內存泄露

在使用模擬器或真機運行時,咱們能夠查看應用程序的內存佔用

關注 Usage over Time 部分, 若是在運行過程當中發現內存峯值存在階梯性增加,那麼頗有可能發生內存泄露。可是此時咱們只知道出現了內存泄露。咱們須要去嘗試一些方法來找到泄露的地方。

循環引用

不可免俗的,對 iOS 來講,真正容易致使內存泄露的仍是循環引用。尤爲是在大量使用 block, delegate,NSTimer、KVO的時候。

以持有關係聲明對內存的引用就會引起一個問題,對象間互相持有,致使持有關係造成一個閉環,最終任何內存都沒法釋放。

如何發現循環引用?

在這裏咱們介紹幾個發現循環引用的方法

memory graph

經過生成 memory graph 來查看內存使用,熟練解讀 memory graph 文件的相關信息,足夠開發者發現開發過程當中的各類問題。讓咱們來調試 iOS 內存 - Memory Graph

Instruments: Allocations 和 Memory Leaks

善用 Instruments 會極大提升咱們的開發效率,而且會應用程序保駕護航。這個 WWDC Getting Started with Instruments,能讓你成爲一個 Instruments 調試高手。

善於使用 dellocdeinit

Apple 組件幾乎不會引發你的循環引用的,問題絕大可能出如今咱們這裏。咱們能夠建立一個 Memory Checker 類用來判斷 Controller 對象是否被釋放,而且 Hook Controller 的 pop 或者 dispresent 方法。

能夠改寫下面的Swift 代碼值 Objective-C 代碼

import Foundation

public class MemoryChecker {
    public static func verifyDealloc(object: AnyObject?) {
        #if DEBUG
            DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak object] in
                if let object = object {
                    fatalError("Class Not Deallocated: \(String(describing: object.classForCoder ?? object)))")
                }
            }
        #endif
    }
}
複製代碼
import UIKit

class NavigationController: UINavigationController {
    override func popViewController(animated: Bool) -> UIViewController? {
        let viewController = super.popViewController(animated: animated)
        MemoryChecker.verifyDealloc(object: viewController)
        return viewController
    }
}
複製代碼

咱們的內存並非當即釋放掉的,因此須要定義一個延遲時間。

如何避免循環引用

經過如下幾個方法來規避開發過程當中的循環引用

對象不該該持有它的父對象,應該用 weak 引用指向它的父對象

當咱們在子類中使用其父對象時, 例如在 subview 或 cell 中須要執行跳轉時,請使用 weak 修飾父對象 Controller,或者是使用代理時,請用 weak 修飾你的 delegate。

鏈接對象不該持有它們的目標對象

目標對象的角色是持有者。鏈接對象包括如下幾種。

  • 使用 delegate 的對象。委託應該被看成目標對象,即持有者。

  • 包含目標和 action 的對象。例如,UIButton

  • 觀察者模式中被觀察的對象。觀察者就是持有者,並會觀察發生在被觀察對象上的變化。

不要讓本身成爲本身的 Target 目標,請不要本身持有本身

避免在 Block 內直接引用外部的變量

避免直接引用外部變量並不意味之本身須要在每一個block前都是用 __weak 獲取一個 weakSelf,無腦添加 weakSelfstrongSelf 。如何分析當前 Block 內到底有沒有造成閉環,須要開發者好好思考。

這裏推薦閱讀霜神的 深刻研究 Block 用 weakSelf、strongSelf、@weakify、@strongify 解決循環引用

在組件移除時,請主動釋放本身

當咱們使用子組件來封裝一些頁面時,例如輪播圖、計時器等操做時,能夠監聽頁面的移除方法,而後在移除時作一些內存釋放。或者執行一些方法時,在方法執行完成,將自身置爲 nil。

- (void)willMoveToSuperview:(UIView *)newSuperview {
    if (!newSuperview) {
        [self invalidateTimer];
    }
}
複製代碼
善於利用 NSProxy 來打破循環

NSProxy 實現根類所需的基本方法,包括那些在 NSObject protocol 協議中定義的方法。可是,做爲一個抽象類,它不提供初始化方法,而且在接收到任何它不響應的消息時引起異常。

NSProxy 一般用來實現消息轉發機制和惰性初始化資源。

使用 NSProxy,你須要寫一個子類繼承它,而後須要實現 init 以及消息轉發的相關方法。

咱們可使用 NSProxy 來處理一些強引用的 target, 打破循環引用。這裏主要處理 NSTimer。

@interface WeakProxy : NSProxy

@property (weak, nonatomic, readonly) id target;

+ (instancetype)proxyWithTarget:(id)target;

- (instancetype)initWithTarget:(id)target;

@end
  
@implementation WeakProxy

+ (instancetype)proxyWithTarget:(id)target{
    return [[self alloc] initWithTarget:target];
}

- (instancetype)initWithTarget:(id)target{
    _target = target;
    return self;
}

- (void)forwardInvocation:(NSInvocation *)invocation{
    SEL sel = [invocation selector];
    if (self.target && [self.target respondsToSelector:sel]) {
        [invocation invokeWithTarget:self.target];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    return [self.target methodSignatureForSelector:aSelector];
}

- (BOOL)respondsToSelector:(SEL)aSelector{
    return [self.target respondsToSelector:aSelector];
}

@end
複製代碼

學會使用自動釋放池

衆所周知,整個 iOS 的應用都是包含在一個自動釋放池 block 中的

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
複製代碼

雖然,Swift 沒有 main 函數,可是有一個 @UIApplicationMain 來替代 main 函數。

系統在每一個runloop迭代中都加入了自動釋放池Push和Pop

AppKit 和 UIKit 框架將事件 - 循環的迭代放入了 autoreleasepool 塊中。所以,一般 不須要你本身再建立 autoreleasepool 塊了。

何時咱們應該建立本身的 autoreleasepool 呢?

  • 當咱們建立一個不少臨時對象的循環時

    咱們說過,AppKit 和 UIKit 框架將事件 - 循環的迭代放入了 autoreleasepool 塊中,因此當循環執行完成之後,內存會降到必定的值,可是咱們能夠經過建立本身的 autoreleasepool 來避免內存峯值。

  • 在異步線程中實現 autoreleasepool

    iOS 應用程序包含在一個主 autoreleasepool 中,因此當主線程的 runloop 完成一次迭代時,autoreleasepool會自動進行釋放操做,而對於異步線程,能夠本身主動實現一個autoreleasepool

參考文檔

《Memory and Virtual Memory》

《Abolish Retain Cycles》

《The case of iOS OOM Crashes at Compass》

《iOS — Advanced Memory Debugging to the Masses》

《iOS Memory Allocation》

《What is the difference between ROM and RAM?》

《Learn more about oom (low memory crash) in iOS》

《iOS Memory Deep Dive - WWDC 2018 - Videos - Apple Developer》

《iOS 的內存分配

《Advanced Memory Management Programming Guide》

《Transitioning to ARC Release Notes》

《Memory Management Programming Guide for Core Foundation》

相關文章
相關標籤/搜索