觸摸iOS底層:Object-C 的類和isa (一)

類和isa(一)

OC中的類是什麼,長什麼樣?

這是一個很好的問題,若是說 runtime 是靈魂,  就是他的使者。瞭解類,是咱們繼續探索的基石。macos

請記住一句話, 萬物皆對象! 哪怕是類,也終究逃不出來自蘋果的這個魔咒markdown

類結構

**對於iOS編譯,**iOS的底層代碼是由C++實現,但系統庫在.h中以C的形式向咱們提供API,因此OC會在編譯時由 Clang 編譯器轉成C++繼續編譯。 想要了解底層源碼的同窗,蘋果也開源了源碼,這裏是蘋果開源代碼Source Browser ,其中就有咱們須要常常用到的 objc4libmalloc ,這倆貨分別是runtime和alloc的不一樣版本的源碼,很是值得你們去選擇一個版本,下載和編譯,推薦選擇最新的編號最大的版本。 什麼是Clang? Clang是一個C語言、C++、Objective-C、C++語言的輕量級編譯器,是LLVM的一個重要組成部分。若是有時間,很是願意和小夥伴們探討iOS的編譯和OC的動態化,這兩個話題在實際的開發生活中是很是有必要的。數據結構

咱們準備一段很普通的OC代碼:在 main.m中定義了一個簡單的類 LYPerson架構

// 定義一個Person類
@interface LYPerson : NSObject
@property (copy,   nonatomic) NSString *name;
- (void)say;
@end

@implementation LYPerson
- (void)say {}
@end
// 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
    return 0;
}
複製代碼

打開終端,定位到 main.m 目錄, 經過下面Clang的命令,將main.m 生成main.cpp 的C++ 代碼。app

clang -rewrite-objc main.m -o main.cpp
複製代碼

看一看main.cpp 也就是main的C++實現。咱們只找LYPerson, cmd+f 搜索「LYPerson」, 很快,咱們看到一段眼熟的代碼,和原始的OC的 main.m 是否是很像 image.png 看到這個C++的LYPerson,咱們會注意到:ide

  1. LYPerson做爲一個類,自己也能夠實例化一個對象,在這裏,居然是結構體 objc_object 的實例,那麼類自己有沒有多是一個對象?結構體 objc_object 是什麼?
  2. LYPerson 內定義了成員:name,同時還定義一個 NSObject_IMP 結構體類型的成員:NSObject_IVARS。
  3. 咱們自定義的屬性name,在C++代碼裏,經過格式拼接成 _I_LYPerson_setName_(LYPerson * self, SEL _cmd, NSString *name)

NSObject_IMPL、objc_class、objc_object

在main.cpp 中咱們能夠找到LYPerson類型objc_object 和 成員類型NSObject_IMP的定義 IMPL 顧名思義,是implementation的意思。函數

// 這個是NSObject的定義
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

// 這個是NSObject的實現
struct NSObject_IMPL {
	Class isa;
};

// 這個是LYPerson的實現
struct LYPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *_name;
};

typedef struct objc_class *Class; // 注意:Class 是一個指針類型,意味着全部的Class類型都是一個指針,而指針指向的是一個objc_class類型的數據
複製代碼

上面就是cpp中的NSObject和LYPerson的IMPL實現,咱們發現, LYPerson 的NSObject_IVARS的isa就是 NSObject 的isa。並且每個類都有isa! LYPerson 爲何要有這麼一個 struct NSObject_IMPL NSObject_IVARS; ?由於類有繼承關係。怎麼繼承?就是經過內部定義這個 struct NSObject_IMPL NSObject_IVARS; 實現僞繼承NSOBject的isa。oop

親愛的夥伴們,大家必定還注意到一點,咱們的 LYPerson 是一個 obj_object 的結構體類型優化

typedef struct objc_object LYPerson;
複製代碼

咱們又知道, obj_object 在蘋果的解釋中,他是一個對象,而咱們的LYPerson是一個自定義的類,請回憶我最開始講的那句話: 萬物皆對象LYPerson 也是一個對象,並且它是一個 類對象。做爲一個對象,他也有屬於本身的類,那麼這個類對象(就是咱們OC裏的類)的類是什麼呢,他叫 元類 ,元類上面還有一個類,他叫 根類 ,根類上面是否是還有類呢?類對象是否是隻有一份?這些都不在咱們這篇文章討論的範疇,太大了,總得分開闡述。 ** 咱們只要知道:類也是一個obj_object對象,而obj_object是一個結構體,換句話說,類的本質是一個結構體,對象也是一個結構體。 ui

