內(nèi)存管理

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

一、內(nèi)存布局

image
* 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

image
  • 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í)際為一個哈希表


image

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


image
問題:為什么SideTable是好幾張表,而不是一張表:

假如是一張表的話,系統(tǒng)的所有對象都會在一張表中,那么當(dāng)對其進(jìn)行操作的時(shí)候,那么必然需要加鎖,當(dāng)有大量相似操作的時(shí)候,效率會大大降低;多張表相當(dāng)于多線程操作,可以提高效率

問題:怎樣實(shí)現(xiàn)快速分流(怎樣根據(jù)key找到SideTable位置):

SideTables本質(zhì)是一張Hash表,通過哈希查找找到下表


image

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


image

不會遍歷所有的表,所以會提高查找效率。

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)遍歷。
image
size_t
image

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

弱引用表
  • 也是一張哈希表
image

四、MRC、ARC

MRC

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


image

其中標(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)

image
  • 通過兩次哈希查找;
  • 第一次找到 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)

image
  • 與 retain 操作基本相同,最后進(jìn)行減一操作

4、retainCount實(shí)現(xiàn)

image
  • 首先聲明一個局部變量為 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)

image

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


image

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


image

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

六、弱引用管理

image

1、添加 weak 變量:

image
一個 __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

源碼解析:


image
image
image
image
image
image

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

image

源碼解析


image
image
image
當(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í)候釋放的?
image
  • AutoreleasePool的實(shí)現(xiàn)原理?
  • AutoreleasePool為何可以嵌套使用?

1、AutoreleasePool

編譯器會將@autoreleasepool{} 改寫為


image

objc_autoreleasePoolPush:

image

objc_autoreleasePoolPop:

image

一次pop操作相當(dāng)于一次批量的pop操作

2、AutoreleasePool的數(shù)據(jù)結(jié)構(gòu)

  • 是以棧為結(jié)點(diǎn)雙向鏈表的形式組合而成;
  • 是和線程一一對應(yīng)的
雙向鏈表
image

后入先出


image
AutoreleasePoolPage
image

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


image

發(fā)生push操作后:


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

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)引用

image

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

image

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

image

注意點(diǎn):

  • 代理
  • block
  • NSTimer
  • 大循環(huán)引用

如何破除循環(huán)引用?

  • 避免產(chǎn)生循環(huán)引用(如使用弱引用修飾)
  • 在合適的時(shí)機(jī)斷掉循環(huán)引用

具體解決方案

  • __weak
  • __block
  • __unsafe_unretained

1、__weak 破解

image

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)如下:


image

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


image

(完成改寫)
image

總結(jié)

  • 什么是 ARC ?
  • 為什么 weak 指針指向的對象在被廢棄之后會被自動置為 nil ?
  • 蘋果是如何實(shí)現(xiàn) AutoreleasePool 的?
  • 什么是循環(huán)引用?有哪些循環(huán)引用?怎樣解決?
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容