Objective-C 引用計數(shù)原理

  • 本文所使用的源碼為 objc4-647 和 CF-1153.18

引用計數(shù)如何存儲

  • 如果有些對象支持使用 TaggedPointer:
    蘋果會直接將對象的指針值作為引用計數(shù)返回肝谭。

  • 如果另外一些對象不支持使用TaggedPointer:
    如果當前設(shè)備是 64 位環(huán)境并且使用 Objective-C 2.0,那么會使用對象的 isa 指針 的 一部分空間 (bits.extra_rc)來存儲它的引用計數(shù);
    否則 Runtime 會使用一張 散列表 (SideTables())來管理引用計數(shù)。

  • 其實還有一種情況會改變引用計數(shù)的存儲策略壁却,那就是是否使用垃圾回收(用UseGC屬性判斷)泽示,但這種早已棄用的東西就不要管了,而且初始化垃圾回收機制的 void gc_init(BOOL wantsGC) 方法一直被傳入 NO缓呛。

TaggedPointer

判斷當前對象是否在使用 TaggedPointer 是看標志位是否為 1 :

/* objc-config.h */
// Define SUPPORT_TAGGED_POINTERS=1 to enable tagged pointer objects
// Be sure to edit tagged pointer SPI in objc-internal.h as well.
#if !(__OBJC2__  &&  __LP64__)
#   define SUPPORT_TAGGED_POINTERS 0
#else
#   define SUPPORT_TAGGED_POINTERS 1
#endif

// Define SUPPORT_MSB_TAGGED_POINTERS to use the MSB 
// as the tagged pointer marker instead of the LSB.
// Be sure to edit tagged pointer SPI in objc-internal.h as well.
// 不支持TaggedPointer棚品,或者不是ios也就是iphone
//(是osx也就是mac平臺)靠欢,SUPPORT_MSB_TAGGED_POINTERS 為 0
#if !SUPPORT_TAGGED_POINTERS  ||  !TARGET_OS_IPHONE 
#   define SUPPORT_MSB_TAGGED_POINTERS 0 
#else
#   define SUPPORT_MSB_TAGGED_POINTERS 1
#endif

#if TARGET_OS_OSX && __x86_64__
    // 64-bit Mac - tag bit is LSB
    // macOS下,始終用最小位作為標志位
#   define TAG_MASK 1UL
#else
    // Everything else - tag bit is MSB
    // 剩下的只有ARM64了铜跑。ARM64只用到64位中的48位门怪,高位全是0,此時mask就要反過來锅纺。
    // 這種情況下掷空,理論上能夠支持的Tagged Pointer的類就多了很多。
#   define TAG_MASK (1UL<<63)
#endif

inline bool 

objc_object::isTaggedPointer() 

{

#if SUPPORT_TAGGED_POINTERS

return ((uintptr_t)this & TAG_MASK);

#else

return false;

#endif

}

OC中的[NSObject alloc]最終調(diào)用的是C標準庫中的malloc,它所返回的地址通是16的整數(shù)拣帽。

16在二進制下最后4位都是0疼电,用最后一位是否為1來識別Tagged Pointer非常合理。

剩下的3位减拭,可以用于區(qū)分不同類型的Tagged Pointer,比如NSTaggedPointerNumber区丑、NSTaggedPointerDate等等拧粪。在objc4-723中,有

#if __has_feature(objc_fixed_enum)  ||  __cplusplus >= 201103L
enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
    OBJC_TAG_NSAtom            = 0, 
    OBJC_TAG_1                 = 1, 
    OBJC_TAG_NSString          = 2, 
    OBJC_TAG_NSNumber          = 3, 
    OBJC_TAG_NSIndexPath       = 4, 
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6, 
    OBJC_TAG_RESERVED_7        = 7, 
    
    OBJC_TAG_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 
    OBJC_TAG_Last52BitPayload  = 263, 

    OBJC_TAG_RESERVED_264      = 264

};

源碼中可以看到:
0 ~ 7 分別表示類型沧侥。而另外 OBJC_TAG_First60BitPayload 等定義分別表示前 60 位或者 52 位為負載內(nèi)容可霎,還是后 60 位或者后 52 位為負載內(nèi)容

創(chuàng)建Command LineTool應(yīng)用