又有小夥伴疑問?爲何 萬物皆對象 ?咱們來看一看obj_object 和 obj_class , 進入咱們從obj4的源碼:

// 這個是objc_class, 繼承於objc_object
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;	// 指向父類
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
	......// 太多省略
}

// 這個是objc_object
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
複製代碼

個人天吶,有沒有注意到什麼。objc_class繼承於objc_object,類也是對象,那麼每個obj_class都會有一個isa,在objc_class裏,除了isa,還有superclass,superclass就是咱們所說的指向的父類。這也是爲何,從NSObject開始,每個類都有isa,原來是obj_object這個孃胎裏就帶了isa,那麼,isa究竟是什麼,而是仍是個Class類型那他指向的又是什麼類?

擴展:自定義類中屬性set方法的工做流程

#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)
static void _I_LYPerson_setName_(LYPerson * self, SEL _cmd, NSString *name)
{
    objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LYPerson, _name), (id)name, 0, 1);
}
複製代碼

在set方法裏,經過固定的API:objc_setProperty實現setName功能,查看objc_setProperty方法,須要到咱們前面提到的objc4 的源碼工程,下面就是objc4中objc_setProperty的源碼和內部的調用:

void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) {
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
// 核心的一步:reallySetProperty
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) {
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}
複製代碼

讀不懂不要緊,經過上面這一段,咱們大體能夠知道:自定義屬性的set實現是經過 底層 **objc_setProperty -> reallySetProperty **來完成。在reallySetProperty中,最終新的屬性值,存在了slot指針指向的位置,而slot指向的位置就是方法中提到的 :

(id*) ((char*)self + offset);
複製代碼

表示便宜地址,對象的指針佔了8個字節,因此當咱們給name賦值時,offset會從8開始,咱們來驗證一下 image.png image.png 能夠看到self是當前對象,_cmd是當前方法也就是setName,newValue是咱們賦的值,offset是8!copy是true,atomic是false,由於咱們定義的是(copy, nonatomic)。

isa

初始化isa

咱們從一個對象的alloc方法開始看,何時產生的isa,依然是在obj4源碼裏,咱們斷點調試 alloc方法 image.png 進入_objc_rootAlloc image.png 再進入callAlloc image.png 再往裏面走,進入_objc_rootAllocWithZone image.png 繼續往裏走,進入_class_createInstanceFromZone, 這個方法就是在建立實例化的對象,有幾個核心的步驟 image.png 其中須要注意的是:

  • instanceSize 是計算須要開闢多少個字節
  • calloc 是正兒八經的開闢空間,建立對象obj,calloc的源碼能夠查看蘋果開源的libmalloc源碼。
  • initInstanceIsa 初始化isa,或者說是給這個對象設置isa
  • object_cxxConstructFromClass 這一步就是完善obj,好比說,設置obj的super_class。

在_class_createInstanceFromZone,咱們一步一步進入到了initInstanceIsa image.png initInstanceIsa看這個方法名,就知道太符合咱們的要求了,初始化一個isa,好,繼續進入這個方法,來到了initIsa image.png initIsa image.png 咱們最初的斷點式[LYPerson alloc] 這句代碼是建立一個LYPerson的對象,咱們看到,這裏的isa是一個isa_t, isa 的shiftcls竟然存的是當前的類,而不是父類。咱們是否是能夠這麼說,對象的isa裏他的類,他的類又是一個對象,類對象的isa存了類對象的類。哇,是否是和咱們常規的類的繼承:子類-父類-父父類-。。。。-NSObject有衝突啊。不衝突,這是另外一條線:isa指向這個對象(也包括類對象)的類。 咱們在建立LYPerson的時候,裏買就有了isa,並且是NSObject的isa,這個上面剛講過。 image.png 咱們是否是能夠得出一個信息:obj的isa->Person,Person的isa->NSObject. 接下來咱們獲取obj、person、NSObject的isa來驗證是否是

獲取isa

咱們平時想要獲取某一個對象的類,是怎麼獲取? 一、經過runtime的 objc_getClass 函數API 二、直接調用class方法:[obj class]; class方法的實現仍是調用了objc_getClass

