虛擬內存機制在這裏就很少說了,主要包括內存管理單元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
能夠簡單理解爲可以被寫入數據的乾淨內存。對開發者而言是read-only,而iOS系統能夠寫入或移除。promise
注意:若是經過文件內存映射機制memory mapped file載入內存的,能夠先清除這部份內存佔用,須要的時候再從文件載入到內存。因此是Clean Memory。緩存
主要強調不可被重複使用的內存。對開發者而言,已經寫入數據。安全
iOS中的內存警告,只會釋放clean memory。由於iOS認爲dirty memory有數據,不能清理。因此,應儘可能避免dirty memory過大。
要清楚地知道Allocations和Dirty Size分別是由於什麼?
值得注意的是,在使用 framework 的過程當中會產生 Dirty Memory,使用單例或者全局初始化方法是減小 Dirty Memory 不錯的方法,由於單例一旦建立就不會銷燬,全局初始化方法會在 class 加載時執行。
下方有測量實驗,如+50dirty的操做,在release環境不生效,因iOS系統自動作了優化。
iOS設備沒有swapped memory,而是採用Compressed Memory機制,通常狀況下能將目標內存壓縮至原有的一半如下。對於緩存數據或可重建數據,儘可能使用NSCache或NSPurableData,收到內存警告時,系統自動處理內存釋放操做。而且是線程安全的。
這裏要注意,壓縮內存機制,使得內存警告與釋放內存變得稍微複雜一些。即,對於已經被壓縮過的內存,若是嘗試釋放其中一部分,則會先將它解壓。而解壓過程帶來的內存增大,可能獲得咱們並不期待的結果。若是選用NSDictionary之類的,內存比較緊張時,嘗試將NSDictionary的部份內存釋放掉。但若NSDictionary以前是壓縮狀態,釋放須要先解壓,解壓過程可能致使內存增大而拔苗助長。
因此,咱們日常開發所關心的內存佔用實際上是 Dirty Size和Compressed Size兩部分,也應儘可能優化這兩部分。而Clean 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;
}
複製代碼
/* * 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消耗的實際物理內存,包括:
獲取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。
類型 | 內存值(MB) | 分析 |
---|---|---|
resident | 59 | App消耗的內存 |
footprint | 13 | 實際物理內存 |
VM | 4770 | App分配的虛擬內存 |
Xcode Navigator | 14.3 | footprint + 調試須要 |
代碼爲:
__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,但不會致使崩潰***。
同時,申請的過程不會耗時。
類型 | 內存值(MB) | 增量 | 分析 |
---|---|---|---|
resident | 60 | +0 | App消耗的內存 |
footprint | 14 | +0 | 實際物理內存 |
VM | 4868 | +51 | App分配的虛擬內存 |
Xcode Navigator | 14.3 | +0 | footprint + 調試須要 |
Resident、footprint、VM都增長。是實實在在的內存消耗,各個工具都會統計。
類型 | 內存值(MB) | 分析 |
---|---|---|
resident | 59 | App消耗的內存 |
footprint | 13 | 實際物理內存 |
VM | 4769 | App分配的虛擬內存 |
Xcode Navigator | 14.3 | footprint + 調試須要 |
代碼爲:
// 僅此一句,依然是僅申請虛擬內存,物理內存不會變
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系統自行的優化。
類型 | 內存值(MB) | 增量 | 分析 |
---|---|---|---|
resident | 160 | +50 | App消耗的內存 |
footprint | 114 | +50 | 實際物理內存 |
VM | 4868 | +51 | App分配的虛擬內存 |
Xcode Navigator | 114.4 | +50 | footprint + 調試須要 |
類型 | 內存值(MB) | 分析 |
---|---|---|
resident | 59 | App消耗的內存 |
footprint | 13 | 實際物理內存 |
VM | 4770 | App分配的虛擬內存 |
Xcode Navigator | 14.3 | footprint + 調試須要 |
代碼爲:
// 申請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。
類型 | 內存值(MB) | 增量 | 分析 |
---|---|---|---|
resident | 80 | +10 | App消耗的內存 |
footprint | 34 | +10 | 實際物理內存 |
VM | 4868 | +51 | App分配的虛擬內存 |
Xcode Navigator | 34.3 | +10 | footprint + 調試須要 |
類型 | 內存值(MB) | 分析 |
---|---|---|
resident | 59 | App消耗的內存 |
footprint | 13 | 實際物理內存 |
VM | 4770 | App分配的虛擬內存 |
Xcode Navigator | 14.3 | footprint + 調試須要 |
代碼爲:
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中會有標記。
類型 | 內存值(MB) | 增量 | 分析 |
---|---|---|---|
resident | 60 | +0 | App消耗的內存 |
footprint | 14 | +0 | 實際物理內存 |
VM | 4967 | +100 | App分配的虛擬內存 |
Xcode Navigator | 14.3 | +0 | footprint + 調試須要 |
圖片大小:map.jpg: 9054*5945
類型 | 內存值(MB) | 分析 |
---|---|---|
resident | 60 | App消耗的內存 |
footprint | 14 | 實際物理內存 |
VM | 4768 | App分配的虛擬內存 |
Xcode Navigator | 14.3 | footprint + 調試須要 |
類型 | 內存值(MB) | 增量 | 分析 |
---|---|---|---|
resident | 61 | +2 | App消耗的內存 |
footprint | 14 | +0 | 實際物理內存 |
VM | 4768 | +0 | App分配的虛擬內存 |
Xcode Navigator | 14.4 | +0.1 | footprint + 調試須要 |
構建UIImage對象所須要的圖片數據消耗其實不大。這裏的數據指的是壓縮的格式化數據。
類型 | 內存值(MB) | 增量 | 分析 |
---|---|---|---|
resident | 61 | +0 | App消耗的內存 |
footprint | 92 | +78 | 實際物理內存 |
VM | 4845 | +77 | App分配的虛擬內存 |
Xcode Navigator | 92 | +77.6 | footprint + 調試須要 |
這個階段,須要將圖片數據解碼成像素數據bitmap,並渲染到屏幕上。解碼過程很是消耗內存和CPU資源,且默認在主線程中執行會阻塞主線程。
關於這裏的一些詳細信息及優化(如異步解碼圖片數據,主線程渲染),請看後文。
經過以上的比較,能夠對各個內存類型有一個初步直觀的認識。
初略展現了真實的物理內存消耗。顏色代表了內存佔用是否合理。Xcode Navigator = footprint + 調試須要。不跟蹤VM。每每初略觀察App的內存佔用狀況,不能做爲精確的參考。
這裏顯示的內存,其實只是整個App佔用內存的一部分,即開發者自行分配的內存,如各類類實例等。簡單而言,就是開發者自行malloc申請的。
開發者手動分配的內存塊,好比一些人臉檢測模型等,還有一些C/C++代碼中的。
沒法由開發者直接控制,通常由系統接口調用申請的。例如圖片之類的大內存,屬於All Anonymous VM -> VM: ImageIO_IOSurface_Data,其餘的還有IOAccelerator與IOSurface等跟GPU關係比較密切的.
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函數,再也不復用。這樣能保證及時釋放掉。
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自己佔用很小。
複製代碼
調用堆棧,通常不須要作啥。每一個線程都須要500KB左右的棧空間,主線程1MB。
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
複製代碼
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,通常由系統生成和管理。
其餘好比MALLOC_LARGE,MALLOC_NANO等都是申請VM的時候設置的tag。
例如:
Type All 那一行說明:
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 */
複製代碼
即 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。
而設置本身的數字也是爲了快速定位到本身的虛擬內存。
複製代碼
該工具能夠很是方便地查看全部對象的內存使用狀況、依賴關係,以及循環引用等。若是將其導出爲memgraph文件,也可使用一些命令來進行分析:
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 memory-info.memgraph
# 按照內存大小來排序
heap memory-info.memgraph -sortBySize
# 查看某個類的全部實例對象的內存地址
heap memory-info.memgraph -addresses all | 'MyDataObject'
複製代碼
# 查看是否有內存泄漏
leaks memory-info.memgraph
# 查看內存地址處的泄漏狀況
leaks --traceTree [內存地址] memory-info.memgraph
複製代碼
須要開啓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函數來分析內存分配狀況。
注意:使用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。
一般狀況下,
主要是開發者自行分配內存的時候要注意。
這一部分主要是圖片、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的時候被複用。
- (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的自身機制有關係。
//
// 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操做。
- (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;
}
複製代碼
典型堆棧:
典型堆棧
UIImage的imageNamed:方法會將圖片數據緩存在內存中。而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大小,以篩選出尺寸過大的圖片。
典型堆棧:
典型堆棧
* 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];
複製代碼
優化措施:適當地作緩存。
通常是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
複製代碼
典型堆棧
mmap
[_CSIRenditionBLockData _allocateImageBytes]
複製代碼
這部分基本是對開發者自行分配的大內存進行檢查。
優化措施:縮小包體積。
將指針傳遞給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。
因此,
UIImage只有在屏幕上渲染(self.imageView.image = image)的時候,纔去解碼的,解碼操做在主線程執行。因此,若是有很是多(如滑動界面下載大量網絡圖片)或者較大圖片的解碼渲染操做,則會阻塞主線程。能夠添加異步解碼的一些使用技巧。
能夠經過以下方式,避免圖片使用時候的一些阻塞、資源消耗過大、頻繁解碼等的狀況。
能夠查看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;
}
複製代碼
若是對於多圖的滾動視圖,渲染到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];
}
複製代碼
建議使用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.
複製代碼
對於一些場景,如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:
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的系統通知便可。
對於UITabBarController這樣有多個子VC的狀況,切換tab時候,若是不顯示的ViewController依然佔用較大內存,能夠考慮釋放,須要時候再加載。
若是UIView的size過大,若是所有繪製,則會消耗大量內存,以及阻塞主線程。
常見的場景如微信消息的超長文本,則可將其分割成多個UIView,而後放到UITableView中,利用cell的複用機制,減小沒必要要的渲染和內存佔用。
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,能夠參考更詳細的這篇博客:
一些濫用的單例,尤爲是包含了很多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的任務未能完成,最終結果就是可能致使卡頓。
實際的release環境下,Apple會對一些場景自動優化,如release環境下,申請50MB的Dirty Memory,但實際footprint和resident不會增長50MB,具體Apple怎麼作的不清楚。
App啓動時,加載相應的二進制文件或者dylib到內存中。當進程訪問一個虛擬內存page,但該page未與物理內存造成映射關係,則會觸發缺頁中斷,而後再分配物理內存。過多的缺頁中斷會致使必定的耗時。
二進制重排的啓動優化方案,是經過減小App啓動時候的缺頁中斷次數,來加速App啓動。
當定義object的時候,儘可能使得內存頁對齊也會有幫助。小內存屬性放一塊兒,大內存屬性放一塊兒。