本文的主要介紹如何理解isa與類的關(guān)系本刽,在介紹之前我們首先要知道OC對象的本質(zhì)是什么赫冬?這里我們先插曲一個Clang編譯器讽坏。
一妇萄、下面是Clang介紹
Clang
是?個C語?蜕企、C++、Objective-C語?
的輕量級編譯器冠句。源代碼發(fā)布于BSD協(xié)議下轻掩。
Clang
將?持其普通lambda表達式
、返回類型的簡化處理
以及更好的處理constexpr關(guān)鍵字
懦底。
Clang
是?個由apple
主導(dǎo)編寫唇牧,基于LLVM的C/C++/Objective-C編譯器
。
2013年4?,Clang
已經(jīng)全??持C++11
標(biāo)準(zhǔn)聚唐,并開始實現(xiàn)C++1y
特性(也就是C++14丐重,這是C++的下?個?更新版本)。Clang
將?持其普通lambda表達式
杆查、返回類型的簡化處理
以及更好的處理constexpr關(guān)鍵字
扮惦。
Clang是?個C++編寫、基于LLVM亲桦、發(fā)布于LLVM BSD許可證下的C/C++/Objective-C/Objective-C++編譯器
崖蜜。它與GNU C語?規(guī)范
?乎完全兼容(當(dāng)然,也有部分不兼容的內(nèi)容客峭,包括編譯命令選項也會有點差異)豫领,并在此基礎(chǔ)上增加了額外的語法特性,?如C函數(shù)重載(通過__attribute__((overloadable))
來修飾函數(shù))桃笙,其?標(biāo)(之?)就是超越GCC氏堤。
- 如何將OC文件編譯為C++文件呢?
clang -rewrite-objc main.m -o main.cpp
把?標(biāo)?件編譯成c++?件
- UIKit報錯問題處理
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /
Applications/Xcode.app/Contents/Developer/Platforms/
iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk main.m
xcode
安裝的時候順帶安裝了xcrun
命令,xcrun
命令在clang
的基礎(chǔ)上進?了?些封裝鼠锈,要更好??些
- 模擬器編譯
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp (模擬器)
- 真機編譯
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main?-arm64.cpp (?機)
二闪檬、什么是對象?购笆?粗悯??
- 我們先使用
clang
將下面這段代碼編譯為cpp文件
同欠,在main
中自定義一個類LGPerson
样傍,有一個屬性name
。
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation LGPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
編譯后的文件
#ifndef _REWRITER_typedef_LGPerson
#define _REWRITER_typedef_LGPerson
typedef struct objc_object LGPerson;
typedef struct {} _objc_exc_LGPerson;
#endif
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_name;
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
// @property (nonatomic, copy) NSString *name;
/* @end */
// @implementation LGPerson
static NSString * _I_LGPerson_name(LGPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_LGPerson_setName_(LGPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGPerson, _name), (id)name, 0, 1); }
// @end
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_99_79b11sqx7xd67mxf4rx5g0tr0000gn_T_main_949408_mi_0);
}
return 0;
}
- 從編譯的
main.cpp
這段代碼铺遂,我們可以看出衫哥,源代碼的LGPerson
編譯后是個結(jié)構(gòu)體struct
。-
LGPerson_IMPL
中的第一個屬性 其實就是 isa
襟锐,是繼承自NSObject
撤逢,屬于偽繼承
, 偽繼承的方式 是直接將LGPerson作為結(jié)構(gòu)體的第一個屬性
粮坞,意味著LGPerson
擁有NSObject中的所有成員變量
蚊荣。 -
LGPerson
中的第一個屬性NSObject_IVARS
等效于NSObject
中的isa
。
-
結(jié)論:對象的本質(zhì)是一個結(jié)構(gòu)體莫杈。
三互例、類的源碼分析
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
我們跟進源碼,發(fā)現(xiàn)NSObject
的定義筝闹,我們發(fā)現(xiàn)了本文要討論的isa
媳叨,由代碼可以看出isa是class類型
,那么這是為什么呢丁存?為什么isa
不是其他的類型呢肩杈?
緊接著我們通過源碼調(diào)試柴我,找到了ISA()方法
解寝,這是isa的get方法
。
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
}
結(jié)論:從下面代碼(Class)isa.bits
或(Class)(isa.bits & ISA_MASK)
我們可以看出艘儒,這兩步操作都是對ISA進行了Class的強制轉(zhuǎn)換聋伦,所以isa是class類型
。
四界睁、 objc_setProperty()方法解析
我們從什么是對象這一段的源碼編譯的cpp文件代碼
可以看出觉增,在添加屬性name
之后,通過Clang編譯
后多了兩個方法翻斟,代碼如下:
static NSString * _I_LGPerson_name(LGPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_LGPerson_setName_(LGPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGPerson, _name), (id)name, 0, 1); }
這里我們可以得知通過設(shè)置屬性逾礁,系統(tǒng)自己會添加屬性的set和get方法
,并且是通過objc_setProperty
方法來設(shè)置的访惜,那么我們就來追蹤一下它的源碼嘹履,
//objc_setProperty的接口實現(xiàn)
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()接口實現(xiàn)
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);
}
通過reallySetProperty()接口實現(xiàn)
我們發(fā)現(xiàn)屬性的設(shè)置腻扇,只是newValue = objc_retain(newValue);
新值的retain
,和objc_release(oldValue);
舊值的release
砾嫉。
五幼苛、cls
與 類
的關(guān)聯(lián)原理
探索一下通過initInstanceIsa
是如何將cls與isa關(guān)聯(lián)的
,在此之前焕刮,需要先了解什么是聯(lián)合體舶沿??配并?括荡?
聯(lián)合體與結(jié)構(gòu)體知識小拓展
- 結(jié)構(gòu)體(struct)中所有變量是
共存
的
- 優(yōu)點是
有容乃?
,全?溉旋;- 缺點是
struct內(nèi)存空間
的分配是粗放的
一汽,不管?不?,全分配
低滩。聯(lián)合體(union)中
是各變量是互斥
的
- 缺點就是不夠
包容
召夹;- 優(yōu)點是內(nèi)存使?更為精細靈活,也節(jié)省了內(nèi)存空間
- 首先我們來看一下
isa_t的結(jié)構(gòu)
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_t是通過
union聯(lián)合體來構(gòu)造的
恕沫,從isa_t的定義
中可以看出:
-
提供了兩個成員监憎,
cls 和 bits
,由聯(lián)合體的定義所知婶溯,這兩個成員是互斥的
鲸阔,也就意味著,當(dāng)初始化isa指針
時迄委,有兩種初始化方式
通過cls初始化褐筛,bits無默認值
通過bits初始化,cls有默認值
除此之外它還提供了一個結(jié)構(gòu)體定義的位域叙身,用于存儲類信息及其他信息渔扎,結(jié)構(gòu)體的成員ISA_BITFIELD
,這是一個宏定義信轿,有兩個版本 __arm64__(對應(yīng)ios 移動端)
和 __x86_64__(對應(yīng)macOS)
晃痴,以下是它們的一些宏定義,具體的宏定義代碼如下:
# 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)
# else
# error unknown architecture for packed isa
# endif
// SUPPORT_PACKED_ISA
#endif
先簡單介紹一下每個宏的意義:
nonpointer
:表示是否對 isa 指針開啟指針優(yōu)化
财忽;0
:純isa指針
倘核,1
:不?是類對象地址
,isa
中包含了類信息、對象的引?計數(shù)
等即彪。
has_assoc
:關(guān)聯(lián)對象標(biāo)志位紧唱,0沒有,
1存在`。
has_cxx_dtor
:該對象是否有 C++ 或者 Objc 的析構(gòu)器
,如果有析構(gòu)函數(shù),則需要做析構(gòu)邏輯, 如果沒有,則可以更快的釋放對象漏益。
shiftcls
:存儲類指針的值
酬凳。開啟指針優(yōu)化的情況下,在arm64 架構(gòu)
中有 33 位?來存儲類指針
遭庶。
magic
:?于調(diào)試器判斷當(dāng)前對象是真的對象還是沒有初始化的空間
宁仔。
weakly_referenced
:標(biāo)志對象是否被指向
或者曾經(jīng)指向?個 ARC 的弱變量
,沒有弱引?的對象可以更快釋放峦睡。
deallocating
:標(biāo)志對象是否正在釋放內(nèi)存
翎苫。
has_sidetable_rc
:當(dāng)對象引?技術(shù)?于 10
時,則需要借?該變量存儲進位榨了。
extra_rc
:當(dāng)表示該對象的引?計數(shù)值煎谍,實際上是引?計數(shù)值減 1
,例如龙屉,如果對象的引?計數(shù)
為 10呐粘,那么extra_rc
為 9。如果引?計數(shù)?于 10
转捕,則需要使?到下?的 has_sidetable_rc
作岖。
源碼追蹤
通過alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone
方法路徑,查找到initInstanceIsa
五芝,并進入其原理實現(xiàn)痘儡。
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
if (!nonpointer) {
//初始化isa指針
isa = isa_t((uintptr_t)cls);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
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;
#endif
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
}
六、isa
與類的關(guān)聯(lián)
isa
與類
的關(guān)聯(lián)枢步,主要是通過initInstanceIsa
方法中的calloc指針
和當(dāng)前類的cls
相關(guān)聯(lián)沉删,用isa指針
中的shiftcls位域
來存儲類
的相關(guān)信息,我們可以有以下幾種驗證方式:
【方式一】通過initIsa方法中的newisa.shiftcls = (uintptr_t)cls >> 3;驗證
我們通過initInstanceIsa->initIsa->newisa.shiftcls -> (uintptr_t)cls >> 3
醉途,打印查看isa指針以及查看newisa.shiftcls
存儲信息來驗證是否shiftcls
位域中存儲著isa指針
矾瑰。
-
我們再來比較一下
bits賦值
前后的結(jié)果殴穴,bits的位域
中有兩處變化cls
由默認值,變成了LGPerson
嵌屎,將isa
與cls
完美關(guān)聯(lián)shiftcls
由0
變成了536871965
推正。圖片來自Style_月月簡書----isa成員值變化過程 下面我們在提出疑問為什么在
shiftcls賦值時需要類型強轉(zhuǎn)
?
因為內(nèi)存的存儲
不能存儲字符串
宝惰,機器碼
只能識別0 、1
這兩種數(shù)字再沧,所以需要將其轉(zhuǎn)換
為uintptr_t
數(shù)據(jù)類型尼夺,這樣shiftcls
中存儲的類信息才能被機器碼理解, 其中uintptr_t
是long
。
- 為什么需要
右移3位
淤堵?
主要是由于shiftcls的信息
處于isa指針地址
的中間部分
寝衫,前面
還有3個位域
,為了不影響前面的3個位域的數(shù)據(jù)
拐邪,需要右移
將其抹零
慰毅。
【方式二】通過isa指針地址與ISA_MSAK 的值 & 來驗證
cla與isa關(guān)聯(lián)后,我們通過lldb扎阶,x/4gx 在控制臺輸出obj的存儲信息汹胃,在通過obj的isa指針&MASK,結(jié)果一樣的是LGPerson
- 注意
arm64
中东臀,ISA_MASK
宏定義的值為0x0000000ffffffff8ULL
x86_64
中着饥,ISA_MASK
宏定義的值為0x00007ffffffffff8ULL
【方式三】通過runtime
的方法object_getClass
驗證
通過查看object_getClass
的源碼實現(xiàn),同樣可以驗證isa與類關(guān)聯(lián)的原理
惰赋,通過runtime
的api
宰掉,即object_getClass函數(shù)
獲取類信息,下面我們來查看一下源碼
/***********************************************************************
* object_getClass.
* Locking: None. If you add locking, tell gdb (rdar://7516456).
**********************************************************************/
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
//接著往下走
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;
}
//往下走到 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
}
我們發(fā)現(xiàn)源碼一樣的是使用的isa.bits & ISA_MASK
赁濒,在用Class類型
做了強制轉(zhuǎn)換
的轨奄。
【方式四】通過位運算驗證
回到_class_createInstanceFromZone
方法。通過x/4gx obj
得到obj
的存儲信息拒炎,當(dāng)前類
的信息存儲在isa指針
中戚绕,且isa
中的shiftcls
此時占44位
(因為處于macOS環(huán)境
)
- 將
右邊3位
,和左邊除去44位
以外的部分都抹零
枝冀,其相對位置是不變的;- 將
isa
地址右移3位
- 在將得到的結(jié)果
左移20位
- 再
右移17位
操作步驟以及結(jié)果輸出isa
與類
的關(guān)聯(lián)舞丛,主要是通過initInstanceIsa
方法中的calloc指針和當(dāng)前類的cls相關(guān)聯(lián),用isa指針
中的shiftcls位域
來存儲
類的相關(guān)信息果漾!
- 將