查看runtime的源碼(objc4)

// 這個就是咱們熟悉的id類型,和Class同樣,也是一個objc_object結構體指針
typedef struct objc_object *id;

- (Class)class {	
	// 仍是調用了object_getClass
    return object_getClass(self);
}

Class object_getClass(id obj) {
    if (obj) return obj->getIsa();
    else return Nil;
}
複製代碼

頗有意思的是,object_getClass的參數是一個id,id是一個objc_object,也在告訴咱們,不管是對象仍是類,其實都是obj_object。都要再去調用obj_object的getIsa()

inline Class objc_object::getIsa() {
    if (fastpath(!isTaggedPointer())) return ISA();

    extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
    uintptr_t slot, ptr = (uintptr_t)this;
    Class cls;

    slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
    cls = objc_tag_classes[slot];
    if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
        slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        cls = objc_tag_ext_classes[slot];
    }
    return cls;
}
複製代碼

在getIsa()裏,咱們關注的是第一行,ISA();

inline Class objc_object::ISA() {
    ASSERT(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    return (Class)(isa.bits & ISA_MASK);
#endif
}
複製代碼

咱們查看isa,會發現,這個isa也是一個isa_t類型。咱們在初始化isa到最後的時候,將對象的 Class 類型的類放到了一個 isa_t 類型的 shiftcls 成員裏,在這,咱們經過 isa.bits & ISA_MASK 返回了一了一樣 Class 類型的東西出去,咱們當初存入的 shiftcls 和這個**isa.bits & ISA_MASK **有什麼關係?

若是isa.bits & ISA_MASK 就是shiftcls,咱們就完美的串出了一個信息:建立對象時,將對象類綁定到shiftcls,從而讓isa指向這個類;經過class方法和objc_getClass函數獲取對象的類,其實是獲取shiftcls裏綁定的類信息。 ** 要證實上面的假設,咱們得先了解一下,isa_t

isa_t 和 isa內存儲的信息

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

    Class cls; // 直接指向Class
    uintptr_t bits; // 經過bit位運算,完成綁定Class 
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};
複製代碼

 爲何這裏用了一個聯合體,咱們的開發過程當中,大部分的iOS開發者不多會用到這種數據結構,這種結構使用了bits位域,他能夠有效的節省咱們的內存空間。

擴展:聯合體和結構體

結構體 結構體是指把不同的數據組合成一個總體,其變量共存的,變量不論是否使用,都會分配內存。

  • 缺點:全部屬性都分配內存,比較浪費內存,假設有4個int成員,一共分配了16字節的內存,可是在使用時,你只使用了4字節,剩餘的12字節就是屬於內存的浪費
  • 優勢:存儲容量較大包容性強,且成員之間不會相互影響

聯合體 聯合體也是由不一樣的數據類型組成,但其變量是互斥的,全部的成員共佔一段內存。並且共用體採用了內存覆蓋技術同一時刻只能保存一個成員的值,若是對新的成員賦值,就會將原來成員的值覆蓋掉

  • 缺點:,包容性弱
  • 優勢:全部成員共用一段內存,使內存的使用更爲精細靈活,同時也節省了內存空間

二者的區別

內存佔用狀況

結構體的各個成員會佔用不一樣的內存,互相之間沒有影響 共用體的全部成員佔用同一段內存,修改一個成員會影響其他全部成員 內存分配大小

結構體內存 >= 全部成員佔用的內存總和(成員之間可能會有縫隙) 共用體佔用的內存等於最大的成員佔用的內存

isa_t的成員:bits、cls、ISA_BITFIELD

若是有bits沒有值,就經過cls完成初始化;若是有bits有值,則經過bits和ISA_BITFIELD位域完成初始化 這裏使用bits 結合 位域ISA_BITFIELD 的目的就是爲了節約內存。咱們知道,一個指針佔8個字節,就是64個二進制位,64位能存儲多少信息呢?答案是2的64次方,而咱們要存儲那些信息呢?咱們要存儲的信息都在位域ISA_BITFIELD裏:

# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \ uintptr_t nonpointer : 1; \ uintptr_t has_assoc : 1; \ uintptr_t has_cxx_dtor : 1; \ 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; \ uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)

# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_BITFIELD \ uintptr_t nonpointer : 1; \ uintptr_t has_assoc : 1; \ uintptr_t has_cxx_dtor : 1; \ uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \ uintptr_t magic : 6; \ uintptr_t weakly_referenced : 1; \ uintptr_t deallocating : 1; \ uintptr_t has_sidetable_rc : 1; \ uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
複製代碼

這裏有兩個架構版本:arm64和X86_64。iOS使用的是arm64,macos使用的是X86_64,因此咱們只看arm64。 我來解釋一下,isa_t中,存儲的幾個字段的意思

  • nonpointer 佔 1 bit位。表示是否對 isa 指針開啓指針優化。
    • 0:純isa指針,
    • 1:不⽌是類對象地址,isa 中包含了類信息、對象的引⽤計數等
  • has_assoc 佔 1 bit位。關聯對象標誌位。
    • 0沒有
    • 1存在
  • has_cxx_dtor 佔 1 bit位。該對象是否有 C++ 或者 Objc 的析構器。
    • 若是有析構函數,則須要作析構邏輯,
    • 若是沒有,則能夠更快的釋放對象
  • shiftcls 佔 33 bit位。 存儲類指針的值。(重點) 
  • magic 佔 6 bit位。⽤於調試器判斷當前對象是真的對象仍是沒有初始化的空間
  • weakly_referenced 佔 1 bit位。對象是否被指向或者曾經指向⼀個 ARC 的弱變量,沒有弱引⽤的對象能夠更快釋放
  • deallocating 佔 1 bit位。標誌對象是否正在釋放內存
  • has_sidetable_rc 佔 1 bit位。當對象引⽤技術⼤於 10 時,則須要借⽤該變量存儲進位
  • extra_rc 佔 19 bit位。當表示該對象的引⽤計數值,其實是引⽤計數值減1。
    • 例如,若是對象的引⽤計數爲 10,那麼 extra_rc 爲 9。
    • 若是引⽤計數⼤於 10,則須要使⽤到下⾯的has_sidetable_rc。

若是這些不用位域,最保守每一個字段用short int,那也不得了,一個short int就佔了2個byte,也就是16bit,如今有10個字段,這就是須要160bit位!。而如今,咱們只須要64bit。

前面的分析,咱們知道在初始化isa的時候,咱們將對象的類指針放到了isa_t的shiftcls裏,咱們在獲取class時使用的isa.bits & ISA_MASK。 ** 在arm64下的isa.bits image.png

對象的isa

咱們使用object_getClass,獲取對象的isa image.png 最終進入到下面這個方法 將ISA_MASK:0x0000000ffffffff8ULL 轉成二進制: image.png 這就是一個簡單的C語言的位預算了:過濾爲1的位。$2(ISA_MASK)64位,爲1的剛好是shiftcls的範圍,因此,isa.bits & ISA_MASK 這個位運算,目的就是爲了得到shiftcls範圍的內容,也就是咱們當初initIsa時放入shiftcls裏的值——對象的類指針! 咱們打印看一下 image.png LYPerson的實例化對象obj 調用class方法,得到的是obj的類「LYPerson」,

咱們這裏用的是LYPerson的實例化對象,獲取的是實例對象obj的isa,那咱們若是要獲取LYPerson這個類的isa,咱們又會獲得什麼?

類的isa

image.png 咱們來看一下LYPerson類的isa指向的是誰 image.png 第一個是對象obj的isa,指向了LYPerson,第二個是類LYPerson的isa也指向了LYPerson,可是內存地址不同,說明這兩個LYPerson不是一個。 若是咱們在這樣繼續下去:不斷的查看isa指向 image.png image.png isa指向關係: obj -> LYPerson -> LYPerson -> NSObject(指向本身)

是否是一個很是經典的圖就出來了: isa流程圖.png

虛線 就是 isa 指向,從類的開始,後面都是前一個的 元類 ,直到NSObject,由於它是OC裏的根元類。 實線 是咱們熟知的類繼承,也就是 objc_class 裏的那個 super_class  存儲的類指針。

到了這裏,isa是幹嗎的,想必你們也已經清楚: isa說白了,就是實例某個對象(在runtime源碼裏類也是對象)的類  isa的順序也比較固定: 對象 -> 類 -> 元類 -> 根元類(NSObject) -> 根根元類(NSObject)

相關文章
相關標籤/搜索