有一定經(jīng)驗(yàn)的iOS開發(fā)者髓绽,或多或少的都聽過Runtime箩言。Runtime雕欺,也就是運(yùn)行時(shí),是Objective-C語言的特性之一索烹。日常開發(fā)中,可能直接和Runtime打交道的機(jī)會(huì)不多弱睦。然而百姓,"發(fā)消息"、"消息轉(zhuǎn)發(fā)"這些名詞開發(fā)者應(yīng)該經(jīng)常聽到况木,這些名詞所用到的技術(shù)基礎(chǔ)就是Runtime垒拢。了解Runtime旬迹,有助于開發(fā)者深入理解Objective-C這門語言。
在具體了解Runtime之前求类,先提一個(gè)問題奔垦,什么是動(dòng)態(tài)語言?
Objective-C是一門動(dòng)態(tài)語言
使用Objective-C做iOS開發(fā)的同學(xué)一定都聽說過一句話:Objective-C是一門動(dòng)態(tài)語言尸疆。動(dòng)態(tài)語言椿猎,肯定是和靜態(tài)語言相對(duì)應(yīng)的。那么寿弱,靜態(tài)語言有哪些特性鸵贬,動(dòng)態(tài)語言又有哪些特性?
回顧一下大學(xué)時(shí)期脖捻,學(xué)的第一門語言C語言阔逼,學(xué)習(xí)C語言的過程中從來沒聽說過運(yùn)行時(shí),也沒聽說過什么靜態(tài)語言地沮,動(dòng)態(tài)語言嗜浮。因此我們有理由相信,C語言是一門靜態(tài)語言摩疑。
事實(shí)上也確實(shí)如此危融,C語言是一門靜態(tài)語言,Objective-C是一門動(dòng)態(tài)語言雷袋。然而吉殃,還是說不出靜態(tài)語言和動(dòng)態(tài)語言到底有什么區(qū)別……
靜態(tài)語言和動(dòng)態(tài)語言
靜態(tài)語言,可以理解成在編譯期間就確定一切的語言楷怒。以C語言來舉例蛋勺,C語言編譯后會(huì)成為一個(gè)可執(zhí)行文件。假設(shè)我們在C代碼中寫了一個(gè)hello函數(shù)鸠删,并且在主程序中調(diào)用了這個(gè)hello函數(shù)抱完。倘若在編譯期間,hello函數(shù)的入口地址相對(duì)于主程序入口地址的偏移量是0x0000abcdef(不要在意這個(gè)值刃泡,只是用來舉例)巧娱,那么在執(zhí)行該程序時(shí),執(zhí)行到hello函數(shù)時(shí)烘贴,一定執(zhí)行的是相對(duì)主程序入口地址偏移量為0x0000abcdef的代碼塊禁添。也就是說,靜態(tài)語言桨踪,在編譯期間就已經(jīng)確定一切老翘,運(yùn)行期間只是遵守編譯期確定的指令在執(zhí)行。
作為對(duì)比,再看一下動(dòng)態(tài)語言酪捡,以經(jīng)常用到的Objective-C為例叁征。假設(shè)在Objective-C中寫了hello方法,并且在主程序中調(diào)用了hello方法逛薇,也就是發(fā)送hello消息捺疼。在編譯期間,只能確定要向某個(gè)對(duì)象發(fā)送hello消息永罚,但是具體執(zhí)行哪個(gè)內(nèi)存塊的代碼是不確定的啤呼,具體執(zhí)行的代碼需要在運(yùn)行期間才能確定。
到這里呢袱,靜態(tài)語言和動(dòng)態(tài)語言的區(qū)別已經(jīng)很明顯了官扣。靜態(tài)語言在編譯期間就已經(jīng)確定一切,而動(dòng)態(tài)語言編譯期間只能確定一部分羞福,還有一部分需要在運(yùn)行期間才能確定惕蹄。也就是說,動(dòng)態(tài)語言成為一個(gè)可執(zhí)行程序并能夠正確的執(zhí)行治专,除了需要一個(gè)編譯器外卖陵,還需要一套運(yùn)行時(shí)系統(tǒng),用于確定到底執(zhí)行哪一塊代碼张峰。Objective-C中的運(yùn)行時(shí)系統(tǒng)內(nèi)就是Runtime泪蔫。
Runtime源碼
Runtime源碼是一套用C語言實(shí)現(xiàn)的API,整套代碼是開源的喘批,可以從蘋果開源網(wǎng)站上下載Runtime源碼撩荣。默認(rèn)下載的Runtime源碼是不能編譯的,通過修改配置和導(dǎo)入必要的頭文件饶深,可以編譯成功Runtime源碼餐曹。我在github上放了編譯成功的Runtime源碼,且有我在看Runtime源碼時(shí)的一些注釋粥喜,本篇文章中的代碼也是基于此Runtime源碼凸主。
由于Runtime源碼代碼量比較大,一篇文章介紹完Runtime源碼是不可能的额湘。因此這篇文章主要介紹Runtime中的isa結(jié)構(gòu)體,作為Runtime的入門旁舰。
isa結(jié)構(gòu)體
有經(jīng)驗(yàn)的iOS開發(fā)者可能都聽過一句話:在Objective-C語言中锋华,類也是對(duì)象,且每個(gè)對(duì)象都包含一個(gè)isa指針箭窜,isa指針指向該對(duì)象所屬的類毯焕。不過現(xiàn)在Runtime中的對(duì)象定義已經(jīng)不是這樣了,現(xiàn)在使用的是isa_t類型的結(jié)構(gòu)體。每一個(gè)對(duì)象都有一個(gè)isa_t類型的結(jié)構(gòu)體isa纳猫。之前的isa指針作用是指向該對(duì)象的類婆咸,那么isa結(jié)構(gòu)體作為isa指針的替代者,是如何完成這個(gè)功能的呢芜辕?
在解決這個(gè)問題之前尚骄,我們先來看一下Runtime源碼中對(duì)象和類的定義。
objc_object
看一下Runtime中對(duì)id類型的定義
typedef struct objc_object *id;
這里的id也就是Objective-C中的id類型侵续,代表任意對(duì)象倔丈,類似于C語言中的 void ∽次希可以看到需五,id實(shí)際上是一個(gè)指向結(jié)構(gòu)體objc_object的指針。
再來看一下objc_object的定義轧坎,該定義位于objc-private.h文件中:
struct objc_object {
// isa結(jié)構(gòu)體
private:
isa_t isa;
}
結(jié)構(gòu)體中還包含一些public的方法宏邮。可以看到缸血,對(duì)象結(jié)構(gòu)體(objc_object)中的第一個(gè)變量就是isa_t 類型的isa蜜氨。關(guān)于isa_t具體是什么,后續(xù)再介紹属百。
Objective-C語言中最主要的就是對(duì)象和類记劝,看完了對(duì)象在Runtime中的定義,再看一下類在Runtime中的定義族扰。
objc_class
Runtime中對(duì)于Class的定義
typedef struct objc_class *Class;
Class實(shí)際上是一個(gè)指向objc_class結(jié)構(gòu)體的指針厌丑。
看一下結(jié)構(gòu)體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
}
結(jié)構(gòu)體中還包含一些方法渔呵。
注意怒竿,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能夠使該對(duì)象找到自己所屬的類。為什么對(duì)象需要知道自己所屬的類呢录豺?這主要是因?yàn)閷?duì)象的方法是存儲(chǔ)在該對(duì)象所屬的類中的朦肘。
這一點(diǎn)是很容易理解的,一個(gè)類可以有多個(gè)對(duì)象双饥,倘若每個(gè)對(duì)象都含有自己能夠執(zhí)行的方法媒抠,那對(duì)于內(nèi)存來說是災(zāi)難級(jí)的。
在向?qū)ο蟀l(fā)送消息咏花,也就是實(shí)例方法被調(diào)用時(shí)趴生,對(duì)象通過自己的isa找到所屬的類,然后在類的結(jié)構(gòu)中找到對(duì)應(yīng)方法的實(shí)現(xiàn)(關(guān)于在類結(jié)構(gòu)中如何找到方法的實(shí)現(xiàn),后續(xù)的文章再介紹)苍匆。
我們知道刘急,Objective-C中區(qū)分類方法和實(shí)例方法。實(shí)例方法是如何找到的我們了解了浸踩,那么類方法是如何找到的呢叔汁?類結(jié)構(gòu)體中也有isa,類對(duì)象的isa指向哪里呢民轴?
元類(metaClass)
為了解決類方法調(diào)用攻柠,Objective-C引入了元類(metaClass),類對(duì)象的isa指向該類的元類后裸,一個(gè)類對(duì)象對(duì)應(yīng)一個(gè)元類對(duì)象瑰钮。
元類對(duì)象也是類對(duì)象,既然是類對(duì)象微驶,那么元類對(duì)象中也有isa浪谴,那么元類的isa又指向哪里呢?總不能指向元元類吧……這樣是無窮無盡的因苹。
Objective-C語言的設(shè)計(jì)者已經(jīng)考慮到了這個(gè)問題苟耻,所有元類的isa都指向一個(gè)元類對(duì)象,該元類對(duì)象就是 meta Root Class,可以理解成根元類扶檐。關(guān)于實(shí)例對(duì)象凶杖、類、元類之間的關(guān)系款筑,蘋果官方給了一張圖智蝠,非常清晰的表明了三者的關(guān)系,如下
isa結(jié)構(gòu)體定義
了解了isa的作用奈梳,現(xiàn)在來看一下isa的定義杈湾。isa是isa_t類型,isa_t也是一個(gè)結(jié)構(gòu)體攘须,其定義在objc-private.h中:
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
// 相當(dāng)于是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架構(gòu)下的漆撞,arm64架構(gòu)下和x86_64架構(gòu)下有區(qū)別,但是不影響我們理解isa_t結(jié)構(gòu)體于宙。
將isa_t結(jié)構(gòu)體中的ISA_BITFIELD使用isa.h文件中的ISA_BITFIELD替換浮驳,isa_t的定義可以表示如下:
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
// 相當(dāng)于是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和內(nèi)部的結(jié)構(gòu)體全都位于同一塊地址空間。
本篇文章主要分析下isa_t中內(nèi)部結(jié)構(gòu)體中各個(gè)變量的作用
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;
};
該結(jié)構(gòu)體共占64位署驻,其內(nèi)存分布如下:
在了解內(nèi)個(gè)結(jié)構(gòu)體各個(gè)變量的作用前,先通過Runtime代碼看一下isa結(jié)構(gòu)體是如何初始化的。
isa結(jié)構(gòu)體初始化
isa結(jié)構(gòu)體初始化定義在objc_object結(jié)構(gòu)體中旺上,看一下官方提供的函數(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);
官方提供的有類對(duì)象初始化isa,協(xié)議對(duì)象初始化isa瓶蚂,實(shí)例對(duì)象初始化isa,其他對(duì)象初始化isa宣吱,分別對(duì)應(yīng)不同的函數(shù)窃这。
看下每個(gè)函數(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);
}
可以看到,無論是類對(duì)象征候,實(shí)例對(duì)象杭攻,協(xié)議對(duì)象,還是其他對(duì)象疤坝,初始化isa結(jié)構(gòu)體最終都調(diào)用了
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
函數(shù)兆解,只是所傳的參數(shù)不同而已。
最終調(diào)用的initIsa函數(shù)的代碼跑揉,經(jīng)過簡化后如下:
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
if (!nonpointer) {
isa.cls = cls;
} else {
// 實(shí)例對(duì)象的isa初始化直接走else分之
// 初始化一個(gè)心得isa_t結(jié)構(gòu)體
isa_t newisa(0);
// 對(duì)新結(jié)構(gòu)體newisa賦值
// ISA_MAGIC_VALUE的值是0x001d800000000001ULL锅睛,轉(zhuǎn)化成二進(jìn)制是64位
// 根據(jù)注釋,使用ISA_MAGIC_VALUE賦值历谍,實(shí)際上只是賦值了isa.magic和isa.nonpointer
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
// 將當(dāng)前對(duì)象的類指針賦值到shiftcls
// 類的指針是按照字節(jié)(8bits)對(duì)齊的现拒,其指針后三位都是沒有意義的0,因此可以右移3位
newisa.shiftcls = (uintptr_t)cls >> 3;
// 賦值望侈∮∈撸看注釋這個(gè)地方不是線程安全的?脱衙?
isa = newisa;
}
}
初始化實(shí)例對(duì)象的isa時(shí)侥猬,傳入的nonpointer參數(shù)是true,所以直接走了else分之岂丘。在else分之中陵究,對(duì)isa的bits分之賦值ISA_MAGIC_VALUE。根據(jù)注釋奥帘,這樣代碼實(shí)際上只是對(duì)isa中的magic和nonpointer進(jìn)行了賦值铜邮,來看一下為什么。
ISA_MAGIC_VALUE的值是0x001d800000000001ULL寨蹋,轉(zhuǎn)化成二進(jìn)制就是0000 0000 0001 1101 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001,將每一位對(duì)應(yīng)到isa內(nèi)部的結(jié)構(gòu)體中松蒜,看一下對(duì)哪些變量產(chǎn)生了影響:
可以看到將nonpointer賦值為1;將magci賦值為110111已旧;其他的仍然都是0秸苗。所以說只賦值了isa.magci和isa.nonpointer。
nonpointer
在文章開頭也提到了运褪,在Objective-C語言中惊楼,類也是對(duì)象玖瘸,且每個(gè)對(duì)象都包含一個(gè)isa指針,現(xiàn)在改為了isa結(jié)構(gòu)體檀咙。nonpointer作用就是區(qū)分這兩者雅倒。
- 如果nonpointer為1,代表不是isa指針弧可,而是isa結(jié)構(gòu)體蔑匣。雖然不是isa指針,但是通過isa結(jié)構(gòu)體仍然能獲得類指針(下面會(huì)分析)棕诵。
- 如果nonpointer為0裁良,代表當(dāng)前是isa指針,訪問對(duì)象的isa會(huì)直接返回類指針校套。
magic
magic的值調(diào)試器會(huì)用到价脾,調(diào)試器根據(jù)magci的值判斷當(dāng)前對(duì)象已經(jīng)初始過了,還是尚未初始化的空間搔确。
has_cxx_dtor
接下來就是對(duì)has_cxx_dtor進(jìn)行賦值彼棍。has_cxx_dtor表示當(dāng)前對(duì)象是否有C++的析構(gòu)函數(shù)(destructor),如果沒有,釋放時(shí)會(huì)快速的釋放內(nèi)存膳算。
shiftcls
在函數(shù)
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
中座硕,參數(shù)cls就是類的指針。而
newisa.shiftcls = (uintptr_t)cls >> 3;
shiftcls存儲(chǔ)的到底是什么呢涕蜂?
實(shí)際上华匾,shiftcls存儲(chǔ)的就是當(dāng)前對(duì)象類的指針。之所以右移三位是出于節(jié)省空間上的考慮机隙。
在Objective-C中蜘拉,類的指針是按照字節(jié)(8 bits)對(duì)齊的,也就是說類指針地址轉(zhuǎn)化成十進(jìn)制后有鹿,都是8的倍數(shù)旭旭,也就是說,類指針地址轉(zhuǎn)化成二進(jìn)制后葱跋,后三位都是0持寄。既然是沒有意義的0,那么在存儲(chǔ)時(shí)就可以省略娱俺,用節(jié)省下來的空間存儲(chǔ)一些其他信息稍味。
在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í)會(huì)調(diào)用該函數(shù)荠卷∧B可以在該函數(shù)中打印類對(duì)象的地址
if (!cls) return nil;
// 這里可以打印類指針的地址,類指針地址最后一位是十六進(jìn)制的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
可以看到類對(duì)象的地址最后一位都是8或者0油宜,說明類對(duì)象確實(shí)是按照字節(jié)對(duì)齊掂碱,后三位都是0怜姿。因此在賦值shiftcls時(shí),右移三位是安全的顶吮,不會(huì)丟失類指針信息社牲。
我們可以寫代碼驗(yàn)證一下對(duì)象的isa和類對(duì)象指針的關(guān)系。代碼如下:
#import <Foundation/Foundation.h>
#import "objc-runtime.h"
// 把一個(gè)十進(jìn)制的數(shù)轉(zhuǎn)為二進(jìn)制
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 {
// 把對(duì)象轉(zhuǎn)為objc_object結(jié)構(gòu)體
struct objc_object *object = (__bridge struct objc_object *)([NSObject new]);
NSLog(@"binary = %@",binaryWithInteger(object->isa));
// uintptr_t實(shí)際上就是unsigned long
NSLog(@"binary = %@",binaryWithInteger((uintptr_t)[NSObject class]));
}
return 0;
}
打印出isa的內(nèi)容是:1011101100000000000000100000000101100010101000101000001悴了,NSObject類對(duì)象的指針是:100000000101100010101000101000000。首先將isa的內(nèi)容補(bǔ)充至64位
0000 0101 1101 1000 0000 0000 0001 0000 0000 1011 0001 0101 0001 0100 0001
取第4位到第47位之間的內(nèi)容违寿,也就是shiftcls的值:
000 0000 0000 0001 0000 0000 1011 0001 0101 0001 0100 0
將類對(duì)象的指針右移三位湃交,即去除后三位的0,得到
100000000101100010101000101000
和上面的shiftcls對(duì)比:
10 0000 0001 0110 0010 1010 0010 1000
0000 0000 0000 0010 0000 0001 0110 0010 1010 0010 1000
可以確認(rèn):shiftcls中的確包含了類對(duì)象的指針藤巢。
其他位
上面已經(jīng)介紹了nonpointer搞莺、magic、shiftcls掂咒、has_cxx_dtor才沧,還有一些其他位沒有介紹,這里簡單了解一下绍刮。
- has_assoc: 表示對(duì)象是否含有關(guān)聯(lián)引用(associatedObject)
- weakly_referenced: 表示對(duì)象是否含有弱引用對(duì)象
- deallocating: 表示對(duì)象是否正在釋放
- has_sidetable_rc: 表示對(duì)象的引用計(jì)數(shù)是否太大温圆,如果太大,則需要用其他的數(shù)據(jù)結(jié)構(gòu)來存
- extra_rc:對(duì)象的引用計(jì)數(shù)大于1孩革,則會(huì)將引用計(jì)數(shù)的個(gè)數(shù)存到extra_rc里面岁歉。比如對(duì)象的引用計(jì)數(shù)為5,則extra_rc的值為4膝蜈。
extra_rc和has_sidetable_c可以一起理解锅移。extra_rc用于存放引用計(jì)數(shù)的個(gè)數(shù),extra_rc占8位饱搏,也就是最大表示255非剃,當(dāng)對(duì)象的引用計(jì)數(shù)個(gè)數(shù)超過257時(shí),has_sidetable_rc的值應(yīng)該為1推沸。
總結(jié)
至此备绽,isa結(jié)構(gòu)體的介紹就完了。需要提醒的是坤学,上面的代碼是運(yùn)行在macOS上疯坤,也就是x86_64架構(gòu)上的,isa結(jié)構(gòu)體也是基于x86_64架構(gòu)的深浮。在arm64架構(gòu)上压怠,isa結(jié)構(gòu)體中變量所占用的位數(shù)和x86_64架構(gòu)是不一樣的,但是表示的含義是一樣的飞苇。理解了x86_64架構(gòu)下的isa結(jié)構(gòu)體菌瘫,相信對(duì)于理解arm架構(gòu)下的isa結(jié)構(gòu)體蜗顽,應(yīng)該不是什么難事。