iOS
中內(nèi)存管理機(jī)制是開發(fā)中一項(xiàng)很重要的知識(shí)舔哪,了解iOS中內(nèi)存管理的規(guī)則不管是在開發(fā)中還是在學(xué)習(xí)中都能很大程度的幫助我們提升效率珠漂。下面我就根據(jù)自己的理解铡羡,詳細(xì)梳理一下內(nèi)存管理相關(guān)的知識(shí)痹筛。
關(guān)于內(nèi)存:
在說內(nèi)存管理
之前葱椭,我們首先要了解什么內(nèi)存
开泽。首先了解一些計(jì)算機(jī)的基本知識(shí)牡拇。
1. 硬件內(nèi)存區(qū)分:
我們的手機(jī)、電腦、或者智能設(shè)備都有
RAM
(運(yùn)行內(nèi)存)和ROM
(硬盤)惠呼。
RAM
是內(nèi)部存儲(chǔ)导俘,ROM
是外部存儲(chǔ)。我們的CPU
直接訪問的是RAM
剔蹋,如果想訪問外部存儲(chǔ)旅薄,則數(shù)據(jù)須先放到RAM
中才能被CPU
訪問。CPU
不能直接從內(nèi)存卡里面讀取指令(需要Flash
驅(qū)動(dòng)等等)泣崩。
2. RAM和ROM的特點(diǎn)和區(qū)別:
- RAM:運(yùn)行內(nèi)存少梁,CPU可以直接訪問,訪問速度快矫付,價(jià)格高凯沪,不能夠掉電存儲(chǔ)-斷電會(huì)失去數(shù)據(jù)-不穩(wěn)定。
- ROM:存儲(chǔ)型內(nèi)存买优,CPU不可以直接訪問妨马,訪問速度慢,價(jià)格低杀赢,可以掉電存儲(chǔ)-穩(wěn)定烘跺。
3. RAM和ROM的協(xié)同工作:
由于
RAM
不支持掉電存儲(chǔ),所以App
程序一般存儲(chǔ)在ROM
中脂崔。
手機(jī)里面使用的ROM
基本都是NandFlash
(閃存)滤淳,CPU
是不能直接訪問的,而是需要文件系統(tǒng)/驅(qū)動(dòng)程序(嵌入式中的EMC
)將其讀到RAM
里面脱篙,CPU
才可以訪問娇钱。另外,RAM
的速度也比NandFlash
快绊困。
4文搂、內(nèi)存分區(qū):
說到內(nèi)存分區(qū),內(nèi)存即指的是RAM秤朗。一塊內(nèi)存條煤蹭,是一個(gè)從下至上地址依次遞增的結(jié)構(gòu),內(nèi)存條中主要分為幾大類:棧區(qū)(stack)取视、堆區(qū)(heap)硝皂、常量區(qū)、代碼區(qū)(.text)作谭、保留區(qū)稽物。常量區(qū)分為未初始化區(qū)域(.bss)和已初始化區(qū)域(.data)
程序員操作的主要是棧區(qū)與堆區(qū)還有常量區(qū)。
棧區(qū)(heap)
:由系統(tǒng)去管理折欠。地址從高到低分配贝或。先進(jìn)后出吼过。會(huì)存一些局部變量,函數(shù)跳轉(zhuǎn)跳轉(zhuǎn)時(shí)現(xiàn)場(chǎng)保護(hù)(寄存器值保存于恢復(fù))咪奖,這些系統(tǒng)都會(huì)幫我們自動(dòng)實(shí)現(xiàn)盗忱,無需我們干預(yù)。所以大量的局部變量羊赵,深遞歸趟佃,函數(shù)循環(huán)調(diào)用都可能耗盡棧內(nèi)存而造成程序崩潰 。堆區(qū)(stack)
:需要我們自己管理內(nèi)存昧捷,alloc
申請(qǐng)內(nèi)存release
釋放內(nèi)存闲昭。創(chuàng)建的對(duì)象也都放在這里。 地址是從低到高分配料身。堆是所有程序共享的內(nèi)存汤纸,當(dāng)N個(gè)這樣的內(nèi)存得不到釋放,堆區(qū)會(huì)被擠爆芹血,程序立馬癱瘓贮泞。這就是內(nèi)存泄漏。全局區(qū)/靜態(tài)區(qū)(staic)
:全局變量和靜態(tài)變量的存儲(chǔ)是放在一塊的幔烛,初始化的全局變量和靜態(tài)變量在一塊區(qū)域啃擦, 未初始化的全局變量和未初始化的靜態(tài)變量在相鄰的另一塊區(qū)域。程序結(jié)束后有系統(tǒng)釋放饿悬。常量區(qū)
:常量字符串就是放在這里的令蛉,還有const
常量。代碼區(qū)
:存放App
代碼狡恬,App
程序會(huì)拷貝到這里珠叔。
4. App程序在內(nèi)存中的運(yùn)行:
當(dāng)我們點(diǎn)擊手機(jī)icon啟動(dòng)一個(gè)App時(shí)(例如微信): 拓展:iOS程序生命周期詳解、
- 操作系統(tǒng)會(huì)為微信開辟
4G
的虛擬內(nèi)存空間弟劲。 - 操作系統(tǒng)會(huì)把存儲(chǔ)在
ROM
里面的微信部分代碼copy
到上一步開辟的4G
內(nèi)存空間中祷安。 -
CPU
可以訪問RAM
來運(yùn)行微信程序了。
假設(shè)我們下載了一段視頻兔乞,那么會(huì)從 Server
一點(diǎn)點(diǎn)下載到 RAM
汇鞭,然后再從RAM寫入到 ROM
,這樣保證關(guān)閉微信再次打開時(shí)庸追,視頻還在霍骄。假設(shè)隔一段時(shí)間,我們要看視頻淡溯,程序會(huì)將它從 ROM
讀到 RAM
然后解碼播放读整。數(shù)據(jù)本地化時(shí)候,頻繁進(jìn)行數(shù)據(jù)讀取咱娶,可能會(huì)涉及到性能優(yōu)化绘沉。
內(nèi)存管理
移動(dòng)設(shè)備的內(nèi)存大小是有限的, 內(nèi)存申請(qǐng)一直不釋放就會(huì)導(dǎo)致內(nèi)存不足煎楣,所以需要內(nèi)存管理。OC
的對(duì)象在內(nèi)存中是以堆的方式分配空間的车伞,堆內(nèi)存是由我們自己釋放的。非 OC
對(duì)象一般是放在棧中喻喳,系統(tǒng)會(huì)自動(dòng)回收另玖。
1. 引用計(jì)數(shù)
- 引用計(jì)數(shù)(Reference counting)是一個(gè)簡(jiǎn)單有效管理對(duì)象生命周期的方式。
-
當(dāng)我們新建一個(gè)新對(duì)象時(shí)候表伦,它的引用計(jì)數(shù)+1谦去,當(dāng)一個(gè)新指針指向該對(duì)象,將引用計(jì)數(shù)+1蹦哼。當(dāng)指針不再指向這個(gè)對(duì)象時(shí)候鳄哭,引用計(jì)數(shù)-1,當(dāng)引用計(jì)數(shù)為0時(shí)纲熏,說明該對(duì)象不再被任何指針引用妆丘,將對(duì)象銷毀,進(jìn)而回收內(nèi)存局劲。
2. TaggedPointer
對(duì)于一個(gè) NSNumber
對(duì)象勺拣,如果存儲(chǔ) NSInteger
的普通變量,那么它所占用的內(nèi)存是與 CPU
的位數(shù)有關(guān)鱼填,在 32 位 CPU
下占4個(gè)字節(jié)药有。而指針類型的大小通常也是與 CPU
位數(shù)相關(guān),一個(gè)指針?biāo)加玫膬?nèi)存在32位 CPU
下為4個(gè)字節(jié)苹丸。但是遷移至64位系統(tǒng)中后愤惰,其占用空間達(dá)到了8字節(jié),以此類推赘理,所有在64位系統(tǒng)中占用空間會(huì)翻倍的對(duì)象宦言,在遷移后會(huì)導(dǎo)致系統(tǒng)內(nèi)存劇增,即時(shí)他們根本用不到這么多的空間感憾。在2013年9月蜡励,蘋果推出了iPhone 5s,該款機(jī)型首次采用64位架構(gòu)的A7雙核處理器阻桅。所以蘋果對(duì)于一些小型數(shù)據(jù)(NSNumber
凉倚、NSDate
、NSString
等)嫂沉,采用了 taggedPointer
這種方式管理內(nèi)存稽寒。
TaggedPointer
是一種為內(nèi)存高效節(jié)省空間的方法,Tagged Pointer是一個(gè)特別的指針,它分為兩部分:
- 一部分直接保存數(shù)據(jù) 趟章;
- 另一部分作為特殊標(biāo)記杏糙,表示這是一個(gè)特別的指針慎王,不指向任何一個(gè)地址;
在一個(gè)程序中運(yùn)行下述代碼宏侍,獲取輸出日志:
NSNumber *number = @(0);
NSNumber *number1 = @(1);
NSNumber *number2 = @(2);
NSNumber *number3 = @(9999999999999999999);
NSString *string = [[@"a" mutableCopy] copy];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
NSLog(@"number ---- %@, %p", [number class], number);
NSLog(@"number1 --- %@, %p", [number1 class], number1);
NSLog(@"number2 --- %@, %p", [number2 class], number2);
NSLog(@"number3 --- %@, %p", [number3 class], number3);
NSLog(@"NSString -- %@, %p", [string class], string);
NSLog(@"indexPath - %@, %p", indexPath.class,indexPath);
/********************* 輸出日志 *********************
number ---- __NSCFNumber, 0xb000000000000002
number1 --- __NSCFNumber, 0xb000000000000012
number2 --- __NSCFNumber, 0xb000000000000022
number3 --- __NSCFNumber, 0x600003b791c0
NSString -- NSTaggedPointerString, 0xa000000000000611
indexPath - NSIndexPath, 0xc000000000000016
*/
分析日志:
-
NSNumber
存儲(chǔ)的數(shù)據(jù)不大時(shí)赖淤,NSNumber *
指針是偽指針Tagged Pointer
; - NSNumber存儲(chǔ)的數(shù)據(jù)很大時(shí)谅河,
NSNumber *
指針一般指針咱旱,指向NSNumber
實(shí)例的地址,如number3
绷耍; -
NSTaggedPointerString
經(jīng)常遇見吐限,它就是Tagged Pointer
對(duì)象;
對(duì)于Tagged Pointer
褂始,是系統(tǒng)實(shí)現(xiàn)的诸典,無需開發(fā)者操心!但是作為開發(fā)者崎苗,也要知道NSTaggedPointerString
等是什么東西狐粱!
objc_objcet
對(duì)象中 isa
指針分為指針型 isa
與非指針型isa(NONPOINTER_ISA)
,運(yùn)用的便是類似這種技術(shù)益缠。下面詳細(xì)解讀一下NONPOINTER_ISA
:
在一個(gè)64位的指針內(nèi)存中
- 第0位存儲(chǔ)的是
indexed
標(biāo)識(shí)符脑奠,它代表一個(gè)指針是否為NONPOINTER
型,0代表不是幅慌,1代表是宋欺。- 第1位
has_assoc
,顧名思義胰伍,1代表其指向的實(shí)例變量含有關(guān)聯(lián)對(duì)象齿诞,0則為否。- 第2位為
has_cxx_dtor
骂租,表明該對(duì)象是否包含C++
相關(guān)的內(nèi)容或者該對(duì)象是否使用ARC
來管理內(nèi)存祷杈,如果含有C++
相關(guān)內(nèi)容或者使用了ARC
來管理對(duì)象,這一塊都表示為YES
渗饮,- 第3-35位
shiftcls
存儲(chǔ)的就是這個(gè)指針的地址但汞。- 第42位為
weakly_referenced
,表明該指針對(duì)象是否有弱引用的指針指向互站。- 第43位為
deallocing
私蕾,表明該對(duì)象是否正在被回收。- 第44位為
has_sidetable_rc
胡桃,顧名思義踩叭,該指針是否引用了sidetable
散列表。第- 45-63位
extra_rc
裝的就是這個(gè)實(shí)例變量的引用計(jì)數(shù),當(dāng)對(duì)象被引用時(shí)容贝,其引用計(jì)數(shù)+1自脯,但少量的引用計(jì)數(shù)是不會(huì)直接存放在sideTables
表中的,對(duì)象的引用計(jì)數(shù)會(huì)先存在NONPOINTER_ISA
的指針中的45-63位斤富,當(dāng)其被存滿后膏潮,才會(huì)相應(yīng)存入sideTables
散列表中。
4. 散列表(sideTables
)
散列表在系統(tǒng)中的體現(xiàn)是一個(gè) sideTables
的哈希映射表满力,其中所有對(duì)象的引用計(jì)數(shù)(除上述存在 NONPOINTER_ISA
中的外)都存在這個(gè) sideTables
散列表中戏罢,而一個(gè)散列表中又包含眾多 sideTable
結(jié)構(gòu)體。每個(gè) SideTable
中又包含了三個(gè)元素脚囊,spinlock_t
自旋鎖,RefcountMap
引用計(jì)數(shù)表桐磁,weak_table_t
弱引用表悔耘。
它使用對(duì)象的內(nèi)存地址當(dāng)它的 key
。管理引用計(jì)數(shù)和 weak
指針就靠它了我擂。
既然 SideTables
是一個(gè)哈希映射的表衬以,為什么不用 SideTables
直接包含自旋鎖,引用技術(shù)表和弱引用表呢校摩?因?yàn)樵诒姸嗑€程同時(shí)訪問這個(gè) SideTables
表的時(shí)候看峻,為了保證數(shù)據(jù)安全,需要給其加上自旋鎖衙吩,如果只有一張 SideTable
的表互妓,那么所有數(shù)據(jù)訪問都會(huì)出一個(gè)進(jìn)一個(gè),單線程進(jìn)行坤塞,非常影響效率冯勉,而且會(huì)帶來不好的用戶體驗(yàn),針對(duì)這種情況摹芙,將一張 SideTables
分為多張表的 SideTable
灼狰,再各自加鎖保證數(shù)據(jù)的安全,這樣就增加了并發(fā)量浮禾,提高了數(shù)據(jù)訪問的效率交胚,所以這就是一張 SideTables
表下涵蓋眾多 SideTable
表的原因。
因?yàn)槭鞘褂脤?duì)象的內(nèi)存地址當(dāng) key
所以 Hash
的分部也很平均盈电。假設(shè) Hash
表有n
個(gè)元素蝴簇,則可以將 Hash
的沖突減少到n
分之一,支持n
路的并發(fā)寫操作挣轨。
自旋鎖
自旋鎖比較適用于鎖使用者保持鎖時(shí)間比較短的情況军熏。正是由于自旋鎖使用者一般保持鎖時(shí)間非常短,因此選擇自旋而不是睡眠是非常必要的卷扮,自旋鎖的效率遠(yuǎn)高于互斥鎖荡澎。信號(hào)量和讀寫信號(hào)量適合于保持時(shí)間較長(zhǎng)的情況均践,它們會(huì)導(dǎo)致調(diào)用者睡眠,因此只能在進(jìn)程上下文使用摩幔,而自旋鎖適合于保持時(shí)間非常短的情況彤委,它可以在任何上下文使用。
對(duì)于引用計(jì)數(shù)的操作其實(shí)是非郴蚝猓快的焦影。所以選擇了雖然不是那么高級(jí)但是確實(shí)效率高的自旋鎖
引用計(jì)數(shù)器(RefcountMap)
對(duì)象具體的引用計(jì)數(shù)數(shù)量是記錄在這里的。
????這里注意RefcountMap其實(shí)是個(gè)C++的Map封断。為什么Hash以后還需要個(gè)Map斯辰?其實(shí)蘋果采用的是分塊化的方法。
????舉個(gè)例子
????假設(shè)現(xiàn)在內(nèi)存中有16個(gè)對(duì)象坡疼。
0x0000彬呻、0x0001、...... 0x000e柄瑰、0x000f
????咱們創(chuàng)建一個(gè)SideTables[8]來存放這16個(gè)對(duì)象闸氮,那么查找的時(shí)候發(fā)生Hash沖突的概率就是八分之一。
????假設(shè)SideTables[0x0000]和SideTables[0x0x000f]沖突,映射到相同的結(jié)果教沾。
SideTables[0x0000] == SideTables[0x0x000f] ==> 都指向同一個(gè)SideTable
蘋果把兩個(gè)對(duì)象的內(nèi)存管理都放到里同一個(gè)SideTable中蒲跨。你在這個(gè)SideTable中需要再次調(diào)用table.refcnts.find(0x0000)或者table.refcnts.find(0x000f)來找到他們真正的引用計(jì)數(shù)器。
????這里是一個(gè)分流授翻。內(nèi)存中對(duì)象的數(shù)量實(shí)在是太龐大了我們通過第一個(gè)Hash表只是過濾了第一次或悲,然后我們還需要再通過這個(gè)Map才能精確的定位到我們要找的對(duì)象的引用計(jì)數(shù)器死陆。
引用計(jì)數(shù)器的存儲(chǔ)結(jié)構(gòu)如下
維護(hù)weak指針的結(jié)構(gòu)體(weak_table_t)
上面的
RefcountMap
是一個(gè)一層結(jié)構(gòu)毙籽,可以通過 find(key)
直接找到對(duì)應(yīng)的值鹃骂。而 weak_entries
是一個(gè)兩層結(jié)構(gòu)肺素。????第一個(gè)元素
weak_entry_t *weak_entries
是一個(gè)數(shù)組,上面的 RefcountMap
是要通過 find(key)
來找到精確的元素的掘殴。weak_entries
則是通過循環(huán)遍歷來找到對(duì)應(yīng)的 entry
磨德。????(上面管理引用計(jì)數(shù)器蘋果使用的是
Map
,這里管理 weak
指針蘋果使用的是數(shù)組,有興趣的朋友可以思考一下為什么蘋果會(huì)分別采用這兩種不同的結(jié)構(gòu))
- referent: 被指對(duì)象的地址豁鲤。前面循環(huán)遍歷查找的時(shí)候就是判斷目標(biāo)地址是否和他相等茎杂。
- referrers 可變數(shù)組,里面保存著所有指向這個(gè)對(duì)象的弱引用的地址兜材。當(dāng)這個(gè)對(duì)象被釋放的時(shí)候理澎,
referrers
里的所有指針都會(huì)被設(shè)置成nil
。 - inline_referrers 只有4個(gè)元素的數(shù)組曙寡,默認(rèn)情況下用它來存儲(chǔ)弱引用的指針糠爬。當(dāng)大于4個(gè)的時(shí)候使用
referrers
來存儲(chǔ)指針。
第二個(gè)元素 num_entries
是用來維護(hù)保證數(shù)組始終有一個(gè)合適的 size
举庶。比如數(shù)組中元素的數(shù)量超過3/4的時(shí)候?qū)?shù)組的大小乘以2执隧。