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.