網(wǎng)上關(guān)于Object-C Runtime的文章已經(jīng)不少了,但大部分人一開始是不是也和我看的一樣,大概知道是怎么回事,卻又不敢確定呢?這就是我寫這篇文章的初衷。本文并不會(huì)花大篇章節(jié)完整而全面的解釋Runtime的源碼,因?yàn)槲业那巴戮W(wǎng)紅魚??的這篇Objective-C Runtime已經(jīng)寫的很好了(強(qiáng)行蹭關(guān)系)。
提出問(wèn)題
帶著問(wèn)題去探究,按圖索驥,往往是最有效的方式,思考下面一個(gè)問(wèn)題:
不用objc的接口,如何判斷一個(gè)指針指向的是否是一個(gè)合法的Objective-C對(duì)象?
內(nèi)存模型
既然不能用objc的接口,所有的信息就只有一個(gè),那就是傳入的參數(shù):指針。若我們能了解Objc的內(nèi)存布局,然后和這個(gè)指針對(duì)比,就能判斷出這是否是一個(gè)合法的Objc對(duì)象。讀到這里,若你沒有基礎(chǔ)的Runtime知識(shí),請(qǐng)先閱讀文章開頭的貼出的博客。下面這張圖相信大家都不陌生 :)

對(duì)象(instance)指針的ISA指向了類(class),類的ISA指向了元類(meta class),元類的ISA指針指向了Root class(meta)這里先不考慮NSProxy。即NSObject的元類。NSObject元類的ISA指針指向自身,從而形成一個(gè)閉環(huán)。我們可以用這個(gè)閉環(huán)來(lái)判斷指針是否合法。
怎么從一個(gè)指針取到它的ISA呢,這得從源碼下手了。準(zhǔn)備好了嗎?下面開始探究源碼吧,去蘋果官方網(wǎng)站下載Runtime源碼,我下的是709的。找到objc_object的實(shí)現(xiàn):
objc-private.h
struct objc_object {
private:
isa_t isa;
public:
...
}
objc-runtime-old.h
struct objc_class : objc_object {
Class superclass;
const char *name;
uint32_t version;
uint32_t info;
uint32_t instance_size;
struct old_ivar_list *ivars;
struct old_method_list **methodLists;
Cache cache;
struct old_protocol_list *protocols;
// CLS_EXT only
const uint8_t *ivar_layout;
struct old_class_ext *ext;
...
}
萬(wàn)物皆對(duì)象,類也是對(duì)象??梢钥吹?,對(duì)象是一個(gè)結(jié)構(gòu)體,結(jié)構(gòu)體的一開頭就是ISA。指針可以強(qiáng)轉(zhuǎn)為二級(jí)指針,判斷邏輯可以這么寫(uintptr是蘋果針對(duì)64位指針的優(yōu)化表示,其實(shí)是unsigned long類型):
Dump *dump = [Dump new];
BOOL res = isValidObjcPointer((uintptr_t)dump);
if (res) {
NSLog(@"The pointer is a valid objc pointer");
} else {
NSLog(@"The pointer is not a valid objc pointer!");
}
static BOOL isValidObjcPointer(uintptr_t p) {
uintptr_t class_p = (uintptr_t)*(void * *)p;//把p指針指向的值作為class_p指針地址
uintptr_t meta_p = (uintptr_t)*(void * *)class_p;//同上
uintptr_t meta_ns_obj = (uintptr_t)*(void * *)meta_p;//同上
uintptr_t meta_ns_obj_isa = (uintptr_t)*(void * *)meta_ns_obj;//同上
if (meta_ns_obj == meta_ns_obj_isa) {
return YES;
} else {
return NO;
}
}
連上手機(jī)跑一下:

BAD_ACCESS,說(shuō)明這塊內(nèi)存區(qū)域被禁止訪問(wèn)。說(shuō)明上面取ISA的邏輯出了些問(wèn)題。源碼里面ISA是一個(gè)isa_t的類型,點(diǎn)進(jìn)去看一下。
objc-private.h
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
...
}

objc_object的初始化方法里面除了cls外還有兩個(gè)參數(shù)nonpointer和hasCxxDtor,關(guān)于hasCxxDtor這里不做過(guò)多解釋,具體可以看sunnyxx的這篇博客。我們發(fā)現(xiàn),根據(jù)標(biāo)志位nonpointer的值,ISA的賦值方式是不同的。根據(jù)nonpoint的命名,我們能猜測(cè)出這個(gè)字段為NO代表ISA的cls字段就是指向類(或者元類)的指針,若為YES則有另外的邏輯。
NONPOINTER_ISA
這個(gè)文件列出了Xcode的編譯選項(xiàng),我們看到最下面有一個(gè)OBJC_DISABLE_NONPOINTER_ISA。從名字即可推測(cè)出OBJC_DISABLE_NONPOINTER_ISA這個(gè)編譯選項(xiàng)決定ISA是否是純指針表示。我們?cè)赬code里面嘗試把這個(gè)編譯選項(xiàng)設(shè)置為YES(即nonpoint == NO);

再跑一次代碼:

看下lldb里面的結(jié)果,果然一切如預(yù)期。那么在OBJC_DISABLE_NONPOINTER_ISA為NO(即nonpoint == YES)的情況下我們?cè)撛趺崔k呢,從第一次的結(jié)果可知在arm64(本人手機(jī)為arm64架構(gòu))環(huán)境下這個(gè)選項(xiàng)默認(rèn)為YES。
繼續(xù)看源碼


