[iOS 理解] 內存管理

ARC 可理解爲,在MRC的基礎上,由編譯器在合適的地方自動插入 retain/release/autorelease 方法,不需要手動管理。因此直接學習 MRC的實現。

先看幾個函數的實現,最後貫穿起來

retain 使對象的引用計數+1
嘗試在bits裏計數+1,如果溢出,把 bits 最大容量的一半(RC_HALF)存入 sidetable
bits 留一半(防止retain release 頻繁操作 sidetable),保存bits

release 使對象的引用計數 -1 或 dealloc
嘗試 bits 裏計數 -1。如果沒有 underflow(舊 bits 爲 1 則 underflow),保存 bits
如果 underflow:
	如果bits 的 has_sidetable_rc 位爲0,即 sidetable沒有計數
		則設置 deallocating 位爲 1,不需要保存新bits,舊 bits 裏計數仍然是 1,
		發消息 dealloc 對象
	如果 has_sidetable_rc, 嘗試從 sidetable 中減少 RC_HALF 個給 bits
		如果減少了0個,則同上 dealloc 對象,
		如果減少了 RC_HALF 個,則計算、保存 bits 新值。

autorelease 之前,先看 AutoreleasePool
AutoreleasePool 之前,需要先學習 runloop
runloop 另一篇文章學過了

AutoreleasePool

可以看這篇文章
也可以直接看總結👇


總結

AutoreleasePool 沒有單獨的結構,是一個 AutoreleasePoolPage 的雙向鏈表
一個線程 - 一個 runloop - 一個 AutoreleasePool - 一個 AutoreleasePoolPage雙向鏈表

AutoreleasePoolPage

爲這個類的對象申請內存時,申請的是物理頁大小,4KB,保存:
自己的實例變量
autorelease 對象數組,id 數組

每當一個對象想 autorelease,就加入 page 中,滿了就申請一個新 page,形成雙向鏈表。

自動釋放原理,很簡單

如果創建一個池子,就在最新 page 的鏈表里加入一個哨兵 = 0,返回插入的位置,記錄下來。
如果一個對象想 autorelease,就加入一個 id。想釋放池子時,就根據記錄的哨兵的位置,一直到最新插入的位置,給中間對象全部發送 release 消息,更新鏈表、最新位置即可。

@autoreleasepool {	
	…
} 
等價於
void* pool = objc_autoreleasePoolPush(); // 返回值就是哨兵
…       
objc_autoreleasePoolPop(pool); // 釋放池子

因此池子可以嵌套。

細節

雙向鏈表當前節點 即當前 page,其指針是保存在線程變量中的。
可以理解爲一個線程範圍的全局變量,稱爲 hotPage。

在第一個池子創建時
objc_autoreleasePoolPush()
	ret AutoreleasePoolPage::push()
		ret autoreleaseFast(0)
			ret autoreleaseNoPage(0)
				ret setEmptyPoolPlaceholder()  
				// hotPage 保存一個地址 1,並不真的創建 page,返回 1
				// 而且,連第一個池子的哨兵都沒加,因爲池子還沒建

也就是說,如果一個釋放池是初次創建,那就先不實際創建,看看是否加入對象,加入時再創建。

第一個對象加入
[self autorelease]
	_objc_rootAutorelease(self) // 中間省略兩步沒用的
		AutoreleasePoolPage::autorelease(self);
			autoreleaseFast(obj);
				取出 hotPage,發現是假 page,創建真 page 並維護雙向鏈表、更新 hotPage
				如果之前是假 page,別忘了加入之前池子的哨兵0
				
				加入 obj

第一個加入的如果不是對象,是直接嵌套一個池子,
則 autoreleaseFast(0) ,其餘和上面一樣,obj = 0 而已。
autoreleaseFast 返回值是加入的地址,保存起來。

後續加入

取出 hotPage,發現沒滿,就加入;
page 滿了就看 page 循環鏈表有沒有下一個,沒有的話創建,有的話使用:
維護循環列表、更新 hotPage。
加入 page

釋放池子

coldPage 是 hotPage 最遠的祖先;
因爲池子可以嵌套(最外層的池子的哨兵地址是 1,最特殊,內層池子哨兵地址正常地址),
所以釋放時,如果是內層的池子:

objc_autoreleasePoolPop(void *token)  // 參數是保存的哨兵地址
	AutoreleasePoolPage::pop(token);
		從 token 所在的頁的位置,一直到最新頁,所有對象都要 release
		所以先找到 token 所在的頁。
		因爲 malloc 4KB 時系統調用返回的地址是 4K 的倍數,這個是操作系統特性
		內存頁就是一頁 4KB,這樣地址後12位關閉就是所在的頁地址。
		得到了 token 所在的頁和 token
		從 hotPage 開始,直到 token 頁內 token 地址,
		之間的對象全部發送 release 消息,更新 hotPage
		
		如果此時 token 頁內剩下的空間超過一半,則釋放後面所有的頁
		如果超過一半,釋放孫子頁及後面所有的頁  

如果是最外層池子:
如果池子沒用過,也就是說 page 沒創建過,更新 hotPage 爲 nil;
如果用過了, 根據上面的邏輯,要麼還有一頁,要麼兩頁,不管怎樣,只有第一頁有內容,先釋放掉這些內容 ,然後如果有第二頁的話,釋放,保留第一頁。結束。
最外層的池子,的確把內容 release 了,但 page 不釋放了,同時這個哨兵地址是 1 的池子還在。

因爲有可能當前線程過一會又需要用釋放池了,然後想創建一個池子(最外層的池子)
這時回想前面創建池子過程:取出 hotPage,發現是真 page!直接存哨兵就好了!
這時創建的池子本質上是第二層,返回的哨兵地址是正常地址。

池子最後剩下一頁怎麼回收?
線程結束時,會銷燬線程變量,也就是前面說過 hotPage 的存儲位置。
線程變量在創建時,要求提供一個銷燬函數,線程結束時會調用,以銷燬可能很複雜的自定義的線程變量。
於是在這個函數裏回收最後的 page。

應用

官方給了三個應用

1 最主要的應用還是,降低內存佔用峯值
一個函數內循環創建大量臨時對象,函數結束後並沒有回收,而是 runloop 進入等待狀態之前回收
所以會增加內層峯值,並不會泄露。
解決辦法:手動使用釋放池包圍內層循環

2 If you spawn a secondary thread.
3 If your detached thread does not make Cocoa calls, you do not need to use an autorelease pool block.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章