原文連接git
有必定經驗的iOS開發者,或多或少的都聽過Runtime。Runtime,也就是運行時,是Objective-C語言的特性之一。平常開發中,可能直接和Runtime打交道的機會很少。然而,"發消息"、"消息轉發"這些名詞開發者應該常常聽到,這些名詞所用到的技術基礎就是Runtime。瞭解Runtime,有助於開發者深刻理解Objective-C這門語言。github
在具體瞭解Runtime以前,先提一個問題,什麼是動態語言?安全
使用Objective-C作iOS開發的同窗必定都據說過一句話:Objective-C是一門動態語言。動態語言,確定是和靜態語言相對應的。那麼,靜態語言有哪些特性,動態語言又有哪些特性?bash
回顧一下大學時期,學的第一門語言C語言,學習C語言的過程當中歷來沒據說過運行時,也沒據說過什麼靜態語言,動態語言。所以咱們有理由相信,C語言是一門靜態語言。數據結構
事實上也確實如此,C語言是一門靜態語言,Objective-C是一門動態語言。然而,仍是說不出靜態語言和動態語言到底有什麼區別……架構
靜態語言,能夠理解成在編譯期間就肯定一切的語言。以C語言來舉例,C語言編譯後會成爲一個可執行文件。假設咱們在C代碼中寫了一個hello函數,而且在主程序中調用了這個hello函數。假若在編譯期間,hello函數的入口地址相對於主程序入口地址的偏移量是0x0000abcdef(不要在乎這個值,只是用來舉例),那麼在執行該程序時,執行到hello函數時,必定執行的是相對主程序入口地址偏移量爲0x0000abcdef的代碼塊。也就是說,靜態語言,在編譯期間就已經肯定一切,運行期間只是遵照編譯期肯定的指令在執行。app
做爲對比,再看一下動態語言,以常常用到的Objective-C爲例。假設在Objective-C中寫了hello方法,而且在主程序中調用了hello方法,也就是發送hello消息。在編譯期間,只能肯定要向某個對象發送hello消息,可是具體執行哪一個內存塊的代碼是不肯定的,具體執行的代碼須要在運行期間才能肯定。ide
到這裏,靜態語言和動態語言的區別已經很明顯了。靜態語言在編譯期間就已經肯定一切,而動態語言編譯期間只能肯定一部分,還有一部分須要在運行期間才能肯定。也就是說,動態語言成爲一個可執行程序並可以正確的執行,除了須要一個編譯器外,還須要一套運行時系統,用於肯定到底執行哪一塊代碼。Objective-C中的運行時系統內就是Runtime。函數
Runtime源碼是一套用C語言實現的API,整套代碼是開源的,能夠從蘋果開源網站上下載Runtime源碼。默認下載的Runtime源碼是不能編譯的,經過修改配置和導入必要的頭文件,能夠編譯成功Runtime源碼。我在github上放了編譯成功的Runtime源碼,且有我在看Runtime源碼時的一些註釋,本篇文章中的代碼也是基於此Runtime源碼。學習
因爲Runtime源碼代碼量比較大,一篇文章介紹完Runtime源碼是不可能的。所以這篇文章主要介紹Runtime中的isa結構體,做爲Runtime的入門。
有經驗的iOS開發者可能都聽過一句話:在Objective-C語言中,類也是對象,且每一個對象都包含一個isa指針,isa指針指向該對象所屬的類。不過如今Runtime中的對象定義已經不是這樣了,如今使用的是isa_t類型的結構體。每個對象都有一個isa_t類型的結構體isa。以前的isa指針做用是指向該對象的類,那麼isa結構體做爲isa指針的替代者,是如何完成這個功能的呢?
在解決這個問題以前,咱們先來看一下Runtime源碼中對象和類的定義。
看一下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具體是什麼,後續再介紹。
Objective-C語言中最主要的就是對象和類,看完了對象在Runtime中的定義,再看一下類在Runtime中的定義。
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找到所屬的類,而後在類的結構中找到對應方法的實現(關於在類結構中如何找到方法的實現,後續的文章再介紹)。
咱們知道,Objective-C中區分類方法和實例方法。實例方法是如何找到的咱們瞭解了,那麼類方法是如何找到的呢?類結構體中也有isa,類對象的isa指向哪裏呢?
爲了解決類方法調用,Objective-C引入了元類(metaClass),類對象的isa指向該類的元類,一個類對象對應一個元類對象。
元類對象也是類對象,既然是類對象,那麼元類對象中也有isa,那麼元類的isa又指向哪裏呢?總不能指向元元類吧……這樣是無窮無盡的。
Objective-C語言的設計者已經考慮到了這個問題,全部元類的isa都指向一個元類對象,該元類對象就是 meta Root Class,能夠理解成根元類。關於實例對象、類、元類之間的關係,蘋果官方給了一張圖,很是清晰的代表了三者的關係,以下
瞭解了isa的做用,如今來看一下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架構下有區別,可是不影響咱們理解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是聯合體,也就是說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結構體初始化定義在objc_object結構體中,看一下官方提供的函數和註釋:
// 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,協議對象初始化isa,實例對象初始化isa,其餘對象初始化isa,分別對應不一樣的函數。
看下每一個函數的實現:
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);
}
複製代碼
能夠看到,不管是類對象,實例對象,協議對象,仍是其餘對象,初始化isa結構體最終都調用了
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
複製代碼
函數,只是所傳的參數不一樣而已。
最終調用的initIsa函數的代碼,通過簡化後以下:
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位
// 根據註釋,使用ISA_MAGIC_VALUE賦值,實際上只是賦值了isa.magic和isa.nonpointer
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
// 將當前對象的類指針賦值到shiftcls
// 類的指針是按照字節(8bits)對齊的,其指針後三位都是沒有意義的0,所以能夠右移3位
newisa.shiftcls = (uintptr_t)cls >> 3;
// 賦值。看註釋這個地方不是線程安全的??
isa = newisa;
}
}
複製代碼
初始化實例對象的isa時,傳入的nonpointer參數是true,因此直接走了else分之。在else分之中,對isa的bits分之賦值ISA_MAGIC_VALUE。根據註釋,這樣代碼實際上只是對isa中的magic和nonpointer進行了賦值,來看一下爲何。
ISA_MAGIC_VALUE的值是0x001d800000000001ULL,轉化成二進制就是0000 0000 0001 1101 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001,將每一位對應到isa內部的結構體中,看一下對哪些變量產生了影響:
能夠看到將nonpointer賦值爲1;將magci賦值爲110111;其餘的仍然都是0。因此說只賦值了isa.magci和isa.nonpointer。
在文章開頭也提到了,在Objective-C語言中,類也是對象,且每一個對象都包含一個isa指針,如今改成了isa結構體。nonpointer做用就是區分這二者。
magic的值調試器會用到,調試器根據magci的值判斷當前對象已經初始過了,仍是還沒有初始化的空間。
接下來就是對has_cxx_dtor進行賦值。has_cxx_dtor表示當前對象是否有C++的析構函數(destructor),若是沒有,釋放時會快速的釋放內存。
在函數
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
複製代碼
中,參數cls就是類的指針。而
newisa.shiftcls = (uintptr_t)cls >> 3;
複製代碼
shiftcls存儲的究竟是什麼呢?
實際上,shiftcls存儲的就是當前對象類的指針。之因此右移三位是出於節省空間上的考慮。
在Objective-C中,類的指針是按照字節(8 bits)對齊的,也就是說類指針地址轉化成十進制後,都是8的倍數,也就是說,類指針地址轉化成二進制後,後三位都是0。既然是沒有意義的0,那麼在存儲時就能夠省略,用節省下來的空間存儲一些其餘信息。
在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)
複製代碼
函數,類初始化時會調用該函數。能夠在該函數中打印類對象的地址
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,說明類對象確實是按照字節對齊,後三位都是0。所以在賦值shiftcls時,右移三位是安全的,不會丟失類指針信息。
咱們能夠寫代碼驗證一下對象的isa和類對象指針的關係。代碼以下:
#import <Foundation/Foundation.h>
#import "objc-runtime.h"
// 把一個十進制的數轉爲二進制
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中的確包含了類對象的指針。
上面已經介紹了nonpointer、magic、shiftcls、has_cxx_dtor,還有一些其餘位沒有介紹,這裏簡單瞭解一下。
extra_rc和has_sidetable_c能夠一塊兒理解。extra_rc用於存放引用計數的個數,extra_rc佔8位,也就是最大表示255,當對象的引用計數個數超過257時,has_sidetable_rc的值應該爲1。
至此,isa結構體的介紹就完了。須要提醒的是,上面的代碼是運行在macOS上,也就是x86_64架構上的,isa結構體也是基於x86_64架構的。在arm64架構上,isa結構體中變量所佔用的位數和x86_64架構是不同的,可是表示的含義是同樣的。理解了x86_64架構下的isa結構體,相信對於理解arm架構下的isa結構體,應該不是什麼難事。