在學習Runtime以前首先須要對isa的本質有必定的瞭解,這樣以後學習Runtime會更便於理解。c++
回顧OC對象的本質,每一個OC對象都含有一個isa指針,__arm64__
以前,isa僅僅是一個指針,保存着對象或類對象內存地址,在__arm64__
架構以後,apple對isa進行了優化,變成了一個共用體(union)結構,同時使用位域來存儲更多的信息。算法
咱們知道OC對象的isa指針並非直接指向類對象或者元類對象,而是須要&ISA_MASK
經過位運算才能獲取到類對象或者元類對象的地址。今天來探尋一下爲何須要&ISA_MASK
才能獲取到類對象或者元類對象的地址,以及這樣的好處。編程
首先在源碼中找到isa指針,看一下isa指針的本質。bash
// 截取objc_object內部分代碼
struct objc_object {
private:
isa_t isa;
}
複製代碼
isa指針實際上是一個isa_t類型的共用體,來到isa_t內部查看其結構架構
// 精簡過的isa_t共用體
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if SUPPORT_PACKED_ISA
# 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)
};
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
struct {
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
#endif
複製代碼
上述源碼中isa_t
是union類型,union表示共用體。能夠看到共用體中有一個結構體,結構體內部分別定義了一些變量,變量後面的值表明的是該變量佔用多少個字節,也就是位域技術。app
共用體:在進行某些算法的C語言編程的時候,須要使幾種不一樣類型的變量存放到同一段內存單元中。也就是使用覆蓋技術,幾個變量互相覆蓋。這種幾個不一樣的變量共同佔用一段內存的結構,在C語言中,被稱做「共用體」類型結構,簡稱共用體。ide
接下來使用共用體的方式來深刻的瞭解apple爲何要使用共用體,以及使用共用體的好處。函數
接下來使用代碼來模仿底層的作法,建立一個person類並含有三個BOOL類型的成員變量。post
@interface Person : NSObject
@property (nonatomic, assign, getter = isTall) BOOL tall;
@property (nonatomic, assign, getter = isRich) BOOL rich;
@property (nonatomic, assign, getter = isHansome) BOOL handsome;
@end
複製代碼
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%zd", class_getInstanceSize([Person class]));
}
return 0;
}
// 打印內容
// Runtime - union探尋[52235:3160607] 16
複製代碼
上述代碼中Person含有3個BOOL類型的屬性,打印Person類對象佔據內存空間爲16,也就是(isa指針 = 8) + (BOOL tall = 1) + (BOOL rich = 1) + (BOOL handsome = 1) = 13
。由於內存對齊原則因此Person類對象佔據內存空間爲16。學習
上面提到過共用體中變量能夠相互覆蓋,可使幾個不一樣的變量存放到同一段內存單元中,能夠很大程度上節省內存空間。
那麼咱們知道BOOL值只有兩種狀況 0 或者 1,可是卻佔據了一個字節的內存空間,而一個內存空間中有8個二進制位,而且二進制只有 0 或者 1 。那麼是否可使用1個二進制位來表示一個BOOL值,也就是說3個BOOL值最終只使用3個二進制位,也就是一個內存空間便可呢?如何實現這種方式?
首先若是使用這種方式須要本身寫方法聲明與實現,不能夠寫屬性,由於一旦寫屬性,系統會自動幫咱們添加成員變量。
另外想要將三個BOOL值存放在一個字節中,咱們能夠添加一個char
類型的成員變量,char
類型佔據一個字節內存空間,也就是8個二進制位。可使用其中最後三個二進制位來存儲3個BOOL值。
@interface Person()
{
char _tallRichHandsome;
}
複製代碼
例如_tallRichHansome的值爲 0b 0000 0010
,那麼只使用8個二進制位中的最後3個,分別爲其賦值0或者1來表明tall、rich、handsome
的值。以下圖所示
那麼如今面臨的問題就是如何取出8個二進制位中的某一位的值,或者爲某一位賦值呢?
首先來看一下取值,假如char類型的成員變量中存儲的二進制爲0b 0000 0010
若是想將倒數第2位的值也就是rich的值取出來,可使用&進行按位與運算進而去除相應位置的值。
&:按位與,同真爲真,其餘都爲假。
// 示例
// 取出倒數第三位 tall
0000 0010
& 0000 0100
------------
0000 0000 // 取出倒數第三位的值爲0,其餘位都置爲0
// 取出倒數第二位 rich
0000 0010
& 0000 0010
------------
0000 0010 // 取出倒數第二位的值爲1,其餘位都置爲0
複製代碼
按位與能夠用來取出特定的位,想取出哪一位就將那一位置爲1,其餘爲都置爲0,而後同原數據進行按位與計算,便可取出特定的位。
那麼此時能夠將get方法寫成以下方式
#define TallMask 0b00000100 // 4
#define RichMask 0b00000010 // 2
#define HandsomeMask 0b00000001 // 1
- (BOOL)tall
{
return !!(_tallRichHandsome & TallMask);
}
- (BOOL)rich
{
return !!(_tallRichHandsome & RichMask);
}
- (BOOL)handsome
{
return !!(_tallRichHandsome & HandsomeMask);
}
複製代碼
上述代碼中使用兩個!!(非)
來將值改成bool類型。一樣使用上面的例子
// 取出倒數第二位 rich
0000 0010 // _tallRichHandsome
& 0000 0010 // RichMask
------------
0000 0010 // 取出rich的值爲1,其餘位都置爲0
複製代碼
上述代碼中(_tallRichHandsome & TallMask)
的值爲0000 0010
也就是2,可是咱們須要的是一個BOOL類型的值 0 或者 1 ,那麼!!2
就將 2 先轉化爲 0 ,以後又轉化爲 1。相反若是按位與取得的值爲 0 時,!!0
將 0 先轉化爲 1 以後又轉化爲 0。 所以使用!!
兩個非操做將值轉化爲 0 或者 1 來表示相應的值。
掩碼 : 上述代碼中定義了三個宏,用來分別進行按位與運算而取出相應的值,通常用來按位與(&)運算的值稱之爲掩碼。
爲了能更清晰的代表掩碼是爲了取出哪一位的值,上述三個宏的定義可使用<<(左移)
來優化
<<:表示左移一位,下圖爲例。
那麼上述宏定義可使用<<(左移)
優化成以下代碼
#define TallMask (1<<2) // 0b00000100 4
#define RichMask (1<<1) // 0b00000010 2
#define HandsomeMask (1<<0) // 0b00000001 1
複製代碼
設值便是將某一位設值爲0或者1,可使用|(按位或)
操做符。 | : 按位或,只要有一個1即爲1,不然爲0。
若是想將某一位置爲1的話,那麼將本來的值與掩碼進行按位或的操做便可,例如咱們想將tall置爲1
// 將倒數第三位 tall置爲1
0000 0010 // _tallRichHandsome
| 0000 0100 // TallMask
------------
0000 0110 // 將tall置爲1,其餘位值都不變
複製代碼
若是想將某一位置爲0的話,須要將掩碼按位取反(~ : 按位取反符),以後在與本來的值進行按位與操做便可。
// 將倒數第二位 rich置爲0
0000 0010 // _tallRichHandsome
& 1111 1101 // RichMask按位取反
------------
0000 0000 // 將rich置爲0,其餘位值都不變
複製代碼
此時set方法內部實現以下
- (void)setTall:(BOOL)tall
{
if (tall) { // 若是須要將值置爲1 // 按位或掩碼
_tallRichHandsome |= TallMask;
}else{ // 若是須要將值置爲0 // 按位與(按位取反的掩碼)
_tallRichHandsome &= ~TallMask;
}
}
- (void)setRich:(BOOL)rich
{
if (rich) {
_tallRichHandsome |= RichMask;
}else{
_tallRichHandsome &= ~RichMask;
}
}
- (void)setHandsome:(BOOL)handsome
{
if (handsome) {
_tallRichHandsome |= HandsomeMask;
}else{
_tallRichHandsome &= ~HandsomeMask;
}
}
複製代碼
寫完set、get方法以後經過代碼來查看一下是否能夠設值、取值成功。
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.tall = YES;
person.rich = NO;
person.handsome = YES;
NSLog(@"tall : %d, rich : %d, handsome : %d", person.tall,person.rich,person.handsome);
}
return 0;
}
複製代碼
打印內容
Runtime - union探尋[58212:3857728] tall : 1, rich : 0, handsome : 1
複製代碼
能夠看出上述代碼能夠正常賦值和取值。可是代碼仍是有必定的侷限性,當須要添加新屬性的時候,須要重複上述工做,而且代碼可讀性比較差。接下來使用結構體的位域特性來優化上述代碼。
將上述代碼進行優化,使用結構體位域,可使代碼可讀性更高。 位域聲明 位域名 : 位域長度;
使用位域須要注意如下3點: 1. 若是一個字節所剩空間不夠存放另外一位域時,應從下一單元起存放該位域。也能夠有意使某位域從下一單元開始。 2. 位域的長度不能大於數據類型自己的長度,好比int類型就不能超過32位二進位。 3. 位域能夠無位域名,這時它只用來做填充或調整位置。無名的位域是不能使用的。
上述代碼使用結構體位域優化以後。
@interface Person()
{
struct {
char handsome : 1; // 位域,表明佔用一位空間
char rich : 1; // 按照順序只佔一位空間
char tall : 1;
}_tallRichHandsome;
}
複製代碼
set、get方法中能夠直接經過結構體賦值和取值
- (void)setTall:(BOOL)tall
{
_tallRichHandsome.tall = tall;
}
- (void)setRich:(BOOL)rich
{
_tallRichHandsome.rich = rich;
}
- (void)setHandsome:(BOOL)handsome
{
_tallRichHandsome.handsome = handsome;
}
- (BOOL)tall
{
return _tallRichHandsome.tall;
}
- (BOOL)rich
{
return _tallRichHandsome.rich;
}
- (BOOL)handsome
{
return _tallRichHandsome.handsome;
}
複製代碼
經過代碼驗證一下是否能夠賦值或取值正確
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.tall = YES;
person.rich = NO;
person.handsome = YES;
NSLog(@"tall : %d, rich : %d, handsome : %d", person.tall,person.rich,person.handsome);
}
return 0;
}
複製代碼
首先在log處打個斷點,查看_tallRichHandsome內存儲的值
由於_tallRichHandsome
佔據一個內存空間,也就是8個二進制位,咱們將05十六進制轉化爲二進制查看
上圖中能夠發現,倒數第三位也就是tall值爲1,倒數第二位也就是rich值爲0,倒數一位也就是handsome值爲1,如此看來和上述代碼中咱們設置的值同樣。能夠成功賦值。
接着繼續打印內容: Runtime - union探尋[59366:4053478] tall : -1, rich : 0, handsome : -1
此時能夠發現問題,tall與handsome咱們設值爲YES,講道理應該輸出的值爲1爲什麼上面輸出爲-1呢?
而且上面經過打印_tallRichHandsome
中存儲的值,也確認tall
和handsome
的值都爲1。咱們再次打印_tallRichHandsome
結構體內變量的值。
上圖中能夠發現,handsome的值爲0x01,經過計算器將其轉化爲二進制
能夠看到值確實爲1的,爲何打印出來值爲-1呢?此時應該能夠想到應該是get方法內部有問題。咱們來到get方法內部經過打印斷點查看獲取到的值。
- (BOOL)handsome
{
BOOL ret = _tallRichHandsome.handsome;
return ret;
}
複製代碼
打印ret的值
經過打印ret的值發現其值爲255,也就是1111 1111
,此時也就能解釋爲何打印出來值爲 -1了,首先此時經過結構體獲取到的handsome
的值爲0b1
只佔一個內存空間中的1位,可是BOOL值佔據一個內存空間,也就是8位。當僅有1位的值擴展成8位的話,其他空位就會根據前面一位的值所有補位成1,所以此時ret的值就被映射成了0b 11111 1111
。
11111111
在一個字節時,有符號數則爲-1,無符號數則爲255。所以咱們在打印時候打印出的值爲-1
爲了驗證當1位的值擴展成8位時,會所有補位,咱們將tall、rich、handsome值設置爲佔據兩位。
@interface Person()
{
struct {
char tall : 2;
char rich : 2;
char handsome : 2;
}_tallRichHandsome;
}
複製代碼
此時在打印就發現值能夠正常打印出來。 Runtime - union探尋[60827:4259630] tall : 1, rich : 0, handsome : 1
這是由於,在get方法內部獲取到的_tallRichHandsome.handsome
爲兩位的也就是0b 01
,此時在賦值給8位的BOOL類型的值時,前面的空值就會自動根據前面一位補全爲0,所以返回的值爲0b 0000 0001
,所以打印出的值也就爲1了。
所以上述問題一樣可使用!!
雙感嘆號來解決問題。!!
的原理上面已經講解過,這裏再也不贅述了。
使用結構體位域優化以後的代碼
@interface Person()
{
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
}_tallRichHandsome;
}
@end
@implementation Person
- (void)setTall:(BOOL)tall
{
_tallRichHandsome.tall = tall;
}
- (void)setRich:(BOOL)rich
{
_tallRichHandsome.rich = rich;
}
- (void)setHandsome:(BOOL)handsome
{
_tallRichHandsome.handsome = handsome;
}
- (BOOL)tall
{
return !!_tallRichHandsome.tall;
}
- (BOOL)rich
{
return !!_tallRichHandsome.rich;
}
- (BOOL)handsome
{
return !!_tallRichHandsome.handsome;
}
複製代碼
上述代碼中使用結構體的位域則不在須要使用掩碼,使代碼可讀性加強了不少,可是效率相比直接使用位運算的方式來講差不少,若是想要高效率的進行數據的讀取與存儲同時又有較強的可讀性就須要使用到共用體了。
爲了使代碼存儲數據高效率的同時,有較強的可讀性,可使用共用體來加強代碼可讀性,同時使用位運算來提升數據存取的效率。
使用共用體優化的代碼
#define TallMask (1<<2) // 0b00000100 4
#define RichMask (1<<1) // 0b00000010 2
#define HandsomeMask (1<<0) // 0b00000001 1
@interface Person()
{
union {
char bits;
// 結構體僅僅是爲了加強代碼可讀性,無實質用處
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
};
}_tallRichHandsome;
}
@end
@implementation Person
- (void)setTall:(BOOL)tall
{
if (tall) {
_tallRichHandsome.bits |= TallMask;
}else{
_tallRichHandsome.bits &= ~TallMask;
}
}
- (void)setRich:(BOOL)rich
{
if (rich) {
_tallRichHandsome.bits |= RichMask;
}else{
_tallRichHandsome.bits &= ~RichMask;
}
}
- (void)setHandsome:(BOOL)handsome
{
if (handsome) {
_tallRichHandsome.bits |= HandsomeMask;
}else{
_tallRichHandsome.bits &= ~HandsomeMask;
}
}
- (BOOL)tall
{
return !!(_tallRichHandsome.bits & TallMask);
}
- (BOOL)rich
{
return !!(_tallRichHandsome.bits & RichMask);
}
- (BOOL)handsome
{
return !!(_tallRichHandsome.bits & HandsomeMask);
}
複製代碼
上述代碼中使用位運算這種比較高效的方式存取值,使用union共用體來對數據進行存儲。增長讀取效率的同時加強代碼可讀性。
其中_tallRichHandsome
共用體只佔用一個字節,由於結構體中tall、rich、handsome都只佔一位二進制空間,因此結構體只佔一個字節,而char類型的bits也只佔一個字節,他們都在共用體中,所以共用一個字節的內存便可。
而且在get、set
方法中並無使用到結構體,結構體僅僅爲了增長代碼可讀性,指明共用體中存儲了哪些值,以及這些值各佔多少位空間。同時存值取值還使用位運算來增長效率,存儲使用共用體,存放的位置依然經過與掩碼進行位運算來控制。
此時代碼已經算是優化完成了,高效的同時可讀性高,那麼此時在回頭看isa_t
共用體的源碼
此時咱們在回頭查看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
};
複製代碼
通過上面對位運算、位域以及共用體的分析,如今再來看源碼已經能夠很清晰的理解其中的內容。源碼中經過共用體的形式存儲了64位的值,這些值在結構體中被展現出來,經過對bits
進行位運算而取出相應位置的值。
這裏主要關注一下shiftcls
,shiftcls
中存儲着Class、Meta-Class
對象的內存地址信息,咱們以前在OC對象的本質中提到過,對象的isa指針須要同ISA_MASK
通過一次&(按位與)運算才能得出真正的Class對象地址。
那麼此時咱們從新來看ISA_MASK
的值0x0000000ffffffff8ULL
,咱們將其轉化爲二進制數
上圖中能夠看出ISA_MASK
的值轉化爲二進制中有33位都爲1,上面提到過按位與的做用是能夠取出這33位中的值。那麼此時很明顯了,同ISA_MASK
進行按位與運算便可以取出Class或Meta-Class的值。
同時能夠看出ISA_MASK
最後三位的值爲0,那麼任何數同ISA_MASK
按位與運算以後,獲得的最後三位一定都爲0,所以任何類對象或元類對象的內存地址最後三位一定爲0,轉化爲十六進制末位一定爲8或者0。
將結構體取出來標記一下這些信息的做用。
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;
};
複製代碼
經過下面一段代碼驗證上述信息存儲的位置及做用
// 如下代碼須要在真機中運行,由於真機中才是__arm64__ 位架構
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
NSLog(@"%p",[person class]);
NSLog(@"%@",person);
}
複製代碼
首先打印person類對象的地址,以後經過斷點打印一下person對象的isa指針地址。
首先來看一下打印的內容
將類對象地址轉化爲二進制
將person的isa指針地址轉化爲二進制
shiftcls : shiftcls
中存儲類對象地址,經過上面兩張圖對比能夠發現存儲類對象地址的33位二進制內容徹底相同。
extra_rc : extra_rc
的19位中存儲着的值爲引用計數減一,由於此時person的引用計數爲1,所以此時extra_rc
的19位二進制中存儲的是0。
magic : magic
的6位用於在調試時分辨對象是否未完成初始化,上述代碼中person已經完成初始化,那麼此時這6位二進制中存儲的值011010
即爲共用體中定義的宏# define ISA_MAGIC_VALUE 0x000001a000000001ULL
的值。
nonpointer : 這裏確定是使用的優化後的isa,所以nonpointer
的值確定爲1
由於此時person對象沒有關聯對象而且沒有弱指針引用過,能夠看出has_assoc
和weakly_referenced
值都爲0,接着咱們爲person對象添加弱引用和關聯對象,來觀察一下has_assoc
和weakly_referenced
的變化。
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
NSLog(@"%p",[person class]);
// 爲person添加弱引用
__weak Person *weakPerson = person;
// 爲person添加關聯對象
objc_setAssociatedObject(person, @"name", @"xx_cc", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSLog(@"%@",person);
}
複製代碼
從新打印person的isa指針地址將其轉化爲二進制能夠看到has_assoc
和weakly_referenced
的值都變成了1
注意:只要設置過關聯對象或者弱引用引用過對象has_assoc
和weakly_referenced
的值就會變成1,不論以後是否將關聯對象置爲nil或斷開弱引用。
若是沒有設置過關聯對象,對象釋放時會更快,這是由於對象在銷燬時會判斷是否有關聯對象進而對關聯對象釋放。來看一下對象銷燬的源碼
void *objc_destructInstance(id obj)
{
if (obj) {
Class isa = obj->getIsa();
// 是否有c++析構函數
if (isa->hasCxxDtor()) {
object_cxxDestruct(obj);
}
// 是否有關聯對象,若是有則移除
if (isa->instancesHaveAssociatedObjects()) {
_object_remove_assocations(obj);
}
objc_clear_deallocating(obj);
}
return obj;
}
複製代碼
相信至此咱們已經對isa指針有了新的認識,__arm64__
架構以後,isa指針不僅僅只存儲了Class或Meta-Class的地址,而是使用共用體的方式存儲了更多信息,其中shiftcls
存儲了Class或Meta-Class的地址,須要同ISA_MASK
進行按位&運算才能夠取出其內存地址值。
文中若是有不對的地方歡迎指出。我是xx_cc,一隻長大好久但尚未二夠的傢伙。須要視頻一塊兒探討學習的coder能夠加我Q:2336684744