摘要:是中管理引用計數(shù)的技術,幫助實現(xiàn)垃圾自動回收,具體實現(xiàn)的原理是由編譯器進行管理的,同時運行時庫協(xié)助編譯器輔助完成。本文主要內容由修飾符拓展開,分別延伸出引用計數(shù)弱引用表自動釋放池等實現(xiàn)原理。判斷使用了優(yōu)化處理則返回對象,否則引用計數(shù)。
ARC 是 iOS 中管理引用計數(shù)的技術,幫助 iOS 實現(xiàn)垃圾自動回收,具體實現(xiàn)的原理是由編譯器進行管理的,同時運行時庫協(xié)助編譯器輔助完成。主要涉及到 Clang (LLVM 編譯器) 和 objc4 運行時庫。
本文主要內容由修飾符 __strong 、 __weak 、 __autorelease 拓展開,分別延伸出引用計數(shù)、弱引用表、自動釋放池等實現(xiàn)原理。在閱讀本文之前,你可以看看下面幾個問題:
在 ARC 下如何存儲引用計數(shù)?
如[NSDictionary dictionary]方法創(chuàng)建的對象在 ARC 中有什么不同之處。
弱引用表的數(shù)據(jù)結構。
解釋一下自動釋放池中的 Hot Page 和 Cold Page。
如果上述幾個問題你已經非常清楚,那本文可能對你的幫助有限,但如果你對這幾個問題還存有疑問,那相信本文一定能解答你的疑問。
一、Clang
在 Objective-C 中,對象的引用關系由引用修飾符來決定,如__strong、__weak、__autorelease等等,編譯器會根據(jù)不同的修飾符生成不同邏輯的代碼來管理內存。
首先看看 Clang 在其中具體起到哪些作用,我們可以在命令行使用下面的命令來將 Objective-C 代碼轉成 LLVM 中間碼:
// 切換到你文件路徑下 cd Path // 利用 main.m 生成中間碼文件 main.ll clang -S -fobjc-arc -emit-llvm main.m -o main.ll
我在main.m文件中加入defaultFunction方法,然后利用的命令行命令將其轉換成中間碼:
void defaultFunction() { id obj = [NSObject new]; }
在命令行輸入命令后你可以在文件夾下面發(fā)現(xiàn)main.ll,它的內容如下:
define void @defaultFunction() #0 { %1 = alloca i8*, align 8 %2 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8 %3 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !8 %4 = bitcast %struct._class_t* %2 to i8* %5 = call i8* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to i8* (i8*, i8*)*)(i8* %4, i8* %3) %6 = bitcast i8* %5 to %0* %7 = bitcast %0* %6 to i8* store i8* %7, i8** %1, align 8 call void @objc_storeStrong(i8** %1, i8* null) #4 ret void }
雖然內容有點多,但是仔細分析下來大概就是以下內容:
void defaultFunction() { id obj = obj_msgSend(NSObject, @selector(new)); objc_storeStrong(obj, null); }
obj_msgSend(NSObject, @selector(new))非常好理解,就是新建一個對象,而objc_storeStrong是 objc4 庫中的方法,具體邏輯如下:
void objc_storeStrong(id *location, id obj) { id prev = *location; if (obj == prev) { return; } objc_retain(obj); *location = obj; objc_release(prev); }
上面的代碼按順序做了以下 4 件事:
檢查輸入的 obj 地址 和指針指向的地址是否相同。
持有對象,引用計數(shù) + 1 。
指針指向 obj。
原來指向的對象引用計數(shù) - 1。
其中objc_retain和objc_release也是 objc4 庫中的方法,在本文后面分析 objc4 庫的章節(jié)會詳細講。
二、 isa
在分析 ARC 相關源碼之前,需要對 isa 有一定了解,其中存儲了一些非常重要的信息,下面是 isa 的結構組成:
union isa_t { Class cls; uintptr_t bits; struct { uintptr_t nonpointer : 1;//->表示使用優(yōu)化的isa指針 uintptr_t has_assoc : 1;//->是否包含關聯(lián)對象 uintptr_t has_cxx_dtor : 1;//->是否設置了析構函數(shù),如果沒有,釋放對象更快 uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 ->類的指針 uintptr_t magic : 6;//->固定值,用于判斷是否完成初始化 uintptr_t weakly_referenced : 1;//->對象是否被弱引用 uintptr_t deallocating : 1;//->對象是否正在銷毀 uintptr_t has_sidetable_rc : 1;//1->在extra_rc存儲引用計數(shù)將要溢出的時候,借助Sidetable(散列表)存儲引用計數(shù),has_sidetable_rc設置成1 uintptr_t extra_rc : 19; //->存儲引用計數(shù) }; };
其中nonpointer、weakly_referenced、has_sidetable_rc和extra_rc都是 ARC 有直接關系的成員變量,其他的大多也有涉及到。
struct objc_object { isa_t isa; };
從下面代碼可以知道,objc_object就是 isa 基礎上一層封裝。
struct objc_class : objc_object { isa_t isa; Class superclass; cache_t cache; 方法實現(xiàn)緩存和 vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags };
objc_class繼承了objc_object,結構如下:
isa:objc_object 指向類,objc_class 指向元類。
superclass:指向父類。
cache:存儲用戶消息轉發(fā)優(yōu)化的方法緩存和 vtable 。
bits:class_rw_t 和 class_ro_t ,保存了方法、協(xié)議、屬性等列表和一些標志位。
三、 __strong 修飾符
在 MRC 時代 Retain 修飾符將會使被引用的對象引用計數(shù) + 1 ,在 ARC 中 __strong 修飾符作為其替代者,具體起到什么樣的作用?我們可以通過 Clang 將 Objective-C 代碼轉成 LLVM 來分析其中原理。
3.1 __strong 修飾符的中間碼
接下來繼續(xù)將 Objective-C 代碼轉成 LLVM 中間碼,這次我們試一下__strong修飾符:
void strongFunction() { id obj = [NSObject new]; __strong id obj1 = obj; }
中間碼(后面的中間碼為了方便理解就刪除無用代碼):
void defaultFunction() { id obj = obj_msgSend(NSObject, @selector(new)); id obj1 = objc_retain(obj) objc_storeStrong(obj, null); objc_storeStrong(obj1, null); }
上面代碼一看就是非常常規(guī)的操作,創(chuàng)建對象、引用計數(shù) + 1 、 分別釋放,將objc_storeStrong里面的邏輯嵌入可得:
void defaultFunction() { id obj = obj_msgSend(NSObject, @selector(new)); id obj1 = objc_retain(obj) objc_release(obj); objc_release(obj1); }
3.2 objc_retain
接下來我們通過分析 objc4 庫的源碼來了解objc_retain和objc_release的內部邏輯。先看objc_retain具體實現(xiàn):
id objc_retain(id obj) { if (!obj) return obj; if (obj->isTaggedPointer()) return obj; return obj->retain(); }
繼續(xù)往下查看最終定位到objc_object::rootRetain方法:
ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, bool handleOverflow) { // 如果是 TaggedPointer 直接返回 if (isTaggedPointer()) return (id)this; bool sideTableLocked = false; bool transcribeToSideTable = false; isa_t oldisa; isa_t newisa; do { transcribeToSideTable = false; // 獲取 isa oldisa = LoadExclusive(&isa.bits); newisa = oldisa; if (slowpath(!newisa.nonpointer)) { // 未優(yōu)化的 isa 部分 ClearExclusive(&isa.bits); if (!tryRetain && sideTableLocked) sidetable_unlock(); // if (tryRetain) return sidetable_tryRetain() ");上面的代碼分成 3 個小分支:
TaggedPointer:值存在指針內,直接返回。
!newisa.nonpointer:未優(yōu)化的 isa ,使用sidetable_retain()。
newisa.nonpointer:已優(yōu)化的 isa , 這其中又分 extra_rc 溢出和未溢出的兩種情況。
未溢出時,isa.extra_rc + 1 完事。
溢出時,將 isa.extra_rc 中一半值轉移至sidetable中,然后將isa.has_sidetable_rc設置為true,表示使用了sidetable來計算引用次數(shù)。
3.3 objc_release
繼續(xù)看objc_release具體實現(xiàn),最終定位到objc_object::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; if (slowpath(!newisa.nonpointer)) { // 未優(yōu)化 isa ClearExclusive(&isa.bits); if (sideTableLocked) sidetable_unlock(); // 入?yún)⑹欠褚獔?zhí)行 Dealloc 函數(shù),如果為 true 則執(zhí)行 SEL_dealloc return sidetable_release(performDealloc); } // extra_rc -- newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc-- if (slowpath(carry)) { // donot ClearExclusive() goto underflow; } // 更新 isa 值 } while (slowpath(!StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits))); if (slowpath(sideTableLocked)) sidetable_unlock(); return false; underflow: // 處理下溢,從 side table 中借位或者釋放 newisa = oldisa; // 如果使用了 sidetable_rc if (slowpath(newisa.has_sidetable_rc)) { if (!handleUnderflow) { // 調用本函數(shù)處理下溢 ClearExclusive(&isa.bits); return rootRelease_underflow(performDealloc); } // 從 sidetable 中借位引用計數(shù)給 extra_rc size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF); if (borrowed > 0) { // extra_rc 是計算額外的引用計數(shù),0 即表示被引用一次 newisa.extra_rc = borrowed - 1; // redo the original decrement too bool stored = StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits); // 保存失敗,恢復現(xiàn)場,重試 if (!stored) { 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); } } } // 如果還是保存失敗,則還回 side table if (!stored) { sidetable_addExtraRC_nolock(borrowed); goto retry; } sidetable_unlock(); return false; } else { // Side table is empty after all. Fall-through to the dealloc path. } } // 沒有使用 sidetable_rc ,或者 sidetable_rc 計數(shù) == 0 的就直接釋放 // 如果已經是釋放中,拋個過度釋放錯誤 if (slowpath(newisa.deallocating)) { ClearExclusive(&isa.bits); if (sideTableLocked) sidetable_unlock(); return overrelease_error(); // does not actually return } // 更新 isa 狀態(tài) newisa.deallocating = true; if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry; if (slowpath(sideTableLocked)) sidetable_unlock(); // 執(zhí)行 SEL_dealloc 事件 __sync_synchronize(); if (performDealloc) { ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc); } return true; }這么一長串代碼,將其分解后和 rootRetain 邏輯類似:
TaggedPointer: 直接返回 false。
!nonpointer: 未優(yōu)化的 isa 執(zhí)行 sidetable_release。
nonpointer:已優(yōu)化的 isa ,分下溢和未下溢兩種情況。
未下溢: extra_rc--。
下溢:從 sidetable 中借位給 extra_rc 達到半滿,如果無法借位則說明引用計數(shù)歸零需要進行釋放。其中借位時可能保存失敗會不斷重試。
到這里可以知道 引用計數(shù)分別保存在isa.extra_rc和sidetable中,當isa.extra_rc溢出時,將一半計數(shù)轉移至sidetable中,而當其下溢時,又會將計數(shù)轉回。當二者都為空時,會執(zhí)行釋放流程 。
3.4 rootRetainCount
objc_object::rootRetainCount方法是用來計算引用計數(shù)的。通過前面rootRetain和rootRelease的源碼分析可以看出引用計數(shù)會分別存在isa.extra_rc和sidetable。中,這一點在rootRetainCount方法中也得到了體現(xiàn)。
inline uintptr_t objc_object::rootRetainCount() { // TaggedPointer 直接返回 if (isTaggedPointer()) return (uintptr_t)this; sidetable_lock(); // 加載 isa isa_t bits = LoadExclusive(&isa.bits); ClearExclusive(&isa.bits); // 優(yōu)化的 isa 需要 sidetable + bits.extra_rc + 1 if (bits.nonpointer) { uintptr_t rc = 1 + bits.extra_rc; if (bits.has_sidetable_rc) { rc += sidetable_getExtraRC_nolock(); } sidetable_unlock(); return rc; } // 未優(yōu)化返回 sidetable_retainCount sidetable_unlock(); return sidetable_retainCount(); }3.5 objc_autoreleaseReturnValue 和 objc_retainAutoreleasedReturnValue
在 MRC 時代有一句話叫 誰創(chuàng)建誰釋放 ,意思是由開發(fā)者通過alloc、new、copy和mutableCopy等方法創(chuàng)建的對象,需要開發(fā)者手動釋放,而由其他方法創(chuàng)建并返回的對象返回給用戶后也不需要開發(fā)者釋放,比如說由[NSMutableArray array]方法創(chuàng)建的數(shù)組,這樣的對象默認由自動釋放池管理。進入 ARC 時代后,針對返回的對象編譯器也做了一些特殊處理,具體通過下面的內容來理解其中奧妙。
首先將下方創(chuàng)建數(shù)組的代碼轉成中間碼:
id strongArrayInitFunction() { return [[NSMutableArray alloc] init]; } void strongArrayFunction() { __strong id obj = [NSMutableArray array]; }中間碼提煉后得到下面的代碼:
strongArrayInitFunction() { id obj = objc_msgSend(objc_msgSend(NSMutableArray, @selector(alloc)), @selector(init)); objc_autoreleaseReturnValue(obj); return obj; } strongArrayFunction() { id obj = objc_msgSend(NSMutableArray, @selector(array)); objc_retainAutoreleasedReturnValue(obj) objc_release(obj); }相對于用戶創(chuàng)建的對象,[NSMutableArray array]方法創(chuàng)建返回的對象轉換后多出了一個objc_retainAutoreleasedReturnValue方法。這涉及到一個最優(yōu)化處理:
為了節(jié)省了一個將對象注冊到autoreleasePool的操作,在執(zhí)行objc_autoreleaseReturnValue時,根據(jù)查看后續(xù)調用的方法列表是否包含objc_retainAutoreleasedReturnValue方法,以此判斷是否走優(yōu)化流程。
在執(zhí)行objc_autoreleaseReturnValue時,優(yōu)化流程將一個標志位存儲在 TLS (Thread Local Storage) 中后直接返回對象。
執(zhí)行后續(xù)方法objc_retainAutoreleasedReturnValue時檢查 TLS 的標志位判斷是否處于優(yōu)化流程,如果處于優(yōu)化流程中則直接返回對象,并且將 TLS 的狀態(tài)還原。
下面再通過源代碼來進行分析,先看看objc_autoreleaseReturnValue具體實現(xiàn):
id objc_autoreleaseReturnValue(id obj) { // 如果走優(yōu)化程序則直接返回對象 if (prepareOptimizedReturn(ReturnAtPlus1)) return obj; // 否則還是走自動釋放池 return objc_autorelease(obj); } static ALWAYS_INLINE bool prepareOptimizedReturn(ReturnDisposition disposition) { assert(getReturnDisposition() == ReturnAtPlus0); // 檢查使用該函數(shù)的方法或調用方的的調用列表,如果緊接著執(zhí)行 objc_retainAutoreleasedReturnValue ,將不注冊到 autoreleasePool 中 if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) { // 設置標記 ReturnAtPlus1 if (disposition) setReturnDisposition(disposition); return true; } return false; } // 將 ReturnAtPlus1 或 ReturnAtPlus0 存入 TLS static ALWAYS_INLINE void setReturnDisposition(ReturnDisposition disposition) { tls_set_direct(RETURN_DISPOSITION_KEY, (void*)(uintptr_t)disposition); } // 取出標記 static ALWAYS_INLINE ReturnDisposition getReturnDisposition() { return (ReturnDisposition)(uintptr_t)tls_get_direct(RETURN_DISPOSITION_KEY); }objc_autoreleaseReturnValue代碼邏輯大概分為:
檢查調用者方法里后面是否緊跟著調用了objc_retainAutoreleasedReturnValue。
保存 ReturnAtPlus1 至 TLS 中。
使用了優(yōu)化處理則返回對象,否則加入自動釋放池。
下面是objc_retainAutoreleasedReturnValue的源碼分析:
id objc_retainAutoreleasedReturnValue(id obj) { // 如果 TLS 中標記表示使用了優(yōu)化程序,則直接返回 if (acceptOptimizedReturn() == ReturnAtPlus1) return obj; return objc_retain(obj); } static ALWAYS_INLINE ReturnDisposition acceptOptimizedReturn() { // 取出標記后返回 ReturnDisposition disposition = getReturnDisposition(); // 還原至未優(yōu)化狀態(tài) setReturnDisposition(ReturnAtPlus0); // reset to the unoptimized state return disposition; }objc_retainAutoreleasedReturnValue代碼邏輯大概分為:
取出 TLS 中標記。
重置 TLS 中標記至 ReturnAtPlus0 。
判斷使用了優(yōu)化處理則返回對象,否則引用計數(shù) + 1。
通過分析源碼可以得知下面這段代碼的優(yōu)化流程和未優(yōu)化流程是有挺大的區(qū)別的:
strongArrayFunction() { id obj = objc_msgSend(NSMutableArray, @selector(array)); objc_retainAutoreleasedReturnValue(obj) objc_release(obj); }最終優(yōu)化流程相當于:
id obj = objc_msgSend(objc_msgSend(NSMutableArray, @selector(alloc)), @selector(init)); objc_release(obj);而未優(yōu)化流程相當于:
id obj = objc_msgSend(objc_msgSend(NSMutableArray, @selector(alloc)), @selector(init)); objc_autorelease(obj); objc_retain(obj); objc_release(obj);四、__weak 修飾符
眾所周知,weak 表示弱引用,引用計數(shù)不會增加。在原對象釋放后,弱引用變量也會隨之被清除,接下來一步步分析其中原理。
4.1 __weak 修飾符的中間碼
首先將下面代碼轉換成中間碼:
void weakFunction() { __weak id obj = [NSObject new]; } void weak1Function() { id obj = [NSObject new]; __weak id obj1 = obj; } void weak2Function() { id obj = [NSObject new]; __weak id obj1 = obj; NSLog(@"%@",obj1); }下面是轉化提煉后的中間碼:
weakFunction() { id temp = objc_msgSend(NSObject, @selector(new)); objc_initWeak(&obj, temp); objc_release(temp); objc_destroyWeak(obj); } weak1Function() { id obj = objc_msgSend(NSObject, @selector(new)); objc_initWeak(&obj1, obj); objc_destroyWeak(obj1); objc_storeStrong(obj, null); } weak2Function() { id obj = objc_msgSend(NSObject, @selector(new)); objc_initWeak(obj1, obj); id temp = objc_loadWeakRetained(obj1); NSLog(@"%@",temp); objc_release(temp); objc_destroyWeak(obj1); objc_storeStrong(obj, null); }
weakFunction: 在該方法中聲明 __weak 對象后并沒有使用到,所以在objc_initWeak后,立即釋放調用了objc_release和objc_destroyWeak方法。
weak1Function:該方法中obj是強引用,obj1是弱引用,objc_initWeak、 objc_destroyWeak先后成對調用,對應著弱引用變量的初始化和釋放方法。
weak2Function:和weak1Function不同之處是使用了弱引用變量obj1,在使用弱引用變量之前,編譯器創(chuàng)建了一個臨時的強引用對象,在用完后立即釋放。
4.2 objc_initWeak 和 objc_destroyWeak
4.2.1 objc_initWeak 和 objc_destroyWeak
下面是objc_initWeak和objc_destroyWeak的代碼實現(xiàn):
id objc_initWeak(id *location, id newObj) { if (!newObj) { *location = nil; return nil; } // 該地址沒有值,正賦予新值,如果正在釋放將會 crash return storeWeak(location, (objc_object*)newObj); } void objc_destroyWeak(id *location) { // 該地址有值,沒有賦予新值,如果正在釋放不 crash (void)storeWeak (location, nil); } 通過源代碼可以發(fā)現(xiàn)最終都是通過storeWeak來實現(xiàn)各自邏輯的,在查看storeWeak實現(xiàn)之前,我們要先了解一下它的模板參數(shù)的含義:
storeWeak(location, (objc_object*)newObj); 其中DontHaveOld、DoHaveNew和DoCrashIfDeallocating都是模板參數(shù),具體含義如下:
enum HaveOld { DontHaveOld = false, DoHaveOld = true }; // 是否有值 enum HaveNew { DontHaveNew = false, DoHaveNew = true }; // 是否有新值 enum CrashIfDeallocating { DontCrashIfDeallocating = false, DoCrashIfDeallocating = true }; // 操作正在釋放中的對象是否 Crash4.2.2 storeWeak
接下來繼續(xù)看storeWeak的實現(xiàn):
templatestatic id storeWeak(id *location, objc_object *newObj) { assert(haveOld || haveNew); if (!haveNew) assert(newObj == nil); Class previouslyInitializedClass = nil; id oldObj; SideTable *oldTable; SideTable *newTable; retry: // 從 SideTables 中取出存儲弱引用表的 SideTable(為弱引用表 weak_table_t 的一層封裝) if (haveOld) { oldObj = *location; oldTable = &SideTables()[oldObj]; } else { oldTable = nil; } if (haveNew) { newTable = &SideTables()[newObj]; } else { newTable = nil; } SideTable::lockTwo (oldTable, newTable); // location 指向的值發(fā)生改變,則重新執(zhí)行獲取 oldObj if (haveOld && *location != oldObj) { SideTable::unlockTwo (oldTable, newTable); goto retry; } // 如果有新值 if (haveNew && newObj) { // 如果該對象類還未初始化則進行初始化 Class cls = newObj->getIsa(); if (cls != previouslyInitializedClass && !((objc_class *)cls)->isInitialized()) { // 創(chuàng)建一個非元類,并且初始化,會調用 +initialize 函數(shù) SideTable::unlockTwo (oldTable, newTable); _class_initialize(_class_getNonMetaClass(cls, (id)newObj)); previouslyInitializedClass = cls; goto retry; } } // 如果有舊值,清除舊值對應的弱引用表 if (haveOld) { weak_unregister_no_lock(&oldTable->weak_table, oldObj, location); } // 如果賦予了新值,注冊新值對應的弱引用表 if (haveNew) { newObj = (objc_object *) weak_register_no_lock(&newTable->weak_table, (id)newObj, location, crashIfDeallocating); // 設置 isa 標志位 weakly_referenced 為 true if (newObj && !newObj->isTaggedPointer()) { newObj->setWeaklyReferenced_nolock(); } // Do not set *location anywhere else. That would introduce a race. *location = (id)newObj; } else { // No new value. The storage is not changed. } SideTable::unlockTwo (oldTable, newTable); return (id)newObj; } 這段代碼大概做了這幾件事:
從全局的哈希表SideTables中,利用對象本身地址進行位運算后得到對應下標,取得該對象的弱引用表。SideTables是一個 64 個元素長度的散列表,發(fā)生碰撞時,可能一個SideTable中存在多個對象共享一個弱引用表。
如果有分配新值,則檢查新值對應的類是否初始化過,如果沒有,則就地初始化。
如果 location 有指向其他舊值,則將舊值對應的弱引用表進行注銷。
如果分配了新值,將新值注冊到對應的弱引用表中。將isa.weakly_referenced設置為true,表示該對象是有弱引用變量,釋放時要去清空弱引用表。
4.2.3 weak_register_no_lock 和 weak_unregister_no_lock
在上面的代碼中使用到weak_register_no_lock和weak_unregister_no_lock來進行弱引用表的注冊和注銷,繼續(xù)查看這兩個方法的實現(xiàn):
id weak_register_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id, bool crashIfDeallocating) { objc_object *referent = (objc_object *)referent_id; // 被引用的對象 objc_object **referrer = (objc_object **)referrer_id; // 弱引用變量 if (!referent || referent->isTaggedPointer()) return referent_id; // 檢查當前對象沒有在釋放中 bool deallocating; if (!referent->ISA()->hasCustomRR()) { deallocating = referent->rootIsDeallocating(); } else { BOOL (*allowsWeakReference)(objc_object *, SEL) = (BOOL(*)(objc_object *, SEL)) object_getMethodImplementation((id)referent, SEL_allowsWeakReference); if ((IMP)allowsWeakReference == _objc_msgForward) { return nil; } deallocating = ! (*allowsWeakReference)(referent, SEL_allowsWeakReference); } // 如果正在釋放中,則根據(jù) crashIfDeallocating 判斷是否觸發(fā) crash if (deallocating) { if (crashIfDeallocating) { _objc_fatal("Cannot form weak reference to instance (%p) of " "class %s. It is possible that this object was " "over-released, or is in the process of deallocation.", (void*)referent, object_getClassName((id)referent)); } else { return nil; } } weak_entry_t *entry; // 每個對象對應的一個弱引用記錄 // 如果當前表中有該對象的記錄則直接加入該 weak 表中對應記錄 if ((entry = weak_entry_for_referent(weak_table, referent))) { append_referrer(entry, referrer); } else { // 沒有在 weak 表中找到對應記錄,則新建一個記錄 weak_entry_t new_entry(referent, referrer); // 查看 weak_table 表是否要擴容 weak_grow_maybe(weak_table); // 將記錄插入 weak 表中 weak_entry_insert(weak_table, &new_entry); } return referent_id; }上面這段代碼主要邏輯:
檢查是否正在被釋放中,如果是則根據(jù)crashIfDeallocating判斷是否觸發(fā) crash 。
檢查weak_table中是否有被引用對象對應的entry,如果有則直接將弱引用變量指針地址加入該entry中。
如果weak_table沒有找到對應的entry,則新建一個entry,并將弱引用變量指針地址加入entry中。同時檢查weak_table是否需要擴容。
下面是weak_unregister_no_lock代碼實現(xiàn):
void weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id) { objc_object *referent = (objc_object *)referent_id; // 被引用的對象 objc_object **referrer = (objc_object **)referrer_id; // 弱引用變量 weak_entry_t *entry; if (!referent) return; if ((entry = weak_entry_for_referent(weak_table, referent))) { // 找到 weak 表中對應記錄后,將引用從記錄中移除 remove_referrer(entry, referrer); // 移除后檢查該引用記錄是否為空 bool empty = true; if (entry->out_of_line() && entry->num_refs != 0) { empty = false; } else { for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) { if (entry->inline_referrers[i]) { empty = false; break; } } } // 如果當前記錄為空則移除記錄 if (empty) { weak_entry_remove(weak_table, entry); } } }上面這段代碼主要邏輯:
從weak_table中根據(jù)找到被引用對象對應的entry,然后將弱引用變量指針referrer從entry中移除。
移除弱引用變量指針referrer之后,檢查entry是否為空,如果為空將其從weak_table中移除。
4.2.4 weak_table
上面的代碼中使用weak_table保存被引用對象的entry,下面繼續(xù)通過分析weak_table的增刪查函數(shù)的具體實現(xiàn):
static weak_entry_t * weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent) { assert(referent); weak_entry_t *weak_entries = weak_table->weak_entries; if (!weak_entries) return nil; // hash_pointer 對地址做位運算得出哈希表下標的方式 size_t begin = hash_pointer(referent) & weak_table->mask; size_t index = begin; size_t hash_displacement = 0; // 線性探測,如果該下標存儲的是其他對象,那往下移,直至找到正確的下標。 while (weak_table->weak_entries[index].referent != referent) { index = (index+1) & weak_table->mask; // 不能超過 weak_table 最大長度限制 // 回到初始下標,異常報錯 if (index == begin) bad_weak_table(weak_table->weak_entries); // 每次沖突下移 hash_displacement + 1,當前位移不超過記錄在案的最大位移 hash_displacement++; if (hash_displacement > weak_table->max_hash_displacement) { return nil; } } return &weak_table->weak_entries[index]; }上述代碼就是weak_table查找entry的過程,也是哈希表尋址過程,使用線性探測的方法解決哈希沖突的問題:
通過被引用對象地址計算獲得哈希表下標。
檢查對應下標存儲的是不是我們要找到地址,如果是則返回該地址。
如果不是則繼續(xù)往下找,直至找到。在下移的過程中,下標不能超過weak_table最大長度,同時hash_displacement不能超過記錄的max_hash_displacement最大哈希位移。max_hash_displacement是所有插入操作時記錄的最大哈希位移,如果超過了,那肯定是出錯了。
下面是weak_table插入entry的代碼實現(xiàn):
static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry) { weak_entry_t *weak_entries = weak_table->weak_entries; assert(weak_entries != nil); // 通過哈希算法得到下標 size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask); size_t index = begin; size_t hash_displacement = 0; // 判斷當前下標是否為空,如果不是繼續(xù)往下尋址空位 while (weak_entries[index].referent != nil) { index = (index+1) & weak_table->mask; if (index == begin) bad_weak_table(weak_entries); hash_displacement++; } // 找到空位后存入 weak_entries[index] = *new_entry; weak_table->num_entries++; // 更新最大哈希位移值 if (hash_displacement > weak_table->max_hash_displacement) { weak_table->max_hash_displacement = hash_displacement; } }和查過過程類似,weak_table插入entry的的步驟:
通過被引用對象地址計算獲得哈希表下標。
檢查對應下標是否為空,如果不為空繼續(xù)往下查找,直至找到空位。
將弱引用變量指針存入空位,同時更新weak_table的當前成員數(shù)量num_entries和最大哈希位移max_hash_displacement。
下面是weak_table移除entry的代碼實現(xiàn):
static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry) { // 釋放 entry 中的所有弱引用 if (entry->out_of_line()) free(entry->referrers); // 置空指針 bzero(entry, sizeof(*entry)); // 更新 weak_table 對象數(shù)量,并檢查是否可以縮減表容量 weak_table->num_entries--; weak_compact_maybe(weak_table); }從weak_table移除entry的的步驟:
釋放entry和其中的弱引用變量。
更新 weak_table 對象數(shù)量,并檢查是否可以縮減表容量
4.2.5 entry 和 referrer
在弱引用表中entry對應著被引用的對象,而referrer代表弱引用變量。每次被弱引用時,都會將弱引用變量指針referrer加入entry中,而當原對象被釋放時,會將entry清空并移除。
下面看往entry中添加referrer的具體實現(xiàn):
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer) { if (! entry->out_of_line()) { // inline_referrers 未超出時,直接加入 inline_referrers 中 for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) { if (entry->inline_referrers[i] == nil) { entry->inline_referrers[i] = new_referrer; return; } } // 如果 inline_referrers 超出 WEAK_INLINE_COUNT 數(shù)量,則執(zhí)行下面代碼 weak_referrer_t *new_referrers = (weak_referrer_t *) calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t)); // 將 inline_referrers 的引用轉移只 new_referrers for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) { new_referrers[i] = entry->inline_referrers[i]; } // 修改 entry 內容及標志位 entry->referrers = new_referrers; entry->num_refs = WEAK_INLINE_COUNT; entry->out_of_line_ness = REFERRERS_OUT_OF_LINE; entry->mask = WEAK_INLINE_COUNT-1; entry->max_hash_displacement = 0; } assert(entry->out_of_line()); // 當負載因子過高進行擴容 if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) { return grow_refs_and_insert(entry, new_referrer); } // 根據(jù)地址計算下標 size_t begin = w_hash_pointer(new_referrer) & (entry->mask); size_t index = begin; size_t hash_displacement = 0; // 該下表位置下不為空,發(fā)生 hash 碰撞了, while (entry->referrers[index] != nil) { // 后移 hash_displacement++; index = (index+1) & entry->mask; if (index == begin) bad_weak_table(entry); } // 記錄最大位移 if (hash_displacement > entry->max_hash_displacement) { entry->max_hash_displacement = hash_displacement; } // 找到合適下標后存儲 weak_referrer_t &ref = entry->referrers[index]; ref = new_referrer; entry->num_refs++; }entry的結構和weak_table相似,都使用了哈希表,并且使用線性探測法尋找對應位置。在此基礎上有一點不同的地方:
entry有一個標志位out_of_line,最初時該標志位為false,entry使用的是一個有序數(shù)組inline_referrers的存儲結構。
當inline_referrers的成員數(shù)量超過了WEAK_INLINE_COUNT,out_of_line標志位變成true,開始使用哈希表存儲結構。每當哈希表負載超過 3/4 時會進行擴容。
繼續(xù)看從entry移除referrer的具體實現(xiàn):
static void remove_referrer(weak_entry_t *entry, objc_object **old_referrer) { if (! entry->out_of_line()) { // 未超出 inline_referrers 時直接將對應位置清空 for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) { if (entry->inline_referrers[i] == old_referrer) { entry->inline_referrers[i] = nil; return; } } _objc_inform("Attempted to unregister unknown __weak variable " "at %p. This is probably incorrect use of " "objc_storeWeak() and objc_loadWeak(). " "Break on objc_weak_error to debug. ", old_referrer); objc_weak_error(); return; } // 超出 inline_referrers 的邏輯 // 根據(jù)地址計算下標 size_t begin = w_hash_pointer(old_referrer) & (entry->mask); size_t index = begin; size_t hash_displacement = 0; // 發(fā)生哈希沖突繼續(xù)往后查找 while (entry->referrers[index] != old_referrer) { index = (index+1) & entry->mask; if (index == begin) bad_weak_table(entry); hash_displacement++; if (hash_displacement > entry->max_hash_displacement) { _objc_inform("Attempted to unregister unknown __weak variable " "at %p. This is probably incorrect use of " "objc_storeWeak() and objc_loadWeak(). " "Break on objc_weak_error to debug. ", old_referrer); objc_weak_error(); return; } } // 找到后將對應位置置空 entry->referrers[index] = nil; entry->num_refs--; }從entry移除referrer的步驟:
out_of_line為false時,從有序數(shù)組inline_referrers中查找并移除。
out_of_line為true時,從哈希表中查找并移除。
4.2.6 dealloc
當被引用的對象被釋放后,會去檢查isa.weakly_referenced標志位,每個被弱引用的對象weakly_referenced標志位都為true。
- (void)dealloc { _objc_rootDealloc(self); }順著dealloc方法邏輯往下走直至clearDeallocating_slow:
NEVER_INLINE void objc_object::clearDeallocating_slow() { assert(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc)); // 根據(jù)指針獲取對應 weak_table SideTable& table = SideTables()[this]; table.lock(); // 判斷如果有被弱引用則清空該對象對應的 entry if (isa.weakly_referenced) { weak_clear_no_lock(&table.weak_table, (id)this); } // 清空該對象存儲在 sidetable 中的引用計數(shù) if (isa.has_sidetable_rc) { table.refcnts.erase(this); } table.unlock(); }從上面的代碼可以看出,在對象釋執(zhí)行dealloc函數(shù)時,會檢查isa.weakly_referenced標志位,然后判斷是否要清理weak_table中的entry。
4.3 objc_loadWeakRetained
通過前面的中間碼分析可以得知,在使用弱引用變量之前,編譯器創(chuàng)建了一個臨時的強引用對象,以此保證使用時不會因為被釋放導致出錯,在用完后立即釋放。
下面看看如何對弱引用指針指向對象進行強引用:
id objc_loadWeakRetained(id *location) { id obj; id result; Class cls; SideTable *table; retry: // 得到弱引用指針指向對象 obj = *location; if (!obj) return nil; if (obj->isTaggedPointer()) return obj; // TaggedPointer 直接返回 // 得到對應 weak_table table = &SideTables()[obj]; // 如果被引用對象在此期間發(fā)生變化則重試 table->lock(); if (*location != obj) { table->unlock(); goto retry; } result = obj; cls = obj->ISA(); if (! cls->hasCustomRR()) { // 類和超類沒有自定義 retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference 等方法 assert(cls->isInitialized()); // 嘗試 retain if (! obj->rootTryRetain()) { result = nil; } } else { if (cls->isInitialized() || _thisThreadIsInitializingClass(cls)) { // 獲取自定義 SEL_retainWeakReference 方法 BOOL (*tryRetain)(id, SEL) = (BOOL(*)(id, SEL)) class_getMethodImplementation(cls, SEL_retainWeakReference); if ((IMP)tryRetain == _objc_msgForward) { result = nil; } // 調用自定義函數(shù) else if (! (*tryRetain)(obj, SEL_retainWeakReference)) { result = nil; } } else { // 類未初始化,則初始化后回到 retry 重新執(zhí)行 table->unlock(); _class_initialize(cls); goto retry; } } table->unlock(); return result; }上面的代碼主要邏輯:
通過弱引用指向的對象,獲取弱引用表,并且將其上鎖,防止在此期間被清除。
判斷是否包含自定義retain方法,如果沒有,則使用默認rootTryRetain方法,使引用計數(shù) + 1 。
如果使用了自定義retain方法,則調用自定義方法,在調用之前會先判斷該對象所屬類是否已經初始化過,如果沒有初始化會先進行初始化然后再調用。
五、__autorelease 修飾符
在 ARC 環(huán)境下, __autorelease 修飾符可以將對象加入自動釋放池中,由自動釋放池管理釋放。
5.1 __autorelease 修飾符的中間碼
將下面的代碼轉換成中間碼查看編譯器的處理:
void autoReleasingFunction() { @autoreleasepool { __autoreleasing id obj = [NSObject new]; } }轉換后的中間碼:
void autoReleasingFunction() { id token = objc_autoreleasePoolPush(); id obj = objc_msgSend(NSObject, @selector(new)); objc_autorelease(obj); objc_autoreleasePoolPop(token); }通過上面的代碼可以分析出:
@autoreleasepool{}關鍵字通過編譯器轉換成objc_autoreleasePoolPush和objc_autoreleasePoolPop這一對方法。
__autoreleasing 修飾符轉換成objc_autorelease,將obj加入自動釋放池中。
從上面的分析可以看出,編譯器對自動釋放池的處理邏輯大致分成:
由objc_autoreleasePoolPush作為自動釋放池作用域的第一個函數(shù)。
使用objc_autorelease將對象加入自動釋放池。
由objc_autoreleasePoolPop作為自動釋放池作用域的最后一個函數(shù)。
5.2 自動釋放池的預備知識
在繼續(xù)往下看之前,我們需要先了解一些關于自動釋放池的相關知識:
自動釋放池都是由一個或者多個AutoreleasePoolPage組成,page的 SIZE 為 4096 bytes ,它們通過parent和child指針組成一個雙向鏈表。
hotPage:是當前正在使用的page,操作都是在hotPage上完成,一般處于鏈表末端或者倒數(shù)第二個位置。存儲在 TLS 中,可以理解為一個每個線程共享一個自動釋放池鏈表。
coldPage:位于鏈表頭部的page,可能同時為hotPage。
POOL_BOUNDARY:nil的宏定義,替代之前的哨兵對象POOL_SENTINEL,在自動釋放池創(chuàng)建時,在objc_autoreleasePoolPush中將其推入自動釋放池中。在調用objc_autoreleasePoolPop時,會將池中對象按順序釋放,直至遇到最近一個POOL_BOUNDARY時停止。
EMPTY_POOL_PLACEHOLDER:當自動釋放池中沒有推入過任何對象時,這個時候推入一個POOL_BOUNDARY,會先將EMPTY_POOL_PLACEHOLDER存儲在 TLS 中作為標識符,并且此次并不推入POOL_BOUNDARY。等再次有對象被推入自動釋放池時,檢查在 TLS 中取出該標識符,這個時候再推入POOL_BOUNDARY。
next:指向AutoreleasePoolPage指向棧頂空位的指針,每次加入新的元素都會往上移動。
5.3 objc_autoreleasePoolPush
objc_autoreleasePoolPush方法的代碼實現(xiàn):
static inline void *push() { id *dest; if (DebugPoolAllocation) { // 測試狀態(tài)下,每個自動釋放池都會新建一個 page dest = autoreleaseNewPage(POOL_BOUNDARY); } else { // 推一個 POOL_BOUNDARY 入棧,表示該釋放池的起點 dest = autoreleaseFast(POOL_BOUNDARY); } assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY); return dest; }objc_autoreleasePoolPush方法其實就是向自動釋放池推入一個POOL_BOUNDARY,作為該autoreleasepool的起點。autoreleaseFast方法的具體邏輯將在后面分析autorelease方法時再進行分析。
5.4 autorelease
下面看加入自動釋放池方法autorelease的代碼實現(xiàn):
static inline id autorelease(id obj) { id *dest __unused = autoreleaseFast(obj); return obj; }繼續(xù)往下看:
static inline id *autoreleaseFast(id obj) { AutoreleasePoolPage *page = hotPage(); if (page && !page->full()) { // 有 hotPage 且不滿 直接加入棧中 return page->add(obj); } else if (page) { // hotPage 已滿 先創(chuàng)建一個 Page 后,加入新 Page 中 return autoreleaseFullPage(obj, page); } else { // 沒有 hotPage 直接新建一個 Page,并加入 Page 中 return autoreleaseNoPage(obj); } }上面這段代碼邏輯:
如果hotPage存在且未滿,則直接推入hotPage。
如果hotPage存在且已滿,調用autoreleaseFullPage。
如果hotPage不存在,調用autoreleaseNoPage。
往下繼續(xù)分析autoreleaseFullPage的實現(xiàn):
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) { assert(page == hotPage()); assert(page->full() || DebugPoolAllocation); // 找到一個未滿的 page , 未找到則新建一個 page ,設置成 hotPage do { if (page->child) page = page->child; else page = new AutoreleasePoolPage(page); } while (page->full()); setHotPage(page); return page->add(obj); }該方法是在hotPage已滿的情況下執(zhí)行,具體邏輯如下:
查看hotPage是否有后繼節(jié)點,如果有直接使用后繼節(jié)點。
如果沒有后繼節(jié)點,則新建一個AutoreleasePoolPage。
將對象加入獲取到的page,并將其設置為hotPage,其實就是存入 TLS 中共享。
下面開始分析autoreleaseNoPage方法代碼實現(xiàn):
id *autoreleaseNoPage(id obj) { // 執(zhí)行 No page 表示目前還沒有釋放池,或者有一個空占位符池,但是還沒有加入對象 assert(!hotPage()); bool pushExtraBoundary = false; if (haveEmptyPoolPlaceholder()) { // 如果是空占位符池,需要加入一個釋放池邊界 pushExtraBoundary = true; } else if (obj != POOL_BOUNDARY && DebugMissingPools) { // We are pushing an object with no pool in place, // and no-pool debugging was requested by environment. _objc_inform("MISSING POOLS: (%p) Object %p of class %s " "autoreleased with no pool in place - " "just leaking - break on " "objc_autoreleaseNoPool() to debug", pthread_self(), (void*)obj, object_getClassName(obj)); objc_autoreleaseNoPool(obj); return nil; } // 如果傳入 POOL_BOUNDARY 則設置空池占位符 else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) { return setEmptyPoolPlaceholder(); } // We are pushing an object or a non-placeholder d pool. // 初始化一個 page 并設置 hotPage AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); setHotPage(page); // 插入釋放池邊界 if (pushExtraBoundary) { page->add(POOL_BOUNDARY); } // 對象加入釋放池 return page->add(obj); }autoreleaseNoPage只有在自動釋放池還沒有page時調用,主要邏輯:
如果當前自動釋放池推入的是一個哨兵POOL_BOUNDARY時,將EmptyPoolPlaceholder存入 TLS 中。
如果 TLS 存儲了EmptyPoolPlaceholder時,在創(chuàng)建好page之后,會先推入一個POOL_BOUNDARY,然后再將加入自動釋放池的對象推入。
5.5 objc_autoreleasePoolPop
在自動釋放池所在作用域結束時,會調用objc_autoreleasePoolPop,對自動釋放池中的對象進行釋放。
static inline void pop(void *token) { AutoreleasePoolPage *page; id *stop; // 如果是空池占位符,要清空整個自動釋放池 if (token == (void*)EMPTY_POOL_PLACEHOLDER) { if (hotPage()) { // 如果存在 hotPage ,則找到 coldPage 的起點 重新 pop pop(coldPage()->begin()); } else { // 未使用過的釋放池,置空 TLS 中存放的 hotPage setHotPage(nil); } return; } page = pageForPointer(token); stop = (id *)token; if (*stop != POOL_BOUNDARY) { // 在 stop 不為 POOL_BOUNDARY 的情況下 只可能是 coldPage()->begin() if (stop == page->begin() && !page->parent) { } else { // 如果不是 POOL_BOUNDARY 也不是 coldPage()->begin() 則報錯 return badPop(token); } } if (PrintPoolHiwat) printHiwat(); // 釋放 stop 后面的所有對象 page->releaseUntil(stop); // 清除后續(xù)節(jié)點 page if (page->child) { // 如果當前 page 沒有達到半滿,則干掉所有后續(xù) page if (page->lessThanHalfFull()) { page->child->kill(); } // 如果當前 page 達到半滿以上,則保留下一頁 else if (page->child->child) { page->child->child->kill(); } } }上面這段代碼邏輯:
檢查入?yún)⑹欠駷榭粘卣嘉环?b>EMPTY_POOL_PLACEHOLDER,如果是則繼續(xù)判斷是否hotPage存在,如果hotPage存在則將釋放的終點改成coldPage()->begin(),如果hotPage不存在,則置空 TLS 存儲中的hotPage。
檢查stop既不是POOL_BOUNDARY也不是coldPage()->begin()的情況將報錯。
清空自動釋放池中stop之后的所有對象。
判斷當前page如果沒有達到半滿,則干掉所有后續(xù)所有 page,如果超過半滿則只保留下一個page。
5.6 add 和 releaseUntil
整個AutoreleasePoolPage就是一個堆棧,通過AutoreleasePoolPage的add方法將對象加入自動釋放池:
id *add(id obj) { assert(!full()); unprotect(); id *ret = next; // 復制指針 *next++ = obj; // 將 obj 存入 next 指向的內存地址后,next 向后移指向下一個空地址 protect(); return ret; }這段邏輯非常簡單,add方法將新加入的對象存入棧頂指針next指向的地址中,然后指向下一個位置。
下面是AutoreleasePoolPage出棧的實現(xiàn):
void releaseUntil(id *stop) { // 當 next 和 stop 不是指向同一塊內存地址,則繼續(xù)執(zhí)行 while (this->next != stop) { AutoreleasePoolPage *page = hotPage(); // 如果 hotPage 空了,則設置上一個 page 為 hotPage while (page->empty()) { page = page->parent; setHotPage(page); } page->unprotect(); id obj = *--page->next; // page->next-- 后指向當前棧頂元素 memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); // 將棧頂元素內存清空 page->protect(); if (obj != POOL_BOUNDARY) { objc_release(obj); // 棧頂元素引用計數(shù) - 1 } } setHotPage(this); }這段代碼大致邏輯:
判斷棧頂指針next和stop不是指向同一塊內存地址時,繼續(xù)出棧。
判斷當前page如果被清空,則繼續(xù)清理鏈表中的上一個page。
出棧,棧頂指針往下移,清空棧頂內存。
如果當前出棧的不是POOL_BOUNDARY,則調用objc_release引用計數(shù) - 1 。
體會
通過閱讀 objc4 源碼,將以前關于 ARC 的知識串聯(lián)起來,其中對細節(jié)的實現(xiàn)原理理解得更加透徹。
如果你覺得本文還不錯的話,可以到原文 【理解 ARC 實現(xiàn)原理】 給個 ? 。
參考
Objective-C 高級編程
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/6716.html
摘要:,這是一個仿支付寶芝麻信用分的一個,其實就是一個動畫儀表盤。首先,上原圖這個是在下支付寶上的截圖,分低各位見笑了。 hi,這是一個仿支付寶芝麻信用分的一個canvas,其實就是一個動畫儀表盤。 首先, 上原圖: showImg(https://segmentfault.com/img/bVbn3D6?w=750&h=1334); 這個是在下支付寶上的截圖,分低各位見笑了。然后看下我用c...
閱讀 820·2021-10-25 09:48
閱讀 611·2021-08-23 09:45
閱讀 2496·2019-08-30 15:53
閱讀 1759·2019-08-30 12:45
閱讀 586·2019-08-29 17:21
閱讀 3407·2019-08-27 10:56
閱讀 2547·2019-08-26 13:48
閱讀 691·2019-08-26 12:24