相關(guān)文獻:
iOS 內(nèi)存管理底層分析(一)- 內(nèi)存相關(guān)
iOS 內(nèi)存管理底層分析(二)- AutoreleasePool底層
本文掌握知識點:
1.內(nèi)存的五大分區(qū)
2.內(nèi)存管理方案:MRC跋涣、ARC、TaggedPointer鸟悴、nonpointer_isa陈辱、SideTables、自動釋放池
3.weak_table_t 弱引用表底層原理细诸、__weak的底層原理沛贪、弱引用對象的引用計數(shù)問題
4.retain、release震贵、dealloc 的源碼分析
一利赋、內(nèi)存的五大分區(qū)
堆區(qū)
:
堆是向高地址擴展的數(shù)據(jù)結(jié)構(gòu);是不連續(xù)的內(nèi)存區(qū)域猩系,類似于鏈表結(jié)構(gòu)(便于增刪媚送,不便于查詢)
,遵循先進先出(FIFO)
原則寇甸;通常以alloc/new/malloc
方式創(chuàng)建的對象塘偎。內(nèi)存地址以0x6
開頭。
優(yōu)點:靈活方便拿霉,數(shù)據(jù)適應(yīng)面廣泛吟秩。
缺點:需手動管理,速度慢绽淘、容易產(chǎn)生內(nèi)存碎片涵防。棧區(qū)
:
棧是系統(tǒng)數(shù)據(jù)結(jié)構(gòu),其對應(yīng)的進程或線程是唯一的沪铭;棧是向低地址擴展數(shù)據(jù)結(jié)構(gòu)壮池,是一塊連續(xù)的存儲區(qū)域,遵循先進后出(FILO)
的原則杀怠。堆區(qū)的分配一般是在運行時分配火窒;存儲局部變量、方法參數(shù)驮肉、對象的指針等熏矿。內(nèi)存地址以0x7
開頭。
優(yōu)點:棧是由編譯器自動分配并釋放的,不會產(chǎn)生內(nèi)存碎片票编,所以快速高效褪储,便于查詢,不便于增刪
慧域。
缺點:內(nèi)存大小有限制鲤竹,數(shù)據(jù)不靈活主線程棧大小是1MB,子線程棧大小是512KB昔榴。全局區(qū)(靜態(tài)區(qū))
:
全局區(qū)是編譯時分配的內(nèi)存空間辛藻,程序運行過程中,此內(nèi)存中的數(shù)據(jù)一直存在互订,程序結(jié)束后由系統(tǒng)釋放吱肌,主要存放:未初始化的全局變量和靜態(tài)變量,即bss區(qū)(.bss)仰禽;已初始化的全局變量和靜態(tài)變量氮墨,即數(shù)據(jù)區(qū)(.data)。內(nèi)存地址以0x1
開頭吐葵。
其中规揪,全局變量是指變量值可以在運行時被動態(tài)修改,而靜態(tài)變量是static修飾的變量温峭,包含靜態(tài)局部變量和靜態(tài)全局變量猛铅。常量區(qū)
:
常量區(qū)是編譯時分配的內(nèi)存空間,在程序結(jié)束后由系統(tǒng)釋放凤藏,主要存放 已經(jīng)使用了的奕坟,且沒有指向的字符串常量代碼區(qū)
:
代碼區(qū)是編譯時分配主要用于存放程序運行時的代碼,代碼會被編譯成二進制存進內(nèi)存的
內(nèi)存五大區(qū)的驗證:
內(nèi)存布局
介紹了內(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)存布局面試題
面試題:全局變量和局部變量在內(nèi)存中是否有區(qū)別?如果有桂塞,是什么區(qū)別凹蜂? 答案是有區(qū)別。
-
全局變量
保存在內(nèi)存的全局存儲區(qū)(即bss+data段)
,占用靜態(tài)的存儲單元玛痊; -
局部變量
保存在棧
中汰瘫,只有在所在函數(shù)被調(diào)用時才動態(tài)的為變量分配存儲單元。
二擂煞、內(nèi)存管理方案
MRC
- 手動引用計數(shù)ARC
- 自動引用計數(shù)nonpointer_isa
- 新版OC對象的isa指針優(yōu)化TaggedPointer
- 小對象優(yōu)化SideTables
- 散列表 (引用計數(shù)表和弱引用表)autoreleasePool
- 自動釋放池
三混弥、MRC 與 ARC (引用計數(shù))
引用計數(shù)
是管理對象聲明周期的一種方式。當新建一個對象它的引用計數(shù)為1对省,當這個對象引用計數(shù)為0的時候蝗拿,這個對象就會被銷毀并釋放其所占用的內(nèi)存空間。
MRC
在MRC時代蒿涎,系統(tǒng)是通過對象的引用計數(shù)來判斷一個是否銷毀哀托,有以下規(guī)則:
- 對象被創(chuàng)建時引用計數(shù)都為1;
- 當對象被其他指針引用時同仆,需要手動調(diào)用
[objc retain]
萤捆,使對象的引用計數(shù)+1; - 當指針變量不再使用對象時俗批,需要手動調(diào)用
[objc release]
來釋放對象俗或,使對象的引用計數(shù)-1; - 當一個對象的引用計數(shù)為0時岁忘,系統(tǒng)就會銷毀這個對象辛慰。
ARC
ARC
模式是在WWDC2011
和iOS5
引入的自動管理機制,即自動引用計數(shù)
干像。是編譯器的一種特性帅腌。其規(guī)則與MRC一致,區(qū)別在于無需程序員手動插入內(nèi)存管理相關(guān)代碼麻汰。
結(jié)論:
1.在MRC模式
下速客,必須遵守:誰創(chuàng)建,誰釋放五鲫,誰引用溺职,誰管理。
2.ARC模式
下不需要手動retain位喂、release浪耘、autorelease
,編譯器會在適當?shù)奈恢貌迦?code>release和autorelease
塑崖。
四七冲、nonpointer_isa - isa指針優(yōu)化
nonpointer_isa
:非指針類型的isa,主要是在創(chuàng)建對象時规婆,用來優(yōu)化isa指針的64位地址澜躺,具體內(nèi)容在Objective-C 對象的底層探索蝉稳。
我們知道在創(chuàng)建OC對象的時候,會初始化一個8字節(jié)的isa指針指向該OC對象的類對象苗踪。在舊版本的OC對象的isa指針主要記錄著對象的引用計數(shù)颠区,很顯然僅僅是記錄這就使用8字節(jié)(64位)是非常奢侈的。于是在新版本的OC對象對isa的64位進行了優(yōu)化通铲。
五毕莱、TaggedPointer - 小對象
TaggedPointer
:是一個被打上標記的指針,在棧上分配8字節(jié)指針(不再需要堆去分配)颅夺,該指針指向的不再是地址朋截,而是真實值
。TaggedPointer專門用來處理小對象吧黄,例如NSNumber部服、NSDate、小NSString等拗慨。(它是在64位iOS系統(tǒng)下提出來的 iPhone5s以后)
以NSString
為例廓八,運行下面代碼打印:
NSString *firstString = @"helloworld"; // __NSCFConstantString 常量區(qū)
NSString *secondString = [NSString stringWithFormat:@"helloworld"]; // __NSCFString 堆區(qū)
NSString *thirdString = @"hello"; // __NSCFConstantString 常量區(qū)
NSString *fourthSting = [NSString stringWithFormat:@"hello"]; // NSTaggedPointerString 棧指針
NSLog(@"%p %@",firstString,[firstString class]); // 0x1058d60c0 __NSCFConstantString
NSLog(@"%p %@",secondString,[secondString class]); // 0x600000b7b960 __NSCFString
NSLog(@"%p %@",thirdString,[thirdString class]); // 0x1058d60e0 __NSCFConstantString
NSLog(@"%p %@",fourthSting,[fourthSting class]); // 0xd9d08f5a3bd7e123 NSTaggedPointerString
注意:此時打印NSTaggedPointerString
類型指針的內(nèi)容0xd9d08f5a3bd7e123
其實是混淆過的赵抢。在下文探討 小對象地址分析 時會介紹剧蹂。
NSString
的內(nèi)存管理主要分為3種:
__NSCFConstantString
:字符串常量,是一種編譯時常量烦却,retainCount值很大宠叼,對其操作不會引起引用計數(shù)變化,存儲在字符串常量區(qū)
其爵。__NSCFString
:是在運行時創(chuàng)建的NSString子類冒冬,創(chuàng)建后引用計數(shù)會加1,存儲在堆上
摩渺。NSTaggedPointerString
:標簽指針简烤,是蘋果在64位環(huán)境下對NSString、NSNumber等對象做的優(yōu)化摇幻。
對于NSString對象來說俱诸,當字符串是由數(shù)字登失、英文字母組合且長度 <= 9時
定欧,會自動成為NSTaggedPointerString類型
实苞,存儲在常量區(qū)
歹篓;當有中文 或者 其他特殊符號 或 長度 > 9
時吗铐,會直接成為__NSCFString類型
溉瓶,存儲在堆區(qū)
万矾。
1.小對象地址分析
以NSString為例:
- 對于一般的
NSString對象指針
伤疙,都是string值 + 指針地址
银酗,兩者是分開的辆影; - 對于
TaggedPointer指針
,是指針 + 值
黍特,都能在小對象中體現(xiàn)蛙讥。所以TaggedPointer 既包含指針,也包含值灭衷。
上文中提到過NSLog(@"%p %@",fourthSting,[fourthSting class]);
打印出來的取地址的內(nèi)容是混淆過的次慢。
這是因為objc4-838.1源碼在類的加載過程中的_objc_init -> map_images -> _read_images -> initializeTaggedPointerObfuscator
我們可以在源碼中通過objc_debug_taggedpointer_obfuscator
查找taggedPointer
的編碼和解碼
:
#define OBJC_TAG_INDEX_MASK 0x7UL
#define OBJC_TAG_INDEX_SHIFT 0
extern uintptr_t objc_debug_taggedpointer_obfuscator;
extern uint8_t objc_debug_tag60_permutations[8];
uintptr_t objc_obfuscatedTagToBasicTag(uintptr_t tag) {
for (unsigned i = 0; i < 7; i++)
if (objc_debug_tag60_permutations[i] == tag)
return i;
return 7;
}
uintptr_t
objc_decodeTaggedPointer(id ptr)
{
uintptr_t value = (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
uintptr_t basicTag = (value >> OBJC_TAG_INDEX_SHIFT) & OBJC_TAG_INDEX_MASK;
value &= ~(OBJC_TAG_INDEX_MASK << OBJC_TAG_INDEX_SHIFT);
value |= objc_obfuscatedTagToBasicTag(basicTag) << OBJC_TAG_INDEX_SHIFT;
return value;
}
static inline uintptr_t objc_basicTagToObfuscatedTag(uintptr_t tag) {
return objc_debug_tag60_permutations[tag];
}
void *
objc_encodeTaggedPointer(uintptr_t ptr)
{
uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
uintptr_t basicTag = (value >> OBJC_TAG_INDEX_SHIFT) & OBJC_TAG_INDEX_MASK;
uintptr_t permutedTag = objc_basicTagToObfuscatedTag(basicTag);
value &= ~(OBJC_TAG_INDEX_MASK << OBJC_TAG_INDEX_SHIFT);
value |= permutedTag << OBJC_TAG_INDEX_SHIFT;
return (void *)value;
}
于是我們就可以通過調(diào)用objc_decodeTaggedPointer
來還原真實指針內(nèi)容:
其中最高位是標記是否是TaggedPointer,最低三位是看是什么類型翔曲,這里010的十進制是2 表示NSString迫像,如下圖:
可以通過objc4源碼中的_objc_makeTaggedPointer
方法的參數(shù)tag類型objc_tag_index_t
進入其枚舉,其中 2表示NSString
瞳遍,3表示NSNumber
:
舉例NSNumber
:
2.taggedpointer面試題
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.queue = dispatch_queue_create("com.wj.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"WJ"]; // 棧(長度<=9)
NSLog(@"%@",self.nameStr);
});
}
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"安安安安"]; // 堆(中文字符)
NSLog(@"%@",self.nameStr);
});
}
}
結(jié)果:引發(fā)崩潰闻妓,崩潰在第二個for循環(huán)里。
原因:在ARC環(huán)境下掠械,這里使用多線程有可能引發(fā)對同一塊堆內(nèi)存多次調(diào)用了release
由缆。
六、SideTables - 散列表
SideTables
:散列表猾蒂,在散列表中主要有兩個表均唉,分別是引用計數(shù)表
和弱引用表
。(每次在訪問SideTables時的加鎖解鎖操作婚夫,會降低效率的)
但是散列表不是一張表浸卦,而是多張表。
打開objc4-838.1源碼案糙,找到objc_retain
-> retain
-> rootRetain
這里在本章節(jié)的 七限嫌、retain源碼分析 部分會著重介紹,有興趣可以滑下去看时捌。主要內(nèi)容就是引用計數(shù)的存儲方案怒医。找到 sidetable_addExtraRC_nolock
:
它是從多張表SideTables
里獲取該對象的SideTable
。
而SideTables
其實是StripedMap
類型:
多張表并不是無限數(shù)量的奢讨,蘋果設(shè)計出: SideTables
在真機下8張表/模擬器下64張表 的方案稚叹,以達到運行效率和節(jié)省內(nèi)存平衡目的。
提問一:那我可以設(shè)計只有一張全局的表來存儲所有對象的引用計數(shù)和弱引用信息嗎拿诸?
答案:可以扒袖。但是每次讀寫對象的時候都得操作這張全局表,而這張全局每次讀寫都需要頻繁地加速/解鎖操作亩码,這樣會導(dǎo)致系統(tǒng)非常地慢季率。
提問二:那我可以設(shè)計每一個對象都有它單獨的表來存儲自己的引用計數(shù)和弱引用信息嗎?
答案:可以描沟。但是每創(chuàng)建一個對象會就會產(chǎn)生一張表飒泻,則會產(chǎn)生好多的表鞭光,引發(fā)大量消耗內(nèi)存的問題。
1.RefcountMap - 引用計數(shù)表
RefcountMap
是用來存儲引用計數(shù)的泞遗。(RefcountMap在一般情況下是用不到的)惰许。
這里的內(nèi)容在本文章節(jié) 七、retain源碼分析 里有分析史辙,這里只給總結(jié):
- a.當
isa指針
不是nonpointer_isa
類型的時候汹买,該對象的引用計數(shù)就存儲在SideTable
里的RefcountMap
; - b.當
isa指針
是nonpointer_isa
類型,并且nonpointer_isa
里的extra_rc
存滿了髓霞,會把另一半的引用計數(shù)存儲到SideTable
里的RefcountMap
里卦睹,而extra_rc
只有原來的一半。
2.weak_table_t
- 弱引用表(weak底層原理)
UIViewController *oldVC = [UIViewController new];
__weak typeof(oldVC) weakVC = oldVC; // 舊的
UIViewController *newVC = [UIViewController new];
weakVC = newVC; // 新的
接下來看看weak底層邏輯吧方库。
使用__weak
修飾的指針指向一個對象時结序,會走源碼中的objc_initWeak
。
打開objc4-838.1源碼纵潦,搜索objc_initWeak
函數(shù)
storeWeak
函數(shù)就是處理__weak修飾的弱對象
指向一個對象的處理:
// Update a weak variable.
// If HaveOld is true, the variable has an existing value
// that needs to be cleaned up. This value might be nil.
// If HaveNew is true, there is a new value that needs to be
// assigned into the variable. This value might be nil.
// If CrashIfDeallocating is true, the process is halted if newObj is
// deallocating or newObj's class does not support weak references.
// If CrashIfDeallocating is false, nil is stored instead.
enum CrashIfDeallocating {
DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};
// HaveOld: weak指針是否之前就指向了對象 即weakVC是否指向過oldVC
// HaveNew: weak指針是否將指向的新的對象 即weakVC是否將要指向newVC
// CrashIfDeallocating: 被弱引用的對象是否正在析構(gòu)徐鹤,如果析構(gòu)則Crash
template <HaveOld haveOld, HaveNew haveNew,
enum CrashIfDeallocating crashIfDeallocating>
// location: 弱引用指針的地址 即weakVC的地址
// newObj: 將被弱引用指針指向的對象 即newVC對象
static id
storeWeak(id *location, objc_object *newObj)
{
ASSERT(haveOld || haveNew);
if (!haveNew) ASSERT(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj; // 獲取 weakVC之前指向的對象oldVC,若沒有指向過邀层,則為nil
SideTable *oldTable; // 獲取 weakVC之前指向的對象oldVC的散列表返敬,若沒有指向過,則為nil
SideTable *newTable; // 獲取 weakVC將被弱引用的對象newVC的散列表
// Acquire locks for old and new values.
// Order by lock address to prevent lock ordering problems.
// Retry if the old value changes underneath us.
retry:
if (haveOld) { // 如果weakVC曾經(jīng)指向過oldVC寥院,獲取 oldVC對象 和 oldVC散列表
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (haveNew) { // 如果weakVC將要指向newVC劲赠,獲取 newVC散列表
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable); // 加鎖
// 排除異常情況(不用管):如果weakVC曾經(jīng)指向過oldVC,并且weakVC指向的還不是oldVC
if (haveOld && *location != oldObj) {
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}
// Prevent a deadlock between the weak reference machinery
// and the +initialize machinery by ensuring that no
// weakly-referenced object has an un-+initialized isa.
// 排除異常情況(不用管):newVC的類對象還沒有被初始化秸谢,就去初始化
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
class_initialize(cls, (id)newObj);
// If this class is finished with +initialize then we're good.
// If this class is still running +initialize on this thread
// (i.e. +initialize called storeWeak on an instance of itself)
// then we may proceed but it will appear initializing and
// not yet initialized to the check above.
// Instead set previouslyInitializedClass to recognize it on retry.
previouslyInitializedClass = cls;
goto retry;
}
}
// Clean up old value, if any.
if (haveOld) { // 如果weakVC曾經(jīng)指向過oldVC凛澎,則把之前的弱引用注銷掉
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// Assign new value, if any.
// weakVC去注冊新的弱引用,并指向newVC
if (haveNew) {
newObj = (objc_object *)
// 怎么注冊估蹄?將weakVC的地址存儲到SideTable里weak_table_t里去
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected
// Set is-weakly-referenced bit in refcount table.
// 如果不是TaggedPointer或nil塑煎,將isa里的是否被弱引用weakly_referenced 置為true
if (!_objc_isTaggedPointerOrNil(newObj)) {
newObj->setWeaklyReferenced_nolock(); // 將isa的是否被弱引用置為true
}
// Do not set *location anywhere else. That would introduce a race.
// 將weakVC的地址保存newVC
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable); // 解鎖
// This must be called without the locks held, as it can invoke
// arbitrary code. In particular, even if _setWeaklyReferenced
// is not implemented, resolveInstanceMethod: may be, and may
// call back into the weak reference machinery.
callSetWeaklyReferenced((id)newObj);
return (id)newObj;
}
weak
底層調(diào)用:objc_initWeak
-> storeWeak
storeWeak
的底層邏輯:
- 1.如果
weak指針
已經(jīng)指向了一個A對象,則會把weak指針地址
從SideTable
散列表里的弱引用表weak_table_t
中注銷weak_unregister_no_lock
臭蚁; - 2.如果
weak指針
要 改變指向/指向新的 B對象最铁,則會把weak指針地址
注冊進SideTable
散列表里的弱引用表weak_table_t
中weak_register_no_lock
,并且將 B對象的類對象的isa
里的weakly_referenced
置為true垮兑,還會將weak指針
指向B對象冷尉。
下面就來研究如何注冊和注銷的。
3.了解weak指針
在弱引用表weak_table_t
是如何注銷和注冊的(探索weak_unregister_no_lock
系枪、weak_register_no_lock
)雀哨。
3.1了解weak_table_t
的數(shù)據(jù)結(jié)構(gòu):
struct weak_table_t {
// weak_entry_t是一個hash表,key:當前對象的地址嗤无,value: 存儲弱引用指針的地址的數(shù)組
weak_entry_t *weak_entries; // (因為一個對象可以被多個弱引用去指向)
size_t num_entries; // 個數(shù)
uintptr_t mask; // 數(shù)組的長度-1(擴容相關(guān))
uintptr_t max_hash_displacement; // 解決hash沖突
};
// hash數(shù)組
struct weak_entry_t {
// referent是一個動態(tài)的hash數(shù)組震束,存儲弱引用指針的地址的數(shù)組
DisguisedPtr<objc_object> referent;
union {
// 1.當前對象被弱引用個數(shù)>4時,就會用這個struct來存儲弱引用指針的地址
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
// 2.當前對象被弱引用個數(shù)<=4時当犯,就會用weak_referrer_t來存儲弱引用指針的地址
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
// out_of_line用來判斷垢村,用哪種1/2方式來存儲弱引用指針的地址
bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}
weak_entry_t& operator=(const weak_entry_t& other) {
memcpy(this, &other, sizeof(other));
return *this;
}
// weak_entry_t的構(gòu)造函數(shù)
weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent)
{
inline_referrers[0] = newReferrer;
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};
注意:SideTable在真機下有8張表,所以weak_table_t也有8張表嚎卫。
weak_table_t
是一個hash表嘉栓,key:當前對象的地址;value:存儲弱引用指針的地址的數(shù)組拓诸。為什么是數(shù)組侵佃?因為一個對象可以同時被多個弱引用去指向。
weak_table_t 數(shù)據(jù)結(jié)構(gòu)類似于:
// [key: value]
{
A對象的地址: [weak1地址, weak2地址],
B對象的地址: [weak3地址, weak4地址],
...
}
3.2 weak_register_no_lock
將弱引用注冊到對象的弱引用表weak_table_t
中
weak_register_no_lock
的底層邏輯:
- a.對TaggedPointer和nil不處理奠支;
- b.通過被引用對象的地址去取出
weak_entry_t哈希數(shù)組
馋辈,如果有哈希數(shù)組,則直接將弱引用指針地址
存入這個哈希數(shù)組倍谜; - c.如果沒有哈希數(shù)組迈螟,則創(chuàng)建一個,再將
弱引用指針地址
插入到哈希數(shù)組尔崔。
3.3 weak_unregister_no_lock
將弱引用從對象的弱引用表weak_table_t
中注冊
weak_unregister_no_lock
的底層邏輯:
- a.通過
被引用對象的地址
去取出weak_entry_t哈希數(shù)組
答毫,如果有哈希數(shù)組,則將弱引用指針地址
從哈希數(shù)組中移除季春。倘若哈希數(shù)組空了洗搂,則需要去清空這個對象的弱引用表weak_table_t
。
weak面試題:(弱引用對象的引用計數(shù)問題)
為什么weakVC打印引用計數(shù)是2呢载弄?
UIViewController *oldVC = [UIViewController new];
__weak typeof(oldVC) weakVC = oldVC; // 舊的
UIViewController *newVC = [UIViewController new];
weakVC = newVC; // 新的
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)(newVC)));//1 斷點這
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)(weakVC)));//2 斷點這
打開匯編調(diào)試Debug
->Debug Workflow
->Always Show Disassembly
當打印強引用newVC的引用計數(shù)時耘拇,可以看到匯編會調(diào)用CFGetRetainCount
,而打印弱引用weakVC的引用計數(shù)時候侦锯,則會調(diào)用objc_loadWeakRetained
打開objc4-838.1源碼驼鞭,找到objc_loadWeakRetained
:
注意:在打印弱引用weakVC的引用計數(shù)時候,會對obj
進行引用計數(shù)+1的操作尺碰,但是由于obj
是一個局部變量挣棕,出了函數(shù)域則會引用計數(shù)-1。
每次打印引用weakVC的引用計數(shù)都是2亲桥,其實是一個假象而已洛心。
七、retain源碼分析
打開objc4-838.1源碼题篷,搜索objc_retain
函數(shù)
rootRetain
函數(shù)就是處理對象的引用計數(shù)的邏輯:
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
if (slowpath(isTaggedPointer())) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
// 先去獲取對象的isa指針词身,因為引用計數(shù)信息存儲在isa里
isa_t oldisa;
isa_t newisa;
oldisa = LoadExclusive(&isa.bits);
if (variant == RRVariant::FastOrMsgSend) {
// These checks are only meaningful for objc_retain()
// They are here so that we avoid a re-load of the isa.
if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
ClearExclusive(&isa.bits);
if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
return swiftRetain.load(memory_order_relaxed)((id)this);
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
}
}
if (slowpath(!oldisa.nonpointer)) {
// a Class is a Class forever, so we can perform this check once
// outside of the CAS loop
if (oldisa.getDecodedClass(false)->isMetaClass()) {
ClearExclusive(&isa.bits);
return (id)this;
}
}
// 核心邏輯在這個do...while!7丁法严!
do {
transcribeToSideTable = false;
newisa = oldisa;
// 1.不是nonpointer_isa的情況损敷,sidetable存儲引用計數(shù)
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain(sideTableLocked);
}
// 下面代碼邏輯 是nonpointer_isa的情況
// don't check newisa.fast_rr; we already called any RR overrides
if (slowpath(newisa.isDeallocating())) {
ClearExclusive(&isa.bits);
if (sideTableLocked) {
ASSERT(variant == RRVariant::Full);
sidetable_unlock();
}
if (slowpath(tryRetain)) {
return nil;
} else {
return (id)this;
}
}
// 2.是nonpointer_isa的情況,引用技術(shù)位extra_rc存得下深啤,直接+1
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
// 3.是nonpointer_isa的情況拗馒,引用技術(shù)位extra_rc存不下,extra_rc保留一半的引用計數(shù)溯街,并準備將另一半copy到sidetable
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
// 判斷extra_rc是否超出
if (variant != RRVariant::Full) {
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.
// extra_rc存不下,保留一半的引用計數(shù)诱桂,并準備將另一半copy到sidetable
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
if (variant == RRVariant::Full) {
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
} else {
ASSERT(!transcribeToSideTable);
ASSERT(!sideTableLocked);
}
return (id)this;
}
總結(jié):
retain
源碼調(diào)用流程:objc_retain
--> objc_object::retain()
--> objc_object::rootRetain
rootRetain
操作邏輯:
- 1.判斷對象是否為
taggedPointer類型
,如果是 則 return呈昔; - 2.獲取對象的
isa指針
里取出對象的引用計數(shù)信息挥等; -
3.判斷
isa
是否是nonpointer_isa
:(通常是nonpointer_isa)
3.1 如果不是nonpointer_isa
,將sidetable散列表
里的引用計數(shù)+1并return堤尾。
3.2 如果是nonpointer_isa
肝劲,并且對象正在被釋放,直接return郭宝。
3.3 如果是nonpointer_isa
涡相,對象不是正在被釋放,進入下一步4剩蟀; -
4.先讓
isa
里的extra_rc
引用計數(shù)+1催蝗,判斷是否能夠存得下:
4.1 如果extra_rc位
能存得下,就存著育特。
4.2 如果extra_rc位
能存不下(少數(shù)情況才會出現(xiàn))丙号,將has_sidetable_rc
標志位為1,extra_rc
保留一半的引用計數(shù)缰冤,將另一半的引用計數(shù)存儲到sidetable
提問:為什么要設(shè)計使用extra_rc
來存儲引用計數(shù)犬缨,extra_rc
存滿了才將另一半的引用計數(shù)存儲到sidetable
呢?
因為sidetable
每次讀寫都需要加鎖解鎖的操作棉浸,系統(tǒng)就沒有那么快怀薛。這樣設(shè)計的目的是提高性能
。
八迷郑、release源碼分析
打開objc4-838.1源碼枝恋,搜索objc_release
函數(shù)
rootRelease
函數(shù)就是處理對象的引用計數(shù)的邏輯:
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
if (slowpath(isTaggedPointer())) return false;
bool sideTableLocked = false;
// 獲取對象的isa,因為引用計數(shù)信息存儲在isa里
isa_t newisa, oldisa;
oldisa = LoadExclusive(&isa.bits);
if (variant == RRVariant::FastOrMsgSend) {
// These checks are only meaningful for objc_release()
// They are here so that we avoid a re-load of the isa.
if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
ClearExclusive(&isa.bits);
if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
swiftRelease.load(memory_order_relaxed)((id)this);
return true;
}
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
return true;
}
}
if (slowpath(!oldisa.nonpointer)) {
// a Class is a Class forever, so we can perform this check once
// outside of the CAS loop
if (oldisa.getDecodedClass(false)->isMetaClass()) {
ClearExclusive(&isa.bits);
return false;
}
}
retry:
// 引用計數(shù)真正操作邏輯在do...while里
do {
newisa = oldisa;
// 1.如果不是nonpointer_isa嗡害,操作sidetable里的引用計數(shù)-1
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
return sidetable_release(sideTableLocked, performDealloc);
}
// 判斷對象是否正在釋放焚碌,如果是,則直接return
if (slowpath(newisa.isDeallocating())) {
ClearExclusive(&isa.bits);
if (sideTableLocked) {
ASSERT(variant == RRVariant::Full);
sidetable_unlock();
}
return false;
}
// don't check newisa.fast_rr; we already called any RR overrides
// 將nonpointer_isa里的引用計數(shù)位extra_rc進行-1操作
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
if (slowpath(carry)) {
// don't ClearExclusive()
goto underflow; // 如果extra_rc的值為0的情況霸妹,走underflow代碼塊
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
if (slowpath(newisa.isDeallocating()))
goto deallocate; // 釋放對象內(nèi)存
if (variant == RRVariant::Full) {
if (slowpath(sideTableLocked)) sidetable_unlock();
} else {
ASSERT(!sideTableLocked);
}
return false;
underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate
// abandon newisa to undo the decrement
newisa = oldisa;
// 如果nonpointer_isa的sidetable標志位 has_sidetable_rc == 1
if (slowpath(newisa.has_sidetable_rc)) {
if (variant != RRVariant::Full) {
ClearExclusive(&isa.bits);
return rootRelease_underflow(performDealloc);
}
// Transfer retain count from side table to inline storage.
// 將引用計數(shù)從sidetable轉(zhuǎn)移回extra_rc
if (!sideTableLocked) {
ClearExclusive(&isa.bits);
sidetable_lock();
sideTableLocked = true;
// Need to start over to avoid a race against
// the nonpointer -> raw pointer transition.
oldisa = LoadExclusive(&isa.bits);
goto retry;
}
// Try to remove some retain counts from the side table.
// 把sidetable里的引用計數(shù)移除
auto borrow = sidetable_subExtraRC_nolock(RC_HALF);
// 如果sidetable上沒有多余的東西十电,我們就把sidetable清理干凈
bool emptySideTable = borrow.remaining == 0; // we'll clear the side table if no refcounts remain there
if (borrow.borrowed > 0) {
// Side table retain count decreased.
// Try to add them to the inline count.
// sidetable保留引用計數(shù)-1。嘗試將它們添加到內(nèi)聯(lián)計數(shù)中。
bool didTransitionToDeallocating = false;
newisa.extra_rc = borrow.borrowed - 1; // redo the original decrement too
newisa.has_sidetable_rc = !emptySideTable;
bool stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);
if (!stored && oldisa.nonpointer) {
// 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.
uintptr_t overflow;
newisa.bits =
addc(oldisa.bits, RC_ONE * (borrow.borrowed-1), 0, &overflow);
newisa.has_sidetable_rc = !emptySideTable;
if (!overflow) {
stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);
if (stored) {
didTransitionToDeallocating = newisa.isDeallocating();
}
}
}
if (!stored) {
// Inline update failed.
// Put the retains back in the side table.
ClearExclusive(&isa.bits);
sidetable_addExtraRC_nolock(borrow.borrowed);
oldisa = LoadExclusive(&isa.bits);
goto retry;
}
// Decrement successful after borrowing from side table.
// 減量成功后鹃骂,從sidetable拿回引用計數(shù)給extra_rc台盯。
if (emptySideTable)
sidetable_clearExtraRC_nolock();
if (!didTransitionToDeallocating) {
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;
}
}
else {
// Side table is empty after all. Fall-through to the dealloc path.
}
}
deallocate:
// Really deallocate.
ASSERT(newisa.isDeallocating());
ASSERT(isa.isDeallocating());
if (slowpath(sideTableLocked)) sidetable_unlock();
__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}
總結(jié):
release
源碼調(diào)用流程:objc_release
--> objc_object::release()
--> objc_object::rootRelease
rootRelease
操作邏輯:
- 1.判斷對象是否為
taggedPointer
類型,如果是則return畏线; - 2.獲取對象的
isa指針
里取出對象的引用計數(shù)信息爷恳; -
3.判斷
isa
是否是nonpointer_isa
:(通常是nonpointer_isa)
3.1 如果不是nonpointer_isa
,就去操作sidetable
里的引用計數(shù)-1象踊,并return。
3.2 如果是nonpointer_isa
棚壁,并且對象正在被釋放杯矩,直接return。
3.3 如果是nonpointer_isa
袖外,對象不是正在被釋放史隆,進入下一步4; -
4.先讓
isa
里的extra_rc
引用計數(shù)-1曼验,判斷extra_rc等于0
:
4.1 如果extra_rc!=0
時泌射,直接return。
4.2 如果extra_rc==0
時鬓照,判斷has_sidetable_rc等于1
:
4.2.1 如果has_sidetable_rc==0
熔酷,說明該對象引用計數(shù)全部清零,需要被回收內(nèi)存dealloc
豺裆。
4.2.2 如果has_sidetable_rc==1
(少數(shù)情況才會出現(xiàn))拒秘,說明該對象借助sidetable
存儲引用計數(shù),將sidetable
的引用計數(shù)賦值給extra_rc
臭猜,將sidetable
的引用計數(shù)清空躺酒,has_sidetable_rc
賦值為0。
九蔑歌、dealloc源碼分析
打開objc4-838.1源碼羹应,搜索- (void)dealloc
方法
rootDealloc
去判斷:
1.如果isa
是nonpointer_isa
、沒有弱引用次屠、沒有關(guān)聯(lián)對象园匹、沒有析構(gòu)函數(shù)、沒有向sidetable
借位劫灶,則去直接釋放free
偎肃;
2.否則調(diào)用object_dispose
object_dispose
的邏輯:
1.如果有c++析構(gòu)函數(shù)
,去調(diào)用析構(gòu)函數(shù)釋放該對象的實例成員變量浑此;
2.如果有關(guān)聯(lián)對象
累颂,去移除關(guān)聯(lián)對象;
3.清除弱引用表
和散列表里引用計數(shù)信息
4.釋放對象 free
dealloc
源碼調(diào)用流程:dealloc
--> _objc_rootDealloc
--> objc_object::rootDealloc()
--> 兩個分支
1. -> free
2. -> object_dispose
-> free
dealloc
總結(jié):
- 1.判斷對象是否為
taggedPointer
類型,如果是則 return紊馏; - 2.判斷如果
isa是nonpointer_isa
料饥、沒有弱引用
、沒有關(guān)聯(lián)對象
朱监、沒有析構(gòu)函數(shù)
岸啡、沒有向sidetable借位
,則直接釋放對象free
赫编。否則進入3巡蘸; - 3.如果有
c++析構(gòu)函數(shù)
,去調(diào)用析構(gòu)函數(shù)釋放該對象的實例成員變量擂送; - 4.如果有
關(guān)聯(lián)對象
悦荒,去移除關(guān)聯(lián)對象; - 5.清除
弱引用表
和散列表里引用計數(shù)信息
嘹吨; - 6.釋放對象
free
搬味。
說到這里本章節(jié)就結(jié)束啦,喜歡的朋友點亮??蟀拷!