Command LineTool應(yīng)用
Command LineTool應(yīng)用運行結(jié)果

創(chuàng)建iOS應(yīng)用


ios應(yīng)用
ios應(yīng)用運行結(jié)果

上面兩個結(jié)果可以看出
在iOS真機上判斷是否TaggedPointer可以直接看指針的最高一個比特位是否為1。(因為十六進制a轉(zhuǎn)換為二進制為1010宴杀,前面的1表示使用TaggedPointer癣朗,而后面的010就是2,表示NSString類型)
mac應(yīng)用判斷是否TaggedPointer可以直接看指針的最低一個比特位是否為1旺罢。(因為十六進制5轉(zhuǎn)換成二進制為0101旷余,最后一位表示使用TaggedPointer,而前面的010就是2扁达,表示NSString類型)

這里只列出了NSString采用這種技術(shù)的結(jié)果正卧。
對于以前的@""符號創(chuàng)建的字符串還是常量字符串,這個沒有改變跪解,但是采用stringWithFormat 等NSString方法的創(chuàng)建字符串對象則有了區(qū)別炉旷。
在蘋果的64位OC實現(xiàn)中,若對象指針的二進制第一位是1叉讥,則該指針為Tagged Pointer窘行。
例如0xa000000000000311其中a的2進制為1010,第一位1表示這是Tagged Pointer,010表示這是一個NSTaggedPointerString類图仓;這個地址最后一位表示字符串的數(shù)目罐盔,這里是0001表示有1位字符串;其中真正用來存儲的位數(shù)只有中間的14位16進制透绩。這個地址本身其實就存儲了字符串的值翘骂,可以說是存儲在&strS內(nèi)存中值,只是偽裝成了地址帚豪,它不需要存儲在數(shù)據(jù)區(qū)碳竟,也不需要申請堆空間。
NSTaggedPointerString的存儲有三種編碼方式:ASCII碼狸臣,六位編碼莹桅,五位編碼。
ASCII碼
我們發(fā)現(xiàn)NSTaggedPointerString存儲內(nèi)容除去第一位和最后一位,其實只有中間的14位16進制字符诈泼,再看ascll碼由8位二進制組成懂拾,所以這里(144) / 8=7,用8位的ascll碼的話最多可以存儲7個字符铐达。字符串數(shù)目0~7之間
[NSString stringWithFormat:@"1"]輸出的地址 0xa000000000000311岖赋,其中31的2進制是0011 0001,在ascll碼表里查找發(fā)現(xiàn)正是對應(yīng)著“1”瓮孙;
[NSString stringWithUTF8String:"abcdabc"] 這里輸出的地址是0xa636261646362617 可以發(fā)現(xiàn)在使用ascll編碼時唐断,字符串對應(yīng)的編碼是從右向左存儲的
六位編碼:
NSTaggedPointerString 采用六位二進制編碼,(14
4)/6=9.333…,可以看出最多存儲9位字符杭抠。字符數(shù)目8~9
五位編碼:
采用五位二進制編碼脸甘,(14*4)/5 = 11.2,可以看出這種編碼最多存儲11位字符偏灿。字符數(shù)目在10~11
NSTaggedPointerString 存儲編碼中的六位和五位編碼都是根據(jù)通常代碼中字母使用頻率來排序的轨帜,但并不是一成不變的撼玄,apple會持續(xù)更新并統(tǒng)計字母使用頻率,系統(tǒng)每次升級都可能不一樣,當前第一位是字母e,之后是i右遭,l长豁,o吮旅,t…生宛;這兩種編碼是從左向右的;根據(jù)編碼位數(shù)我們顯然也能推測出并不是所有字符都可以進行ascll或者六位五位編碼的邢疙,當出現(xiàn)這樣不能編碼的時候棍弄,系統(tǒng)也就不會使用NSTaggedPointerString類。

isa 指針(NONPOINTER_ISA)

用 64 bit 存儲一個內(nèi)存地址顯然是種浪費疟游,畢竟很少有那么大內(nèi)存的設(shè)備呼畸。于是可以優(yōu)化存儲方案,用一部分額外空間存儲其他內(nèi)容颁虐。isa 指針第一位為 1 即表示使用優(yōu)化的 isa 指針蛮原,這里列出不同架構(gòu)下的 64 位環(huán)境中 isa 指針結(jié)構(gòu):


