關於iOS內存的深刻排查和優化

一些內存相關的名詞

虛擬內存VM

虛擬內存機制在這裏就很少說了,主要包括內存管理單元MMU、內存映射、分段、分頁。在iOS中,一頁一般有16KB的內存空間。html

分配內存的時候,先分配虛擬內存,而後使用的時候再映射到實際的物理內存。ios

一個VM Region指的是一段連續的虛擬內存頁,這些頁的屬性都相同。git

/* localized structure - cannot be safely passed between tasks of differing sizes */
/* Don't use this, use MACH_TASK_BASIC_INFO instead */
struct task_basic_info {
    integer_t       suspend_count;  /* suspend count for task */
    vm_size_t       virtual_size;   /* virtual memory size (bytes) */
    vm_size_t       resident_size;  /* resident memory size (bytes) */
    time_value_t    user_time;      /* total user run time for * terminated threads */
    time_value_t    system_time;    /* total system run time for * terminated threads */
    policy_t        policy;         /* default policy for new threads */
};

struct mach_task_basic_info {
    mach_vm_size_t  virtual_size;       /* virtual memory size (bytes) */
    mach_vm_size_t  resident_size;      /* resident memory size (bytes) */
    mach_vm_size_t  resident_size_max;  /* maximum resident memory size (bytes) */
    time_value_t    user_time;          /* total user run time for * terminated threads */
    time_value_t    system_time;        /* total system run time for * terminated threads */
    policy_t        policy;             /* default policy for new threads */
    integer_t       suspend_count;      /* suspend count for task */
};
複製代碼

VM分爲Clean Memory和Dirty Memory。即:github

虛擬內存 Virtual Memory = Dirty Memory + Clean Memory + Compressed Memory。
複製代碼

使用malloc函數,申請一段堆內存,則該內存爲Clean的。一旦寫入數據,一般這塊內存會變成Dirty。web

獲取App申請到的全部虛擬內存:shell

- (int64_t)memoryVirtualSize {
    struct task_basic_info info;
    mach_msg_type_number_t size = (sizeof(task_basic_info_data_t) / sizeof(natural_t));
    kern_return_t ret = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size);
    if (ret != KERN_SUCCESS) {
        return 0;
    }
    return info.virtual_size;
}
複製代碼

mach_task_self()表示獲取當前的Mach task。macos

Clean Memory

能夠簡單理解爲可以被寫入數據的乾淨內存。對開發者而言是read-only,而iOS系統能夠寫入或移除。promise

  1. System Framework、Binary Executable佔用的內存
  2. 能夠被釋放(Page Out,iOS上是壓縮內存的方式)的文件,包括內存映射文件Memory mapped file(如image、data、model等)。內存映射文件一般是隻讀的。
  3. 系統中可回收、可複用的內存,實際不會當即申請到物理內存,而是真正須要的時候再給。
  4. 每一個framework都有_DATA_CONST段,當App運行時使用到了某個framework,該framework對應的_DATA_CONST的內存就由clean變爲dirty了。

注意:若是經過文件內存映射機制memory mapped file載入內存的,能夠先清除這部份內存佔用,須要的時候再從文件載入到內存。因此是Clean Memory。緩存

Dirty Memory

主要強調不可被重複使用的內存。對開發者而言,已經寫入數據。安全

  1. 被寫入數據的內存,包括全部heap中的對象、圖像解碼緩衝(ImageIO, CGRasterData,IOSurface)。
  2. 已使用的實際物理內存,系統沒法自動回收。
  3. heap allocation、caches、decompressed images。
  4. 每一個framework的_DATA段和_DATA_DIRTY段。

iOS中的內存警告,只會釋放clean memory。由於iOS認爲dirty memory有數據,不能清理。因此,應儘可能避免dirty memory過大。

要清楚地知道Allocations和Dirty Size分別是由於什麼?

值得注意的是,在使用 framework 的過程當中會產生 Dirty Memory,使用單例或者全局初始化方法是減小 Dirty Memory 不錯的方法,由於單例一旦建立就不會銷燬,全局初始化方法會在 class 加載時執行。

下方有測量實驗,如+50dirty的操做,在release環境不生效,因iOS系統自動作了優化。

Compressed Memory

iOS設備沒有swapped memory,而是採用Compressed Memory機制,通常狀況下能將目標內存壓縮至原有的一半如下。對於緩存數據或可重建數據,儘可能使用NSCache或NSPurableData,收到內存警告時,系統自動處理內存釋放操做。而且是線程安全的。

這裏要注意,壓縮內存機制,使得內存警告與釋放內存變得稍微複雜一些。即,對於已經被壓縮過的內存,若是嘗試釋放其中一部分,則會先將它解壓。而解壓過程帶來的內存增大,可能獲得咱們並不期待的結果。若是選用NSDictionary之類的,內存比較緊張時,嘗試將NSDictionary的部份內存釋放掉。但若NSDictionary以前是壓縮狀態,釋放須要先解壓,解壓過程可能致使內存增大而拔苗助長。

因此,咱們日常開發所關心的內存佔用實際上是 Dirty Size和Compressed Size兩部分,也應儘可能優化這兩部分。而Clean Memory通常不用太多關注。

Resident Memory

已經被映射到虛擬內存中的物理內存。而phys_footprint纔是真正消耗的物理內存。

Resident Memory = Dirty Memory + Clean Memory that loaded in pysical memory。
複製代碼

獲取App消耗的Resident Memory:

- (int64_t)memoryResidentSize {
    struct task_basic_info info;
    mach_msg_type_number_t size = sizeof(task_basic_info_data_t) / sizeof(natural_t);
    kern_return_t ret = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size);
    if (ret != KERN_SUCCESS) {
        return 0;
    }
    return info.resident_size;
}
複製代碼

Memory Footprint

/* * phys_footprint * Physical footprint: This is the sum of: * + (internal - alternate_accounting) * + (internal_compressed - alternate_accounting_compressed) * + iokit_mapped * + purgeable_nonvolatile * + purgeable_nonvolatile_compressed * + page_table * * internal * The task's anonymous memory, which on iOS is always resident. * * internal_compressed * Amount of this task's internal memory which is held by the compressor. * Such memory is no longer actually resident for the task [i.e., resident in its pmap], * and could be either decompressed back into memory, or paged out to storage, depending * on our implementation. * * iokit_mapped * IOKit mappings: The total size of all IOKit mappings in this task, regardless of clean/dirty or internal/external state]. * * alternate_accounting * The number of internal dirty pages which are part of IOKit mappings. By definition, these pages * are counted in both internal *and* iokit_mapped, so we must subtract them from the total to avoid * double counting. */
複製代碼

App消耗的實際物理內存,包括:

  1. Dirty Memory
  2. Clean memory but loaded in pysical memory
  3. Page Table
  4. Compressed Memory
  5. IOKit used
  6. NSCache, Purgeable等

獲取App的Footprint:

- (int64_t)memoryPhysFootprint {
    task_vm_info_data_t vmInfo;
    mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
    kern_return_t ret = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&vmInfo, &count);
    if (ret != KERN_SUCCESS) {
        return 0;
    }
    return vmInfo.phys_footprint;
}
複製代碼

XNU中Jetsam判斷內存過大,使用的也是phys_footprint,而非resident size。

獲取設備的全部物理內存大小,可使用

[NSProcessInfo processInfo].physicalMemory
複製代碼

內存測量結果

測量環境

iPhone 7, iOS 13.3。

Clean Memory

初始狀態

類型 內存值(MB) 分析
resident 59 App消耗的內存
footprint 13 實際物理內存
VM 4770 App分配的虛擬內存
Xcode Navigator 14.3 footprint + 調試須要

加50MB的clean memory

代碼爲:

__unused char *buf = malloc(50 * 1024 * 1024);
複製代碼
類型 內存值(MB) 增量 分析
resident 60 +1 App消耗的內存
footprint 14 +1 實際物理內存
VM 4817 +47 App分配的虛擬內存
Xcode Navigator 14.3 +0 footprint + 調試須要

實際,僅增長50MB的VM,而這裏額外會有1~2MB的footprint增長,猜想是用於內存映射所需的。

到達虛擬內存上限會報錯: error: can't allocate region,但不會致使崩潰***。

同時,申請的過程不會耗時

再加50MB的clean memory

類型 內存值(MB) 增量 分析
resident 60 +0 App消耗的內存
footprint 14 +0 實際物理內存
VM 4868 +51 App分配的虛擬內存
Xcode Navigator 14.3 +0 footprint + 調試須要

