本文將介紹痢甘,內(nèi)存分布咱士、內(nèi)存管理
一骑晶、內(nèi)存分布
內(nèi)存主要分為五大區(qū),按照地址從高向低依次為:棧區(qū) -> 堆區(qū) -> 全局區(qū) -> 常量區(qū) -> 代碼區(qū)(__text)
補(bǔ)
這里內(nèi)存指的是程序加載到cpu時(shí)的虛擬內(nèi)存
iOS應(yīng)用的虛擬內(nèi)存默認(rèn)分配4G大小铸敏,五大區(qū)占3G,還有1G是五大區(qū)之外的系統(tǒng)內(nèi)核區(qū)
每個(gè)區(qū)放置的內(nèi)容不一樣
- 棧區(qū):函數(shù),方法,局部變量女气,對象指針肮之。由系統(tǒng)自動(dòng)管理(高地址像地址擴(kuò)展,是一塊連續(xù)的內(nèi)存區(qū)域)
- 堆區(qū):通過alloc衣摩、malloc昂验、realloc開辟的對象,是不連續(xù)的內(nèi)存區(qū)域艾扮,以鏈表結(jié)構(gòu)存在既琴。手動(dòng)管理(目前ARC自動(dòng)管理);
- 全局區(qū):全局變量泡嘴,靜態(tài)變量甫恩,空間由系統(tǒng)管理,static修飾的變量僅執(zhí)行一次酌予,生命周期為整個(gè)程序運(yùn)行期磺箕。
- 常量區(qū):常量(整型奖慌、字符型,浮點(diǎn)松靡,字符串等))简僧。空間由系統(tǒng)管理雕欺,生命周期為整個(gè)程序運(yùn)行期岛马。
- 代碼區(qū)(.text):存放代碼的區(qū)域,編譯完后屠列,是cpu可執(zhí)行的指令蛛枚。
補(bǔ):
1、全局區(qū)也叫靜態(tài)區(qū)分為DATA段和BSS段脸哀。DATA段(全局初始化區(qū))存放初始化的全局變量和靜態(tài)變量蹦浦;BSS段(全局未初始化區(qū))存放未初始化的全局變量和靜態(tài)變量。程序運(yùn)行結(jié)束時(shí)自動(dòng)釋放撞蜂。其中盲镶,BSS段在程序執(zhí)行之前會(huì)被系統(tǒng)自動(dòng)清零,所以未初始化全局變量和靜態(tài)變量在程序執(zhí)行之前已經(jīng)為0蝌诡。
2溉贿、在其他文件中定義的全局變量,在本文件中更改浦旱,只對本文件有效宇色。
通過一些例子可以測試不同區(qū)的地址不同
1、棧區(qū)和堆區(qū)
2颁湖、全局靜態(tài)區(qū)和常量區(qū)
可以看出宣蠕,
棧區(qū)
是以0x7
開頭的地址,
全局區(qū)
和常量區(qū)
一般以0x1
開頭的地址甥捺,
堆區(qū)
以0x6
開頭的地址
二抢蚀、內(nèi)存管理方案
先介紹兩個(gè)概念:TaggedPointer和Nonpointer
nonpointer
:其實(shí)指的是使用nonpointer-isa(非指針對象,對isa進(jìn)行地址優(yōu)化對象)镰禾,我們一般創(chuàng)建的對象都是這個(gè)nonpointer對象
Taggedpointer
:小對象皿曲,短的string,NSNumber和NSDate對象
它的指針的值不再是地址吴侦,而是真正的值屋休。所以實(shí)際上它不再是一個(gè)對象,只是叫做對象备韧,其實(shí)只是一個(gè)普通變量劫樟。它的內(nèi)存并不存儲(chǔ)在堆中,所以不需要malloc和free。
下面我們用NSString
的一個(gè)例子來說明這兩種類型
同樣的對self.nameStr賦值毅哗,執(zhí)行第二個(gè)听怕,頁面就崩潰了
且崩到了objc_release里捧挺,是因?yàn)檫@里存在過度釋放虑绵。我們可以看下這里的namestr類型
第一個(gè)
第二個(gè)
同一個(gè)對象,類型變了闽烙?是的翅睛。
- 因?yàn)榈谝粋€(gè)里面,字符串比較短黑竞,所以系統(tǒng)會(huì)安排其為小對象(TaggedPointer)捕发,第二個(gè)字符串比較長,所以安排其為nonpointer對象很魂。
- 所以第一個(gè)方法里扎酷,namestr不是一個(gè)真正的對象,只是一個(gè)常量遏匆,不需要set法挨、get方法,由系統(tǒng)負(fù)責(zé)管理內(nèi)存空間幅聘。
- 而第二個(gè)方法中凡纳,namestr是一個(gè)對象,賦值時(shí)帝蒿,調(diào)用set方法(新值的retain荐糜,舊值的release),所以多線程操作時(shí)葛超,可能存在上一個(gè)舊值剛release完暴氏,其他線程又要release,導(dǎo)致過度釋放绣张,所以崩潰了偏序。
-
那么多少長度的string就切換指針類型呢,如下
2.1胖替、分析taggedpointer
之所以用小對象研儒,是因?yàn)椋ο笠?個(gè)字節(jié)独令,就是64位端朵,而有些值根本用不完64位,所以就用小對象(地址里就包含值)燃箭,可以節(jié)省內(nèi)存冲呢,提高性能。
先來打印幾個(gè)小對象看下內(nèi)存地址
以上的a招狸、b都是小對象~
但從打印結(jié)果看來敬拓,值和地址間也看不出有關(guān)聯(lián)關(guān)系邻薯。??當(dāng)然表面看不出來啦,因?yàn)閠aggedpointer在初始化時(shí)肯定要進(jìn)行混淆~
1乘凸、結(jié)構(gòu)
到objc源碼中看下taggedpointer的初始化厕诡,可以大致看出,進(jìn)行了混淆营勤。
- 再去搜這個(gè)
objc_debug_taggedpointer_obfuscator
- 看到了tagpointer指針地址的解碼和編碼方法灵嫌,用的是異或,那么兩次異或就會(huì)還原指針地址葛作。我們就在外面用一下解碼函數(shù)
_objc_decodeTaggedPointer_
寿羞,拿出string真正的指針地址
- 結(jié)果顯示:61就是a的ASCII碼,62就是b的ASCCII碼赂蠢,那么taggedpinter指針包含了值绪穆!
再試一下number的地址
-
nstring和nsnumber的頭部不一樣(0xa和0xb),這又代表什么虱岂?
猜測是為了表示是否是tagged指針玖院,去源碼中搜索
找到判斷函數(shù),其中
# define _OBJC_TAG_MASK (1UL<<63)
代表這個(gè)mask是最高位是1量瓜,那么上面那個(gè)isTaggedPointer函數(shù)里的算法意思就是司恳,只要最高位為1,那么它就是tagged指針類型0xa和0xb化為二進(jìn)制分別為(1010绍傲,1011)扔傅,最高位都是1,所以它們都是tagged類型烫饼。(此處也可以驗(yàn)證非tagged類型的值)
-
最高位用來確定了tagged類型猎塞,那么后面10、11又用來代表什么呢杠纵?猜測是為了代表不同的類型(NSString和NSSNumber)
找到判斷類型的函數(shù)
點(diǎn)進(jìn) OBJC_TAG_Last60BitPayload
這個(gè)判斷條件荠耽,
- 果然是用來確定類型的,下面驗(yàn)證一下NSDate比藻,是否是這套邏輯铝量,
其中(e:1110
)-> 最高位是1
,說明是tagged
指針银亲,后面三位是6
慢叨,對照上面的enum,是TAG_NSDate
!
由此务蝠,我們可以得到taggedPointer的結(jié)構(gòu)
1拍谐、指針地址
2、tagged類型的flag
3、值
4轩拨、是否是tagged
- 這樣的一個(gè)類型践瓷,包含了這么多信息,而且是存在
常量區(qū)
亡蓉,由系統(tǒng)自動(dòng)管理
晕翠,讀取的效率
是相當(dāng)高
的。根據(jù)官方寸宵,是非taggedpointter的3倍崖面,創(chuàng)建的速度比非tagged快
106倍元咙。
所以日常開發(fā)中梯影,給NSString、NSNumber庶香、NSDate
賦值時(shí)甲棍,盡可能直接使用常量,有助于提高性能
赶掖。
2.2感猛、分析nonponiter內(nèi)存管理
說到內(nèi)存管理,自然想到引用計(jì)數(shù)奢赂,和引用計(jì)數(shù)相關(guān)的陪白,就是這幾個(gè)操作:alloc,retain膳灶、release咱士、dealloc
使用最多的set方法就是包含了新值的retain,舊值的release轧钓,那么就從set方法開始
從源碼中可以得到set流程(此源碼可以自己查看):
objc_setProperty
-> reallySetProperty
-> objc_retain(newValue)
->objc_release(oldValue)
2.2.1 objc_retain
源碼顯示序厉,objc_retain
又調(diào)用了retain
-> rootRetain
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
//判斷是否是tag,如果是毕箍,直接返回
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
//因?yàn)橐糜?jì)數(shù)存在isa里的extra_c里
isa_t oldisa;
isa_t newisa;
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//如果不是nonpointer弛房,直接操作散列表對引用計(jì)數(shù)操作+1
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return (id)this;
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
// don't check newisa.fast_rr; we already called any RR overrides
//如果正在釋放,清空散列表而柑,
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}
uintptr_t carry;
//執(zhí)行引用計(jì)數(shù)+1操作文捶,即對bits中的 1ULL<<45(arm64) 即extra_rc,用于該對象存儲(chǔ)引用計(jì)數(shù)值
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
//判斷extra_rc是否滿了媒咳,carry是標(biāo)識(shí)符
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
if (!handleOverflow) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// Leave half of the retain counts inline and
// prepare to copy the other half to the side table.
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
// //如果extra_rc滿了粹排,則直接將滿狀態(tài)的一半拿出來存到extra_rc
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
//將另一半存在散列表的中,即滿狀態(tài)下是8位伟葫,RC_HALF=一半就是1左移7位恨搓,即除以2
//這么操作的目的在于提高性能,因?yàn)槿绻即嬖谏⒘斜碇校?dāng)需要release時(shí)斧抱,引用計(jì)數(shù)-1常拓,都需要去訪問散列表,每次都需要開解鎖辉浦,比較消耗性能弄抬。extra_rc存儲(chǔ)一半的話,可以優(yōu)先直接操作extra_rc即可宪郊,不需要操作散列表掂恕。性能會(huì)提高很多
sidetable_addExtraRC_nolock(RC_HALF);
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}
以上主要分為幾個(gè)步驟
- 判斷是否時(shí)taggedPointer,如果是弛槐,則直接返回自身懊亡,不操作任何
- 判斷是否是Nonpointer_isa(do-while)
- 引用計(jì)數(shù)操作
1、如果不是nonponinter_isa,直接操作散列表SideTable乎串。進(jìn)行開鎖解鎖店枣。
2、判斷是否正在釋放叹誉,如果是鸯两,調(diào)用dealloc,
3长豁、如果不是钧唐,則對extra_c?1操作,并給一個(gè)引用計(jì)數(shù)的狀態(tài)標(biāo)識(shí)carry匠襟,用于表示extra_rc是否滿了
4钝侠、如果extra_rc滿了,那么操作散列表宅此,將一半的引用計(jì)數(shù)存在散列表里机错。
下面查看釋放過程
2.2.2 objc_release
搜索objc_release
,可以得到調(diào)用流程:objc_release
->release
-> rootRelease
rootRelease源碼:
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
if (isTaggedPointer()) return false;
bool sideTableLocked = false;
isa_t oldisa;
isa_t newisa;
retry:
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//判斷是否是Nonpointer isa
if (slowpath(!newisa.nonpointer)) {
//如果不是,則直接操作散列表-1
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return false;
if (sideTableLocked) sidetable_unlock();
return sidetable_release(performDealloc);
}
// don't check newisa.fast_rr; we already called any RR overrides
uintptr_t carry;
//進(jìn)行引用計(jì)數(shù)-1操作父腕,即extra_rc-1
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
//如果此時(shí)extra_rc的值為0了弱匪,則走到underflow
if (slowpath(carry)) {
// don't ClearExclusive()
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits)));
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;
underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate
// abandon newisa to undo the decrement
newisa = oldisa;
//判斷散列表中是否存儲(chǔ)了一半的引用計(jì)數(shù)
if (slowpath(newisa.has_sidetable_rc)) {
if (!handleUnderflow) {
ClearExclusive(&isa.bits);
return rootRelease_underflow(performDealloc);
}
// Transfer retain count from side table to inline storage.
if (!sideTableLocked) {
ClearExclusive(&isa.bits);
sidetable_lock();
sideTableLocked = true;
// Need to start over to avoid a race against
// the nonpointer -> raw pointer transition.
goto retry;
}
// Try to remove some retain counts from the side table.
//從散列表中取出存儲(chǔ)的一半引用計(jì)數(shù)
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);
// To avoid races, has_sidetable_rc must remain set
// even if the side table count is now zero.
if (borrowed > 0) {
// Side table retain count decreased.
// Try to add them to the inline count.
//進(jìn)行-1操作,然后存儲(chǔ)到extra_rc中
newisa.extra_rc = borrowed - 1; // redo the original decrement too
bool stored = StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits);
if (!stored) {
// Inline update failed.
// Try it again right now. This prevents livelock on LL/SC
// architectures where the side table access itself may have
// dropped the reservation.
isa_t oldisa2 = LoadExclusive(&isa.bits);
isa_t newisa2 = oldisa2;
if (newisa2.nonpointer) {
uintptr_t overflow;
newisa2.bits =
addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
if (!overflow) {
stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits,
newisa2.bits);
}
}
}
if (!stored) {
// Inline update failed.
// Put the retains back in the side table.
sidetable_addExtraRC_nolock(borrowed);
goto retry;
}
// Decrement successful after borrowing from side table.
// This decrement cannot be the deallocating decrement - the side
// table lock and has_sidetable_rc bit ensure that if everyone
// else tried to -release while we worked, the last one would block.
sidetable_unlock();
return false;
}
else {
// Side table is empty after all. Fall-through to the dealloc path.
}
}
//此時(shí)extra_rc中值為0璧亮,散列表中也是空的萧诫,則直接進(jìn)行析構(gòu),即自動(dòng)觸發(fā)dealloc流程
// Really deallocate.
//觸發(fā)dealloc的時(shí)機(jī)
if (slowpath(newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return overrelease_error();
// does not actually return
}
newisa.deallocating = true;
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
if (slowpath(sideTableLocked)) sidetable_unlock();
__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);
if (performDealloc) {
//發(fā)送一個(gè)dealloc消息
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}
以上分為這幾個(gè)步驟
- 判斷是否是
taggedPointer
枝嘶,若是帘饶,返回no,不做任何操作群扶, - 判斷時(shí)否是
nonPointer
及刻,如果不是镀裤,直接操作散列表side table
,引用計(jì)數(shù)-1
, - 如果是nonPointer缴饭,則
1暑劝、對extra_rc -1
,并存儲(chǔ)當(dāng)前extra_rc
的狀態(tài)為carry
(一直減1,直到extrc_rc==0
時(shí)跳到下一步)
2颗搂、extrc_rc
==0担猛,跳到underflow
underflow
:
3、判斷散列表中是否存儲(chǔ)了一半
的引用計(jì)數(shù)丢氢,如果是的傅联,則從散列表中取出存儲(chǔ)的一半
引用計(jì)數(shù),-1
操作疚察,存儲(chǔ)到extra_rc
中
4蒸走、如果散列表中為空
,而此時(shí)extra_rc也為空
稍浆,則直接進(jìn)行析構(gòu)
载碌,即自動(dòng)觸發(fā)dealloc
操作
從retain和release操作猜嘱,可以發(fā)現(xiàn)這是兩個(gè)相反
的操作流程衅枫,那么其中的散列表sideTable
具體是啥呢?
繼續(xù)往下分析
2.3朗伶、散列表sideTable
從2.2中弦撩,知曉sideTable的作用是用于
一個(gè)是非nonpointer_isa對象的引用計(jì)數(shù)使用
另外一個(gè)重要的作用是 nonpointer時(shí),當(dāng)引用計(jì)數(shù)值過大
時(shí)论皆,會(huì)將一半
的引用計(jì)數(shù)存
到它里面
我們先去看下stable的結(jié)構(gòu)
struct SideTable {
spinlock_t slock;//開/解鎖
RefcountMap refcnts;//引用計(jì)數(shù)表
weak_table_t weak_table;//弱引用表
....
}
從類型看出益楼,它是一個(gè)結(jié)構(gòu)體,包含了鎖
点晴、引用計(jì)數(shù)表
感凤、弱引用表
,所以上面的引用計(jì)數(shù)都存到它其中的引用計(jì)數(shù)表中了粒督。
那么它就是一張表么陪竿,還是多個(gè)?
- 查看
sidetable_unlock
方法屠橄,定位到SideTables
,
objc_object::sidetable_unlock()
{
SideTable& table = SideTables()[this];
table.unlock();
}
- 看出
SideTables
其實(shí)是一個(gè)數(shù)組族跛,在操作開|解鎖時(shí),其實(shí)只是操作其中一張表
再看一下SideTables的獲取
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;
static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}
- 是由
StripedMap
通過get方法獲取
再看一下StripedMap
結(jié)構(gòu)
- 內(nèi)存中最多只有8張散列表(真機(jī))锐墙,64張(非真機(jī))礁哄,并且重構(gòu)了
[ ]
操作符,直接通過對象內(nèi)存地址通過indexForPointer
得到下標(biāo)溪北,再使用[ ]
獲取到對應(yīng)的sidetable
2.3.1 為什么只有8張表(真機(jī))
- 如果每個(gè)對象都對應(yīng)一張散列表桐绒,首先那占用內(nèi)存很多夺脾,第二,每次操作引用計(jì)數(shù)時(shí)都要開/解鎖茉继,對整個(gè)程序性能不好
- 如果整個(gè)內(nèi)存只有一張散列表共用劳翰,那么每個(gè)對象操作時(shí),都要開/解鎖馒疹,會(huì)暴露所有對象的引用計(jì)數(shù)佳簸、弱引用等信息,不安全~
2.3.2颖变、散列表是屬于哪種表結(jié)構(gòu)
- 散列表是一種
哈希表
生均,key是關(guān)聯(lián)對象內(nèi)存地址
的。哈希表的特點(diǎn)就是:查詢快
腥刹、增刪改方便·马胧,整體性能好
。(比如于tls衔峰,存儲(chǔ)結(jié)構(gòu)就是拉鏈形式的) - 而沒有使用鏈表和數(shù)組佩脊,因?yàn)?code>鏈表特點(diǎn)是:找到節(jié)點(diǎn)
增刪改方便
,但查詢慢
(需要從頭節(jié)點(diǎn)開始遍歷查詢)垫卤,它屬于存儲(chǔ)快
威彰,讀取慢
。而數(shù)組
特點(diǎn)是:查詢方便
(即通過下標(biāo)訪問)穴肘,增刪改
比較麻煩
歇盼,它屬于讀取快
,存儲(chǔ)改不方便
评抚。
2.3.3豹缀、上面retain過程為什么只存儲(chǔ)一半引用計(jì)數(shù)到表里
- 為了提高性能
extra_rc
的引用計(jì)數(shù)滿
了,就需要操作散列表慨代,將滿狀態(tài)的一
半拿出來存
到extra_rc
邢笙,另一半
是存
到散列表
中。是因?yàn)槿绻?code>都存儲(chǔ)在散列表
侍匙,每次對散列表操作都需要開/解鎖
氮惯,操作耗時(shí)
,消耗性能
大丈积,這么一半分
操作目的
就是提高性能
*以上是散列表的補(bǔ)充筐骇,那么還有一個(gè)重要的函數(shù)dealloc
2.3、dealloc分析
搜索源碼中dealloc
得到調(diào)用順序:dealloc -> _objc_rootDealloc -> object_dispose
rootDealloc源碼:
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
//沒有弱引用表江滨、關(guān)聯(lián)對象铛纬、c++函數(shù)、引用計(jì)數(shù)表唬滑,直接free
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
//如果有任何一個(gè)告唆,調(diào)用dispose
object_dispose((id)this);
}
}
object_dispose源碼:
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
再跳入objc_destructInstance源碼:
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
//C++調(diào)用析構(gòu)函數(shù)棺弊、刪除關(guān)聯(lián)對象引用、
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
inline void
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
//如果不是nonpoint擒悬,則直接釋放散列表
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// 如果是nonponter模她,清空弱引用表 + 散列表
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
dealloc步驟
- 調(diào)用c++析構(gòu)函數(shù)
- 刪除關(guān)聯(lián)對象引用
- 釋放引用計(jì)數(shù)表
- 清空弱引用表
- free釋放自己
至此,整個(gè)retain-release流程分析完畢懂牧,下面歸納一下流程
后補(bǔ)