union isa_t 

{

isa_t() { }

isa_t(uintptr_t value) : bits(value) { }

Class cls;

uintptr_t bits;

#if SUPPORT_NONPOINTER_ISA

# if __arm64__

#   define ISA_MASK        0x00000001fffffff8ULL

#   define ISA_MAGIC_MASK  0x000003fe00000001ULL

#   define ISA_MAGIC_VALUE 0x000001a400000001ULL

// 變量意義來源于:http://www.sealiesoftware.com/blog/archive/2013/09/24/objc_explain_Non-pointer_isa.html
    // 其意義可能已經(jīng)有些改變,這里列出來僅供參考另绩。
struct {

uintptr_t indexed           : 1;  // 0 表示純粹的 isa 指針儒陨,1 表示 non-pointer isa
        uintptr_t has_assoc         : 1;  // 是否有 associated object,沒有的話 dealloc 會更快
        uintptr_t has_cxx_dtor      : 1;  // 是否有 C++/ARC 的析構(gòu)函數(shù)笋籽,沒有的話 dealloc 會更快
        uintptr_t shiftcls          : 44; // 指向類的指針
        uintptr_t magic             : 6;  // 0x02 用于在調(diào)試時區(qū)分未初始化的垃圾數(shù)據(jù)和已經(jīng)初始化的對象
        uintptr_t weakly_referenced : 1;  // 是否被 weak 變量引用過蹦漠,沒有的話 dealloc 會更快
        uintptr_t deallocating      : 1;  // 是否正在 deallocating
        uintptr_t has_sidetable_rc  : 1;  // 引用計數(shù)值是否太大,以至于無法存在 isa 中车海,需要 SideTable 輔助存儲
        uintptr_t extra_rc          : 8;  /* 額外的引用計數(shù)值笛园。對象實例化時的本身的引用計數(shù)值為 1,而該值為 0。 
                                            向該對象發(fā)送 retain 消息后研铆,extra_rc 增加 1埋同。當 extra_rc 太大時,則需要 SideTable 輔助計數(shù)棵红。*/
        
         #define RC_ONE   (1ULL<<56)       // bits + RC_ONE 等于 extra_rc + 1
         #define RC_HALF  (1ULL<<7)

};

# elif __x86_64__

#   define ISA_MASK        0x00007ffffffffff8ULL

#   define ISA_MAGIC_MASK  0x0000000000000001ULL

#   define ISA_MAGIC_VALUE 0x0000000000000001ULL

struct {

uintptr_t indexed           : 1;

uintptr_t has_assoc         : 1;

uintptr_t has_cxx_dtor      : 1;

uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000

uintptr_t weakly_referenced : 1;

uintptr_t deallocating      : 1;

uintptr_t has_sidetable_rc  : 1;

uintptr_t extra_rc          : 14;

#       define RC_ONE   (1ULL<<50)

#       define RC_HALF  (1ULL<<13)

};

# else

// Available bits in isa field are architecture-specific.

#   error unknown architecture

# endif

// SUPPORT_NONPOINTER_ISA

#endif

};

優(yōu)化后的isa值針

SUPPORT_NONPOINTER_ISA 用于標記是否支持優(yōu)化的 isa 指針凶赁,其字面含義意思是 isa 的內(nèi)容不再是類的指針了,而是包含了更多信息窄赋,比如引用計數(shù)哟冬,析構(gòu)狀態(tài),被其他 weak 變量引用情況忆绰。判斷方法也是根據(jù)設(shè)備類型:(SUPPORT_NONPOINTER_ISA 定義)


// Define SUPPORT_NONPOINTER_ISA=1 to enable extra data in the isa field.

#if !__LP64__  ||  TARGET_OS_WIN32  ||  TARGET_IPHONE_SIMULATOR  ||  __x86_64__

#   define SUPPORT_NONPOINTER_ISA 0

#else

#   define SUPPORT_NONPOINTER_ISA 1

#endif

綜合看來目前只有 arm64 架構(gòu)的設(shè)備支持,下面列出了 isa 指針中變量對應(yīng)的含義:

blob.png