Dirty Memory

Resident、footprint、VM都增長。是實實在在的內存消耗,各個工具都會統計。

初始狀態

類型 內存值(MB) 分析
resident 59 App消耗的內存
footprint 13 實際物理內存
VM 4769 App分配的虛擬內存
Xcode Navigator 14.3 footprint + 調試須要

加50MB的dirty memory

代碼爲:

// 僅此一句,依然是僅申請虛擬內存,物理內存不會變
char *buf = malloc(50 * 1024 * 1024 * sizeof(char));

// 內存使用了,因此是實際的物理內存被使用了。即內存有數據了,變成dirty memory。
for (int i = 0; i < 50 * 1024 * 1024; i++) {
    buf[i] = (char)rand();
}
複製代碼
類型 內存值(MB) 增量 分析
resident 110 +51 App消耗的內存
footprint 64 +51 實際物理內存
VM 4817 +48 App分配的虛擬內存
Xcode Navigator 64.4 +50.1 footprint + 調試須要

實際增長了50MB的物理內存,Resident Memory也會變化,同時額外多了1~2MB。

申請過程比較耗時,超出上限會致使崩潰

但該操做僅在debug下生效,release環境不生效,應該是iOS系統自行的優化。

再加50MB的dirty memory

類型 內存值(MB) 增量 分析
resident 160 +50 App消耗的內存
footprint 114 +50 實際物理內存
VM 4868 +51 App分配的虛擬內存
Xcode Navigator 114.4 +50 footprint + 調試須要

Clean Memory + Dirty Memory

初始狀態

類型 內存值(MB) 分析
resident 59 App消耗的內存
footprint 13 實際物理內存
VM 4770 App分配的虛擬內存
Xcode Navigator 14.3 footprint + 調試須要

加50MB的clean memory,使用其中10MB

代碼爲:

// 申請50MB的虛擬內存
char *buf = malloc(50 * 1024 * 1024 * sizeof(char));

// 實際只用了10MB,因此10MB的dirty memory
for (int i = 0; i < 10 * 1024 * 1024; i++) {
    buf[i] = (char)rand();
}
複製代碼
類型 內存值(MB) 增量 分析
resident 70 +11 App消耗的內存
footprint 24 +11 實際物理內存
VM 4817 +47 App分配的虛擬內存
Xcode Navigator 24.3 +10 footprint + 調試須要

申請了50MB,但實際僅使用了10MB,所以只有這10MB爲Dirty Memory。

再加50MB的clean memory,使用其中10MB

類型 內存值(MB) 增量 分析
resident 80 +10 App消耗的內存
footprint 34 +10 實際物理內存
VM 4868 +51 App分配的虛擬內存
Xcode Navigator 34.3 +10 footprint + 調試須要

VM

初始狀態

類型 內存值(MB) 分析
resident 59 App消耗的內存
footprint 13 實際物理內存
VM 4770 App分配的虛擬內存
Xcode Navigator 14.3 footprint + 調試須要

加100MB的VM

代碼爲:

vm_address_t address;
vm_size_t size = 100*1024*1024;
// VM Tracker中顯示Memory Tag 200
vm_allocate((vm_map_t)mach_task_self(), &address, size, VM_MAKE_TAG(200) | VM_FLAGS_ANYWHERE);
// VM Tracker中顯示VM_MEMORY_MALLOC_HUGE
// vm_allocate((vm_map_t)mach_task_self(), &address, size, VM_MAKE_TAG(VM_MEMORY_MALLOC_HUGE) | VM_FLAGS_ANYWHERE);
複製代碼
類型 內存值(MB) 增量 分析
resident 60 +1 App消耗的內存
footprint 14 +1 實際物理內存
VM 4867 +97 App分配的虛擬內存
Xcode Navigator 14.3 +0 footprint + 調試須要

這裏,mach_task_self()表示在本身的進程空間內申請,size的單位是byte。使用參數VM_MAKE_TAG(200)給申請的內存提供一個Tag標記,該數字在VM Tracker中會有標記。

再加100MB的VM

類型 內存值(MB) 增量 分析
resident 60 +0 App消耗的內存
footprint 14 +0 實際物理內存
VM 4967 +100 App分配的虛擬內存
Xcode Navigator 14.3 +0 footprint + 調試須要

UIImage

圖片大小:map.jpg: 9054*5945

初始狀態

類型 內存值(MB) 分析
resident 60 App消耗的內存
footprint 14 實際物理內存
VM 4768 App分配的虛擬內存
Xcode Navigator 14.3 footprint + 調試須要

self.image = [UIImage imageNamed:@"map.jpg"]

類型 內存值(MB) 增量 分析
resident 61 +2 App消耗的內存
footprint 14 +0 實際物理內存
VM 4768 +0 App分配的虛擬內存
Xcode Navigator 14.4 +0.1 footprint + 調試須要

構建UIImage對象所須要的圖片數據消耗其實不大。這裏的數據指的是壓縮的格式化數據。

self.imageView.image = self.image;

類型 內存值(MB) 增量 分析
resident 61 +0 App消耗的內存
footprint 92 +78 實際物理內存
VM 4845 +77 App分配的虛擬內存
Xcode Navigator 92 +77.6 footprint + 調試須要

這個階段,須要將圖片數據解碼成像素數據bitmap,並渲染到屏幕上。解碼過程很是消耗內存和CPU資源,且默認在主線程中執行會阻塞主線程。

關於這裏的一些詳細信息及優化(如異步解碼圖片數據,主線程渲染),請看後文。

結論

經過以上的比較,能夠對各個內存類型有一個初步直觀的認識。

  1. footprint是App實際消耗的物理內存
  2. resident是實際映射到虛擬內存的物理內存
  3. 一般看到的Xcode Navigator顯示的最接近footprint,另外還有一些調試須要的內存。

幾種內存查看方式的區別

Xcode Navigator

初略展現了真實的物理內存消耗。顏色代表了內存佔用是否合理。Xcode Navigator = footprint + 調試須要。不跟蹤VM。每每初略觀察App的內存佔用狀況,不能做爲精確的參考。

Instuments Allocations

這裏顯示的內存,其實只是整個App佔用內存的一部分,即開發者自行分配的內存,如各類類實例等。簡單而言,就是開發者自行malloc申請的。

  1. 主要是MALLOC_XXX, VM Region, 以及部分App進程建立的VM Region。
  2. 非動態的內存,及部分其餘動態庫建立的VM Region並不在Allocations的統計範圍內。
  3. 主程序或動態庫的_DATA數據段、Stack函數棧,並不是經過malloc分配,所以不在Allocations統計內。

All Heap Allocations

  1. malloc
  2. CFData
  3. 其餘手動申請的內存,如 *char buf = malloc(50 * 1024 * 1024 * sizeof(char));
Malloc

開發者手動分配的內存塊,好比一些人臉檢測模型等,還有一些C/C++代碼中的。

All Anonymous VM

沒法由開發者直接控制,通常由系統接口調用申請的。例如圖片之類的大內存,屬於All Anonymous VM -> VM: ImageIO_IOSurface_Data,其餘的還有IOAccelerator與IOSurface等跟GPU關係比較密切的.

VM: IOAccelerator

CVPixelBuffer: An image buffer that holds pixels in main memory.

A Core Video pixel buffer is an image buffer that holds pixels in main memory. Applications generating frames, compressing or decompressing video, or using Core Image can all make use of Core Video pixel buffers.

主要是CVPixelBuffer,一般使用Pool來管理,交給系統自動釋放。而釋放的時機徹底由系統決定,開發者沒法控制。

若是不太須要複用的話,能夠考慮改成直接使用create函數,再也不復用。這樣能保證及時釋放掉。

VM: IOSurface

IOSurface是用於存儲FBO、RBO等渲染數據的底層數據結構,是跨進程的,一般在CoreGraphics、OpenGLES、Metal之間傳遞紋理數據。該結構和硬件相關。提供CPU訪問VRAM的方式,如建立IOSurface對象後,在CPU往對象裏塞紋理數據,GPU就能夠直接使用該紋理了。能夠簡單理解爲IOSurface,爲CPU和GPU直接搭建了一個傳遞紋理數據的橋樑。

Share hardware-accelerated buffer data (framebuffers and textures) across multiple processes. Manage image memory more efficiently.

The IOSurface framework provides a framebuffer object suitable for sharing across process boundaries. It is commonly used to allow applications to move complex image decompression and draw logic into a separate process to enhance security.

