前段時間,公司內部開發小組進行了一場Runtime分享交流會,我也從新拾遺了一些與Runtime相關的知識,現分享出來,一塊兒學習。git
以前文章:
Runtime在工做中的運用github
源碼:算法
關鍵詞:數組
OC對象、Class、isa指針緩存
2.1 Class類型bash
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
複製代碼
在Runtime源碼中,咱們能發現NSObject對象只有一個Class
類型的成員變量:isa
架構
typedef struct objc_class *Class;
複製代碼
Class對象實際上是一個指向objc_class結構體的指針。app
struct objc_object {
private:
isa_t isa;
// 這裏省略成員變量以及方法...
}
複製代碼
Class
類型本質是個結構體,該結構體中存儲了該NSObject
中的全部信息。
那麼一個NSObject對象佔用多少內存?
NSObjcet其實是隻有一個名爲isa的指針的結構體,所以佔用一個指針變量所佔用的內存空間大小,若是64bit(64位架構中)佔用8個字節,若是32bit佔用4個字節。
2.2 Class方法
- (Class)class {
return object_getClass(self);
}
複製代碼
在Runtime源碼中,咱們調用Class方法,實際上是在調用object_getClass(self)
,最終經過下面代碼獲取結果值。
inline Class
objc_object::ISA()
{
// 忽略其它方法
return (Class)(isa.bits & ISA_MASK);
}
複製代碼
2.3 isa.bits & ISA_MASK
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
};
複製代碼
上述源碼能夠知道,isa_t
是個聯合體。
typedef unsigned long uintptr_t;
複製代碼
bits
是long
類型的數值。
在isa.h
中,能夠找到ISA_MASK
源碼
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# else
# error unknown architecture for packed isa
# endif
複製代碼
可知,其實ISA_MASK
仍是個數值類型
咱們能夠看到class方法最終獲取的便是:
結構體
objc_object
的isa.bits & ISA_MASK
的數值計算結果。
3.1 isa_t
// 精簡過的isa_t共用體
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
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;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
#endif
};
複製代碼
上述源碼中isa_t
是union(共用體)類型。能夠看到共用體中有一個結構體,結構體內部分別定義了一些變量,變量後面的值表明的是該變量佔用多少個二進制位,也就是位域技術。
源碼中經過共用體的形式存儲了64位的值,這些值在結構體中被展現出來,經過對bits
進行位運算而取出相應位置的值。
3.2 共用體
在進行某些算法的C語言編程的時候,須要使幾種不一樣類型的變量存放到同一段內存單元中。也就是使用覆蓋技術,幾個變量互相覆蓋。這種幾個不一樣的變量共同佔用一段內存的結構,在C語言中,被稱做「共用體」類型結構,簡稱共用體,也叫聯合體。
優勢:能夠很大程度上節省內存空間。
union U1
{
int n;
char s[11];
double d;
};
複製代碼
對於U1共用體,s佔11字節,n佔4字節,d佔8字節,所以其至少需11字節的空間。然而其實際大小並非11,用運算符sizeof測試其大小爲16。這是由於內存對齊原則,11既不能被4整除,也不能被8整除。所以補充字節到16,這樣就符合全部成員的自身對齊了。因此聯合體的內存除了取最大成員內存外,還要保證是全部成員類型size的最小公倍數
對比類的內存對齊:
原則 1. 前面的地址必須是後面的地址正數倍,不是就補齊。 原則 2. 整個Struct的地址必須是最大字節的整數倍。
@interface MXRPerson : NSObject{
int _age;
}
複製代碼
person對象的第一個地址要存放isa指針須要8個字節,第二個地址要存放_age成員變量須要4個字節,所以person對象就佔用16個字節空間。
代碼驗證:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 驗證內存地址
NSObject *obj = [[NSObject alloc] init];
NSLog(@"%zd",class_getInstanceSize([NSObject class]));
NSLog(@"%zd",class_getInstanceSize([MXRPerson class]));
}
return 0;
}
// 8 16
複製代碼
3.3 isa中存儲的信息及做用
struct {
// 0表明普通的指針,存儲着Class,Meta-Class對象的內存地址。
// 1表明優化後的使用位域存儲更多的信息。
uintptr_t nonpointer : 1;
// 是否有設置過關聯對象,若是沒有,釋放時會更快
uintptr_t has_assoc : 1;
// 是否有C++析構函數,若是沒有,釋放時會更快
uintptr_t has_cxx_dtor : 1;
// 存儲着Class、Meta-Class對象的內存地址信息
uintptr_t shiftcls : 33;
// 用於在調試時分辨對象是否未完成初始化
uintptr_t magic : 6;
// 是否有被弱引用指向過。
uintptr_t weakly_referenced : 1;
// 對象是否正在釋放
uintptr_t deallocating : 1;
// 引用計數器是否過大沒法存儲在isa中
// 若是爲1,那麼引用計數會存儲在一個叫SideTable的類的屬性中
uintptr_t has_sidetable_rc : 1;
// 裏面存儲的值是引用計數器減1
uintptr_t extra_rc : 19;
};
複製代碼
此時咱們從新來看ISA_MASK
的值 0000000ffffffff8 轉爲二進制:
111111111111111111111111111111111000
能夠看出ISA_MASK
的值轉化爲二進制中有33位都爲1,因此按位與的做用是能夠取出這33位中的值。咱們再回頭看看isa_t
的源碼,不難發現,這33位對應的是結構體的shiftcls
的位域。那麼ISA_MASK
同shiftcls
進行按位與運算便可以取出Class或Meta-Class的值(內存地址的值)。
同時能夠看出ISA_MASK
最後三位的值爲0,那麼任何數同ISA_MASK
按位與運算以後,獲得的最後三位一定都爲0,所以任何類對象或元類對象的內存地址最後三位一定爲0,轉化爲十六進制末位一定爲8或者0。
對象的isa指針須要同
ISA_MASK
通過一次&(按位與)運算才能得出真正的Class對象地址。
代碼驗證
- (void)viewDidLoad {
[super viewDidLoad];
MXRPerson *person = [[MXRPerson alloc]init];
NSLog(@"%p",[person class]);
NSLog(@"%@",person);
}
複製代碼
2019-04-24 18:21:30.424630+0800 IsaTestDemo[58799:8221193] 0x1005c8db0
(lldb) p/x person->isa
(Class) $0 = 0x000001a1005c8db1 MXRPerson
複製代碼
shiftcls
中存儲類對象地址。把轉爲2進制的實例對象isa地址與轉爲2進制的類對象地址做對比,能夠看出存儲類對象地址的33位二進制內容徹底相同。
4.1 instance對象在內存中存儲的信息包括
4.2 class對象在內存中存儲的信息主要包括
4.3 class_rw_t & class_ro_t
咱們發現class_rw_t中存儲着方法列表,屬性列表,協議列表等內容。
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // 方法進行緩存
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
複製代碼
class_rw_t
中的methods是二維數組的結構,而且可讀可寫,所以能夠動態的添加方法,而且更加便於分類方法的添加。
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
複製代碼
而class_rw_t是經過bits調用data方法得來的,咱們來到data方法內部實現。咱們能夠看到,data函數內部僅僅對bits進行&FAST_DATA_MASK操做
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
複製代碼
成員變量信息則是存儲在class_ro_t內部中的,咱們來到class_ro_t內查看。
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize; //實例對象大小
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name; // 類名
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars; // 成員變量
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
複製代碼
4.4 每一個類在內存中有且只有一個meta-class對象,在內存中存儲的信息主要包括
1.當對象調用實例方法的時候,咱們上面講到,實例方法信息是存儲在class類對象中的,那麼要想找到實例方法,就必須找到class類對象,那麼此時isa的做用就來了
instance的isa指向class,當調用對象方法時,經過instance的isa找到class,最後找到對象方法的實現進行調用。
2.當類對象調用類方法的時候,同上,類方法是存儲在meta-class元類對象中的。那麼要找到類方法,就須要找到meta-class元類對象,而class類對象的isa指針就指向元類對象
class的isa指向meta-class當調用類方法時,經過class的isa找到meta-class,最後找到類方法的實現進行調用
3.當對象調用其父類對象方法的時候,又是怎麼找到父類對象方法的呢?,此時就須要使用到class類對象superclass指針。
當Student的instance對象要調用Person的對象方法時,會先經過isa找到Student的class,而後經過superclass找到Person的class,最後找到對象方法的實現進行調用,一樣若是Person發現本身沒有響應的對象方法,又會經過Person的superclass指針找到NSObject的class對象,去尋找響應的方法
4.當類對象調用父類的類方法時,就須要先經過isa指針找到meta-class,而後經過superclass去尋找響應的方法
當Student的class要調用Person的類方法時,會先經過isa找到Student的meta-class,而後經過superclass找到Person的meta-class,最後找到類方法的實現進行調用
代碼驗證:
struct mxr_objc_class{
Class isa;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 如何證實isa指針的指向真的如上面所說?
NSObject *object = [[NSObject alloc] init];
Class objectClass = [NSObject class];
// 咱們本身建立一個一樣的結構體並經過強制轉化拿到isa指針。
struct mxr_objc_class *objectClass2 = (__bridge struct mxr_objc_class *)(objectClass);
Class objectMetaClass = object_getClass([NSObject class]);
NSLog(@"%p %p %p", object, objectClass, objectMetaClass);
}
return 0;
}
複製代碼
驗證結果1
(lldb) p/x object->isa
(Class) $0 = 0x001d800100b16141 NSObject
(lldb) p/x objectClass
(Class) $1 = 0x0000000100b16140 NSObject
(lldb) p/x 0x00007ffffffffff8 & 0x001d800100b16141
(long) $2 = 0x0000000100b16140
複製代碼
object-isa指針地址0x001dffff96537141通過同0x00007ffffffffff8位運算,得出objectClass的地址0x00007fff96537140
驗證結果2
咱們來驗證class對象的isa指針是否一樣須要位運算計算出meta-class
對象的地址。 以一樣的方式打印objectClass->isa
指針時,發現沒法打印。
(lldb) p/x objectClass->isa
error: member reference base type 'Class' is not a structure or union
複製代碼
爲了拿到isa指針的地址,咱們本身建立一個一樣的結構體並經過強制轉化拿到isa指針。
(lldb) p/x objectClass2->isa
(Class) $0 = 0x001d800100b160f1
(lldb) p/x objectMetaClass
(Class) $1 = 0x0000000100b160f0
(lldb) p/x 0x00007ffffffffff8 & 0x001d800100b160f1
(long) $2 = 0x0000000100b160f0
複製代碼
objectClass2的isa指針通過位運算以後的地址是meta-class的地址。
最新Runtime源碼objc4-750編譯
探尋OC對象的本質
淺析NSObject對象的Class
神經病院Objective-C Runtime入院第一天——isa和Class