本文主要是分析內(nèi)存管理中的內(nèi)存管理方案,以及retain
、retainCount
、release
棉浸、dealloc
的底層源碼分析
ARC & MRC
iOS中的內(nèi)存管理方案三热,大致可以分為兩類:MRC
(手動內(nèi)存管理)和ARC(自動內(nèi)存管理)
MRC
-
在
MRC
時代,系統(tǒng)是通過對象的引用計數(shù)來判斷一個是否銷毀,有以下規(guī)則對象被
創(chuàng)建時
引用計數(shù)都為1
當(dāng)對象
被其他指針引用
時,需要手動調(diào)用[objc retain]
有缆,使對象的引用計數(shù)+1
當(dāng)指針變量不再使用對象時袖外,需要手動調(diào)用
[objc release]
來釋放
對象,使對象的引用計數(shù)-1
當(dāng)一個對象的
引用計數(shù)為0
時熔酷,系統(tǒng)就會銷毀
這個對象
所以翼抠,在MRC模式下,必須遵守:
誰創(chuàng)建,誰釋放,誰引用累颂,誰管理
ARCARC
模式是在WWDC2011和iOS5引入的自動管理機制朱监,即自動引用計數(shù)
。是編譯器的一種特性。其規(guī)則與MRC一致逾冬,區(qū)別在于匹厘,ARC模式下不需要手動retain牛隅、release、autorelease欢嘿。編譯器會在適當(dāng)?shù)奈恢貌迦雛elease和autorelease
掐隐。
內(nèi)存布局
我們在iOS-底層原理 24:內(nèi)存五大區(qū)文章中,介紹了內(nèi)存的五大區(qū)慷妙。其實除了內(nèi)存區(qū)架馋,還有內(nèi)核區(qū)
和保留區(qū)
,以4GB
手機為例勘纯,如下所示,系統(tǒng)將其中的3GB
給了五大區(qū)+保留區(qū)
,剩余的1GB
給內(nèi)核區(qū)使用
內(nèi)核區(qū)
:系統(tǒng)用來進行內(nèi)核處理操作的區(qū)域五大區(qū):這里不再作說明,具體請參考上面的鏈接
保留區(qū)
:預(yù)留給系統(tǒng)處理nil等
這里有個疑問,為什么五大區(qū)的最后內(nèi)存地址是從0x00400000
開始的。其主要原因是0x00000000
表示nil
帽衙,不能直接用nil表示一個段谴垫,所以單獨給了一段內(nèi)存用于處理nil
等情況
內(nèi)存布局相關(guān)面試題
面試題1:全局變量和局部變量在內(nèi)存中是否有區(qū)別?如果有,是什么區(qū)別?
有區(qū)別
全局變量
保存在內(nèi)存的全局存儲區(qū)(即bss+data段)
,占用靜態(tài)的存儲單元局部變量
保存在棧
中狈醉,只有在所在函數(shù)被調(diào)用時才動態(tài)的為變量分配存儲單元
面試題2:Block中可以修改全局變量班巩,全局靜態(tài)變量,局部靜態(tài)變量,局部變量嗎?
可以修改
全局變量,全局靜態(tài)變量
玲躯,因為全局變量 和 靜態(tài)全局變量是全局
的朽缴,作用域很廣
-
可以修改局部靜態(tài)變量誓斥,不可以修改局部斌量
局部靜態(tài)變量(static修飾的) 和 局部變量
成畦,被block從外面捕獲舀武,成為__main_block_impl_0
這個結(jié)構(gòu)體的成員變量局部變量
是以值方式
傳遞到block的構(gòu)造函數(shù)中的寻馏,只會捕獲block中會用到的變量核偿,由于只捕獲了變量的值轰绵,并非內(nèi)存地址,所以在block內(nèi)部不能改變
局部變量的值局部靜態(tài)變量
是以指針形式
蝗羊,被block捕獲的藏澳,由于捕獲的是指針,所以可以修改
局部靜態(tài)變量的值
ARC環(huán)境下耀找,一旦使用
__block
修飾并在block中修改,就會觸發(fā)copy
,block就會從棧區(qū)copy到堆區(qū)
审洞,此時的block是堆區(qū)block
ARC模式下,Block中引用
id類型
的數(shù)據(jù),無論有沒有__block修飾,都會retain
,對于基礎(chǔ)數(shù)據(jù)類型
抛人,沒有__block就無法修改變量值
酷鸦;如果有__block修飾
,也是在底層修改__Block_byref_a_0
結(jié)構(gòu)體徊哑,將其內(nèi)部的forwarding
指針指向copy后的地址
,來達到值的修改
內(nèi)存管理方案
內(nèi)存管理方案除了前文提及的MRC
和ARC
,還有以下三種
Tagged Pointer
:專門用來處理小對象,例如NSNumber坞嘀、NSDate、小NSString等Nonpointer_isa
:非指針類型的isa饰抒,主要是用來優(yōu)化64位地址躲查,這個在iOS-底層原理 07:isa與類關(guān)聯(lián)的原理一文中典唇,已經(jīng)介紹了SideTables
:散列表
蟆豫,在散列表中主要有兩個表芍锚,分別是引用計數(shù)表
、弱引用表
這里主要著重介紹Tagged Pointer
和SideTables
蔓榄,我們通過一個面試題來引入Tagged Pointer
面試題
以下代碼會有什么問題并炮?
//*********代碼1*********
- (void)taggedPointerDemo {
self.queue = dispatch_queue_create("com.cjl.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"CJL"]; // alloc 堆 iOS優(yōu)化 - taggedpointer
NSLog(@"%@",self.nameStr);
});
}
}
//*********代碼2*********
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"來了");
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"CJL_越努力,越幸運H笥!Tァ!"];
NSLog(@"%@",self.nameStr);
});
}
}
運行以上代碼壹若,發(fā)現(xiàn)taggedPointerDemo
單獨運行沒有問題嗅钻,當(dāng)觸發(fā)touchesBegan
方法后。程序會崩潰店展,崩潰的原因是多條線程同時對一個對象進行釋放
养篓,導(dǎo)致了 過渡釋放
所以崩潰。其根本原因是因為nameStr
在底層的類型不一致導(dǎo)致的赂蕴,我們可以通過調(diào)試看出
taggedPointerDemo
方法中的nameStr
類型是NSTaggedPointerString
柳弄,存儲在常量區(qū)
。因為nameStr
在alloc
分配時在堆區(qū)
概说,由于較小碧注,所以經(jīng)過xcode中iOS的優(yōu)化,成了NSTaggedPointerString
類型糖赔,存儲在常量區(qū)touchesBegan
方法中的nameStr
類型是NSCFString
類型萍丐,存儲在堆上
NSString的內(nèi)存管理
我們可以通過NSString初始化的兩種方式,來測試NSString的內(nèi)存管理
通過
WithString + @""
方式初始化通過
WithFormat
方式初始化
#define KLog(_c) NSLog(@"%@ -- %p -- %@",_c,_c,[_c class]);
- (void)testNSString{
//初始化方式一:通過 WithString + @""方式
NSString *s1 = @"1";
NSString *s2 = [[NSString alloc] initWithString:@"222"];
NSString *s3 = [NSString stringWithString:@"33"];
KLog(s1);
KLog(s2);
KLog(s3);
//初始化方式二:通過 WithFormat
//字符串長度在9以內(nèi)
NSString *s4 = [NSString stringWithFormat:@"123456789"];
NSString *s5 = [[NSString alloc] initWithFormat:@"123456789"];
//字符串長度大于9
NSString *s6 = [NSString stringWithFormat:@"1234567890"];
NSString *s7 = [[NSString alloc] initWithFormat:@"1234567890"];
KLog(s4);
KLog(s5);
KLog(s6);
KLog(s7);
}
以下是運行的結(jié)果
所以放典,從上面可以總結(jié)出逝变,NSString的內(nèi)存管理
主要分為3種
__NSCFConstantString
:字符串常量,是一種編譯時常量
奋构,retainCount值很大壳影,對其操作,不會引起引用計數(shù)變化弥臼,存儲在字符串常量區(qū)
__NSCFString
:是在運行時
創(chuàng)建的NSString子類
宴咧,創(chuàng)建后引用計數(shù)會加1,存儲在堆上
-
NSTaggedPointerString
:標(biāo)簽指針径缅,是蘋果在64位環(huán)境下對NSString悠汽、NSNumber
等對象做的優(yōu)化
箱吕。對于NSString對象來說當(dāng)
字符串是由數(shù)字、英文字母組合且長度小于等于9
時柿冲,會自動成為NSTaggedPointerString
類型茬高,存儲在常量區(qū)
當(dāng)有
中文或者其他特殊符號
時,會直接成為__NSCFString
類型假抄,存儲在堆區(qū)
Tagged Pointer 小對象
由一個NSString的面試題怎栽,引出了Tagged Pointer
,為了探索小對象的引用計數(shù)處理宿饱,所以我們需要進入objc
源碼中查看retain熏瞄、release
源碼 中對 Tagged Pointer
小對象的處理
小對象的引用計數(shù)處理分析
-
查看
setProperty -> reallySetProperty
源碼,其中是對新值retain谬以,舊值release
進入
objc_retain
强饮、objc_release
源碼,在這里都判斷是否是小對象,如果是小對象为黎,則不會進行retain或者release邮丰,會直接返回。因此可以得出一個結(jié)論:如果對象是小對象铭乾,不會進行retain 和 release
//****************objc_retain****************
__attribute__((aligned(16), flatten, noinline))
id
objc_retain(id obj)
{
if (!obj) return obj;
//判斷是否是小對象剪廉,如果是,則直接返回對象
if (obj->isTaggedPointer()) return obj;
//如果不是小對象炕檩,則retain
return obj->retain();
}
//****************objc_release****************
__attribute__((aligned(16), flatten, noinline))
void
objc_release(id obj)
{
if (!obj) return;
//如果是小對象斗蒋,則直接返回
if (obj->isTaggedPointer()) return;
//如果不是小對象,則release
return obj->release();
}
小對象的地址分析
繼續(xù)以NSString為例笛质,對于NSString來說
一般的
NSString
對象指針泉沾,都是string值 + 指針地址
,兩者是分開的對于
Tagged Pointer
指針妇押,其指針+值
跷究,都能在小對象中體現(xiàn)。所以Tagged Pointer
既包含指針舆吮,也包含值
在之前的文章講類的加載時,其中的_read_images
源碼有一個方法對小對象進行了處理队贱,即initializeTaggedPointerObfuscator
方法
- 進入
_read_images -> initializeTaggedPointerObfuscator
源碼實現(xiàn)
static void
initializeTaggedPointerObfuscator(void)
{
if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
// Set the obfuscator to zero for apps linked against older SDKs,
// in case they're relying on the tagged pointer representation.
DisableTaggedPointerObfuscation) {
objc_debug_taggedpointer_obfuscator = 0;
}
//在iOS14之后色冀,對小對象進行了混淆,通過與操作+_OBJC_TAG_MASK混淆
else {
// Pull random data into the variable, then shift away all non-payload bits.
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
}
在實現(xiàn)中柱嫌,我們可以看出锋恬,在iOS14之后,Tagged Pointer
采用了混淆處理编丘,如下所示
- 我們可以在源碼中通過
objc_debug_taggedpointer_obfuscator
查找taggedPointer的編碼
和解碼
与学,來查看底層是如何混淆處理的
//編碼
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
//編碼
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
通過實現(xiàn)彤悔,我們可以得知,在編碼和解碼部分索守,經(jīng)過了兩層異或
晕窑,其目的是得到小對象自己
,例如以 1010 0001
為例卵佛,假設(shè)mask
為 0101 1000
1010 0001
^0101 1000 mask(編碼)
1111 1001
^0101 1000 mask(解碼)
1010 0001
-
所以在外界杨赤,為了
獲取小對象的真實地址
,我們可以將解碼的源碼拷貝到外面截汪,將NSString混淆部分進行解碼
疾牲,如下所示觀察解碼后的
小對象地址
,其中的62
表示b
的ASCII
碼衙解,再以NSNumber為例阳柔,同樣可以看出,1
就是我們實際的值
到這里蚓峦,我們驗證了小對象指針地址中確實存儲了值
舌剂,那么小對象地址高位其中的0xa、0xb
又是什么含義呢枫匾?
//NSString
0xa000000000000621
//NSNumber
0xb000000000000012
0xb000000000000025
- 需要去源碼中查看
_objc_isTaggedPointer
源碼架诞,主要是通過保留最高位的值
(即64位的值),判斷是否等于_OBJC_TAG_MASK
(即2^63),來判斷是否是小對象
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
//等價于 ptr & 1左移63干茉,即2^63谴忧,相當(dāng)于除了64位,其他位都為0角虫,即只是保留了最高位的值
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
所以0xa沾谓、0xb
主要是用于判斷是否是小對象taggedpointer,即判斷條件
戳鹅,判斷第64位上是否為1(taggedpointer
指針地址即表示指針地址均驶,也表示值)
0xa
轉(zhuǎn)換成二進制為1 010
(64為為1,63~61后三位表示 tagType類型 - 2)枫虏,表示NSString
類型0xb
轉(zhuǎn)換為二進制為1 011
(64為為1妇穴,63~61后三位表示 tagType類型 - 3),表示NSNumber
類型隶债,這里需要注意一點腾它,如果NSNumber
的值是-1
,其地址中的值是用補碼
表示的
這里可以通過_objc_makeTaggedPointer
方法的參數(shù)tag類型objc_tag_index_t
進入其枚舉死讹,其中 2
表示NSString
瞒滴,3
表示NSNumber
-
同理,我們可以定義一個
NSDate對象
,來驗證其tagType
是否為6
妓忍。通過打印結(jié)果虏两,其地址高位是0xe
,轉(zhuǎn)換為二進制為1 110
世剖,排除64位的1定罢,剩余的3位正好轉(zhuǎn)換為十進制是6,符合上面的枚舉值
Tagged Pointer 總結(jié)
Tagged Pointer
小對象類型(用于存儲NSNumber搁廓、NSDate引颈、小NSString
),小對象指針不再是簡單的地址境蜕,而是地址 + 值
蝙场,即真正的值
,所以粱年,實際上它不再是一個對象了售滤,它只是一個披著對象皮的普通變量
而以。所以可以直接進行讀取台诗。優(yōu)點是占用空間小 節(jié)省內(nèi)存
Tagged Pointer
小對象不會進入retain 和 release
完箩,而是直接返回了
兄淫,意味著不需要ARC進行管理
万皿,所以可以直接被系統(tǒng)自主的釋放和回收
Tagged Pointer
的內(nèi)存并不存儲在堆
中褐桌,而是在常量區(qū)
中扬蕊,也不需要malloc和free
,所以可以直接讀取伸眶,相比存儲在堆區(qū)的數(shù)據(jù)讀取肛冶,效率上快了3倍左右
览徒。創(chuàng)建
的效率相比堆區(qū)快了近100倍左右
所以事哭,綜合來說漫雷,
taggedPointer
的內(nèi)存管理方案,比常規(guī)的內(nèi)存管理鳍咱,要快很多Tagged Pointer
的64位地址中降盹,前4
位代表類型
,后4位主要適用于系統(tǒng)做一些處理谤辜,中間56位用于存儲值
優(yōu)化內(nèi)存建議:對于
NSString
來說蓄坏,當(dāng)字符串較小
時,建議直接通過@""
初始化丑念,因為存儲在常量區(qū)
涡戳,可以直接進行讀取。會比WithFormat初始化方式
更加快速
SideTables 散列表
當(dāng)引用計數(shù)
存儲到一定值是渠欺,并不會再存儲到Nonpointer_isa
的位域的extra_rc
中妹蔽,而是會存儲到SideTables
散列表中
下面我們就來繼續(xù)探索引用計數(shù)retain的底層實現(xiàn)
retain 源碼分析
- 進入
objc_retain -> retain -> rootRetain
源碼實現(xiàn)椎眯,主要有以下幾部分邏輯:【第一步】判斷是否為
Nonpointer_isa
-
【第二步】操作引用計數(shù)
1挠将、如果不是
Nonpointer_isa
胳岂,則直接操作SideTables
散列表,此時的散列表并不是只有一張舔稀,而是有很多張(后續(xù)會分析乳丰,為什么需要多張)2、判斷
是否正在釋放
内贮,如果正在釋放产园,則執(zhí)行dealloc流程3、執(zhí)行
extra_rc+1
夜郁,即引用計數(shù)+1操作什燕,并給一個引用計數(shù)的狀態(tài)標(biāo)識carry
,用于表示extra_rc
是否滿了4竞端、如果
carray
的狀態(tài)表示extra_rc的引用計數(shù)滿
了屎即,此時需要操作散列表
,即 將滿狀態(tài)的一半拿出來存到extra_rc
事富,另一半存在 散列表的rc_half
技俐。這么做的原因是因為如果都存儲在散列表,每次對散列表操作都需要開解鎖统台,操作耗時雕擂,消耗性能大,這么對半分
操作的目的在于提高性能
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
//為什么有isa贱勃?因為需要對引用計數(shù)+1井赌,即retain+1,而引用計數(shù)存儲在isa的bits中募寨,需要進行新舊isa的替換
isa_t oldisa;
isa_t newisa;
//重點
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//判斷是否為nonpointer isa
if (slowpath(!newisa.nonpointer)) {
//如果不是 nonpointer isa族展,直接操作散列表sidetable
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
//dealloc源碼
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}
uintptr_t carry;
//執(zhí)行引用計數(shù)+1操作,即對bits中的 1ULL<<45(arm64) 即extra_rc拔鹰,用于該對象存儲引用計數(shù)值
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
//判斷extra_rc是否滿了仪缸,carry是標(biāo)識符
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;
//給一個標(biāo)識符為YES列肢,表示需要存儲到散列表
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.
//將另一半存在散列表的rc_half中恰画,即滿狀態(tài)下是8位,一半就是1左移7位瓷马,即除以2
//這么操作的目的在于提高性能拴还,因為如果都存在散列表中,當(dāng)需要release-1時欧聘,需要去訪問散列表片林,每次都需要開解鎖,比較消耗性能。extra_rc存儲一半的話费封,可以直接操作extra_rc即可焕妙,不需要操作散列表。性能會提高很多
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}
問題1:散列表為什么在內(nèi)存有多張弓摘?最多能夠多少張焚鹊?
如果散列表只有一張表
,意味著全局所有的對象都會存儲在一張表中韧献,都會進行開鎖解鎖(鎖是鎖整個表的讀寫)末患。當(dāng)開鎖時,由于所有數(shù)據(jù)都在一張表锤窑,則意味著數(shù)據(jù)不安全
如果
每個對象都開一個表
璧针,會耗費性能
,所以也不能有無數(shù)個表散列表的類型是
SideTable
渊啰,有如下定義
struct SideTable {
spinlock_t slock;//開/解鎖
RefcountMap refcnts;//引用計數(shù)表
weak_table_t weak_table;//弱引用表
....
}
- 通過查看
sidetable_unlock
方法定位SideTables
陈莽,其內(nèi)部是通過SideTablesMap
的get方法獲取。而SideTablesMap
是通過StripedMap<SideTable>
定義的
void
objc_object::sidetable_unlock()
{
//SideTables散列表并不只是一張虽抄,而是很多張走搁,與關(guān)聯(lián)對象表類似
SideTable& table = SideTables()[this];
table.unlock();
}
??
static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}
??
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;
從而進入StripedMap
的定義,從這里可以看出迈窟,同一時間私植,真機中散列表最多只能有8張
問題2:為什么在用散列表,而不用數(shù)組车酣、鏈表曲稼?
數(shù)組
:特點在于查詢方便(即通過下標(biāo)訪問),增刪比較麻煩
(類似于之前講過的methodList
湖员,通過memcopy贫悄、memmove
增刪,非常麻煩)娘摔,所以數(shù)據(jù)的特性是讀取快窄坦,存儲不方便
鏈表
:特點在于增刪方便,查詢慢(需要從頭節(jié)點開始遍歷查詢)
凳寺,所以鏈表的特性是存儲快鸭津,讀取慢
-
散列表
的本質(zhì)
就是一張哈希表
,哈希表集合了數(shù)組和鏈表的長處
肠缨,增刪改查都比較方便
逆趋,例如拉鏈哈希表
(在之前鎖的文章中,講過的tls
的存儲結(jié)構(gòu)就是拉鏈形式
的)晒奕,是最常用的闻书,如下所示可以從
SideTables -> StripedMap -> indexForPointer
中驗證是通過哈希函數(shù)計算哈希下標(biāo)
以及sideTables
為什么可以使用[]
的原因
所以名斟,綜上所述,retain
的底層流程如下所示
總結(jié):retain 完整回答
retain
在底層首先會判斷是否是 Nonpointer isa
魄眉,如果不是蒸眠,則直接操作散列表 進行+1操作
如果
是Nonpointer isa
,還需要判斷是否正在釋放
杆融,如果正在釋放,則執(zhí)行dealloc流程
霜运,釋放弱引用表和引用技術(shù)表脾歇,最后free釋放對象內(nèi)存如果
不是正在釋放,則對Nonpointer isa進行常規(guī)的引用計數(shù)+1.
這里需要注意一點的是淘捡,extra_rc
在真機上只有8位用于存儲引用計數(shù)的值
藕各,當(dāng)存儲滿了
時,需要借助散列表
用于存儲焦除。需要將滿了的extra_rc
對半分激况,一半(即2^7)存儲在散列表
中。另一半還是存儲在extra_rc
中膘魄,用于常規(guī)的引用計數(shù)的+1或者-1操作乌逐,然后再返回
release 源碼分析
分析了retain
的底層實現(xiàn),下面來分析release
的底層實現(xiàn)
- 通過
setProperty -> reallySetProperty -> objc_release -> release -> rootRelease -> rootRelease
順序创葡,進入rootRelease
源碼浙踢,其操作與retain 相反判斷是否是
Nonpointer isa
,如果不是灿渴,則直接對散列表進行-1操作
如果是
Nonpointer isa
洛波,則對extra_rc
中的引用計數(shù)值進行-1
操作,并存儲此時的extra_rc狀態(tài)到carry
中如果此時的狀態(tài)
carray
為0骚露,則走到underflow
流程-
underflow
流程有以下幾步:判斷
散列表
中是否存儲了一半的引用計數(shù)
如果是蹬挤,則從
散列表
中取出
存儲的一半引用計數(shù),進行-1操作
棘幸,然后存儲到extra_rc
中如果此時
extra_rc
沒有值焰扳,散列表中也是空的,則直接進行析構(gòu)误续,即dealloc
操作蓝翰,屬于自動觸發(fā)
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;
//進行引用計數(shù)-1操作女嘲,即extra_rc-1
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
//如果此時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;
//判斷散列表中是否存儲了一半的引用計數(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.
//從散列表中取出存儲的一半引用計數(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.
//進行-1操作,然后存儲到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.
}
}
//此時extra_rc中值為0欣尼,散列表中也是空的爆雹,則直接進行析構(gòu)停蕉,即自動觸發(fā)dealloc流程
// Really deallocate.
//觸發(fā)dealloc的時機
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ā)送一個dealloc消息
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}
所以,綜上所述钙态,release
的底層流程如下圖所示
dealloc 源碼分析
在retain
和release
的底層實現(xiàn)中慧起,都提及了dealloc
析構(gòu)函數(shù),下面來分析dealloc
的底層的實現(xiàn)
- 進入
dealloc -> _objc_rootDealloc -> rootDealloc
源碼實現(xiàn)册倒,主要有兩件事:- 根據(jù)條件
判斷是否有isa蚓挤、cxx、關(guān)聯(lián)對象驻子、弱引用表灿意、引用計數(shù)表
,如果沒有崇呵,則直接free釋放內(nèi)存
- 如果有缤剧,則進入
object_dispose
方法
- 根據(jù)條件
inline void
objc_object::rootDealloc()
{
//對象要釋放,需要做哪些事情域慷?
//1荒辕、isa - cxx - 關(guān)聯(lián)對象 - 弱引用表 - 引用計數(shù)表
//2、free
if (isTaggedPointer()) return; // fixme necessary?
//如果沒有這些犹褒,則直接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 {
//如果有
object_dispose((id)this);
}
}
- 進入
object_dispose
源碼抵窒,其目的有以下幾個-
銷毀實例,主要有以下操作
調(diào)用c++析構(gòu)函數(shù)
刪除關(guān)聯(lián)引用
釋放散列表
清空弱引用表
free釋放內(nèi)存
-
id
object_dispose(id obj)
{
if (!obj) return nil;
//銷毀實例而不會釋放內(nèi)存
objc_destructInstance(obj);
//釋放內(nèi)存
free(obj);
return nil;
}
??
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.
//調(diào)用C ++析構(gòu)函數(shù)
if (cxx) object_cxxDestruct(obj);
//刪除關(guān)聯(lián)引用
if (assoc) _object_remove_assocations(obj);
//釋放
obj->clearDeallocating();
}
return obj;
}
??
inline void
objc_object::clearDeallocating()
{
//判斷是否為nonpointer isa
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
//如果不是叠骑,則直接釋放散列表
sidetable_clearDeallocating();
}
//如果是估脆,清空弱引用表 + 散列表
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
??
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
ASSERT(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
SideTable& table = SideTables()[this];
table.lock();
if (isa.weakly_referenced) {
//清空弱引用表
weak_clear_no_lock(&table.weak_table, (id)this);
}
if (isa.has_sidetable_rc) {
//清空引用計數(shù)
table.refcnts.erase(this);
}
table.unlock();
}
所以,綜上所述座云,dealloc
底層的流程圖如圖所示
所以疙赠,到目前為止,從最開始的alloc
底層分析(見iOS-底層原理 02:alloc & init & new 源碼分析)-> retain
-> release
-> dealloc
就全部串聯(lián)起來了
retainCount 源碼分析
引用計數(shù)的分析通過一個面試題來說明
面試題:alloc創(chuàng)建的對象的引用計數(shù)為多少朦拖?
- 定義如下代碼圃阳,打印其引用計數(shù)
NSObject *objc = [NSObject alloc];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc));
打印結(jié)果如下
- 進入
retainCount -> _objc_rootRetainCount -> rootRetainCount
源碼,其實現(xiàn)如下
- (NSUInteger)retainCount {
return _objc_rootRetainCount(self);
}
??
uintptr_t
_objc_rootRetainCount(id obj)
{
ASSERT(obj);
return obj->rootRetainCount();
}
??
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
//如果是nonpointer isa璧帝,才有引用計數(shù)的下層處理
if (bits.nonpointer) {
//alloc創(chuàng)建的對象引用計數(shù)為0捍岳,包括sideTable,所以對于alloc來說,是 0+1=1睬隶,這也是為什么通過retaincount獲取的引用計數(shù)為1的原因
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
//如果不是,則正常返回
sidetable_unlock();
return sidetable_retainCount();
}
在這里我們可以通過源碼斷點調(diào)試银萍,來查看此時的extra_rc
的值贴唇,結(jié)果如下
答案:綜上所述搀绣,alloc
創(chuàng)建的對象實際的引用計數(shù)為0
,其引用計數(shù)打印結(jié)果為1
戳气,是因為在底層rootRetainCount
方法中链患,引用計數(shù)默認(rèn)+1
了,但是這里只有
對引用計數(shù)的讀取
操作瓶您,是沒有寫入操作的麻捻,簡單來說就是:為了防止alloc創(chuàng)建的對象被釋放(引用計數(shù)為0會被釋放),所以在編譯階段呀袱,程序底層默認(rèn)進行了+1操作贸毕。實際上在extra_rc中的引用計數(shù)仍然為0
總結(jié)
alloc
創(chuàng)建的對象沒有retain和release
alloc
創(chuàng)建對象的引用計數(shù)為0
,會在編譯時期
压鉴,程序默認(rèn)加1
,所以讀取引用計數(shù)時為1
強應(yīng)用(強持有)
假設(shè)此時有兩個界面A锻拘、B油吭,從A push
到B界面,在B界面中有如下定時器代碼署拟。當(dāng)從B pop
回到A界面[圖片上傳中...(E70D3F5D-8815-4138-BFDD-017B1BFCE0E7.png-6861f8-1609331145410-0)]
時婉宰,發(fā)現(xiàn)定時器沒有停止,其方法仍然在執(zhí)行推穷,為什么?
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
其主要原因是B界面沒有釋放
蟹腾,即沒有執(zhí)行dealloc
方法,導(dǎo)致timer也無法停止和釋放
解決方式一
- 重寫
didMoveToParentViewController
方法
- (void)didMoveToParentViewController:(UIViewController *)parent{
// 無論push 進來 還是 pop 出去 正常跑
// 就算繼續(xù)push 到下一層 pop 回去還是繼續(xù)
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 走了");
}
}
解決方式二
- 定義timer時炉爆,采用
閉包
的形式,因此不需要指定target
- (void)blockTimer{
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer fire - %@",timer);
}];
}
現(xiàn)在,我們從底層來深入研究艺晴,為什么B
界面有了timer
之后然评,導(dǎo)致B界面釋放不掉,即不會走到dealloc
方法亿眠。我們可以通過官方文檔查看timerWithTimeInterval:target:selector:userInfo:repeats:
方法中對target的描述
從文檔中可以看出,timer對傳入的target具有強持有竟趾,即timer
持有self
。由于timer是定義在B界面中犀勒,所以self也持有timer
,因此 self -> timer -> self
構(gòu)成了循環(huán)引用
在iOS-底層原理 30:Block底層原理文章中铸本,針對循環(huán)應(yīng)用提供了幾種解決方式。我們我們嘗試通過__weak
即弱引用
來解決锡足,代碼修改如下
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
我們再次運行程序,進行push-pop跳轉(zhuǎn)沐批。發(fā)現(xiàn)問題還是存在先馆,即定時器方法仍然在執(zhí)行,并沒有執(zhí)行B的dealloc方法仿野,為什么呢?
-
我們使用
__weak
雖然打破了self -> timer -> self
之前的循環(huán)引用球涛,即引用鏈變成了self -> timer -> weakSelf -> self
酿秸。但是在這里我們的分析并不全面肝箱,此時還有一個Runloop對timer的強持有
,因為Runloop
的生命周期
比B
界面更長
骏融,所以導(dǎo)致了timer無法釋放
,同時也導(dǎo)致了B界面的self也無法釋放
误趴。所以枣申,最初引用鏈
應(yīng)該是這樣的加上
weakSelf
之后泊窘,變成了這樣
weakSelf 與 self
對于weakSelf
和 self
瓜贾,主要有以下兩個疑問
1、
weakSelf
會對引用計數(shù)進行+1
操作嗎龟劲?2、
weakSelf
和self
的指針地址相同嗎蚕愤,是指向同一片內(nèi)存嗎污呼?帶著疑問籍凝,我們在
weakSelf
前后打印self
的引用計數(shù)
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
__weak typeof(self) weakSelf = self;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
運行結(jié)果如下,發(fā)現(xiàn)前后self
的引用計數(shù)都是8
因此可以得出一個結(jié)論:weakSelf沒有對內(nèi)存進行+1操作
- 繼續(xù)打印
weakSelf
和self
對象,以及指針地址
po weakSelf
po self
po &weakSelf
po &self
結(jié)果如下
從打印結(jié)果可以看出囤攀,當(dāng)前self
取地址 和 weakSelf
取地址的值是不一樣的漓骚。意味著有兩個指針地址噩斟,指向的是同一片內(nèi)存空間
,即weakSelf 和 self 的內(nèi)存地址是不一樣,都指向同一片內(nèi)存空間
的
從上面打印可以看出,此時
timer
捕獲的是<LGTimerViewController: 0x7f890741f5b0>
淆九,是一個對象
拧抖,所以無法通過weakSelf來解決強持有
擦盾。即引用鏈關(guān)系為:NSRunLoop -> timer -> weakSelf(<LGTimerViewController: 0x7f890741f5b0>)
徒仓。所以RunLoop對整個 對象的空間有強持有
喂走,runloop沒停,timer 和 weakSelf是無法釋放的而我們在
Block
原理中提及的block的循環(huán)引用
,與timer
的是有區(qū)別的睡汹。通過block底層原理的方法__Block_object_assign
可知帮孔,block
捕獲的是對象的指針地址
,即weakself 是 臨時變量的指針地址
不撑,跟self
沒有關(guān)系文兢,因為weakSelf是新的地址空間
。所以此時的weakSelf相當(dāng)于中間值
焕檬。其引用關(guān)系鏈為self -> block -> weakSelf(臨時變量的指針地址)
姆坚,可以通過地址
拿到指針
所以在這里击喂,我們需要區(qū)別下block
和timer
循環(huán)引用的模型
timer模型:
self -> timer -> weakSelf -> self
,當(dāng)前的timer
捕獲的是B界面的內(nèi)存铲敛,即vc對象的內(nèi)存
,即weakSelf
表示的是vc對象
Block模型:
self -> block -> weakSelf -> self
寺枉,當(dāng)前的block捕獲的是指針地址
避归,即weakSelf
表示的是指向self的臨時變量的指針地址
解決 強引用(強持有)
以下幾種方法的思路均是:依賴中介者模式
奸柬,打破強持有
蹲蒲,其中推薦思路四
思路一:pop時在其他方法中銷毀timer
根據(jù)前面的解釋表锻,我們知道由于Runloop對timer的強持有
拷肌,導(dǎo)致了Runloop間接的強持有了self
(因為timer中捕獲的是vc對象
)拴清。所以導(dǎo)致dealloc
方法無法執(zhí)行。需要查看在pop
時蛛株,是否還有其他方法可以銷毀timer
。這個方法就是didMoveToParentViewController
didMoveToParentViewController
方法东涡,是用于當(dāng)一個視圖控制器中添加或者移除viewController后,必須調(diào)用的方法择诈。目的是為了告訴iOS,已經(jīng)完成添加/刪除子控制器的操作顶掉。在B界面中重寫
didMoveToParentViewController
方法
- (void)didMoveToParentViewController:(UIViewController *)parent{
// 無論push 進來 還是 pop 出去 正常跑
// 就算繼續(xù)push 到下一層 pop 回去還是繼續(xù)
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 走了");
}
}
思路二:中介者模式,即不使用self,依賴于其他對象
在timer模式中巷嚣,我們重點關(guān)注的是fireHome
能執(zhí)行蜕煌,并不關(guān)心timer捕獲的target
是誰,由于這里不方便使用self
(因為會有強持有問題)绿贞,所以可以將target換成其他對象
因块,例如將target換成NSObject對象
,將fireHome
交給target
執(zhí)行
- 將timer的target 由self改成objc
//**********1籍铁、定義其他對象**********
@property (nonatomic, strong) id target;
//**********1涡上、修改target**********
self.target = [[NSObject alloc] init];
class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:");
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(fireHome) userInfo:nil repeats:YES];
//**********3、imp**********
void fireHomeObjc(id obj){
NSLog(@"%s -- %@",__func__,obj);
}
運行結(jié)果如下
運行發(fā)現(xiàn)執(zhí)行dealloc
之后寨辩,timer還是會繼續(xù)執(zhí)行
吓懈。原因是解決了中介者的釋放
,但是沒有解決中介者的回收
靡狞,即self.target
的回收耻警。所以這種方式有缺陷
可以通過在dealloc
方法中,取消定時器來解決甸怕,代碼如下
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}
運行結(jié)果如下甘穿,發(fā)現(xiàn)pop之后,timer釋放梢杭,從而中介者也會進行回收釋放
思路三:自定義封裝timer
這種方式是根據(jù)思路二的原理温兼,自定義封裝timer,其步驟如下
- 自定義timerWapper
-
在初始化方法中武契,定義一個timer募判,其target是自己。即
timerWapper
中的timer
咒唆,一直監(jiān)聽自己届垫,判斷selector
,此時的selector已交給了傳入的target(即vc對象)全释,此時有一個方法fireHomeWapper
装处,在方法中,判斷target是否存在如果
target存在
浸船,則需要讓vc知道妄迁,即向傳入的target發(fā)送selector消息寝蹈,并將此時的timer參數(shù)也一并傳入,所以vc就可以得知fireHome
方法登淘,就這事這種方式定時器方法能夠執(zhí)行的原因如果
target不存在
箫老,已經(jīng)釋放了,則釋放當(dāng)前的timerWrapper形帮,即打破了RunLoop對timeWrapper的強持有 (timeWrapper <-×- RunLoop
)
自定義
cjl_invalidate
方法中釋放timer槽惫。這個方法在vc的dealloc方法中調(diào)用周叮,即vc釋放,從而導(dǎo)致timerWapper釋放
辩撑,打破了vc
對timeWrapper
的的強持有(vc -×-> timeWrapper
)
-
//*********** .h文件 ***********
@interface CJLTimerWapper : NSObject
- (instancetype)cjl_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- (void)cjl_invalidate;
@end
//*********** .m文件 ***********
#import "CJLTimerWapper.h"
#import <objc/message.h>
@interface CJLTimerWapper ()
@property(nonatomic, weak) id target;
@property(nonatomic, assign) SEL aSelector;
@property(nonatomic, strong) NSTimer *timer;
@end
@implementation CJLTimerWapper
- (instancetype)cjl_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
if (self == [super init]) {
//傳入vc
self.target = aTarget;
//傳入的定時器方法
self.aSelector = aSelector;
if ([self.target respondsToSelector:self.aSelector]) {
Method method = class_getInstanceMethod([self.target class], aSelector);
const char *type = method_getTypeEncoding(method);
//給timerWapper添加方法
class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type);
//啟動一個timer,target是self仿耽,即監(jiān)聽自己
self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
}
}
return self;
}
//一直跑runloop
void fireHomeWapper(CJLTimerWapper *wapper){
//判斷target是否存在
if (wapper.target) {
//如果存在則需要讓vc知道合冀,即向傳入的target發(fā)送selector消息,并將此時的timer參數(shù)也一并傳入项贺,所以vc就可以得知`fireHome`方法君躺,就這事這種方式定時器方法能夠執(zhí)行的原因
//objc_msgSend發(fā)送消息,執(zhí)行定時器方法
void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
lg_msgSend((__bridge void *)(wapper.target), wapper.aSelector,wapper.timer);
}else{
//如果target不存在开缎,已經(jīng)釋放了棕叫,則釋放當(dāng)前的timerWrapper
[wapper.timer invalidate];
wapper.timer = nil;
}
}
//在vc的dealloc方法中調(diào)用,通過vc釋放奕删,從而讓timer釋放
- (void)cjl_invalidate{
[self.timer invalidate];
self.timer = nil;
}
- (void)dealloc
{
NSLog(@"%s",__func__);
}
@end
- timerWapper的使用
//定義
self.timerWapper = [[CJLTimerWapper alloc] cjl_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
//釋放
- (void)dealloc{
[self.timerWapper cjl_invalidate];
}
運行結(jié)果如下
這種方式看起來比較繁瑣俺泣,步驟很多,而且針對timerWapper
完残,需要不斷的添加method伏钠,需要進行一系列的處理。
思路四:利用NSProxy虛基類的子類
下面來介紹一種timer
強引用最常用
的處理方式:NSProxy子類
可以通過NSProxy
虛基類谨设,可以交給其子類實現(xiàn)熟掂,NSProxy的介紹在iOS-底層原理 30:Block底層原理已經(jīng)介紹過了,這里不再重復(fù)
- 首先定義一個繼承自
NSProxy
的子類
//************NSProxy子類************
@interface CJLProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end
@interface CJLProxy()
@property (nonatomic, weak) id object;
@end
@implementation CJLProxy
+ (instancetype)proxyWithTransformObject:(id)object{
CJLProxy *proxy = [CJLProxy alloc];
proxy.object = object;
return proxy;
}
-(id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
- 將
timer
中的target
傳入NSProxy子類對象
扎拣,即timer持有NSProxy子類對象
//************解決timer強持有問題************
self.proxy = [CJLProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
//在dealloc中將timer正常釋放
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
}
這樣做的主要目的是將強引用的注意力轉(zhuǎn)移成了消息轉(zhuǎn)發(fā)
赴肚。虛基類只負(fù)責(zé)消息轉(zhuǎn)發(fā),即使用NSProxy
作為中間代理二蓝、中間者
這里有個疑問誉券,定義的proxy
對象,在dealloc釋放時侣夷,還存在嗎横朋?
-
proxy
對象會正常釋放,因為vc
正常釋放了百拓,所以可以釋放其持有者琴锭,即timer和proxy
晰甚,timer
的釋放也打破了runLoop對proxy的強持有
。完美的達到了兩層釋放
决帖,即vc -×-> proxy <-×- runloop
厕九,解釋如下:vc釋放,導(dǎo)致了
proxy
的釋放dealloc方法中地回,timer進行了釋放扁远,所以runloop強引用也釋放了