如下內容參考自:iOS 內存管理研究,總結得很是到位了。

(CGImage是一個能夠惰性初始化(持有原始壓縮格式DataBuffer),而且經過相似引用計數管理真正的Image Bitmap Buffer的設計,
只有渲染時經過RetainBytePtr拿到Bitmap Buffer塞給VRAM(IOSurface),不渲染時ReleaseBytePtr釋放Bitmap Buffer,DataBuffer佔用自己就小)。
一般咱們使用UIImageView,系統會自動處理解碼過程,在主線程上解碼和渲染,會佔用CPU,容易引發卡頓。
推薦使用ImageIO在後臺線程執行圖片的解碼操做(可參考SDWebImageCoder)。可是ImageIO不支持webp。

ASDK的原理:拿空間換時間,換取流暢,犧牲內存,但內存開銷比UIKit高。
	正經常使用一個全屏的UIImageView,直接用image = UIImage(named:xxx)來設置圖片,要在主線程解碼,但消耗內存反而較小,只有4MB(正常須要10MB)。
	應該是IOSurface對圖片數據作了一些優化。但若是是很是大的圖片就會阻塞,不建議直接渲染。
	CGImage是一個能夠惰性初始化(持有原始壓縮格式DataBuffer),而且經過相似ARC管理真正的Image Bitmap Buffer的設計。
	只有渲染時候經過RatainBytePtr拿到Bitmap Buffer塞給VRAM(IOSurface),不渲染時ReleaseBytePtr釋放Bitmap Buffer,DataBuffer自己佔用很小。
複製代碼
VM: Stack

調用堆棧,通常不須要作啥。每一個線程都須要500KB左右的棧空間,主線程1MB。

VM: CG raster data

SDWebImage的圖片解碼數據的緩存,爲了不渲染時在主線程解碼致使阻塞。若是對於這一點比較介意,能夠作相應設置便可:

/// Decompressing images that are downloaded and cached can improve peformance but can consume lot of memory.
/// Defaults to YES. Set this to NO if you are experiencing a crash due to excessive memory consumption.
[[SDImageCache sharedImageCache] setShouldDecompressImages:NO];
[[SDWebImageDownloader sharedDownloader] setShouldDecompressImages:NO];
[[SDImageCache sharedImageCache] setShouldCacheImagesInMemory:NO];
複製代碼

常見堆棧:

mmap
CGDataProvicerCreateWithCopyOfData
CGBitmapContextCreateImage
[SDWebImageWebPCoder decodedImageWithData:]
[SDWebImageCodersManager decodedImageWithData:]
[SDImageCache diskImageForKey:data:options:]
[SDImageCache queryCacheOperationForKey:options:done:]_block_invoke
複製代碼

Instuments VM Tracker

interesting VM regions such as graphics- and Core Data-related. Hides mapped files, dylibs, and some large reserved VM regions.

比較大塊的內存佔用,如WebKit、ImageIO、CoreAnimation等VM Region,通常由系統生成和管理。

  1. 數據段_DATA,如佔用VM爲10.6MB,Resident爲6261KB,Dirty爲1930KB。
  2. 數據段_DATA_CONST,每一個framework都有,當App在運行時用到了該framework,則此段內存由clean變爲dirty。如佔用VM爲33.9MB,Resident爲31.5MB,Dirty爲4466KB。
  3. 數據段_DATA_DIRTY,每一個framework都有_DATA段和_DATA_DIRTY段,內存是dirty的。如佔用VM爲862KB,Resident爲798KB,Dirty爲451KB。
  4. 有_LINKEDIT,包含了方法和變量的元數據(位置、偏移量),及代碼簽名等信息。如佔用VM爲98MB,Resident爲22.4MB,Dirty爲0KB. 注意:Dirty爲0.
  5. 代碼段_TEXT,如佔用VM爲252.9MB,Resident爲133.7MB,Dirty爲80KB。 注意:Dirty幾乎爲0.
  6. mapped file,如佔用VM爲104.4MB,Resident爲7472KB,Dirty爲32KB。clean memory。
  7. shared memory,如佔用VM爲64KB,Resident爲64KB,Dirty爲64KB。
  8. unused but dirty shlib __DATA,如佔用VM爲721KB,Resident爲721KB,Dirty爲721KB。

其餘好比MALLOC_LARGE,MALLOC_NANO等都是申請VM的時候設置的tag。

  1. MALLOC_LARGE, 如佔用VM爲384KB,Resident爲384KB,Dirty爲384KB。
  2. MALLOC_NANO, 如佔用VM爲512MB,Resident爲1584KB,Dirty爲1568KB。
  3. MALLOC_SMALL, 如佔用VM爲24MB,Resident爲896KB,Dirty爲800KB。
  4. MALLOC_TINY, 如佔用VM爲4096KB,Resident爲432KB,Dirty爲432KB。
  5. Stack, 如佔用VM爲2096KB,Resident爲144KB,Dirty爲128KB。
  6. Performance tool data, 調試所需,如佔用VM爲336KB,Resident爲336KB,Dirty爲336KB。

分析一個VM Tracker的截圖

例如:

Type All 那一行說明:

  1. App一共申請了1.55GB的虛擬內存
  2. App實際使用的虛擬內存(Resident + Swapped = 488.91MB + 157.75MB = 646.66GB)
  3. iOS Swapped 157.75MB,不懂。其實就是Compressed。
  4. 實際物理內存Resident Memory爲488.91MB
  5. Resident Memory中一共包含Dirty Memory爲371.91MB

VM Tracker中的內存Type

VM_Tracker如何識別出每一個內存塊的Type?答案即爲vm_allocate函數調用時的最後一個參數flags。如MALLOC_TINY, MALLOC_SMALL, MALLOC_LARGE, ImageIO等。 vm_allocate((vm_map_t)mach_task_self(), &address, size, VM_MAKE_TAG(200) | VM_FLAGS_ANYWHERE); VM_FLAGS_ANYWHERE是flags中控制內存分配方式的flag,表示能夠接受任意位置。

#define VM_FLAGS_FIXED 0x0000
#define VM_FLAGS_ANYWHERE 0x0001
#define VM_FLAGS_PURGABLE 0x0002
#define VM_FLAGS_4GB_CHUNK 0x0004
#define VM_FLAGS_RANDOM_ADDR 0x0008
#define VM_FLAGS_NO_CACHE 0x0010
#define VM_FLAGS_RESILIENT_CODESIGN 0x0020
#define VM_FLAGS_RESILIENT_MEDIA 0x0040
#define VM_FLAGS_OVERWRITE 0x4000 /* delete any existing mappings first */
複製代碼

參考:iOS內存深刻探索之VM Tracker

即 2個字節就可存儲該flag,而int4個字節的剩下兩個就可用於存儲標記內存類型的Type了。
VM_MAKE_TAG可快速設置Type。
#define VM_MAKE_TAG(tag) ((tag) << 24)
將值左移24個bit,即3個字節,則一個字節表示內存類型。

蘋果內置的Type有:
#define VM_MEMORY_MALLOC 1
#define VM_MEMORY_MALLOC_SMALL 2
#define VM_MEMORY_MALLOC_LARGE 3
#define VM_MEMORY_MALLOC_HUGE 4
#define VM_MEMORY_SBRK 5// uninteresting -- no one should call
#define VM_MEMORY_REALLOC 6
#define VM_MEMORY_MALLOC_TINY 7
#define VM_MEMORY_MALLOC_LARGE_REUSABLE 8
#define VM_MEMORY_MALLOC_LARGE_REUSED 9
因此,這個地方的Type即爲VM Tracker中顯示的Type。
而設置本身的數字也是爲了快速定位到本身的虛擬內存。
複製代碼

Xcode Memory Debugger

該工具能夠很是方便地查看全部對象的內存使用狀況、依賴關係,以及循環引用等。若是將其導出爲memgraph文件,也可使用一些命令來進行分析:

vmmap

vmmap memory-info.memgraph
# 查看摘要
vmmap --summary memory-info.memgraph
複製代碼

結合shell中的grep、awk等命令,能夠得到任何想要的內存數據。

# 查看全部dylib的Dirty Pages的總和
vmmap -pages memory-info.memgraph | grep '.dylib' | awk '{sum += $6} END { print "Total Dirty Pages:"sum}'
# 查看CG image相關的內存數據
vmmap memory-info.memgraph | grep 'CG image'
複製代碼

heap

查看堆內存

