Objc Runtime ISA源碼初探-跟著指針起舞

網(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ì)象模型.jpg

對(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.jpg

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;
    ...
}
initIsa_no_mark.jpg

objc_object的初始化方法里面除了cls外還有兩個(gè)參數(shù)nonpointerhasCxxDtor,關(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);

編譯選項(xiàng).jpg

再跑一次代碼:


disable_nonpoint.jpg

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

繼續(xù)看源碼

initIsa.jpg
ISA_MASK宏.jpg

OBJC_DISABLE_NONPOINTER_ISANO時(shí),會(huì)先判斷SUPPORT_INDEXED_ISASUPPORT_PACKED_ISA。SUPPORT_INDEXED_ISAYES時(shí),isa用一個(gè)indexcls字段來(lái)存classArrayIndex()中的引用,這里猜測(cè)classArrayIndex能從某個(gè)全局的數(shù)組中拿到這個(gè)類的指針,代碼里面暫時(shí)沒有想到方法取到這個(gè)全局的數(shù)組,所以本篇文章不做討論,有讀者明白的話可以和我討論。

我們這里只研究SUPPORT_PACKED_ISAYES的情況,這種情況下64位的ISA指針存了很多信息,有nonpointer、has_assochas_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)證下:

nonpoint.jpg

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為10110xb最后4位為00100x2。中間的數(shù)值即為NSNumber的值.。比如NSNumber *number3 = @3;,,number3的指針值即為0xb000000000000032。以此類推。
可以通過(guò)編譯選項(xiàng)OBJC_DISABLE_TAGGED_POINTERS手動(dòng)關(guān)閉Tagged Pointer優(yōu)化。

一些坑

  1. objc4源碼不同版本的差異,其實(shí)objc4709中initIsa的實(shí)現(xiàn)是這樣的:
inline void 
objc_object::initIsa(Class cls)
{
   assert(!isTaggedPointer()); 
   isa = (uintptr_t)cls; 
}

完全看不出對(duì)nonpoint的判斷邏輯,我上面initIsa的源碼是在github上找到的。

  1. 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/

http://blog.sunnyxx.com/2014/04/02/objc_dig_arc_dealloc/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容