- 本文所使用的源碼為 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)用
創(chuàng)建iOS應(yīng)用
上面兩個結(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 采用六位二進制編碼,(144)/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
};
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)的含義:
在 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 之類脑漫。髓抑。。