# 查看Heap上的全部對象
heap memory-info.memgraph
# 按照內存大小來排序
heap memory-info.memgraph -sortBySize
# 查看某個類的全部實例對象的內存地址
heap memory-info.memgraph -addresses all | 'MyDataObject'
複製代碼

leaks

# 查看是否有內存泄漏
leaks memory-info.memgraph
# 查看內存地址處的泄漏狀況
leaks --traceTree [內存地址] memory-info.memgraph
複製代碼

malloc_history

須要開啓Run->Diagnostics中的Malloc Stack功能,建議使用Live Allocations Only。則lldb會記錄debug過程當中的對象建立的堆棧,配合malloc_history,便可定位對象的建立過程。

malloc_history memory-info.memgraph [address]
malloc_history memory-info.memgraph --fullStacks [address]
複製代碼

自定義內存統計工具

內存分配的源碼探究

經過學習libmalloc的源碼,能夠知道,咱們一般都使用malloc來申請內存,其本質就是從vmpage映射獲取內存。

malloc有一系列相關方法,calloc,ralloc,valloc,malloc_zone_malloc,malloc_zone_calloc, malloc_zone_valloc, malloc_zone_realloc, malloc_zone_batch_malloc等。大內存的分配都是經過scalable_zone進行分配。

在libmalloc/src/malloc.c中:

/********* Generic ANSI callouts ************/

void * malloc(size_t size) {
	void *retval;
	retval = malloc_zone_malloc(default_zone, size);
	if (retval == NULL) {
		errno = ENOMEM;
	}
	return retval;
}

void * calloc(size_t num_items, size_t size) {
	void *retval;
	retval = malloc_zone_calloc(default_zone, num_items, size);
	if (retval == NULL) {
		errno = ENOMEM;
	}
	return retval;
}

void free(void *ptr) {
	malloc_zone_t *zone;
	size_t size;
	if (!ptr) {
		return;
	}

	zone = find_registered_zone(ptr, &size);
	if (!zone) {
		int flags = MALLOC_REPORT_DEBUG | MALLOC_REPORT_NOLOG;
		if ((malloc_debug_flags & (MALLOC_ABORT_ON_CORRUPTION | MALLOC_ABORT_ON_ERROR))) {
			flags = MALLOC_REPORT_CRASH | MALLOC_REPORT_NOLOG;
		}
		malloc_report(flags,
				"*** error for object %p: pointer being freed was not allocated\n", ptr);
	} else if (zone->version >= 6 && zone->free_definite_size) {
		malloc_zone_free_definite_size(zone, ptr, size);
	} else {
		malloc_zone_free(zone, ptr);
	}
}

void * realloc(void *in_ptr, size_t new_size) {
	void *retval = NULL;
	void *old_ptr;
	malloc_zone_t *zone;

	// SUSv3: "If size is 0 and ptr is not a null pointer, the object
	// pointed to is freed. If the space cannot be allocated, the object
	// shall remain unchanged." Also "If size is 0, either a null pointer
	// or a unique pointer that can be successfully passed to free() shall
	// be returned." We choose to allocate a minimum size object by calling
	// malloc_zone_malloc with zero size, which matches "If ptr is a null
	// pointer, realloc() shall be equivalent to malloc() for the specified
	// size." So we only free the original memory if the allocation succeeds.
	old_ptr = (new_size == 0) ? NULL : in_ptr;
	if (!old_ptr) {
		retval = malloc_zone_malloc(default_zone, new_size);
	} else {
		zone = find_registered_zone(old_ptr, NULL);
		if (!zone) {
			int flags = MALLOC_REPORT_DEBUG | MALLOC_REPORT_NOLOG;
			if (malloc_debug_flags & (MALLOC_ABORT_ON_CORRUPTION | MALLOC_ABORT_ON_ERROR)) {
				flags = MALLOC_REPORT_CRASH | MALLOC_REPORT_NOLOG;
			}
			malloc_report(flags, "*** error for object %p: pointer being realloc'd was not allocated\n", in_ptr);
		} else {
			retval = malloc_zone_realloc(zone, old_ptr, new_size);
		}
	}

	if (retval == NULL) {
		errno = ENOMEM;
	} else if (new_size == 0) {
		free(in_ptr);
	}
	return retval;
}

void * valloc(size_t size) {
	void *retval;
	malloc_zone_t *zone = default_zone;
	retval = malloc_zone_valloc(zone, size);
	if (retval == NULL) {
		errno = ENOMEM;
	}
	return retval;
}

extern void vfree(void *ptr) {
	free(ptr);
}
複製代碼

相似malloc_zone_malloc的函數,會真正執行內存分配的操做,注意其中的malloc_logger,系統會有默認的malloc_logger函數對內存分配狀況進行記錄。

void * malloc_zone_malloc(malloc_zone_t *zone, size_t size) {
	MALLOC_TRACE(TRACE_malloc | DBG_FUNC_START, (uintptr_t)zone, size, 0, 0);

	void *ptr;
	if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
		internal_check();
	}
	if (size > MALLOC_ABSOLUTE_MAX_SIZE) {
		return NULL;
	}

	ptr = zone->malloc(zone, size);		// if lite zone is passed in then we still call the lite methods

	
	if (malloc_logger) {
		malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE, (uintptr_t)zone, (uintptr_t)size, 0, (uintptr_t)ptr, 0);
	}

	MALLOC_TRACE(TRACE_malloc | DBG_FUNC_END, (uintptr_t)zone, size, (uintptr_t)ptr, 0);
	return ptr;
}
複製代碼

在malloc_zone_malloc,malloc_zone_calloc,malloc_zone_valloc,malloc_zone_realloc,malloc_zone_free,malloc_zone_free_definite_size,malloc_zone_memalign,malloc_zone_batch_malloc一系列內存相關函數中,都有malloc_logger的使用。

所以,能夠經過hook malloc_logger函數來分析內存分配狀況。

對malloc_logger函數進行hook

注意:使用fishhook對malloc_logger函數進行hook,而後就能夠對內存進行詳細的統計了。這個說法是錯誤的!!!

因malloc_logger自己就是一個函數指針,須要的時候,直接給其傳遞一個實現便可。iOS系統即有一個默認的實現。

在libmalloc的源碼中能夠看到:

typedef void(malloc_logger_t)(uint32_t type, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3, uintptr_t result, uint32_t num_hot_frames_to_skip);

extern malloc_logger_t *__syscall_logger; // use this to set up syscall logging (e.g., vm_allocate, vm_deallocate, mmap, munmap)
複製代碼
// Only setup stack logging hooks once lazy initialization is complete, the
// malloc_zone calls above would otherwise initialize malloc stack logging,
// which calls into malloc re-entrantly from Libc upcalls and so deadlocks
// in the lazy initialization os_once(). rdar://13046853
if (stack_logging_enable_logging) {
	switch (stack_logging_mode) {
	case stack_logging_mode_malloc:
		malloc_logger = __disk_stack_logging_log_stack;
		break;
	case stack_logging_mode_vm:
		__syscall_logger = __disk_stack_logging_log_stack;
		break;
	case stack_logging_mode_all:
		malloc_logger = __disk_stack_logging_log_stack;
		__syscall_logger = __disk_stack_logging_log_stack;
		break;
	case stack_logging_mode_lite:
		__syscall_logger = __disk_stack_logging_log_stack;
		create_and_insert_lite_zone_while_locked();
		enable_stack_logging_lite();
		break;
	case stack_logging_mode_vmlite:
		__syscall_logger = __disk_stack_logging_log_stack;
		break;
	}
}
複製代碼

咱們只須要對其傳遞一個實現函數便可作到hook。同時,注意不要將系統默認的mallc_logger實現覆蓋掉了。

typedef void (malloc_logger_t)(uint32_t type, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3, uintptr_t result, uint32_t num_hot_frames_to_skip);
extern malloc_logger_t *malloc_logger;
extern malloc_logger_t *__syscall_logger; // use this to set up syscall logging (e.g., vm_allocate, vm_deallocate, mmap, munmap)

malloc_logger_t *orig_malloc_logger;
void __my_malloc_logger(uint32_t type, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3, uintptr_t result, uint32_t num_hot_frames_to_skip);
void my_malloc_logger(uint32_t type, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3, uintptr_t result, uint32_t num_hot_frames_to_skip) {
    if (orig_malloc_logger) {
        /// 系統的
        orig_malloc_logger(type, arg1, arg2, arg3, result, num_hot_frames_to_skip);
    }
    /// 添加本身的一些統計等操做。
    __my_malloc_logger(type, arg1, arg2, arg3, result, num_hot_frames_to_skip);
}

