前言
在寫這篇博客之前齐婴,我在想要從哪里切入单匣,才能讓iOS開發(fā)者能更通俗的理解 isa
蚁阳。思來想去铃绒,我覺得還是從我們最熟悉的“對象”入手吧。
在Foundation層韵吨,創(chuàng)建對象的代碼是這樣的
Person *p = [[Person alloc] init];
那么你有沒有想過這樣一個問題匿垄?我們自定義了一個Person類移宅,沒有任何屬性和方法,為什么我們可以調(diào)用 alloc
和 init
呢 椿疗?或許你可以脫口而出漏峰,因為Person類繼承自NSObject,NSObject里有默認的實現(xiàn)
+ (id)alloc {
return _objc_rootAlloc(self);
}
那為什么繼承自NSOject的類就可以調(diào)用NSObject的方法呢届榄?是不是這中間兩者通過某些線索進行了關聯(lián)呢浅乔?帶著這個疑問我們往下看。
初識 isa
對象的本質(zhì)是 結構體
铝条,這很好理解靖苇,因為OC 是 C 與 C++ 的超集。一個對象可以有多種不同數(shù)據(jù)類型的屬性班缰,那可以容納不同數(shù)據(jù)類型的復雜結構贤壁,當然是結構體
了。我們通過查看蘋果的源碼也可以佐證這一說法埠忘。
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
我們看到脾拆,對象就是這樣一個結構體,且被typedef為 id
莹妒。在Foundation層 id
就表示一個對象 名船。
同時我們注意到在對象結構體內(nèi),有一個 Class
類型的 isa
變量旨怠,看變量類型這是一個類渠驼。對象內(nèi)有一個類 ?這聽起來有些奇怪鉴腻;對象內(nèi)有一個指向該對象類型的指針 迷扇?這似乎還蠻符合我們以往的認知:在面向?qū)ο缶幊讨校瑢ο笫怯深悇?chuàng)建的拘哨,對象可以通過 isa
變量找到自己所屬的類谋梭。
那為什么對象需要知道自己的類呢?這主要是因為對象的信息是存儲在該對象所屬的類中的倦青。
這也很容易理解瓮床,一個類可以有多個對象,如果每個對象的信息都存儲在各自的本身产镐,那隨著對象的不斷創(chuàng)建隘庄,對于內(nèi)存來說是災難級的。
既然對象的 isa
指針指向了類癣亚,那不妨也看看類的結構:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
Class _Nullable super_class
.......
.......
}
類里面有個 super_class
丑掺,指向了類的父類; 同時類也有一個 isa
指針述雾,那類的 isa
指針指向了哪里呢街州?
對象是按照 類 所定義的各個屬性和方法“生產(chǎn)”的兼丰,類 作為對象的模板,也可看成是對象唆缴。正如工廠里面的模子也是要專門制作模子的機器生產(chǎn)鳍征。元類 (meta class) 就是設計、管理類(class)的模板面徽。對象是 類 的實例艳丛,類是 元類 的實例。
所以類的 isa
指針指向了 元類趟紊。
按照這個規(guī)則氮双,那 元類 也是對象,元類對象中也有 isa
霎匈,那么元類的 isa
又指向哪里呢戴差?總不能指向元元類吧……這樣是無窮無盡的。
Objective-C語言的設計者已經(jīng)考慮到了這個問題唧躲,所有元類的 isa
都指向 根元類(meta Root Class)造挽。關于實例對象、類弄痹、元類之間的關系,蘋果官方給了一張圖嵌器,非常清晰的表明了三者的關系肛真。
實線是 super_class 指針,虛線是 isa 指針爽航。
- Root class(class) 通常是 NSObject蚓让,NSObject 是沒有超類的,所以 Root class(class)的 superclass 指向 nil讥珍。
- 每個 Class 都有一個 isa 指針指向唯一的 Meta class
- Root class(meta)的 superclass 指向 Root class(class)历极,也就是 NSObject,形成一個回路衷佃。
- 每個 Meta class 的 isa 指針都指向 Root class(meta)趟卸。
一個對象 可以通過 isa
找到類,根據(jù)類的 isa
和 super_class
找到 元類 與 父類 氏义,進而直到 根元類 和 根類 锄列,所以 對于最開始的例子 Person *p = [[Person alloc] init];
Person可以調(diào)用NSObject的方法,在這中間 isa
起到至關重要的作用惯悠。
小結:
Object-C 是基于類的對象系統(tǒng)邻邮。每一個對象都是一些類的實例;這個對象的
isa
指針指向它所屬的類克婶。
- 該類描述這個 對象的數(shù)據(jù)信息 :內(nèi)存分配大小(allocation size)和實例變量的類型(ivar types )與布局(layout)筒严;
- 也描述了 對象的行為 :它能夠響應的選擇器(selectors)和它實現(xiàn)的實例方法(instance methods)丹泉。
每個 Object-C 類也是一個對象,它的
isa
指針指向元類鸭蛙,元類是關于類對象的描述摹恨,就像類是普通實例對象的描述一樣。一個元類是根元類的實例规惰;根元類是它自身的實例睬塌。
isa
指針鏈以一個環(huán)結束:實例指向類-指向元類-指向根元類-到自身。元類的isa
指針并不重要歇万,因為在現(xiàn)實世界中揩晴,沒人會向元類對象發(fā)送消息。
總之贪磺, isa
很棒~ 很重要~
isa的優(yōu)化
隨著Apple公司的發(fā)展硫兰,iPhone 不斷更新迭代,技術不斷提升寒锚,底層源碼也是在不斷優(yōu)化的劫映。64位架構CPU問世,Apple更新優(yōu)化了許多地方刹前,其中就包括 isa
的結構泳赋。
/// Represents an instance of a class.
struct objc_object {
private:
isa_t isa;
..........太多 以下省略
..........
}
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
}
在 objc2.0 中,所有的對象都會包含一個 isa_t
類型的結構體喇喉。同時祖今,因為 objc_class
繼承自 objc_object
,所以所有的類也包含這樣一個 isa
拣技。在優(yōu)化之前千诬,isa
只是一個指向類或元類的指針,而優(yōu)化之后膏斤,采取了聯(lián)合體結構徐绑,同樣是占用8字節(jié)空間,但存儲了更多的內(nèi)容莫辨。
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
其中 ISA_BITFIELD 為宏傲茄,定義在 isa.h 中,這樣做的目的是為了區(qū)分不同架構
深入 isa
我們以 arm64 架構為例衔掸,則 isa_t
可以表示成如下所示的代碼
(以下內(nèi)容探討如不特殊說明烫幕,默認均是以 arm64 架構為例)
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
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
}
};
isa_t
是一個聯(lián)合體,這里所占空間為 8字節(jié)敞映,共64位 较曼,內(nèi)存布局從低位到高位情況如下圖
解釋一下各存儲內(nèi)容的含義:
nonpointer
(存儲在第0字節(jié)) : 是否為優(yōu)化isa標志。0代表是優(yōu)化前的isa振愿,一個純指向類或元類的指針捷犹;1表示優(yōu)化后的isa弛饭,不止是一個指針,isa中包含類信息萍歉、對象的引用計數(shù)等÷滤蹋現(xiàn)在基本上都是優(yōu)化后的isa。has_assoc
(存儲在第1個字節(jié)): 關聯(lián)對象標志位枪孩。對象含有或者曾經(jīng)含有關聯(lián)引用憔晒,0表示沒有,1表示有蔑舞,沒有關聯(lián)引用的可以更快地釋放內(nèi)存(dealloc的底層代碼有體現(xiàn))拒担。has_cxx_dtor
(存儲在第2個字節(jié)): 析構函數(shù)標志位,如果有析構函數(shù)攻询,則需進行析構邏輯从撼,如果沒有,則可以更快速地釋放對象(dealloc的底層代碼有體現(xiàn))钧栖。shiftcls
:(存儲在第3-35字節(jié))存儲類的指針低零,其實就是優(yōu)化之前 isa 指向的內(nèi)容。在arm64架構中有33位用來存儲類指針拯杠。x86_64架構有44位掏婶。magic
(存儲在第36-41字節(jié)):判斷對象是否初始化完成, 是調(diào)試器判斷當前對象是真的對象還是沒有初始化的空間潭陪。weakly_referenced
(存儲在第42字節(jié)):對象被指向或者曾經(jīng)指向一個 ARC 的弱變量气堕,沒有弱引用的對象可以更快釋放(dealloc的底層代碼有體現(xiàn))。deallocating
(存儲在第43字節(jié)):標志對象是否正在釋放內(nèi)存畔咧。has_sidetable_rc
(存儲在第44字節(jié)):判斷該對象的引用計數(shù)是否過大,如果過大則需要其他散列表來進行存儲揖膜。extra_rc
(存儲在第45-63字節(jié)誓沸。):存放該對象的引用計數(shù)值減1后的結果。對象的引用計數(shù)超過 1壹粟,會存在這個里面拜隧,如果引用計數(shù)為 10,extra_rc 的值就為 9趁仙。
如上洪添,優(yōu)化之后的 isa,保留了優(yōu)化之前類的指針(shiftcls)雀费,所以依然可以通過isa找到對應的類干奢,在類中通過super_class找到父類,這對于 isa 的指向圖的部分是一樣子盏袄。同時還包含了更多其他的內(nèi)容忿峻,這個設計和
taggedpointer
有些類似薄啥,把內(nèi)存用到極致。
接下來我們做一些有趣的事情:
定義一個繼承自 NSObject 的類 Person逛尚,不添加任何屬性與方法等垄惧,保證它是剛剛創(chuàng)建出來的樣子。
Person *p = [[Person alloc] init];
以16進制格式化打印4段內(nèi)存情況
(lldb) x/4gx p
0x10201f950: 0x001d8001000024dd 0x0000000000000000
0x10201f960: 0x0000000000000000 0x0000000000000000
(lldb)
因為Person繼承自NSObject绰寞,默認有一個 isa
到逊,所以 0x001d8001000024dd 就是 isa_t
結構 ,我們將這個值
右移3位滤钱,左移31位觉壶,再右移28位,看看得到什么菩暗?
(lldb) x/4gx p
0x10201f950: 0x001d8001000024dd 0x0000000000000000
0x10201f960: 0x0000000000000000 0x0000000000000000
(lldb) po 0x001d8001000024dd >> 3
1037939513492635
(lldb) po 1037939513492635 << 30
562951189692416
(lldb) po 562951189692416 >> 27
Person
(lldb)
最終結果顯示是拿到了類信息掰曾,我們來畫圖分析一下這個過程,用藍色表示內(nèi)存中被保留的值停团,灰色表示內(nèi)存中被抹除的值
- 起始時旷坦,完整內(nèi)存的值均保留
- 右移3位
內(nèi)存整體右移3位,那么高3位將空缺佑稠,低3位被移出isa_t內(nèi)存邊界(用透明度表示)秒梅,所以相當于抹除。
我們只關注isa_t結構內(nèi)的內(nèi)存分布舌胶,不考慮邊界內(nèi)存的影響捆蜀,簡化繪圖為:
- 3.左移31位
低31位被抹除
- 4.右移28位
最終內(nèi)存中被保留的內(nèi)容 僅剩第3到第35字節(jié)的,對應前面所講的 isa_t
內(nèi)存布局情況幔嫂,剛好是 shiftcls 的數(shù)據(jù)信息辆它。所以我們上面的操作可以取到Person類信息。
我們再看一下apple的開發(fā)人員是怎么取類的信息的呢履恩?
inline Class
objc_object::ISA()
{
return (Class)(isa.bits & ISA_MASK);
}
通過 isa
中的 bits & ISA_MASK
看看 ISA_MASK 是什么锰茉?
# define ISA_MASK 0x0000000ffffffff8ULL
將它轉換成2進制
從低位3開始到35位為1,其他位均為0切心。所以 & ISA_MASK 就相當于保留第3-35位數(shù)據(jù)飒筑,抹除其他位數(shù)據(jù)。依然是取 shiftcls 绽昏!
isa 的初始化
了解了isa的結構协屡,我們來看一下isa的初始化(去除一些宏定義,斷言以及條件判斷等全谤,我們直接將代碼減少到它執(zhí)行的代碼)
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
isa_t newisa(0);
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
isa = newisa;
}
}
- 首先對整個bits進行賦值肤晓,傳入 ISA_MAGIC_VALUE ,在arm64架構下,該值為
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
將該值轉換為2進制
對應 isa_t
中內(nèi)存布局的位置材原,可以看出對 bits 賦值 就是對 nonpinter 和 magic 賦值的過程沸久。
其次對has_cxx_dtor賦值。
最后對shifcls賦值
newisa.shiftcls = (uintptr_t)cls >> 3;
這里 余蟹,對當前傳入類進行右移3位的原因是卷胯,將cls指針后三位清除以減小內(nèi)存消耗,因為指針是要按照8字節(jié)對齊的威酒,實際后三位是沒有意義的窑睁。這和 isa_t
中的內(nèi)存布局沒有關系,因為類可不是按照isa_t
進行內(nèi)存布局的葵孤。
至此isa的賦值過程就完成了担钮。
總結
對于 isa
,我們了解了底層原理,對其作用以及相關操作尤仍,我們會更加清晰箫津。當然,在這里我們也要學習Apple的設計模式宰啦,試著站在開發(fā)人員的角度考慮它的設計思想苏遥。
然后你一定要熟記 isa
與 super_class
的指向流程,這真的很重要赡模。
最后田炭,希望在此時或者以后的某一天,你可以大膽的對它說:isa漓柑,我看透你了教硫!