- 內(nèi)存布局
- 內(nèi)存管理方案
- 數(shù)據(jù)結(jié)構(gòu)
- ARC & MRC
- 引用計(jì)數(shù)
- 弱引用
- 自動釋放池
- 循環(huán)引用
一、內(nèi)存布局

* stack 方法調(diào)用
* heap 通過alloc分配的對象
* bss 未初始化的全局變量等
* data 已初始化的全局變量等
* text 程序代碼
二、內(nèi)存管理方案
- TaggedPointer ---- 一些小對象,如 NSNumber
- NONPOINTER_ISA ---- 64位架構(gòu)下的 ios 程序
- 散列表 ---- 復(fù)雜的數(shù)據(jù)結(jié)構(gòu),包括引用計(jì)數(shù)表和弱引用表
三、數(shù)據(jù)結(jié)構(gòu)
1. NONPOINTER_ISA

- indexed -- 標(biāo)志位,若為0:此 isa 指針為純的 isa 指針,里面的內(nèi)容代表了當(dāng)前對象類對象的地址;若為1:為非指針型的 isa ,不僅存儲了類對象的地址,而且還有內(nèi)存管理的數(shù)據(jù)
- has_assoc -- 是否有關(guān)聯(lián)對象;0-無;1-有;
- has_cxx_dtor -- 當(dāng)前對象是否使用到 c++ 相關(guān)代碼
- shiftcls -- 當(dāng)前對象類對象的指針地址,共33位
- magic -- 對內(nèi)存管理無影響
- weakly_referenced -- 此對象是否有弱引用指針
- deallocating -- 是否有 dealloc 操作
- has_sidetable_rc -- 當(dāng)前引用計(jì)數(shù)是否達(dá)到上限,是否引用散列表計(jì)數(shù)
- extra_rc -- 額外引用計(jì)數(shù)(若超出則使用散列表存儲)
2. 散列表
SideTables()結(jié)構(gòu) -- 實(shí)際為一個哈希表

SideTable結(jié)構(gòu)

問題:為什么SideTable是好幾張表,而不是一張表:
假如是一張表的話,系統(tǒng)的所有對象都會在一張表中,那么當(dāng)對其進(jìn)行操作的時(shí)候,那么必然需要加鎖,當(dāng)有大量相似操作的時(shí)候,效率會大大降低;多張表相當(dāng)于多線程操作,可以提高效率
問題:怎樣實(shí)現(xiàn)快速分流(怎樣根據(jù)key找到SideTable位置):
SideTables本質(zhì)是一張Hash表,通過哈希查找找到下表

例如:給定值是對象的內(nèi)存地址,目標(biāo)值是數(shù)組下標(biāo)索引

不會遍歷所有的表,所以會提高查找效率。
3.散列表的數(shù)據(jù)結(jié)構(gòu)
- 自旋鎖 --- spinlock_t
- 引用計(jì)數(shù)表 --- RefcountMap
- 弱引用表 --- weak_table_t
自旋鎖
- 是“忙等”的鎖。即若當(dāng)前所被其他線程所獲取,當(dāng)前線程會不斷探索這個鎖是否被釋放,若被釋放會第一時(shí)間獲??;
- 適用于輕量訪問。例如對當(dāng)前對象做加一減一操作;
引用計(jì)數(shù)表
- 是一個哈希表,為了查找提高查找效率,其中插入和獲取都是通過同一個哈希算法來實(shí)現(xiàn)的,從而避免了 for 循環(huán)遍歷。

size_t

實(shí)際是一個無符號的long型的值;第一位是是否有弱引用;第二位是是否有dealloc;其它位是引用計(jì)數(shù)位,所以在計(jì)算引用計(jì)數(shù)位的時(shí)候,需要向右偏移兩位;
弱引用表
- 也是一張哈希表

四、MRC、ARC
MRC
手動引用計(jì)數(shù)進(jìn)行對象的內(nèi)存管理

其中標(biāo)紅的是MRC特有的方法
ARC
- ARC 是 LLVM(編譯器) 和 Runtime 協(xié)作的結(jié)果
- ARC 中禁止手動調(diào)用 retain/release/retainCount/dealloc
- ARC 中新增 weak、strong 關(guān)鍵字
五、引用計(jì)數(shù)管理
實(shí)現(xiàn)原理分析
- alloc
- retain
- release
- retainCount
- dealloc
1、alloc實(shí)現(xiàn)
經(jīng)過一系列的調(diào)用,最終調(diào)用了C函數(shù) calloc;
此時(shí)并沒有設(shè)置引用計(jì)數(shù)位1;
2、retain實(shí)現(xiàn)

- 通過兩次哈希查找;
- 第一次找到 SideTables 中對應(yīng)的 SideTable 的位置,然后獲取對應(yīng)的引用計(jì)數(shù)表,
- 第二次查找是查找引用計(jì)數(shù)表中存儲的引用計(jì)數(shù)值是 size_t 類型
- 然后對 size_t 進(jìn)行加一操作。(其中,SIDE_RC_ONE 并非是1,而是4,因?yàn)榍皟晌淮鎯Φ牟⒉皇且糜?jì)數(shù)相關(guān)內(nèi)容,需要右移2位)
3、release實(shí)現(xiàn)

- 與 retain 操作基本相同,最后進(jìn)行減一操作
4、retainCount實(shí)現(xiàn)