void __my_malloc_logger(uint32_t type, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3, uintptr_t result, uint32_t num_hot_frames_to_skip)
{
  // 根據type對內存進行分析。
}
複製代碼
int main(int argc, char * argv[]) {   
    /// malloc_logger自己就是一個hook函數,若是須要的話,只給其指定一個實現便可。
    /// 注意:不要影響了系統對其的實現。因此要先保存系統的,而後在自定義的實現中調用系統的。
    if (malloc_logger && malloc_logger != my_malloc_logger) {
        orig_malloc_logger = malloc_logger;
    }
    malloc_logger = (malloc_logger_t *)my_malloc_logger;

    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
複製代碼

對內存的詳細分析,能夠參考 OOMDetector 以及 MTHawkeye

幾種內存測量方式的使用建議

一般狀況下,

  1. 各個工具展現的內存值可能不一致,由於其統計的方式及包含內存類型不一致。如Xcode Navigator一般只反映內存佔用的大概狀況,詳細信息須要經過Allocations來查看。
  2. 開發者自行分配的內存在堆(Heap)上,使用Allocations來查看便可。
  3. 開發者調用iOS系統接口也會致使大量內存分配,須要使用VM Tracker來查看。尤爲是一些OpenGL渲染、CoreVideo所需、ImageIO等的大內存。
  4. 對於內存泄漏等,可使用Leaks,或Xcode Memory Debugger便可。固然,並不是全部的泄漏都能經過這些工具檢測出來,有些狀況下須要使用MLeaksFinder等,或者自行根據Memory Graph的狀況來進行分析。
  5. Xcode Memory Debugger很強大。若是以爲打開Instruments很麻煩,能夠在開發調試過程當中將Memory Graph及時導出進行分析。
  6. 自定義內存統計工具比較考驗底層功底,有時間建議深刻研究一番,會有很多收穫。如性能監控工具中,內存監控、OOM監控等就是必不可少的。

針對內存類型的優化措施

Allocations

主要是開發者自行分配內存的時候要注意。

IOKit

這一部分主要是圖片、OpenGL紋理、CVPixelBuffer等,好比一般是OpenGL的紋理,glTexImage2d調用產生的。iOS系統有相關釋放接口。但可能釋放不及時。

CPU和GPU的都算在VM中。Allocations不包含GL紋理,建立必定數量紋理後,到達極限值,則以後建立紋理就會失敗,App可能不會崩潰,可是出現異常,花屏,或者拍後頁白屏。

顯存可能被映射到某塊虛擬內存,所以能夠經過IOKit來查看紋理增加狀況。手機的顯存就是內存,而Mac才區分顯存和內存。

紋理是在內核態分配的,不計算到Allocations裏邊。如包含OpenGL的紋理,是Dirty Size,須要降下來。

若GL分配紋理不釋放,則IOKit的Virtual Size不斷增加;若是紋理正確釋放,則Virtual Size比較穩定。

After some research, I found this post about Finding iOS Memory, which mentions that OpenGL’s textures are shown Dirty memory labelled as IOKit.

Some drivers may keep the storage allocated so that they can reuse it for satisfying future allocations (rather than having to allocate new storage – a common misunderstanding this behaviour leads to is people thinking they have a memory leak), other drivers may not.

因此,一般狀況下,開發者已經正確調用了釋放內存的操做,可是OpenGL本身作的優化,使得內存並未真正地及時釋放掉,僅僅是爲了重用。

glDeleteTextures is the standard way to delete texture objects in OpenGL, but note that this isn't like malloc/free - glDeleteTextures only promises that the texture names become available for subsequent reuse, it says nothing about the actual memory used for storage, which will be driver-dependent behaviour.

Some drivers may keep the storage allocated so that they can reuse it for satisfying future allocations (rather than having to allocate new storage - a common misunderstanding this behaviour leads to is people thinking they have a memory leak), other drivers may not.

This is consistent with the API specification for all other GL objects; glGen* to create object names, glBind* to use them, glDelete* to make the names available for reuse. See e.g. glDeleteBuffers for another example.

So, actually releasing the backing storage is not something you need to worry about yourself; drivers will handle this automatically and you can work on the basis that the memory usage pattern is selected by the driver writers using their own knowledge of what's best for the hardware.
複製代碼
glDeleteTextures deletes n textures named by the elements of the array textures. After a texture is deleted, it has no contents or dimensionality, and its name is again unused. If a texture that is currently bound is deleted, the binding reverts to 0 (the default texture).

Unused names in textures that have been marked as used for the purposes of glGenTextures are marked as unused again. glDeleteTextures silently ignores 0's and names that do not correspond to existing textures.
複製代碼

glDeleteTextures函數,並不是必定會當即釋放掉紋理,而是代表該紋理能夠再次在glGenTextures的時候被複用。

[[GPUImageContext sharedFramebufferCache] purgeAllUnassignedFramebuffers];

- (void)purgeAllUnassignedFramebuffers;
{
    runAsynchronouslyOnVideoProcessingQueue(^{
        [framebufferCache removeAllObjects];
        [framebufferTypeCounts removeAllObjects];
#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE
        CVOpenGLESTextureCacheFlush([[GPUImageContext sharedImageProcessingContext] coreVideoTextureCache], 0);
#else
#endif
    });
}
複製代碼

這裏有一點須要格外注意:CVOpenGLESTextureCacheFlush調用後,內存可能依然不會當即釋放。假設延遲5s執行,則可能釋放(而延遲1s,則可能只釋放部份內存)。

這與CVPixelBuffer以及CVOpenGLESTextureCacheFlush的自身機制有關係。

  1. 如默認狀況下紋理會延遲1s進行page out操做;
  2. CVOpenGLESTextureCacheFlush的方法註釋中刻意添加了週期性調用(This call must be made periodically)的提示,以保證紋理釋放操做的執行。
//
// cacheAttributes
//
// By default, textures will age out after one second.  Setting a maximum
// texture age of zero will disable the age-out mechanism completely.
// CVOpenGLESTextureCacheFlush() can be used to force eviction in either case.
	
CV_EXPORT const CFStringRef CV_NONNULL kCVOpenGLESTextureCacheMaximumTextureAgeKey COREVIDEO_GL_DEPRECATED(ios, 5.0, 12.0) COREVIDEO_GL_DEPRECATED(tvos, 9.0, 12.0) API_UNAVAILABLE(macosx) __WATCHOS_PROHIBITED;

/*!
    @function   CVOpenGLESTextureCacheFlush
    @abstract   Performs internal housekeeping/recycling operations
    @discussion This call must be made periodically to give the texture cache a chance to make OpenGLES calls
                on the OpenGLES context used to create it in order to do housekeeping operations.  The EAGLContext
	            associated with the cache may be used to delete or unbind textures.
    @param      textureCache The texture cache object to flush
    @param      options Currently unused, set to 0.
*/
CV_EXPORT void CVOpenGLESTextureCacheFlush( CVOpenGLESTextureCacheRef CV_NONNULL textureCache, CVOptionFlags options ) COREVIDEO_GL_DEPRECATED(ios, 5.0, 12.0) COREVIDEO_GL_DEPRECATED(tvos, 9.0, 12.0) API_UNAVAILABLE(macosx) __WATCHOS_PROHIBITED;
複製代碼

注意,這裏的periodically確定是有坑的。若是遇到內存未當即釋放的狀況,試一下延遲幾秒鐘執行CVOpenGLESTextureCacheFlush操做。

CVPixelBuffer

- (void)dealloc {
    if (_pixelBufferPool) {
        CVPixelBufferPoolFlush(_pixelBufferPool, kCVPixelBufferPoolFlushExcessBuffers);
        CVPixelBufferPoolRelease(_pixelBufferPool);
        _pixelBufferPool = nil;
    }
}

- (CVPixelBufferRef)createPixelBufferFromCGImage:(CGImageRef )image {
    size_t height = CGImageGetHeight(image);
    size_t width = CGImageGetWidth(image);
    if (!_pixelBufferPool || !CGSizeEqualToSize(_pixelPoolSize, CGSizeMake(width, height))) {
        
        if (_pixelBufferPool) {
            CVPixelBufferPoolFlush(_pixelBufferPool, kCVPixelBufferPoolFlushExcessBuffers);
            CVPixelBufferPoolRelease(_pixelBufferPool);
            _pixelBufferPool = nil;
        }

        NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
        [attributes setObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(NSString *)kCVPixelBufferPixelFormatTypeKey];
        [attributes setObject:@(width) forKey:(NSString *)kCVPixelBufferWidthKey];
        [attributes setObject:@(height) forKey:(NSString *)kCVPixelBufferHeightKey];
        [attributes setObject:@(32) forKey:(NSString *)kCVPixelBufferBytesPerRowAlignmentKey];
        [attributes setObject:[NSDictionary dictionary] forKey:(NSString *)kCVPixelBufferIOSurfacePropertiesKey];
        CVPixelBufferPoolCreate(kCFAllocatorDefault, NULL, (__bridge CFDictionaryRef _Nullable)(attributes), &_pixelBufferPool);
        _pixelPoolSize = CGSizeMake(width, height);
    }
    
    CVPixelBufferRef pxbuffer = NULL;
    CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, _pixelBufferPool,&pxbuffer);
    NSParameterAssert(pxbuffer != NULL);
    
    CIImage *ciimage = [[CIImage alloc] initWithCGImage:image];
    
    [_ciContext render:ciimage toCVPixelBuffer:pxbuffer];
    
    return pxbuffer;
}
複製代碼

