為什么不能直接訪問物理內(nèi)存?
- 內(nèi)存不夠用。
- 內(nèi)存數(shù)據(jù)不安全。
內(nèi)存管理方案
- taggedPointer: 專門用來處理小對象,例如NSNumber、NSDate、小NSString等
- Nonpointer_isa: 非指針類型的isa
- SideTables:散列表,包括引用計數(shù)列表和弱引用表
- TaggedPointer
TaggedPointer 是一種高效節(jié)省內(nèi)存空間的方法,是一個特別的指針,它分為兩部分:
一部分直接保存數(shù)據(jù) ;
另一部分作為特殊標(biāo)記,表示這是一個特別的指針,不指向任何一個地址
先看看原有的對象為什么會浪費(fèi)內(nèi)存?
假設(shè)存儲一個 NSNumber 對象,值是一個整數(shù)。
正常情況下,如果這個整數(shù)只是一個 NSInteger 的普通變量,那么它所占用的內(nèi)存是與 CPU 的位數(shù)有關(guān),在 32 位 CPU 下占 4 個字節(jié),在 64 位 CPU 下是占 8 個字節(jié)的。
而指針類型的大小也與 CPU 位數(shù)相關(guān), 32 位下為 4 個字節(jié),64 位下是 8 個字節(jié)。
為了改進(jìn)上面提到的內(nèi)存占用和效率問題,蘋果提出了Tagged Pointer對象。
由于 NSNumber、NSDate 一類的變量本身的值需要占用的內(nèi)存大小常常不需要 8 個字節(jié)

- Tagged Pointer專門用來存儲小的對象,例如NSNumber和NSDate
- Tagged Pointer指針的值不再是地址了,而是真正的值。實際上它不再是一個對象了,只是一個披著對象皮的普通變量而已。所以,它的內(nèi)存并不存儲在堆中,也不需要 malloc 和 free。
- 在內(nèi)存讀取上有著 3 倍的效率,創(chuàng)建時比以前快 106 倍。
EX
- NSCFConstantString:字符串常量,是一種編譯時常量,retainCount值很大,對其操作,不會引起引用計數(shù)變化,存儲在字符串常量區(qū)
- NSCFString:是在運(yùn)行時創(chuàng)建的NSString子類,創(chuàng)建后引用計數(shù)會加1,存儲在堆上
- NSTaggedPointerString:標(biāo)簽指針,是蘋果在64位環(huán)境下對NSString、NSNumber等對象做的優(yōu)化。
對于NSString對象來說,當(dāng)字符串是由數(shù)字、英文字母組合且長度小于10時,會自動成為NSTaggedPointerString類型,存儲在常量區(qū)。
當(dāng)有中文或者其他特殊符號時,會直接成為__NSCFString類型,存儲在堆區(qū)
對于NSString來說,當(dāng)字符串較小時,建議直接通過@""初始化,因為存儲在常量區(qū),可以直接進(jìn)行讀取。會比WithFormat初始化方式更加快速
NSNumber 存儲的數(shù)據(jù)不大時,NSNumber *指針是偽指針Tagged Pointer;
NSNumber存儲的數(shù)據(jù)很大時,NSNumber * 指針一般指針,指向NSNumber 實例的地址
- NONPOINTER_ISA
nonpointer:表示是否對 isa 指針開啟指針優(yōu)化
0:純isa指針,1:不?是類對象地址,isa 中包含了類信息、對象的引?計數(shù)等
extra_rc:表示該對象的引?計數(shù)值,如對象的引?計數(shù)為10,那么extra_rc為9。如果引?計數(shù)?于10,則需要使用到has_sidetable_rc
- 散列表(sideTables)
參考
在 runtime 中,有四個數(shù)據(jù)結(jié)構(gòu)非常重要,分別是 SideTables,SideTable,weak_table_t和weak_entry_t。它們和對象的引用計數(shù),以及 weak引用 相關(guān)
四個數(shù)據(jù)結(jié)構(gòu)的關(guān)系?
在 runtime 內(nèi)存空間中,SideTables是一個8個元素長度 的hash數(shù)組,里面存儲了 SideTable。SideTables 的 hash鍵值 就是一個 對象obj的 address。
因此可以說,一個obj對應(yīng)了一個 SideTable。但是一個 SideTable會對應(yīng)多個 obj。因為 SideTable 的數(shù)量只有64個,所以會有很多 obj 共用同一個 SideTable(如果是真機(jī)環(huán)境下最大就是 8 張表)
SideTables 是多張表的形式,就是考慮到性能問題,當(dāng)所有對象都共用一張表的話,因為要考慮到多線程的問題,當(dāng)對引用計數(shù)操作的時候就會對表的加鎖和關(guān)鎖,會比較消耗性能,當(dāng)使用多張表的時候,系統(tǒng)可以根據(jù)一定的算法,對不使用的表進(jìn)行內(nèi)存回收,而不是持續(xù)占用空間。但是也不能每個對象開一張表,因為開表的內(nèi)存太大了,對象很多的話就會有很多的內(nèi)存開辟與回收,也會很消耗性能。所以表的數(shù)量要在一個合理的范圍內(nèi)。