在 64 位環(huán)境下可岂,優(yōu)化的 isa 指針并不是就一定會存儲引用計數(shù)错敢,畢竟用 19bit (iOS 系統(tǒng))保存引用計數(shù)不一定夠。需要注意的是這 19 位保存的是引用計數(shù)的值減一缕粹。has_sidetable_rc 的值如果為 1稚茅,那么引用計數(shù)會存儲在一個叫 SideTable 的類的屬性中,后面會詳細講平斩。

canAllocNonpointer and SUPPORT_NONPOINTER_ISA
兩個都帶nonpointer,
SUPPORT_NONPOINTER_ISA是來標識當前平臺是否支持優(yōu)化的isa,但即使平臺支持,具體到某一個類卻是不一定的,要具體的去驗證類的元類的信息.不過可以放心大多系統(tǒng)的類的isa都是支持優(yōu)化的,我們自定義的類的isa也是支持優(yōu)化的.
canAllocNonpointer則是具體標記某個類是否支持優(yōu)化的isa.
在閱讀源碼時還有會各種帶nonpointer字樣的針對優(yōu)化isa的標記,除SUPPORT_NONPOINTER_ISA外,全是針對某個類而言的.

散列表

散列表來存儲引用計數(shù)具體是用 DenseMap 類來實現(xiàn)亚享,這個類中包含好多映射實例到其引用計數(shù)的鍵值對,并支持用 DenseMapIterator 迭代器快速查找遍歷這些鍵值對绘面。接著說鍵值對的格式:鍵的類型為 DisguisedPtr<objc_object>欺税,DisguisedPtr 類是對 objc_object * 指針及其一些操作進行的封裝,目的就是為了讓它給人看起來不會有內(nèi)存泄露的樣子(真是心機裱)揭璃,其內(nèi)容可以理解為對象的內(nèi)存地址晚凿;值的類型為 __darwin_size_t,在 darwin 內(nèi)核一般等同于 unsigned long瘦馍。其實這里保存的值也是等于引用計數(shù)減一歼秽。使用散列表保存引用計數(shù)的設(shè)計很好,即使出現(xiàn)故障導致對象的內(nèi)存塊損壞情组,只要引用計數(shù)表沒有被破壞燥筷,依然可以順藤摸瓜找到內(nèi)存塊的位置。

之前說引用計數(shù)表是個散列表院崇,這里簡要說下散列的方法肆氓。有個專門處理鍵的 DenseMapInfo 結(jié)構(gòu)體,它針對 DisguisedPtr 做了些優(yōu)化匹配鍵值速度的方法:

struct DenseMapInfo {

static inline DisguisedPtr getEmptyKey() {

return DisguisedPtr((T*)(uintptr_t)-1);

}

static inline DisguisedPtr getTombstoneKey() {

return DisguisedPtr((T*)(uintptr_t)-2);

}

static unsigned getHashValue(const T *PtrVal) {

return ptr_hash((uintptr_t)PtrVal);

}

static bool isEqual(const DisguisedPtr &LHS, const DisguisedPtr &RHS) {

return LHS == RHS; 

}

};

當然這里的哈希算法會根據(jù)是否為 64 位平臺來進行優(yōu)化亚脆,算法具體細節(jié)就不深究了做院,我總覺得蘋果在這里的 hardcode 是隨便寫的:


#if __LP64__

static inline uint32_t ptr_hash(uint64_t key)

{

key ^= key >> 4;

key *= 0x8a970be7488fda55;

key ^= __builtin_bswap64(key);

return (uint32_t)key;

}

#else

static inline uint32_t ptr_hash(uint32_t key)

{

key ^= key >> 4;

key *= 0x5052acdb;

key ^= __builtin_bswap32(key);

return key;

}

#endif

再介紹下 SideTable 這個類,它用于管理引用計數(shù)表和后面將要提到的 weak 表,并使用 spinlock_lock 自旋鎖來防止操作表結(jié)構(gòu)時可能的競態(tài)條件键耕。

獲取引用計數(shù)

在非 ARC 環(huán)境可以使用 retainCount 方法獲取某個對象的引用計數(shù)寺滚,其會調(diào)用 objc_object 的 rootRetainCount() 方法:

- (NSUInteger)retainCount {

return ((id)self)->rootRetainCount();
}

在 ARC 時代除了使用 Core Foundation 庫的 CFGetRetainCount() 方法,也可以使用 Runtime 的 _objc_rootRetainCount(id obj) 方法來獲取引用計數(shù)屈雄,此時需要引入頭文件村视。這個函數(shù)也是調(diào)用 objc_object 的 rootRetainCount() 方法:

