有一定經(jīng)驗的iOS開發(fā)者,或多或少的都聽過Runtime。Runtime,也就是運行時,是Objective-C語言的特性之一。日常開發(fā)中,可能直接和Runtime打交道的機會不多。然而,"發(fā)消息"、"消息轉發(fā)"這些名詞開發(fā)者應該經(jīng)常聽到,這些名詞所用到的技術基礎就是Runtime。了解Runtime,有助于開發(fā)者深入理解Objective-C這門語言。
在具體了解Runtime之前,先提一個問題,什么是動態(tài)語言?
Objective-C是一門動態(tài)語言
使用Objective-C做iOS開發(fā)的同學一定都聽說過一句話:Objective-C是一門動態(tài)語言。動態(tài)語言,肯定是和靜態(tài)語言相對應的。那么,靜態(tài)語言有哪些特性,動態(tài)語言又有哪些特性?
回顧一下大學時期,學的第一門語言C語言,學習C語言的過程中從來沒聽說過運行時,也沒聽說過什么靜態(tài)語言,動態(tài)語言。因此我們有理由相信,C語言是一門靜態(tài)語言。
事實上也確實如此,C語言是一門靜態(tài)語言,Objective-C是一門動態(tài)語言。然而,還是說不出靜態(tài)語言和動態(tài)語言到底有什么區(qū)別……
靜態(tài)語言和動態(tài)語言
靜態(tài)語言,可以理解成在編譯期間就確定一切的語言。以C語言來舉例,C語言編譯后會成為一個可執(zhí)行文件。假設我們在C代碼中寫了一個hello函數(shù),并且在主程序中調用了這個hello函數(shù)。倘若在編譯期間,hello函數(shù)的入口地址相對于主程序入口地址的偏移量是0x0000abcdef(不要在意這個值,只是用來舉例),那么在執(zhí)行該程序時,執(zhí)行到hello函數(shù)時,一定執(zhí)行的是相對主程序入口地址偏移量為0x0000abcdef的代碼塊。也就是說,靜態(tài)語言,在編譯期間就已經(jīng)確定一切,運行期間只是遵守編譯期確定的指令在執(zhí)行。
作為對比,再看一下動態(tài)語言,以經(jīng)常用到的Objective-C為例。假設在Objective-C中寫了hello方法,并且在主程序中調用了hello方法,也就是發(fā)送hello消息。在編譯期間,只能確定要向某個對象發(fā)送hello消息,但是具體執(zhí)行哪個內存塊的代碼是不確定的,具體執(zhí)行的代碼需要在運行期間才能確定。
到這里,靜態(tài)語言和動態(tài)語言的區(qū)別已經(jīng)很明顯了。靜態(tài)語言在編譯期間就已經(jīng)確定一切,而動態(tài)語言編譯期間只能確定一部分,還有一部分需要在運行期間才能確定。也就是說,動態(tài)語言成為一個可執(zhí)行程序并能夠正確的執(zhí)行,除了需要一個編譯器外,還需要一套運行時系統(tǒng),用于確定到底執(zhí)行哪一塊代碼。Objective-C中的運行時系統(tǒng)內就是Runtime。
Runtime源碼
Runtime源碼是一套用C語言實現(xiàn)的API,整套代碼是開源的,可以從蘋果開源網(wǎng)站上下載Runtime源碼。默認下載的Runtime源碼是不能編譯的,通過修改配置和導入必要的頭文件,可以編譯成功Runtime源碼。我在github上放了編譯成功的Runtime源碼,且有我在看Runtime源碼時的一些注釋,本篇文章中的代碼也是基于此Runtime源碼。
由于Runtime源碼代碼量比較大,一篇文章介紹完Runtime源碼是不可能的。因此這篇文章主要介紹Runtime中的isa結構體,作為Runtime的入門。
isa結構體
有經(jīng)驗的iOS開發(fā)者可能都聽過一句話:在Objective-C語言中,類也是對象,且每個對象都包含一個isa指針,isa指針指向該對象所屬的類。不過現(xiàn)在Runtime中的對象定義已經(jīng)不是這樣了,現(xiàn)在使用的是isa_t類型的結構體。每一個對象都有一個isa_t類型的結構體isa。之前的isa指針作用是指向該對象的類,那么isa結構體作為isa指針的替代者,是如何完成這個功能的呢?
在解決這個問題之前,我們先來看一下Runtime源碼中對象和類的定義。
objc_object
看一下Runtime中對id類型的定義
typedef struct objc_object *id;
這里的id也就是Objective-C中的id類型,代表任意對象,類似于C語言中的 void 。可以看到,id實際上是一個指向結構體objc_object的指針。
再來看一下objc_object的定義,該定義位于objc-private.h文件中:
struct objc_object {
// isa結構體
private:
isa_t isa;
}
結構體中還包含一些public的方法。可以看到,對象結構體(objc_object)中的第一個變量就是isa_t 類型的isa。關于isa_t具體是什么,后續(xù)再介紹。
Objective-C語言中最主要的就是對象和類,看完了對象在Runtime中的定義,再看一下類在Runtime中的定義。
objc_class
Runtime中對于Class的定義
typedef struct objc_class *Class;
Class實際上是一個指向objc_class結構體的指針。
看一下結構體objc_class的定義,objc_class的定義位于objc-runtime-new.h文件中
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_class是繼承于objc_object的,因此objc_class中也包含isa_t類型的isa。objc_class的定義可以理解成下面這樣:
struct objc_class {
isa_t isa;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
isa的作用
上面也提到了,isa能夠使該對象找到自己所屬的類。為什么對象需要知道自己所屬的類呢?這主要是因為對象的方法是存儲在該對象所屬的類中的。
這一點是很容易理解的,一個類可以有多個對象,倘若每個對象都含有自己能夠執(zhí)行的方法,那對于內存來說是災難級的。
在向對象發(fā)送消息,也就是實例方法被調用時,對象通過自己的isa找到所屬的類,然后在類的結構中找到對應方法的實現(xiàn)(關于在類結構中如何找到方法的實現(xiàn),后續(xù)的文章再介紹)。
我們知道,Objective-C中區(qū)分類方法和實例方法。實例方法是如何找到的我們了解了,那么類方法是如何找到的呢?類結構體中也有isa,類對象的isa指向哪里呢?
元類(metaClass)
為了解決類方法調用,Objective-C引入了元類(metaClass),類對象的isa指向該類的元類,一個類對象對應一個元類對象。
元類對象也是類對象,既然是類對象,那么元類對象中也有isa,那么元類的isa又指向哪里呢?總不能指向元元類吧……這樣是無窮無盡的。
Objective-C語言的設計者已經(jīng)考慮到了這個問題,所有元類的isa都指向一個元類對象,該元類對象就是 meta Root Class,可以理解成根元類。關于實例對象、類、元類之間的關系,蘋果官方給了一張圖,非常清晰的表明了三者的關系,如下
isa結構體定義
了解了isa的作用,現(xiàn)在來看一下isa的定義。isa是isa_t類型,isa_t也是一個結構體,其定義在objc-private.h中:
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
// 相當于是unsigned long bits;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
ISA_BITFIELD的定義在 isa.h文件中:
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
注意:這里的代碼都是x86_64架構下的,arm64架構下和x86_64架構下有區(qū)別,但是不影響我們理解isa_t結構體。
將isa_t結構體中的ISA_BITFIELD使用isa.h文件中的ISA_BITFIELD替換,isa_t的定義可以表示如下:
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
// 相當于是unsigned long bits;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8;
};
#endif
};
注意isa_t是聯(lián)合體,也就是說isa_t中的變量,cls、bits和內部的結構體全都位于同一塊地址空間。
本篇文章主要分析下isa_t中內部結構體中各個變量的作用
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8;
};
該結構體共占64位,其內存分布如下:
在了解內個結構體各個變量的作用前,先通過Runtime代碼看一下isa結構體是如何初始化的。
isa結構體初始化
isa結構體初始化定義在objc_object結構體中,看一下官方提供的函數(shù)和注釋:
// initIsa() should be used to init the isa of new objects only.
// If this object already has an isa, use changeIsa() for correctness.
// initInstanceIsa(): objects with no custom RR/AWZ
// initClassIsa(): class objects
// initProtocolIsa(): protocol objects
// initIsa(): other objects
void initIsa(Class cls /*nonpointer=false*/);
void initClassIsa(Class cls /*nonpointer=maybe*/);
void initProtocolIsa(Class cls /*nonpointer=maybe*/);
void initInstanceIsa(Class cls, bool hasCxxDtor);
官方提供的有類對象初始化isa,協(xié)議對象初始化isa,實例對象初始化isa,其他對象初始化isa,分別對應不同的函數(shù)。
看下每個函數(shù)的實現(xiàn):
inline void objc_object::initIsa(Class cls)
{
initIsa(cls, false, false);
}
inline void objc_object::initClassIsa(Class cls)
{
if (DisableNonpointerIsa || cls->instancesRequireRawIsa()) {
initIsa(cls, false/*not nonpointer*/, false);
} else {
initIsa(cls, true/*nonpointer*/, false);
}
}
inline void objc_object::initProtocolIsa(Class cls)
{
return initClassIsa(cls);
}
inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
assert(!cls->instancesRequireRawIsa());
assert(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
可以看到,無論是類對象,實例對象,協(xié)議對象,還是其他對象,初始化isa結構體最終都調用了
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
函數(shù),只是所傳的參數(shù)不同而已。
最終調用的initIsa函數(shù)的代碼,經(jīng)過簡化后如下:
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
if (!nonpointer) {
isa.cls = cls;
} else {
// 實例對象的isa初始化直接走else分之
// 初始化一個心得isa_t結構體
isa_t newisa(0);
// 對新結構體newisa賦值
// ISA_MAGIC_VALUE的值是0x001d800000000001ULL,轉化成二進制是64位
// 根據(jù)注釋,使用ISA_MAGIC_VALUE賦值,實際上只是賦值了isa.magic和isa.nonpointer
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
// 將當前對象的類指針賦值到shiftcls
// 類的指針是按照字節(jié)(8bits)對齊的,其指針后三位都是沒有意義的0,因此可以右移3位
newisa.shiftcls = (uintptr_t)cls >> 3;
// 賦值??醋⑨屵@個地方不是線程安全的??
isa = newisa;
}
}
初始化實例對象的isa時,傳入的nonpointer參數(shù)是true,所以直接走了else分之。在else分之中,對isa的bits分之賦值ISA_MAGIC_VALUE。根據(jù)注釋,這樣代碼實際上只是對isa中的magic和nonpointer進行了賦值,來看一下為什么。
ISA_MAGIC_VALUE的值是0x001d800000000001ULL,轉化成二進制就是0000 0000 0001 1101 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001,將每一位對應到isa內部的結構體中,看一下對哪些變量產(chǎn)生了影響:
可以看到將nonpointer賦值為1;將magci賦值為110111;其他的仍然都是0。所以說只賦值了isa.magci和isa.nonpointer。
nonpointer
在文章開頭也提到了,在Objective-C語言中,類也是對象,且每個對象都包含一個isa指針,現(xiàn)在改為了isa結構體。nonpointer作用就是區(qū)分這兩者。
- 如果nonpointer為1,代表不是isa指針,而是isa結構體。雖然不是isa指針,但是通過isa結構體仍然能獲得類指針(下面會分析)。
- 如果nonpointer為0,代表當前是isa指針,訪問對象的isa會直接返回類指針。
magic
magic的值調試器會用到,調試器根據(jù)magci的值判斷當前對象已經(jīng)初始過了,還是尚未初始化的空間。
has_cxx_dtor
接下來就是對has_cxx_dtor進行賦值。has_cxx_dtor表示當前對象是否有C++的析構函數(shù)(destructor),如果沒有,釋放時會快速的釋放內存。
shiftcls
在函數(shù)
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
中,參數(shù)cls就是類的指針。而
newisa.shiftcls = (uintptr_t)cls >> 3;
shiftcls存儲的到底是什么呢?
實際上,shiftcls存儲的就是當前對象類的指針。之所以右移三位是出于節(jié)省空間上的考慮。
在Objective-C中,類的指針是按照字節(jié)(8 bits)對齊的,也就是說類指針地址轉化成十進制后,都是8的倍數(shù),也就是說,類指針地址轉化成二進制后,后三位都是0。既然是沒有意義的0,那么在存儲時就可以省略,用節(jié)省下來的空間存儲一些其他信息。
在objc-runtime-new.mm文件的
static __attribute__((always_inline)) id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
函數(shù),類初始化時會調用該函數(shù)??梢栽谠摵瘮?shù)中打印類對象的地址
if (!cls) return nil;
// 這里可以打印類指針的地址,類指針地址最后一位是十六進制的8或者0,說明
// 類指針地址后三位都是0
printf("cls address = %p\n",cls);
打印出的部分信息如下:
cls address = 0x7fff83bca218
cls address = 0x7fff83bcab28
cls address = 0x7fff83bc5290
cls address = 0x7fff83717f58
cls address = 0x7fff83717f58
cls address = 0x100b15140
cls address = 0x7fff83717fa8
cls address = 0x7fff837164c8
cls address = 0x7fff837164c8
cls address = 0x7fff83716e78
cls address = 0x100b15140
cls address = 0x7fff837175a8
cls address = 0x7fff837175a8
cls address = 0x7fff83717fa8
可以看到類對象的地址最后一位都是8或者0,說明類對象確實是按照字節(jié)對齊,后三位都是0。因此在賦值shiftcls時,右移三位是安全的,不會丟失類指針信息。
我們可以寫代碼驗證一下對象的isa和類對象指針的關系。代碼如下:
#import <Foundation/Foundation.h>
#import "objc-runtime.h"
// 把一個十進制的數(shù)轉為二進制
NSString * binaryWithInteger(NSUInteger decInt){
NSString *string = @"";
NSUInteger x = decInt;
while(x > 0){
string = [[NSString stringWithFormat:@"%lu",x&1] stringByAppendingString:string];
x = x >> 1;
}
return string;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 把對象轉為objc_object結構體
struct objc_object *object = (__bridge struct objc_object *)([NSObject new]);
NSLog(@"binary = %@",binaryWithInteger(object->isa));
// uintptr_t實際上就是unsigned long
NSLog(@"binary = %@",binaryWithInteger((uintptr_t)[NSObject class]));
}
return 0;
}
打印出isa的內容是:1011101100000000000000100000000101100010101000101000001,NSObject類對象的指針是:100000000101100010101000101000000。首先將isa的內容補充至64位
0000 0101 1101 1000 0000 0000 0001 0000 0000 1011 0001 0101 0001 0100 0001
取第4位到第47位之間的內容,也就是shiftcls的值:
000 0000 0000 0001 0000 0000 1011 0001 0101 0001 0100 0
將類對象的指針右移三位,即去除后三位的0,得到
100000000101100010101000101000
和上面的shiftcls對比:
10 0000 0001 0110 0010 1010 0010 1000
0000 0000 0000 0010 0000 0001 0110 0010 1010 0010 1000
可以確認:shiftcls中的確包含了類對象的指針。
其他位
上面已經(jīng)介紹了nonpointer、magic、shiftcls、has_cxx_dtor,還有一些其他位沒有介紹,這里簡單了解一下。
- has_assoc: 表示對象是否含有關聯(lián)引用(associatedObject)
- weakly_referenced: 表示對象是否含有弱引用對象
- deallocating: 表示對象是否正在釋放
- has_sidetable_rc: 表示對象的引用計數(shù)是否太大,如果太大,則需要用其他的數(shù)據(jù)結構來存
- extra_rc:對象的引用計數(shù)大于1,則會將引用計數(shù)的個數(shù)存到extra_rc里面。比如對象的引用計數(shù)為5,則extra_rc的值為4。
extra_rc和has_sidetable_c可以一起理解。extra_rc用于存放引用計數(shù)的個數(shù),extra_rc占8位,也就是最大表示255,當對象的引用計數(shù)個數(shù)超過257時,has_sidetable_rc的值應該為1。
總結
至此,isa結構體的介紹就完了。需要提醒的是,上面的代碼是運行在macOS上,也就是x86_64架構上的,isa結構體也是基于x86_64架構的。在arm64架構上,isa結構體中變量所占用的位數(shù)和x86_64架構是不一樣的,但是表示的含義是一樣的。理解了x86_64架構下的isa結構體,相信對于理解arm架構下的isa結構體,應該不是什么難事。