在OBJC_DISABLE_NONPOINTER_ISA為NO時(shí),會(huì)先判斷SUPPORT_INDEXED_ISA和SUPPORT_PACKED_ISA。SUPPORT_INDEXED_ISA為YES時(shí),isa用一個(gè)indexcls字段來(lái)存classArrayIndex()中的引用,這里猜測(cè)classArrayIndex能從某個(gè)全局的數(shù)組中拿到這個(gè)類的指針,代碼里面暫時(shí)沒有想到方法取到這個(gè)全局的數(shù)組,所以本篇文章不做討論,有讀者明白的話可以和我討論。
我們這里只研究SUPPORT_PACKED_ISA為YES的情況,這種情況下64位的ISA指針存了很多信息,有nonpointer、has_assoc、 has_cxx_dtor、 weakly_referenced、 shiftcls等,這是apple的一種優(yōu)化,用一個(gè)指針存儲(chǔ)了更多的信息,具體的字段介紹可以看這篇這篇文章;下面是各個(gè)字段含義的表格:
| 名稱 | 說(shuō)明 |
|---|---|
| nonpointer | value of nonpointer |
| has_assoc | Object has or once had an associated reference. Object with no associated references can deallocate faster. |
| has_cxx_dtor | Object has a C++ or ARC destructor. Objects with no destructor can deallocate faster. |
| shiftcls | Class pointer's non-zero bits. |
| magic | Equals 0xd2 Used by the debugger to distinguish real objects from uninitialized junk. |
| weakly_referenced | Object is or once was pointed to by an ARC weak variable. Objects not weakly referenced can deallocate faster. |
| deallocating | Object is currently deallocating |
| has_sidetable_rc | Object's retain count is too large to store inline. |
| extra_rc | Object's retain count above 1. (For example, if extra_rc is 5 then the object's real retain count is 6) |
看圖中紅圈標(biāo)出來(lái)的,我們可以用ISA_MASK取出原始的class指針(在arm64架構(gòu)下和x86下布局不同,ISA_MASK也不同)。用代碼 驗(yàn)證下:

bingo! 正確運(yùn)行!
Tagged Pointer
其實(shí)文章到這里本該結(jié)束了,但是好奇心害死貓,runtime有一個(gè)API叫做object_getClass根據(jù)一個(gè)指針返回這個(gè)指針?biāo)鶎俚念?。這個(gè)API不出意外也是根據(jù)ISA去查找實(shí)現(xiàn)的,我們看看他的源碼:
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
inline Class
objc_object::getIsa()
{
if (!isTaggedPointer()) return ISA();
uintptr_t ptr = (uintptr_t)this;
if (isExtTaggedPointer()) {
uintptr_t slot =
(ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
return objc_tag_ext_classes[slot];
} else {
uintptr_t slot =
(ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
return objc_tag_classes[slot];
}
}
inline bool
objc_object::isTaggedPointer()
{
return _objc_isTaggedPointer(this);
}
原來(lái)還有一個(gè)TaggedPointer的字段,在isTaggedPoint的情況下,對(duì)象模型有所不同。關(guān)于TaggedPointer可以看這篇博客。Tagged Pointer是蘋果的一項(xiàng)優(yōu)化技術(shù),由于64的指針很長(zhǎng),這么大的地址空間完全是浪費(fèi)的。也就是說(shuō)64位環(huán)境下,內(nèi)存地址的中有很多位都是0。指針地址僅僅作為內(nèi)存的地址是比較浪費(fèi)的,我們可以在指針地址中保存或附加更多的信息。這就引入了Tagged Pointer概念(其實(shí)和NONPOINTER_ISA理念都是相同的,往64位的指針中存放更多信息,節(jié)省空間,存取速度也會(huì)提高)。Tagged Pointer是指那些指針中包含特殊屬性或信息的指針。其中指針對(duì)齊概念可以讓我們來(lái)標(biāo)識(shí)一個(gè)指針是否是Tagged Pointer以及相關(guān)類型。iOS 7的64位環(huán)境和Mac OS 10.7(Lion)中開始引入了Tagged Pointer。
Tagged Pointer比較適用于那些可以用指針地址的值的線性增長(zhǎng)來(lái)表示的對(duì)象。一個(gè)比較典型的應(yīng)用就是NSNumber,在64位環(huán)境下,對(duì)于一般的數(shù)字,NSNumber不再為其在堆上分配內(nèi)存,直接用指針就能表示。以我真機(jī)上的測(cè)試的數(shù)據(jù)來(lái)看NSNumber類型的指針前4bit為1011即0xb最后4位為0010即0x2。中間的數(shù)值即為NSNumber的值.。比如NSNumber *number3 = @3;,,number3的指針值即為0xb000000000000032。以此類推。
可以通過(guò)編譯選項(xiàng)OBJC_DISABLE_TAGGED_POINTERS手動(dòng)關(guān)閉Tagged Pointer優(yōu)化。
一些坑
- objc4源碼不同版本的差異,其實(shí)objc4709中
initIsa的實(shí)現(xiàn)是這樣的:
inline void
objc_object::initIsa(Class cls)
{
assert(!isTaggedPointer());
isa = (uintptr_t)cls;
}
完全看不出對(duì)nonpoint的判斷邏輯,我上面initIsa的源碼是在github上找到的。
- iOS&macOS真正的實(shí)現(xiàn)和源碼里面是有差異的,源碼只能作為一份參考,很多時(shí)候全靠猜 :)
(可憐的程序猿
Reference
http://www.itdecent.cn/p/b5955a3811d7
http://blog.xcodev.com/2013/10/21/2013-10-21-tagged-pointer-and-64-bit/
http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/