若是PixelBuffer重用,則使用Pool,釋放操做須要調用Pool的flush函數。而iOS系統中實際的內存釋放時機會有延遲,且這裏拍照的pixelBuffer並不會頻繁複用,所以直接使用create方法來替代Pool更合理。用完就釋放。

修改成:

- (CVPixelBufferRef)createPixelBufferFromCGImage:(CGImageRef )image {
    size_t height = CGImageGetHeight(image);
    size_t width = CGImageGetWidth(image);
    CVPixelBufferRef pxbuffer = NULL;
    
    CFDictionaryRef empty; // empty value for attr value.
    CFMutableDictionaryRef attrs;
    empty = CFDictionaryCreate(kCFAllocatorDefault, NULL, NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); // our empty IOSurface properties dictionary
    attrs = CFDictionaryCreateMutable(kCFAllocatorDefault, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    CFDictionarySetValue(attrs, kCVPixelBufferIOSurfacePropertiesKey, empty);
    CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32BGRA, attrs, &pxbuffer);
    
    CFRelease(attrs);
    CFRelease(empty);
    NSParameterAssert(pxbuffer != NULL);
    
    CIImage *ciimage = [[CIImage alloc] initWithCGImage:image];
    
    [_ciContext render:ciimage toCVPixelBuffer:pxbuffer];
    
    return pxbuffer;
}
複製代碼

VM:ImageIO_IOSurface_Data

典型堆棧:

VM:ImageIO_PNG_Data

典型堆棧

UIImage的imageNamed:方法會將圖片數據緩存在內存中。而imageWithContentsOfFile:方法則不會進行緩存,用完當即釋放掉了。優化建議:

  1. 對於常常須要使用的小圖,能夠放到Assets.xcassets中,使用imageNamed:方法。
  2. 對於不常用的大圖,不要放到Assets.xcassets中,且使用imageWithContentsOfFile:方法。

若是對於多圖的滾動視圖,渲染到imageView中後,可使用autoreleasepool來儘早釋放:

for (int i=0;i<10;i++) {
	UIImageView *imageView = xxx;
	NSString *imageFile = xxx;
	@autoreleasepool {
		imageView.image = [UIImage imageWithContentsOfFile:imageFile];
	}
	[self.scrollView addSubview:imageView];
}
複製代碼

優化措施:適當地使用imageNamed:和imageWithContentsOfFile:方法。對於比較老的項目,能夠在調試環境對imageNamed:方法進行hook,檢測UIImage的size大小,以篩選出尺寸過大的圖片。

VM:Image IO

典型堆棧:

VM:IOAccelerator

典型堆棧

VM:CG raster data

* Decompressing images that are downloaded and cached can improve peformance but can consume lot of memory.
* Defaults to YES. Set this to NO if you are experiencing a crash due to excessive memory consumption.
複製代碼

光柵數據,即爲UIImage的解碼數據。SDWebImage將解碼數據作了緩存,避免渲染時候在主線程解碼而形成阻塞。

優化措施:

[[SDImageCache sharedImageCache] setShouldDecompressImages:NO];
[[SDWebImageDownloader sharedDownloader] setShouldDecompressImages:NO];
[[SDImageCache sharedImageCache] setShouldCacheImagesInMemory:NO];
複製代碼

優化措施:適當地作緩存。

VM:CoreAnimation

通常是UIView,CALayer。若有個5.78MB的,沒法看出是哪一個View,只知道是一個很大的View。

CA::Render::Shmem::new_bitmap xxxxx
CABackingStorePrepareUpdate_(CABackingStore*,xxxxxxx)
CABackingStoreUpdate_
invocation function  for block in CA::Layer::display_()
複製代碼

優化措施:不要用太大的UIView和CALayer。

典型堆棧:

mach_vm_allocate
vm_allocate
CA::Render::Shmem::new_shmem
CA::Render::Shmem::new_bitmap
CABackingStorePrepareUpdates_
CABackingStoreUpdate_
invocation function for block in CA::Layer::display_()
x_blame_allocations
[CALayer _display]
CA::Context::commit_transaction
CA::Transaction::commit()
[UIApplication _firstCommitBlock] _block_invoke_2
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
__CFRunLoopDoBlocks
__CFRunLoopRun
CFRunLoopRunSpecific
GSEventRunModal
UIApplicationMain
main
start
複製代碼

VM: CoreUI image data

典型堆棧

mmap
[_CSIRenditionBLockData _allocateImageBytes]
複製代碼

VM_ALLOCATE

這部分基本是對開發者自行分配的大內存進行檢查。

代碼段__TEXT

優化措施:縮小包體積。

針對使用場景的優化措施

如何計算對象的佔用內存大小

將指針傳遞給malloc_size函數便可獲取對象佔用的內存size,單位是byte。

malloc_size((__bridge const void *)(object))
複製代碼

圖像優化

圖片佔用的內存大小實際與其分辨率相關的,若是一個像素點佔用4個byte的話,width * height * 4 / 1024 / 1024 MB。

參考:WWDC 2018 Session 219:Image and Graphics Best Practices

imageNamed和imageWithContentsOfFile

  1. UIImage的imageNamed:方法會將圖片數據緩存在內存中,緩存使用的時NSCache,收到內存警告會釋放。
  2. 而imageWithContentsOfFile:方法則不會進行緩存,不須要的時候就當即釋放掉了。

因此,

  1. 對於頻繁使用的小圖,能夠放到Assets.xcassets中,使用imageNamed:方法。
  2. 對於不常用的大圖,不要放到Assets.xcassets中,且使用imageWithContentsOfFile:方法。

UIImage的異步解碼和渲染

UIImage只有在屏幕上渲染(self.imageView.image = image)的時候,纔去解碼的,解碼操做在主線程執行。因此,若是有很是多(如滑動界面下載大量網絡圖片)或者較大圖片的解碼渲染操做,則會阻塞主線程。能夠添加異步解碼的一些使用技巧。

能夠經過以下方式,避免圖片使用時候的一些阻塞、資源消耗過大、頻繁解碼等的狀況。

  1. 異步下載網絡圖片,進行內存和磁盤緩存
  2. 對圖片進行異步解碼,將解碼後的數據放到內存緩存
  3. 主線程進行圖片的渲染

能夠查看SDWebImage的UIImage的ForceDecode擴展:

/**
 UIImage category about force decode feature (avoid Image/IO's lazy decoding during rendering behavior).
 */
@interface UIImage (ForceDecode)

/**
 Decode the provided image. This is useful if you want to force decode the image before rendering to improve performance.
 @param image The image to be decoded
 @return The decoded image
 */
+ (nullable UIImage *)sd_decodedImageWithImage:(nullable UIImage *)image;

@end
複製代碼

異步解碼的詳細實現,能夠查看SDWebImage的SDImageCoderHelper.m文件:

+ (UIImage *)decodedImageWithImage:(UIImage *)image {
#if SD_MAC
    return image;
#else
    if (![self shouldDecodeImage:image]) {
        return image;
    }
    
    CGImageRef imageRef = [self CGImageCreateDecoded:image.CGImage];
    if (!imageRef) {
        return image;
    }
    UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:image.imageOrientation];
    CGImageRelease(imageRef);
    SDImageCopyAssociatedObject(image, decodedImage);
    decodedImage.sd_isDecoded = YES;
    return decodedImage;