而在一個 SideTable 中,又有兩個成員,分別是
RefcountMap refcnts; // 對象引用計數(shù)相關(guān) map
weak_table_t weak_table; // 對象弱引用相關(guān) table
- 其中,refcents 是一個 hash map,其key是obj的地址,而value,則是obj對象的引用計數(shù)。
- 而 weak_table 則存儲了 弱引用obj 的指針的地址,其本質(zhì)是一個以 obj 地址為 key,弱引用obj 的指針的地址作為 value 的 hash表。hash表 的節(jié)點(diǎn)類型是 weak_entry_t
一個NSObject對象占用多少字節(jié)?
系統(tǒng)分配了16個字節(jié)給NSObject對象(通過malloc_size函數(shù)獲得),但是NSObject對象內(nèi)部只使用了8個字節(jié)空間
有內(nèi)存對齊的原因,結(jié)構(gòu)體的大小必須是最大成員大小(16)的倍數(shù)

內(nèi)存閾值
Apple 并沒有準(zhǔn)確的文檔說明每個設(shè)備的內(nèi)存限制。對于設(shè)備的內(nèi)存 OOM 閾值大概有以下幾個方法獲取。這里獲取的限制最好是在重啟 iPhone 以后,使得設(shè)備清空 RAM 緩存。
- 方法一: Jetsam 日志
Jetsam 機(jī)制可以理解為操作系統(tǒng)為控制內(nèi)存資源過度使用而采用的一種管理機(jī)制。Jetsam是一個獨(dú)立運(yùn)行的進(jìn)程,每個進(jìn)程都有一個內(nèi)存閾值,一旦超過這個閾值,Jetsam將立即殺死該進(jìn)程。
當(dāng)我們的應(yīng)用被 Jetsam 機(jī)制殺死時,手機(jī)會生成系統(tǒng)日志。在手機(jī)系統(tǒng)設(shè)置隱私分析中,找到以 JetSamEvent. 的開頭的系統(tǒng)日志。在這些日志中,你可以獲取一些關(guān)于應(yīng)用程序的內(nèi)存信息??梢栽谌罩镜拈_頭,看到了pageSize,并找到了 perprocesslimit 項(不是所有日志都有,但是可以找到它)
從 Jetsam 日志中通過使用項目的 rpages * pageSize 可以得到 OOM 的閾值。
{"bug_type":"298","timestamp":"2020-10-15 17:29:58.79
+0100","os_version":"iPhone OS 14.2
(18B5061e)","incident_id":"B04A36B1-19EC-4895-B203-6AE21BE52B02"
}
{
"crashReporterKey" :
"d3e622273dd1296e8599964c99f70e07d25c8ddc",
"kernel" : "Darwin Kernel Version 20.1.0: Mon Sep 21 00:09:01
PDT 2020; root:xnu-7195.40.113.0.2~22\/RELEASE_ARM64_T8030",
"product" : "iPhone12,1",
"incident" : "B04A36B1-19EC-4895-B203-6AE21BE52B02",
"date" : "2020-10-15 17:29:58.79 +0100",
"build" : "iPhone OS 14.2 (18B5061e)",
"timeDelta" : 7,
"memoryStatus" : {
"compressorSize" : 96635,
"compressions" : 3009015,
"decompressions" : 2533158,
"zoneMapCap" : 1472872448,
"largestZone" : "APFS_4K_OBJS",
"largestZoneSize" : 41271296,
"pageSize" : 16384,
"uncompressed" : 257255,
"zoneMapSize" : 193200128,
"memoryPages" : {
"active" : 45459,
"throttled" : 0,
"fileBacked" : 34023,
"wired" : 49236,
"anonymous" : 55900,
"purgeable" : 12,
"inactive" : 40671,
"free" : 5142,
"speculative" : 3793
}
},
"largestProcess" : "AppStore",
"genCounter" : 1,
"processes" : [
{
"uuid" : "7607487f-d2b1-3251-a2a6-562c8c4be18c",
"states" : [
"daemon",
"idle"
],
"age" : 3724485992920,
"purgeable" : 0,
"fds" : 25,
"coalition" : 68,
"rpages" : 229,
"priority" : 0,
"physicalPages" : {
"internal" : [
6,
183
]
},
"pid" : 350,
"cpuTime" : 0.066796999999999995,
"name" : "SBRendererService",
"lifetimeMax" : 976
},
.
.
{
"uuid" : "f71f1e2b-a7ca-332d-bf87-42193c153ef8",
"states" : [
"daemon",
"idle"
],
"lifetimeMax" : 385,
"killDelta" : 13595,
"age" : 94337735133,
"purgeable" : 0,
"fds" : 50,
"genCount" : 0,
"coalition" : 320,
"rpages" : 382,
"priority" : 1,
"reason" : "highwater",
"physicalPages" : {
"internal" : [
327,
41
]
},
"pid" : 2527,
"idleDelta" : 41601646,
"name" : "wifianalyticsd",
"cpuTime" : 0.634077
},
.
.
- 方法二: 網(wǎng)上資料
ios app maximum memory budget
大致就是60%
Split 工具
- 方法三: 主動觸發(fā) didReceiveMemoryWarning
當(dāng)內(nèi)存不夠用時,iOS 會發(fā)出內(nèi)存警告,告知進(jìn)程去清理自己的內(nèi)存, 在當(dāng)前頁面(Controller)中,這個方法是 - (void)didReceiveMemoryWarning??梢酝ㄟ^不停地增加內(nèi)存,來獲取當(dāng)前設(shè)備的 OOM 閾值。
RAM 和 ROM
- RAM(random access memory)隨機(jī)存儲內(nèi)存,這種存儲器在斷電時將丟失其存儲內(nèi)容,故主要用于存儲短時間使用的程序
- ROM(Read-Only Memory)只讀內(nèi)存,是一種只能讀出事先所存數(shù)據(jù)的固態(tài)半導(dǎo)體存儲器
App 啟動時,系統(tǒng)會將 App 程序從ROM 中拷貝到內(nèi)存(RAM),然后在RAM 里面執(zhí)行代碼
內(nèi)存分頁
虛擬內(nèi)存以page(頁)為單位進(jìn)行管理(每頁的容量,32位機(jī)器大小為 4 KB 而 64 位機(jī)器大小為 16 KB)
內(nèi)存頁也有分類,一般來說分為 Clean Memory 、 Dirty Memory 和 Compressed Memory
Clean Memory
例如,image.jpg,F(xiàn)rameworksDirty Memory
所有堆分配對象(例如 malloc,Array,NSCache,UIViews,String 和 圖像解碼緩沖區(qū),例如CGRasterData,ImageIO 和 Frameworks)都會是 Dirty MemoryCompressed Memory
內(nèi)存壓縮主要執(zhí)行兩個操作
- 壓縮未訪問的頁面
- 訪問時解壓縮頁面
壓縮內(nèi)存能夠在內(nèi)存緊張時將最近使用的內(nèi)存使用率壓縮到原始大小的一半以下,并在需要時可以解壓縮和重新使用。它不僅節(jié)省了內(nèi)存,而且提高了系統(tǒng)的響應(yīng)速度。
例如,當(dāng)我們使用 NSDictionary 來緩存數(shù)據(jù)時,假設(shè)現(xiàn)在我們已經(jīng)使用了 3 頁內(nèi)存,當(dāng)我們不訪問它時,它可能被壓縮為 1 頁,而當(dāng)我們再次使用它時,它將被解壓縮為 3 頁
內(nèi)存分區(qū)
- 代碼區(qū):函數(shù)體的二進(jìn)制代碼
- 常量區(qū):常量字符串,const常量
- 全局/靜態(tài)區(qū):全局變量和靜態(tài)變量、靜態(tài)全局變量
- 堆(heap):存OC對象,地址越來越淡,一般由程序員分配釋放
- 棧(stack):函數(shù)的參數(shù)值,局部變量的值、對象指針地址等
棧區(qū)和堆區(qū)的比較
分配方式不同
棧是自動分配和釋放,堆是由程序員來分配和釋放申請大小的限制
棧區(qū):容量大小一般是 2M,比較小
堆區(qū):不連續(xù)的,空間比較大申請效率的比較
棧:系統(tǒng)自動分配,速度較快,但是不受程序員控制。
堆:由 alloc 分配的內(nèi)存,速度較慢,并且容易產(chǎn)生內(nèi)存碎片。
ARC
ARC 背后的原理是依賴編譯器的靜態(tài)分析能力,通過在編譯時找出合理的插入引用計數(shù)管理代碼,從而徹底解放程序員。
自動釋放池
自動釋放池就是一個雙向列表
參考資料:
從 OOM 到 iOS 內(nèi)存管理