inline uintptr_t 

objc_object::rootRetainCount()

{

assert(!UseGC);

if (isTaggedPointer()) return (uintptr_t) this;`

sidetable_lock();

isa_t bits = LoadExclusive(&isa.bits);

if (bits.indexed) {

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();

}

rootRetainCount() 方法對引用計數(shù)存儲邏輯進行了判斷,因為 TaggedPointer 前面已經(jīng)說過了酒奶,可以直接獲取引用計數(shù)蚁孔;64 位環(huán)境優(yōu)化的 isa 指針前面也說過了,所以這里的重頭戲是在 TaggedPointer 無法使用時調(diào)用的 sidetable_retainCount() 方法:

uintptr_t

objc_object::sidetable_retainCount()

{

SideTable *table = SideTable::tableForPointer(this);

size_t refcnt_result = 1;

spinlock_lock(&table->slock);

RefcountMap::iterator it = table->refcnts.find(this);

if (it != table->refcnts.end()) {

// this is valid for SIDE_TABLE_RC_PINNED too

refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;

}

spinlock_unlock(&table->slock);

return refcnt_result;

}

sidetable_retainCount() 方法的邏輯就是先從 SideTable 的靜態(tài)方法獲取當前實例對應(yīng)的 SideTable 對象惋嚎,其 refcnts 屬性就是之前說的存儲引用計數(shù)的散列表杠氢,這里將其類型簡寫為 RefcountMap:

typedef objc::DenseMap RefcountMap;

然后在引用計數(shù)表中用迭代器查找當前實例對應(yīng)的鍵值對,獲取引用計數(shù)值另伍,并在此基礎(chǔ)上 +1 并將結(jié)果返回鼻百。這也就是為什么之前說引用計數(shù)表存儲的值為實際引用計數(shù)減一。

需要注意的是為什么這里把鍵值對的值做了向右移位操作(it->second >> SIDE_TABLE_RC_SHIFT):


#ifdef __LP64__

#   define WORD_BITS 64

#else

#   define WORD_BITS 32

#endif

// The order of these bits is important.

#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)

#define SIDE_TABLE_DEALLOCATING      (1UL<<1)  // MSB-ward of weak bit

#define SIDE_TABLE_RC_ONE            (1UL<<2)  // MSB-ward of deallocating bit

#define SIDE_TABLE_RC_PINNED         (1UL<<(WORD_BITS-1))

#define SIDE_TABLE_RC_SHIFT 2

#define SIDE_TABLE_FLAG_MASK (SIDE_TABLE_RC_ONE-1)RefcountMap

可以看出值的第一個 bit 表示該對象是否有過 weak 對象摆尝,如果沒有温艇,在析構(gòu)釋放內(nèi)存時可以更快;第二個 bit 表示該對象是否正在析構(gòu)堕汞。從第三個 bit 開始才是存儲引用計數(shù)數(shù)值的地方勺爱。所以這里要做向右移兩位的操作,而對引用計數(shù)的 +1 和 -1 可以使用 SIDE_TABLE_RC_ONE,還可以用 SIDE_TABLE_RC_PINNED 來判斷是否引用計數(shù)值有可能溢出讯检。

當然不能夠完全信任這個 _objc_rootRetainCount(id obj) 函數(shù)琐鲁,對于已釋放的對象以及不正確的對象地址,有時也返回 “1”视哑。它所返回的引用計數(shù)只是某個給定時間點上的值绣否,該方法并未考慮到系統(tǒng)稍后會把自動釋放吃池清空,因而不會將后續(xù)的釋放操作從返回值里減去挡毅。clang 會盡可能把 NSString 實現(xiàn)成單例對象蒜撮,其引用計數(shù)會很大。如果使用了 TaggedPointer跪呈,NSNumber 的內(nèi)容有可能就不再放到堆中段磨,而是直接寫在寬敞的64位棧指針值里。其看上去和真正的 NSNumber 對象一樣耗绿,只是使用 TaggedPointer 優(yōu)化了下苹支,但其引用計數(shù)可能不準確。

修改引用計數(shù)

retain 和 release

在非 ARC 環(huán)境下可以使用 retain 和 release 方法對引用計數(shù)進行加一減一操作误阻,它們分別調(diào)用了 _objc_rootRetain(id obj) 和 _objc_rootRelease(id obj) 函數(shù)债蜜,不過后兩者在 ARC 環(huán)境下也可使用晴埂。最后這兩個函數(shù)又會調(diào)用 objc_object 的下面兩個方法:


inline id 

objc_object::rootRetain()

{

assert(!UseGC);

if (isTaggedPointer()) return (id)this;

return sidetable_retain();

}

inline bool 

objc_object::rootRelease()

{

assert(!UseGC);

if (isTaggedPointer()) return false;

return sidetable_release(true);

}

這樣的實現(xiàn)跟獲取引用計數(shù)類似,先是看是否支持 TaggedPointer(畢竟數(shù)據(jù)存在棧指針而不是堆中寻定,棧的管理本來就是自動的)儒洛,否則去操作 SideTable 中的 refcnts 屬性,這與獲取引用計數(shù)策略類似狼速。sidetable_retain() 將 引用計數(shù)加一后返回對象琅锻,sidetable_release() 返回是否要執(zhí)行 dealloc 方法:

bool 

objc_object::sidetable_release(bool performDealloc)

{

#if SUPPORT_NONPOINTER_ISA

assert(!isa.indexed);

#endif

SideTable *table = SideTable::tableForPointer(this);

bool do_dealloc = false;

if (spinlock_trylock(&table->slock)) {

RefcountMap::iterator it = table->refcnts.find(this);

if (it == table->refcnts.end()) {

do_dealloc = true;

table->refcnts[this] = SIDE_TABLE_DEALLOCATING;

} else if (it->second < SIDE_TABLE_DEALLOCATING) {

// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.

do_dealloc = true;

it->second |= SIDE_TABLE_DEALLOCATING;

} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {

it->second -= SIDE_TABLE_RC_ONE;

}

spinlock_unlock(&table->slock);

if (do_dealloc  &&  performDealloc) {

((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);

}

return do_dealloc;

}

return sidetable_release_slow(table, performDealloc);

}

看到這里知道為什么在存儲引用計數(shù)時總是真正的引用計數(shù)值減一了吧。因為 release 本來是要將引用計數(shù)減一向胡,所以存儲引用計數(shù)時先預(yù)留了個“一”恼蓬,在減一之前先看看存儲的引用計數(shù)值是否為 0 (it->second < SIDE_TABLE_DEALLOCATING),如果是僵芹,那就將對象標記為“正在析構(gòu)”(it->second |= SIDE_TABLE_DEALLOCATING),并發(fā)送 dealloc 消息处硬,返回 YES;否則就將引用計數(shù)減一(it->second -= SIDE_TABLE_RC_ONE)拇派。這樣做避免了負數(shù)的產(chǎn)生郁油。

除此之外,Core Foundation 庫中也提供了增減引用計數(shù)的方法攀痊。比如在使用 Toll-Free Bridge 轉(zhuǎn)換時使用的 CFBridgingRetain 和 CFBridgingRelease 方法,其本質(zhì)是使用 __bridge_retained 和 __bridge_transfer 告訴編譯器此處需要如何修改引用計數(shù):


NS_INLINE CF_RETURNS_RETAINED CFTypeRef __nullable CFBridgingRetain(id __nullable X) {

return (__bridge_retained CFTypeRef)X;

}

NS_INLINE id __nullable CFBridgingRelease(CFTypeRef CF_CONSUMED __nullable X) {

return (__bridge_transfer id)X;

}

此外 Objective-C 很多實現(xiàn)是靠 Core Foundation Runtime 來實現(xiàn)拄显, Objective-C Runtime 源碼中有些地方明確注明:”// Replaced by CF“苟径,那就是意思說這塊任務(wù)被 Core Foundation 庫接管了。當然 Core Foundation 有一部分是開源的躬审。還有一些 Objective-C Runtime 函數(shù)的實現(xiàn)被諸如 ObjectAlloc 和 NSZombie 這樣的內(nèi)存管理工具所替代:


// Replaced by ObjectAlloc

+ (id)allocWithZone:(struct _NSZone *)zone {

return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);

}

// Replaced by CF (throws an NSException)

+ (id)init {

return (id)self;

}

// Replaced by NSZombies

- (void)dealloc {

_objc_rootDealloc(self);

}

alloc, new, copy, mutableCopy

根據(jù)編譯器的約定棘街,這以這四個單詞開頭的方法都會使引用計數(shù)加一。而 new 相當于調(diào)用 alloc 后再調(diào)用 init:

id

_objc_rootAlloc(Class cls)

{

return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);

}

+ (id)alloc {

return _objc_rootAlloc(self);

}

+ (id)new {

return [callAlloc(self, false/*checkNil*/) init];

}

可以看出 alloc 和 new 最終都會調(diào)用 callAlloc承边,默認使用 Objective-C 2.0 且忽視垃圾回收和 NSZone遭殉,那么后續(xù)的調(diào)用順序依次是為:

class_createInstance()

_class_createInstanceFromZone()

calloc()

calloc() 函數(shù)相比于 malloc() 函數(shù)的優(yōu)點是它將分配的內(nèi)存區(qū)域初始化為0,相當于 malloc() 后再用 memset() 方法初始化一遍博助。

copy 和 mutableCopy 都是基于 NSCopying 和 NSMutableCopying 方法約定险污,分別調(diào)用各類自己實現(xiàn)的 copyWithZone: 和 mutableCopyWithZone: 方法。這些方法無論實現(xiàn)方式是深拷貝還是淺拷貝富岳,都會增加引用計數(shù)蛔糯。(有些類的策略是懶拷貝,只增加引用計數(shù)但并不真的拷貝窖式,等對象內(nèi)容發(fā)生變化時再拷貝一份出來蚁飒,比如 NSArray)。

在 retain 方法加符號斷點會發(fā)現(xiàn) alloc, new, copy, mutableCopy 這四個方法都會通過 Core Foundation 的 CFBasicHashAddValue() 函數(shù)來調(diào)用 retain 方法萝喘。其實 CF 有個修改和查看引用計數(shù)的入口函數(shù) __CFDoExternRefOperation淮逻,在 CFRuntime.c 文件中實現(xiàn)琼懊。

autorelease

本想貼上一堆 Runtime 中關(guān)于自動釋放池的源碼然后說上一大堆,然后發(fā)現(xiàn)了太陽神的這篇黑幕背后的Autorelease把我想說的都說了爬早,把我不知道的也說了哼丈,簡直太屌了。

其實通過看源碼可以知道好多細節(jié)凸椿,沒事點進去各種宏定義往往會得到驚喜:哇削祈,原來是這么回事,XX 就是 XX 之類脑漫。髓抑。。

鏈接:http://www.cnblogs.com/xiaosong666/p/5045494.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末优幸,一起剝皮案震驚了整個濱河市吨拍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌网杆,老刑警劉巖羹饰,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異碳却,居然都是意外死亡队秩,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門昼浦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來馍资,“玉大人,你說我怎么就攤上這事关噪∧裥罚” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵使兔,是天一觀的道長建钥。 經(jīng)常有香客問我,道長虐沥,這世上最難降的妖魔是什么熊经? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮置蜀,結(jié)果婚禮上奈搜,老公的妹妹穿的比我還像新娘。我一直安慰自己盯荤,他們只是感情好馋吗,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著秋秤,像睡著了一般宏粤。 火紅的嫁衣襯著肌膚如雪脚翘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天绍哎,我揣著相機與錄音来农,去河邊找鬼。 笑死崇堰,一個胖子當著我的面吹牛沃于,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播海诲,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼繁莹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了特幔?” 一聲冷哼從身側(cè)響起咨演,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蚯斯,沒想到半個月后薄风,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡拍嵌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年遭赂,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片横辆。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡嵌牺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出龄糊,到底是詐尸還是另有隱情,我是刑警寧澤募疮,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布炫惩,位于F島的核電站,受9級特大地震影響阿浓,放射性物質(zhì)發(fā)生泄漏他嚷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一芭毙、第九天 我趴在偏房一處隱蔽的房頂上張望筋蓖。 院中可真熱鬧,春花似錦退敦、人聲如沸粘咖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瓮下。三九已至翰铡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間讽坏,已是汗流浹背锭魔。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留路呜,地道東北人迷捧。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像胀葱,于是被迫代替她去往敵國和親漠秋。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345