#endif
}

+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation {
    if (!cgImage) {
        return NULL;
    }
    size_t width = CGImageGetWidth(cgImage);
    size_t height = CGImageGetHeight(cgImage);
    if (width == 0 || height == 0) return NULL;
    size_t newWidth;
    size_t newHeight;
    switch (orientation) {
        case kCGImagePropertyOrientationLeft:
        case kCGImagePropertyOrientationLeftMirrored:
        case kCGImagePropertyOrientationRight:
        case kCGImagePropertyOrientationRightMirrored: {
            // These orientation should swap width & height
            newWidth = height;
            newHeight = width;
        }
            break;
        default: {
            newWidth = width;
            newHeight = height;
        }
            break;
    }
    
    BOOL hasAlpha = [self CGImageContainsAlpha:cgImage];
    // iOS prefer BGRA8888 (premultiplied) or BGRX8888 bitmapInfo for screen rendering, which is same as `UIGraphicsBeginImageContext()` or `- [CALayer drawInContext:]`
    // Though you can use any supported bitmapInfo (see: https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html#//apple_ref/doc/uid/TP30001066-CH203-BCIBHHBB ) and let Core Graphics reorder it when you call `CGContextDrawImage`
    // But since our build-in coders use this bitmapInfo, this can have a little performance benefit
    CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
    bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
    CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
    if (!context) {
        return NULL;
    }
    
    // Apply transform
    CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight));
    CGContextConcatCTM(context, transform);
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); // The rect is bounding box of CGImage, don't swap width & height
    CGImageRef newImageRef = CGBitmapContextCreateImage(context);
    CGContextRelease(context);
    
    return newImageRef;
}
複製代碼

適當使用autoreleasepool

若是對於多圖的滾動視圖,渲染到imageView中後,可使用autoreleasepool來儘早釋放:

for (int i=0;i<10;i++) {
	UIImageView *imageView = xxx;
	NSString *imageFile = xxx;
	@autoreleasepool {
		imageView.image = [UIImage imageWithContentsOfFile:imageFile];
	}
	[self.scrollView addSubview:imageView];
}
複製代碼

UIGraphicsImageRenderer

建議使用iOS 10以後的UIGraphicsImageRenderer來執行繪製任務。該API在iOS 12中會根據場景自動選擇最合適的渲染格式,更合理地使用內存。

另外一個方式,採用UIGraphicsBeginImageContextWithOptions與UIGraphicsGetImageFromCurrentImageContext獲得的圖片,每一個像素點都須要4個byte。可能會有較大內存空間上的浪費。

- (UIImage *)drawImageUsingUIGraphicsImageRenderer {
    CGRect rect = CGRectMake(0, 0, 300, 300);
    UIGraphicsImageRenderer *imageRenderer = [[UIGraphicsImageRenderer alloc] initWithSize:rect.size];
    UIImage *image = [imageRenderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
        [UIColor.greenColor setFill];
        UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect
                                                   byRoundingCorners:UIRectCornerAllCorners
                                                         cornerRadii:CGSizeMake(20, 20)];
        [path addClip];
        UIRectFill(rect);
    }];
    return image;
}
複製代碼
UIGraphicsImageRenderer:
A graphics renderer for creating Core Graphics-backed images.
複製代碼

Downsampling

對於一些場景,如UIImageView尺寸較小,而UIImage較大時,直接展現原圖,會有沒必要要的內存和CPU消耗。

以前的方式

將大圖縮小的時候,即downsampling的過程,通常須要將原始大圖加載到內存,而後作一些座標空間的轉換,再生成小圖。此過程當中,若是使用UIGraphicsImageRenderer的繪製操做,會消耗比較多的資源。

UIImage *scaledImage = [self scaleImage:image newSize:CGSizeMake(2048, 2048)];

- (UIImage *)scaleImage:(UIImage *)image newSize:(CGSize)newSize {
    // 14.6
    // 這一步只是根據size建立一個bitmap的上下文,參數scale比較關鍵。
    UIGraphicsBeginImageContextWithOptions(newSize, NO, 1); // 31.5, +16。16MB,2048*2048*4/1024/1024=16
//    UIGraphicsBeginImageContextWithOptions(newSize, NO, 0); // 79.5, +64。64MB,2048*2048*4/1024/1024*2*2=64
    [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; // 79.7 +0.2MB,最高282.3,+202.6。渲染時的峯值很高。
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); // 79.7
    UIGraphicsEndImageContext(); // 15.7MB
    return newImage;
}
複製代碼
UIGraphicsBeginImageContextWithOptions:
Creates a bitmap-based graphics context with the specified options.
size:圖片縮放的目標尺寸,也就是新的bitmap context的尺寸。
scale:若是傳遞0,則實際取scale會按照設備的屏幕比例,如2x屏幕就取2倍,如消耗內存2048*2048*4/1024/1024*2*2=64;若scale傳遞1,則消耗內存2048*2048*4/1024/1024=16。
複製代碼

UIGraphicsBeginImageContextWithOptions須要跟接收參數相關的context消耗,消耗的內存與三個參數相關。其實不大。

關鍵在於:UIImage的drawInRect:方法在繪製時,會將圖片先解碼,再生成原始分辨率大小的bitmap,內存峯值可能很高。這一步的內存消耗很是關鍵,若是圖片很大,很容易就會增長几十MB的內存峯值。

這種方式的耗時很少,主要是內存消耗巨大。

推薦的方式

使用ImageIO的接口,避免調用UIImage的drawInRect:方法執行帶來的中間bitmap的產生。能夠在不產生Dirty Memory的狀況下,直接讀取圖像大小和元數據信息,不會帶來額外的內存開銷。其內存消耗即爲目標尺寸須要的內存。

extension UIImage {
    @objc
    static func downsampling(imageAt imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage {
        let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
        let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOptions)!
        
        let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
        let downsampleOptions = [
            kCGImageSourceCreateThumbnailFromImageAlways: true,
            kCGImageSourceCreateThumbnailWithTransform: true,
            kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels,
            kCGImageSourceShouldCacheImmediately: false
            ] as CFDictionary
        
        let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions)!
        /// Core Foundation objects returned from annotated APIs are automatically memory managed in Swift
        /// you do not need to invoke the CFRetain, CFRelease, or CFAutorelease functions yourself.
        return UIImage(cgImage: downsampledImage)
    }
    
    @objc
    static func downsampling(imageWith imageData: Data, to pointSize: CGSize, scale: CGFloat) -> UIImage {
        let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
        let imageSource = CGImageSourceCreateWithData(imageData as CFData, imageSourceOptions)!
        
        let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
        let downsampleOptions = [
            kCGImageSourceCreateThumbnailFromImageAlways: true,
            kCGImageSourceCreateThumbnailWithTransform: true,
            kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels,
            kCGImageSourceShouldCacheImmediately: false
            ] as CFDictionary
        
        let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions)!
        /// Core Foundation objects returned from annotated APIs are automatically memory managed in Swift
        /// you do not need to invoke the CFRetain, CFRelease, or CFAutorelease functions yourself.
        return UIImage(cgImage: downsampledImage)
    }
}
複製代碼

其中,有一些選項設置downsampleOptions:

  1. kCGImageSourceCreateThumbnailFromImageAlways
  2. kCGImageSourceThumbnailMaxPixelSize
  3. kCGImageSourceShouldCache 能夠設置爲NO,避免緩存解碼後的數據。默認爲YES。
  4. kCGImageSourceShouldCacheImmediately 能夠設置爲YES,避免在須要渲染的時候才作圖片解碼。默認是NO,不會當即進行解碼渲染,而是在屏幕上顯示時纔去渲染。

kCGImageSourceShouldCache: specifies whether image decoding and caching should happen at image creation time. The value of this key must be a CFBooleanRef. The default value is kCFBooleanFalse (image decoding will happen at rendering time).

即默認不會解碼UIImage,而是在渲染時候纔去解碼,在主線程執行。

而該downsampling過程很是佔用CPU資源,必定要放到異步線程去執行,會阻塞主線程。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    UIImage *image = [self downsamplingImageAt:url withSize:size scale:1];
    dispatch_async(dispatch_get_main_queue(), ^{
        self.imageView.image = image
    });
});
複製代碼

若是採用第二個接口,先將UIImage轉換成NSData,在執行ImageIO對應的縮放操做,須要的僅是NSData的內存,而不會有實際圖片的解碼帶來的內存消耗。