- 首先聲明一個局部變量為 1 ;
- 然后查找引用計(jì)數(shù)表中的引用計(jì)數(shù)
- 最后進(jìn)行位與操作,獲取引用計(jì)數(shù)
問題:新alloc的對象為什么引用計(jì)數(shù)為1?
解答:因?yàn)?,新alloc出來的對象,在引用計(jì)數(shù)表中是沒有數(shù)值的,所以上面 it->second 讀出的值為0,又聲明的局部變量為1,所以相加之后 retainCount 為1.
5、dealloc實(shí)現(xiàn)

object_dispse()的實(shí)現(xiàn):

objc_destructInstance() (銷毀實(shí)例對象)的實(shí)現(xiàn):

clearDeallocating() 的實(shí)現(xiàn);

六、弱引用管理

1、添加 weak 變量:

一個 __weak 修飾的對象,系統(tǒng)會調(diào)用objc_initWeak()方法對其處理,最終會調(diào)用 weak_register_no_lock()方法對其進(jìn)行具體操作,通過弱引用對象進(jìn)行一個哈希運(yùn)算查找到在對應(yīng)弱引用表的位置,若此位置有弱引用數(shù)組則添加新的對象到數(shù)組中,若沒有則重新創(chuàng)建弱引用地址,在第0個位置添加新的weak指針,后面的初始化為0或者nil
源碼解析:






2、清除weak變量,同時(shí)設(shè)置指向?yàn)閚il的過程:

源碼解析



當(dāng)一個對象進(jìn)行dealloc操作,系統(tǒng)會調(diào)用 weak_clear_no_lock() 方法對其操作,具體是系統(tǒng)會通過哈希算法查找對應(yīng)的弱引用表中的位置,若找到則返回一個數(shù)組,數(shù)組中存儲所有的弱引用關(guān)系表,然后系統(tǒng)對數(shù)組進(jìn)行遍歷,把所有的弱引用指針分別指向nil
七、自動釋放出
問題:
- 請問array的內(nèi)存是在什么時(shí)候釋放的?

- AutoreleasePool的實(shí)現(xiàn)原理?
- AutoreleasePool為何可以嵌套使用?
1、AutoreleasePool
編譯器會將@autoreleasepool{} 改寫為

objc_autoreleasePoolPush:

objc_autoreleasePoolPop:

一次pop操作相當(dāng)于一次批量的pop操作
2、AutoreleasePool的數(shù)據(jù)結(jié)構(gòu)
- 是以棧為結(jié)點(diǎn)的雙向鏈表的形式組合而成;
- 是和線程一一對應(yīng)的
雙向鏈表

棧
后入先出

AutoreleasePoolPage

原本的內(nèi)存圖:

發(fā)生push操作后:

[obj autorelease]的系統(tǒng)實(shí)現(xiàn)過程:

AutoreleasePoolPage::pop
- 根據(jù)傳入的哨兵對象找到對應(yīng)的文職
- 給上次 push 操作之后添加的對象依次發(fā)送 release 消息
- 回退到 next 指針的正確位置
自動釋放池總結(jié)
- 在當(dāng)次的 RunLoop 將要結(jié)束的時(shí)候調(diào)用 AutoreleasePoolPage::pop();
- 多層嵌套就是多次插入哨兵對象;
- 在 for 循環(huán)中 alloc 圖片數(shù)據(jù)等內(nèi)存消耗較大的場景手動插入 AutoreleasePool,降低內(nèi)存的峰值(使用場景)
八、循環(huán)引用
三種循環(huán)引用:
- 自循環(huán)引用
- 相互循環(huán)引用
- 多循環(huán)引用
1、自循環(huán)引用

2、相互循環(huán)引用

3、多循環(huán)引用

注意點(diǎn):
- 代理
- block
- NSTimer
- 大循環(huán)引用
如何破除循環(huán)引用?
- 避免產(chǎn)生循環(huán)引用(如使用弱引用修飾)
- 在合適的時(shí)機(jī)斷掉循環(huán)引用
具體解決方案
- __weak
- __block
- __unsafe_unretained
1、__weak 破解

2、__block 破解
注意:
- MRC 下,__block 修飾的對象不會增加其引用計(jì)數(shù),避免了循環(huán)引用
- ARC 下,__block 修飾的對象會被強(qiáng)引用,無法避免循環(huán)引用,需手動解決
3、__unsafe_unretained 破解
- 修飾對象不會增加其引用計(jì)數(shù),避免循環(huán)引用
- 如果被修飾對象在某一時(shí)機(jī)被釋放,此時(shí)再次訪問時(shí),會產(chǎn)生懸垂指針,導(dǎo)致內(nèi)存泄漏
NSTimer 的循環(huán)引用問題
1、若非循環(huán) timer
可將 timer 設(shè)置無效,然后置空
2、循環(huán) timer
設(shè)置中間對象,中間對象持有對 timer 和 VC 的弱引用變量。NSTimer 分派的回調(diào)是在中間變量中實(shí)現(xiàn)。在中間變量回調(diào)的方法中對其所持有的 target 進(jìn)行判斷,若當(dāng)前值存在則將NSTimer的值回調(diào)給原對象;若不存在(已經(jīng)被釋放),則設(shè)置timer為無效狀態(tài),具體代碼實(shí)現(xiàn)如下:

(修改系統(tǒng)方法實(shí)現(xiàn))

(完成改寫)

總結(jié)
- 什么是 ARC ?
- 為什么 weak 指針指向的對象在被廢棄之后會被自動置為 nil ?
- 蘋果是如何實(shí)現(xiàn) AutoreleasePool 的?
- 什么是循環(huán)引用?有哪些循環(huán)引用?怎樣解決?