緩存優化

對於緩存數據或可重建數據,儘可能使用NSCache或NSPurableData,收到內存警告時,系統自動處理內存釋放操做。而且是線程安全的。

下邊代碼是SDWebImage的cache:

// A memory cache which auto purge the cache on memory warning and support weak cache.
@interface SDMemoryCache <KeyType, ObjectType> : NSCache <KeyType, ObjectType>

@end

// Private
@interface SDMemoryCache <KeyType, ObjectType> ()

@property (nonatomic, strong, nonnull) SDImageCacheConfig *config;
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache; // strong-weak cache
@property (nonatomic, strong, nonnull) dispatch_semaphore_t weakCacheLock; // a lock to keep the access to `weakCache` thread-safe

- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithConfig:(nonnull SDImageCacheConfig *)config;

@end

@implementation SDMemoryCache

// Current this seems no use on macOS (macOS use virtual memory and do not clear cache when memory warning). So we only override on iOS/tvOS platform.
// But in the future there may be more options and features for this subclass.
#if SD_UIKIT

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}

- (instancetype)initWithConfig:(SDImageCacheConfig *)config {
    self = [super init];
    if (self) {
        // Use a strong-weak maptable storing the secondary cache. Follow the doc that NSCache does not copy keys
        // This is useful when the memory warning, the cache was purged. However, the image instance can be retained by other instance such as imageViews and alive.
        // At this case, we can sync weak cache back and do not need to load from disk cache
        self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
        self.weakCacheLock = dispatch_semaphore_create(1);
        self.config = config;
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(didReceiveMemoryWarning:)
                                                     name:UIApplicationDidReceiveMemoryWarningNotification
                                                   object:nil];
    }
    return self;
}

- (void)didReceiveMemoryWarning:(NSNotification *)notification {
    // Only remove cache, but keep weak cache
    [super removeAllObjects];
}

// `setObject:forKey:` just call this with 0 cost. Override this is enough
- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g {
    [super setObject:obj forKey:key cost:g];
    if (!self.config.shouldUseWeakMemoryCache) {
        return;
    }
    if (key && obj) {
        // Store weak cache
        LOCK(self.weakCacheLock);
        [self.weakCache setObject:obj forKey:key];
        UNLOCK(self.weakCacheLock);
    }
}

- (id)objectForKey:(id)key {
    id obj = [super objectForKey:key];
    if (!self.config.shouldUseWeakMemoryCache) {
        return obj;
    }
    /// 內存緩存中若沒有,則從weakCache中找,找到了,再緩存到內存中?
    if (key && !obj) {
        // Check weak cache
        LOCK(self.weakCacheLock);
        obj = [self.weakCache objectForKey:key];
        UNLOCK(self.weakCacheLock);
        if (obj) {
            // Sync cache
            NSUInteger cost = 0;
            if ([obj isKindOfClass:[UIImage class]]) {
                cost = SDCacheCostForImage(obj);
            }
            [super setObject:obj forKey:key cost:cost];
        }
    }
    return obj;
}

- (void)removeObjectForKey:(id)key {
    [super removeObjectForKey:key];
    if (!self.config.shouldUseWeakMemoryCache) {
        return;
    }
    if (key) {
        // Remove weak cache
        LOCK(self.weakCacheLock);
        [self.weakCache removeObjectForKey:key];
        UNLOCK(self.weakCacheLock);
    }
}

- (void)removeAllObjects {
    [super removeAllObjects];
    if (!self.config.shouldUseWeakMemoryCache) {
        return;
    }
    // Manually remove should also remove weak cache
    LOCK(self.weakCacheLock);
    [self.weakCache removeAllObjects];
    UNLOCK(self.weakCacheLock);
}
複製代碼

SDMemoryCache繼承自NSCache,且使用NSMapTable來存儲strong-weak cache(key是strong,value是weak的)。

/**
 * The option to control weak memory cache for images. When enable, `SDImageCache`'s memory cache will use a weak maptable to store the image at the same time when it stored to memory, and get removed at the same time.
 * However when memory warning is triggered, since the weak maptable does not hold a strong reference to image instacnce, even when the memory cache itself is purged, some images which are held strongly by UIImageViews or other live instances can be recovered again, to avoid later re-query from disk cache or network. This may be helpful for the case, for example, when app enter background and memory is purged, cause cell flashing after re-enter foreground.
 * Defautls to YES. You can change this option dynamically.
 */
@property (assign, nonatomic) BOOL shouldUseWeakMemoryCache;
複製代碼

shouldUseWeakMemoryCache爲YES,則將圖片數據緩存到內存的同時,使用一個weak maptable存儲該image,如image key(strong)->image(weak)。

若內存警告,則緩存的image被清除,一些image能夠恢復,則該weak maptable就不受影響。不然,image被清除,則SD就要從新處理該內存緩存,如從disk查詢或網絡請求。

如App進入後臺,釋放掉內存,再進入前臺時,view的cell中的image能夠重建,而後放到weak maptable中,而不須要再從disk讀取。

加載超大圖片的正確姿式

對於一些微信長圖/微博長圖之類的,或者一些須要展現全圖,而後拖動來查看細節的場景,可使用CATiledLayer來進行分片加載,避免直接對圖片的全部部分進行解碼和渲染,以節省資源。在滑動時,指定目標位置,映射原圖指定位置的部分圖片進行解碼和渲染。

進入後臺

釋放佔用較大的內存,再次進入前臺時按需加載。防止App在後臺時被系統殺掉。

通常監聽UIApplicationDidEnterBackground的系統通知便可。

ViewController相關的優化

對於UITabBarController這樣有多個子VC的狀況,切換tab時候,若是不顯示的ViewController依然佔用較大內存,能夠考慮釋放,須要時候再加載。

超大UIView相關的優化

若是UIView的size過大,若是所有繪製,則會消耗大量內存,以及阻塞主線程。

常見的場景如微信消息的超長文本,則可將其分割成多個UIView,而後放到UITableView中,利用cell的複用機制,減小沒必要要的渲染和內存佔用。

EXC_RESOURCE_EXCEPTION異常

iOS中沒有交換空間,而是採用了JetSam機制。

當App使用的內存超出限制時,系統會拋出EXC_RESOURCE_EXCEPTION異常。

內存泄漏

內存泄漏,有些是能經過工具檢測出來的。而還有一些沒法檢測,須要自行分析。

循環引用

一般對象間相互持有或者構成環狀持有關係,則會引發循環引用。

常見的有對象間引用、委託模式下的delegate,以及Block引發的:

@property (nonatomic) id<SomeRetainedDelegate> delegate;

self.delegate = self;
複製代碼
[[NSNotificationCenter defaultCenter]
        addObserverForName:UIApplicationDidReceiveMemoryWarningNotification
                    object:nil
                     queue:nil
                usingBlock:^(NSNotification *_Nonnull note) {
                    // Warning, memory leak
                    self.testProp = @"test";
                }];
複製代碼
UIAlertAction *ok = [UIAlertAction actionWithTitle:@"肯定"
                                             style:UIAlertActionStyleDefault
                                           handler:^(UIAlertAction *_Nonnull action) {
                                                // Warning, memory leak
                                                self.testProp = @"test";
                    }];
複製代碼

NSTimer

關於NSTimer,能夠參考更詳細的這篇博客:

其餘場景

一些濫用的單例,尤爲是包含了很多block的單例,很容易產生內存泄漏。排查時候須要格外細心。

離屏渲染

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

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.

離屏渲染未必會致使性能下降,而是會額外加劇GPU的負擔,可能致使一個V-sync信號週期內,GPU的任務未能完成,最終結果就是可能致使卡頓。

iOS系統對於Release環境下的優化

實際的release環境下,Apple會對一些場景自動優化,如release環境下,申請50MB的Dirty Memory,但實際footprint和resident不會增長50MB,具體Apple怎麼作的不清楚。

啓動優化

App啓動時,加載相應的二進制文件或者dylib到內存中。當進程訪問一個虛擬內存page,但該page未與物理內存造成映射關係,則會觸發缺頁中斷,而後再分配物理內存。過多的缺頁中斷會致使必定的耗時。

二進制重排的啓動優化方案,是經過減小App啓動時候的缺頁中斷次數,來加速App啓動。

字節對齊

當定義object的時候,儘可能使得內存頁對齊也會有幫助。小內存屬性放一塊兒,大內存屬性放一塊兒。

參考資料

相關文章
相關標籤/搜索