Runtime原理探究(一)—— isa的深刻體會(蘋果對isa的優化)面試
Runtime原理探究(二)—— Class結構的深刻分析編程
Runtime原理探究(三)—— OC Class的方法緩存cache_t緩存
Runtime原理探究(四)—— 刨根問底消息機制markdown
Runtime原理探究(六)—— 面試題中的Runtime編程語言
☕️☕️本文篇幅比較長,創做的目的並非爲了在簡書上刷贊和閱讀量,而是爲了本身往後溫習知識所用。若是有幸被你發現這篇文章,而且引發了你的閱讀興趣,請休息充分,靜下心來,精力充足地開始閱讀,但願這篇文章能對你有所幫助。如發現任何有誤之處,肯請留言糾正,謝謝。☕️☕️ide
不少靜態編程語言,編寫完代碼後,通過編譯鏈接生成可執行文件,最後就能夠在電腦上運行起來。函數
以C語言爲例oop
void test() {
printf("Hello World");
}
int main() {
test();
}
複製代碼
以上代碼通過編譯以後,main
函數裏面就必定會調用test()
,而test()
的實現也必定會是和代碼中寫的同樣,這些在編譯完成那一刻就決定了,運行過程當中不會發生改變的。C能夠說就是典型的靜態語言。佈局
與之相比,Objective-C就能夠在運行階段修改以前編譯階段肯定好的一些函數和方法。
************************main.m*************************
#import <Foundation/Foundation.h>
#import "CLPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
CLPerson *person = [[CLPerson alloc] init];
[person test];
}
return 0;
}
***********************CLPerson.h************************
#import <Foundation/Foundation.h>
@interface CLPerson : NSObject
- (void)test;
@end
***********************CLPerson.m************************
#import "CLPerson.h"
@implementation CLPerson
- (void)test {
NSLog(@"%s", __func__);
}
- (void)abc {
}
@end
複製代碼
如上面所示代碼,[person test];
這句代碼,在運行階段,能夠調用CLPerson
的test
方法,也能夠經過OC的動態特性,使其最終調用別的方法,例如abc
方法,甚至,還能夠調用另一個類的方法。除此以外,OC還能夠在程序運行階段,給類增長方法等,這就是所謂的動態特性。
深刻Runtime以前,先要解決一個比較重要的概念——isa
。在早期的Runtime裏面,isa
指針直接指向class/meta-class
對象的地址,isa
就是一個普通的指針。
後來,蘋果從ARM64位架構開始,對isa
進行了優化,將其定義成一個共用體(union
)結構,結合 位域 的概念以及 位運算 的方式來存儲更多類相關信息。isa
指針須要經過與一個叫ISA_MASK的值(掩碼)進行二進制&
運算,才能獲得真實的class/meta-class
對象的地址。接下來,就具體探究一下蘋果到底是怎麼優化的。
首先從源碼角度,對比一下變化isa
優化先後的變化
***************************************
typedef struct objc_class *Class;
typedef struct objc_object {
Class isa;
} *id;
複製代碼
上面是64位以前,objc_object
的定義如上,isa直接指向objc_class
。
再看看優化後objc_object
的定義
struct objc_object {
private:
isa_t isa;
public:
複製代碼
arm64開始,isa
的類型變成了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
// SUPPORT_PACKED_ISA
#endif
#if SUPPORT_INDEXED_ISA
# if __ARM_ARCH_7K__ >= 2
# define ISA_INDEX_IS_NPI 1
# define ISA_INDEX_MASK 0x0001FFFC
# define ISA_INDEX_SHIFT 2
# define ISA_INDEX_BITS 15
# define ISA_INDEX_COUNT (1 << ISA_INDEX_BITS)
# define ISA_INDEX_MAGIC_MASK 0x001E0001
# define ISA_INDEX_MAGIC_VALUE 0x001C0001
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t indexcls : 15;
uintptr_t magic : 4;
uintptr_t has_cxx_dtor : 1;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 7;
# define RC_ONE (1ULL<<25)
# define RC_HALF (1ULL<<6)
};
# else
# error unknown architecture for indexed isa
# endif
// SUPPORT_INDEXED_ISA
#endif
};
複製代碼
上面的代碼就是蘋果對於isa優化的精華所在,爲了看懂上面的代碼,首先須要從一些基礎知識開始說。
首先定義一個類CLPerson,首先給CLPerson增長几個屬性以及成員變量
@interface CLPerson : NSObject
{
BOOL _tall;
BOOL _rich;
BOOL _handsome;
}
@property (nonatomic, assign, getter=isRich) BOOL rich;
@property (nonatomic, assign, getter=isTall) BOOL tall;
@property (nonatomic, assign, getter=isHandsome) BOOL handsome;
複製代碼
對於它們的使用,無需多說,以下
#import <Foundation/Foundation.h>
#import "CLPerson.h"
#import <objc/runtime.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
CLPerson *person = [[CLPerson alloc] init];
person.rich = YES;
person.tall = NO;
person.handsome = YES;
NSLog(@"%zu", class_getInstanceSize([CLPerson class]));
}
return 0;
}
複製代碼
經過runtime,咱們能夠查看到CLPerson類對象的內存佔用狀況
2019-07-16 13:15:04.083828+0800 OC底層Runtime[2509:80387] 16
Program ended with exit code: 0
複製代碼
經過我以前對與對象內存佈局的分析的文章,這裏能夠得出以下結論:
isa
佔用了8個字節_rich
、_tall
、_handsome
這三個成員變量個佔用1個字節🐞🐞🐞可是
_rich
、_tall
、_handsome
實際上只可能有2個值,YES
/NO
,也就是0和1,它們徹底能夠用一個二進制位來表示,三個加在一塊兒也就只須要佔用3個二進制位,連半個字節都用不了。有什麼方法能夠實現這種節約內存的需求呢?🐞🐞🐞
若是直接用屬性的話,確定就會自動生成帶下劃線的成員變量,這樣就沒法精簡內存。因此須要手動實現getter/setter方法以替代屬性。
#import <Foundation/Foundation.h>
@interface CLPerson : NSObject
- (void)setTall:(BOOL)tall;
- (void)setRich:(BOOL)rich;
- (void)setHandsome:(BOOL)handsome;
- (BOOL)isTall;
- (BOOL)isRich;
- (BOOL)isHandsome;
@end
複製代碼
而後在.m文件裏面,用一個char _tallRichHandsome;
(一個字節)來存儲tall/rich/handsome的信息。
#import "CLPerson.h"
@interface CLPerson()
{
char _tallRichHandsome; // 0b 0000 0000
}
@end
@implementation CLPerson
- (void)setTall:(BOOL)tall {
}
- (void)setRich:(BOOL)rich {
}
- (void)setHandsome:(BOOL)handsome {
}
- (BOOL)isTall {
}
- (BOOL)isRich {
}
- (BOOL)isHandsome {
}
@end
複製代碼
若是我想利用_tallRichHandsome
的後三位來分別存放tall
、rich
、handsome
這三個信息,有什麼方法能夠辦到呢?
首先咱們來解決getter
方法,也就是取值問題。如何從特定的位裏面取出值呢?沒錯,——&
(按位與運算)。
假設咱們規定
_tallRichHandsome
的右起第1
位表示,_tallRichHandsome
的右起第2
位表示,_tallRichHandsome
的右起第3
位表示,YES
,rich=NO
, handsome=YES
,那麼_tallRichHandsome
的值應該是 0000 0101
tall (YES) | rich (NO) | handsome (YES) | |
---|---|---|---|
_tallRichHandsome | 0000 0101 | 0000 0101 | 0000 0101 |
mask碼(用來取值) | &0000 0001 | &0000 0010 | &0000 0100 |
經過&運算獲得結果 | 0000 0001 | 0000 0000 | 0000 0100 |
根據&
運算的特色,想要取出特定位上面的值,只需將mask
碼中對應位設置爲1,由於 原來值 & 1 = 原來值
,將mask碼中其餘位的設置爲0,就能夠屏蔽出特定位以外其他位上面的值,由於 原來值 & 0 = 0
,這個應該很好理解。至於取出來的值如何轉化成咱們所須要的值(在這裏咱們須要的是YES/NO
),就有不少辦法了。好了,如今去代碼裏面實現一下。以下所示
*************************main.m*****************************
#import <Foundation/Foundation.h>
#import "CLPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
CLPerson *person = [[CLPerson alloc] init];
NSLog(@"tall-%d, rich-%d, handsome%d", person.isTall, person.isRich, person.isHandsome);
}
return 0;
}
*************************CLPerson.m*****************************
#import "CLPerson.h"
@interface CLPerson()
{
char _tallRichHandsome;
}
@end
@implementation CLPerson
- (instancetype)init
{
self = [super init];
if (self) {
_tallRichHandsome = 0b00000101;//設定一個初值
}
return self;
}
/* mask碼 tall的mask碼:二進制 0b 00000001 ---> 十進制 1 rich的mask碼:二進制 0b 00000010 ---> 十進制 2 handsome的mask碼:二進制 0b 00000100 ---> 十進制 4 */
- (BOOL)isTall {
return !!(_tallRichHandsome & 1);
}
- (BOOL)isRich {
return !!(_tallRichHandsome & 2);
}
- (BOOL)isHandsome {
return !!(_tallRichHandsome & 4);
}
@end
**************************運行結果**************************
2019-07-16 17:54:32.915636+0800 OC底層Runtime[2828:156639]
tall = 1,
rich = 0,
handsome = 1
Program ended with exit code: 0
複製代碼
上面的解決方案裏面,我是經過!!(_tallRichHandsome & mask值);
來轉換成BOOL值的,由於_tallRichHandsome & mask值
得出的結果,要麼是0,要麼是一個大於0的整數,所以經過兩次!
運算,能夠獲得對應的BOOL值,0對應NO
,大於0的數對應YES
。
mask碼的值能夠用二進制表示,也能夠用十進制表示,可是在具體的使用中,須要大量註釋代碼說明mask碼所表明的含義,所以更好的處理方法,能夠將它們定義爲宏,經過宏的名字來表述所須要的含義。改寫以下:
#define CLTallMask 1
#define CLRichMask 2
#define CLHandsomeMask 4
- (BOOL)isTall {
return !!(_tallRichHandsome & CLTallMask);
}
- (BOOL)isRich {
return !!(_tallRichHandsome & CLRichMask);
}
- (BOOL)isHandsome {
return !!(_tallRichHandsome & CLHandsomeMask);
}
複製代碼
可是還有一個問題,從宏定義裏面,咱們不容易看出到底mask碼是要取出哪一位的值,因此,改爲二進制表示更好,以下
#define CLTallMask 0b00000001
#define CLRichMask 0b00000010
#define CLHandsomeMask 0b00000100
複製代碼
可是仍然不完美,作開發的哪一個沒有點強迫症,寫這麼一大串二進制,太麻煩了,因此咱們有更犀利的方法,沒錯,經過位移運算符來表示,以下
#define CLTallMask (1 << 0)
#define CLRichMask (1 << 1)
#define CLHandsomeMask (1 << 2)
複製代碼
1表明0b00000001,也就是二進制的1,
1 << 0
表示左移0位,也就是不移動,那麼就表明去右邊最低位上的值,同理,1 << 1
,1<< 2
就分別表示取右起第二位和第三位上的值,這樣就清晰易懂了。
爲何叫MASK?
剛接觸編程的時候,我曾經很困惑,用來獲取特定位上的內容的這一串二進制碼爲何在英文裏叫mask
,這個mask
爲何要翻譯成掩碼?不知道你們有沒有困惑過。後來想着想着,忽然開竅了了,這個mask
是用來拿到特定位上的值,也就是查看你想要看到的部位。mask
這個單詞的含義裏面有 面具 的意思,面具總知道吧,就下面這個面具上的幾個洞,分別是眼睛和嘴,由於你去參加面具party的時候,只想讓人看見眼鏡和嘴巴,其餘地方都遮掩起來。咱們在特定位上面的取值,不是跟這個同樣嗎,所以老外給這個東西取名叫mask碼,其實就是爲了形象生動,根本不是啥高大上的東西。只不過中文翻譯我我的以爲太生硬了,翻譯成 面具碼 豈不是更好。小感慨一下,英文技術文檔裏面有挺多這種翻譯過來很奇怪的名詞,其實就是文化差別,老外從他們的文化角度去給一些概念進行了生動形象的命名,但到了咱們這邊的確是翻譯的慘不忍睹,簡直就是量產羅玉鳳啊!!因此學好英文仍是很重要的,有些翻譯真是害死人。
接下來看一看如何把外部設定的值保存到對應的位上面去,並且不能影響到其餘位上面的值。 (1)設值爲1。正好按位或運算(|
)就能實現這裏的要求。來回顧一下或運算的規則
- 0 | 0 = 0
- 0 | 1 = 1
- 1 | 0 = 1
- 1 | 1 = 1
所以根據上面的特色,跟mask
碼進行或運算(|
)就能夠將特定值設置到目標位中。由於mask
碼中,對應目標位的就是1,對應非目標位的就是0。
(2)設值爲0。上面還介紹了經過按位與運算(&
)取值,結合這裏的需求,能夠發現,只須要將mask
碼按位取反以後,在與目標對象進行與運算(&
),即可以將指定位設置爲0。 對飲實現代碼以下
#import "CLPerson.h"
#define CLTallMask (1 << 0)
#define CLRichMask (1 << 1)
#define CLHandsomeMask (1 << 2)
@interface CLPerson()
{
char _tallRichHandsome;
}
@end
@implementation CLPerson
- (instancetype)init
{
self = [super init];
if (self) {
_tallRichHandsome = 0b00000101;
}
return self;
}
//取值操做
- (BOOL)isTall {
return !!(_tallRichHandsome & CLTallMask);
}
- (BOOL)isRich {
return !!(_tallRichHandsome & CLRichMask);
}
- (BOOL)isHandsome {
return !!(_tallRichHandsome & CLHandsomeMask);
}
//設定值操做
- (void)setTall:(BOOL)tall {
if(tall) {
_tallRichHandsome |= CLTallMask;
} else {
_tallRichHandsome &= ~CLTallMask;
}
}
- (void)setRich:(BOOL)rich {
if(rich) {
_tallRichHandsome |= CLRichMask;
} else {
_tallRichHandsome &= ~CLRichMask;
}
}
- (void)setHandsome:(BOOL)handsome {
if(handsome) {
_tallRichHandsome |= CLHandsomeMask;
} else {
_tallRichHandsome &= ~CLHandsomeMask;
}
}
@end
複製代碼
調用及打印結果
int main(int argc, const char * argv[]) {
@autoreleasepool {
CLPerson *person = [[CLPerson alloc] init];
person.rich = YES;
person.tall = YES;
person.handsome = YES;
NSLog(@"\ntall = %d, \nrich = %d, \nhandsome = %d", person.isTall, person.isRich, person.isHandsome);
}
return 0;
}
***********************************************
2019-08-02 11:36:49.081651+0800 OC底層Runtime[1147:65497]
tall = 1,
rich = 1,
handsome = 1
Program ended with exit code: 0
複製代碼
能夠看到設定成功。經過以上的嘗試,就將本來須要3個字節來表示的信息,存儲到了一個字節裏面,以達到節省空間的目的。
上面的篇幅,咱們經過|
和&
兩種位運算,實現節約內存的目標,請思考一下,這樣是否完美了呢? 細細分析一下,會發現有以下不足:
- 後期的維護時,假如咱們有須要新增一個新的屬性,那麼就須要 增長一個對應的
mask
碼,增長對應的set
方法, 增長對應的getter
方法,仍是相對麻煩的,並且代碼體積也會迅速增長。- 咱們經過
char _tallRichHandsome;
表達了三個信息——tall
、rich
、handsome
,若是須要表示10個信息,可想而知這裏的命名會很是長,顯然擴展性和可讀性都很是差。
如今來看一下下面這段代碼
@interface CLPerson()
{
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
}_tallRichHandsome;
// char _tallRichHandsome;
}
@end
複製代碼
代碼中,使用結構體struct取代以前的char _tallRichHandsome
,結構體內有三個成員——tall
、rich
、handsome
。每一個成員後面的: 1
表明這個成員佔用1個位。成員前面的類型關鍵字不產生實際做用,只不過定義變量的語法規定須要有類型關鍵字,這裏爲了統一都寫成char
,成員實際佔用內存的大小由後面的這個: X
來表示,X就表示佔用的位數。這個就是位域,關於這個概念的具體內容,能夠自行查看C語言相關基礎知識。由於struct
做爲一個總體單元,分配內存的最小單位是一個字節,那麼tall
、rich
、handsome
這三個成員會按照前後定義的順序,在這一個字節的8位空間裏面,從右至左排布。 相應地,下面須要調整一下對應的getter
/setter
方法
******************************CLPerson.m*************************************
@implementation CLPerson
- (BOOL)isTall {
return _tallRichHandsome.tall;
}
- (BOOL)isRich {
return _tallRichHandsome.rich;
}
- (BOOL)isHandsome {
return _tallRichHandsome.handsome;
}
- (void)setTall:(BOOL)tall {
_tallRichHandsome.tall = tall;
}
- (void)setRich:(BOOL)rich {
_tallRichHandsome.rich = rich;
}
- (void)setHandsome:(BOOL)handsome {
_tallRichHandsome.handsome = handsome;
}
@end
******************************main.m*************************************
int main(int argc, const char * argv[]) {
@autoreleasepool {
CLPerson *person = [[CLPerson alloc] init];
person.tall = NO;
person.rich = YES;
person.handsome = NO;
NSLog(@"\ntall = %d, \nrich = %d, \nhandsome = %d", person.isTall, person.isRich, person.isHandsome);
}
return 0;
}
******************************打印輸出*************************************
2019-08-02 14:53:31.980516+0800 OC底層Runtime[1333:126711]
tall = 0,
rich = -1,
handsome = 0
Program ended with exit code: 0
複製代碼
從上面的輸出結果能夠看出,貌似getter
/setter
像是生效了,可是好像rich
有點問題,設置成YES
,最後打印出來了是-1
,應該是1
才符合預期的,這個問題先無論後面來解決,咱們能夠加一個斷點,開看一下結構體_tallRichHandsome
狀況 也能夠在lldb窗口經過命令獲得
lldb) p/x person->_tallRichHandsome
((anonymous struct)) $0 = (tall = 0x00, rich = 0x01, handsome = 0x00)
(lldb)
複製代碼
結果很清晰的顯示了,三個成員tall
、rich
、handsome
的值確實是被正確設置了。 此外,還能夠經過直接查看_tallRichHandsome
的內存中的狀況,來講明結果。 首先經過下面命令拿到_tallRichHandsome
的內存地址
(lldb) p/x &(person->_tallRichHandsome)
((anonymous struct) *) $1 = 0x00000001033025c8
複製代碼
而後經過命令查看該地址所對應內存的狀況
(lldb) x 0x00000001033025c8
0x1033025c8: 02 00 00 00 00 00 00 00 41 f0 2f 96 ff ff 1d 00 ........A./.....
0x1033025d8: 80 12 00 00 01 00 00 00 06 00 05 00 05 00 00 00 ................
複製代碼
這個結果怎麼看呢,首先要知道,這種打印方式,是按照16進制來顯示的,那麼每2個數字就表明一個字節,上面咱們說了_tallRichHandsome
實際佔用1個字節大小,因此它對應的值應該是打印結果中的最開始的2個數字 02
,而這個值轉換成二進制是0000 0010
,三個成員tall
、rich
、handsome
在其中對應的位上的值分別是0
、1
、0
,這樣就和咱們的設定吻合了,證實了咱們的getter
/setter
方法生效了。
回到上面咱們遺留的問題,爲何被設置成YES
的成員,內存裏面驗證了沒有問題,爲什麼最終被打印出來的倒是-1
呢?緣由在於,getter
方法中返回值的時候,作了一次強制轉換。如何理解呢 咱們經過下面的方法驗證,將rich的getter方法調整以下,並在返回的地方加上斷點
- (BOOL)isRich {
BOOL ret = _tallRichHandsome.rich;
return ret;
}
複製代碼
經過lldb打印ret的內存結果以下
(lldb) p/x &ret (BOOL *) $0 = 0x00007ffeefbff42f 255
(lldb) x 0x00007ffeefbff42f
0x7ffeefbff42f: ff bc 9e a9 7b ff 7f 00 00 70 e4 80 01 01 00 00 ....{....p......
0x7ffeefbff43f: 00 80 f4 bf ef fe 7f 00 00 95 0c 00 00 01 00 00 ................
(lldb)
複製代碼
能夠看到ret內存裏面是ff,也就是二進制的11111111,確實如咱們上面所說,結果在強轉是有這個問題,
實際上,在轉換的時候,是根據對象值的最左邊位上的值進行補值填充操做的,由於NO對應的是0,一位二進制的0轉換成BOOL,其他位上都補0,因此不會影響最終結果。
至於這裏爲何一個字節上的11111111被輸出的時候顯示-1,有疑問的話請複習一下有符號數的表達方式,這裏不作贅述。 對於當前的這個問題,解決辦法也很多,咱們能夠用以前進行兩次!運算,就能夠獲得1了
- (BOOL)isRich {
return !!_tallRichHandsome.rich;;
}
複製代碼
或者,能夠擴充一下成員信息所須要的位數
@interface CLPerson()
{
struct {
char tall : 2;
char rich : 2;
char handsome : 2;
}_tallRichHandsome;
// char _tallRichHandsome;
}
@end
複製代碼
這樣,若是誰須要設置成YES
,由於佔用了2位,因此結果會是0b01
,按照補位填充的規則,應該是0b0000 0001
,不會影響最終值。
小結:使用上面的優化方案,咱們精減了getter/setter的代碼實現,還省去了mask碼。缺點是在取值的時候因爲存在補位轉換,致使最終取值不夠精準(第一種方案經過位運算取值的方式不存在這個問題)。
接下來,咱們來研究一下蘋果採用的優化方案。蘋果其實是基於上面第一種方案中的位運算方法,結合聯合體/共用體(union
)這個技術來實現的。
首先來回顧一下union
這個概念,
union Person {
char * name;//佔用8個字節
int age; // 佔用 4個字節
bool isMale ; //佔用1個字節
};
複製代碼
系統會爲union Person
分配8個字節空間,它的3個成員共用這一段8字節的空間。對比一下struct
的定義
struct Person {
char * name;//佔用8個字節
int age; // 佔用 4個字節
bool isMale ; //佔用1個字節
};
複製代碼
根據內存對其原則,系統爲struct Person
分配16字節內存,其3個成員會擁有各自獨立使用的內存空間。 用一張圖來總結以下
回到關於蘋果優化的問題,首先看以下代碼
@interface CLPerson()
{
union {
char bits;
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
};
} _tallRichHandsome;
}
@end
複製代碼
將_tallRichHandsome
定義成一個union
,union
中的定義的成員是共享內存空間的,按照上面的寫法,咱們在實際進行位運算實現getter/setter
的時候,使用char bits;
,bits
就是不少位的意思,具體要多少位,靠它前面的類型關鍵字來肯定,這裏咱們須要8位就夠,因此經過char
來定義。由於下面的struct
和char bits;
是共享內存的,實際使用中不會用到這個struct
,可是能夠藉助它來解釋bits
裏面各個位所表明的含義,體會一下。那麼getter/setter修改以下
@implementation CLPerson
- (BOOL)isTall {
return !!(_tallRichHandsome.bits & CLTallMask);
}
- (BOOL)isRich {
return !!(_tallRichHandsome.bits & CLRichMask);
}
- (BOOL)isHandsome {
return !!(_tallRichHandsome.bits & CLHandsomeMask);
}
- (void)setTall:(BOOL)tall {
if(tall) {
_tallRichHandsome.bits |= CLTallMask;
} else {
_tallRichHandsome.bits &= ~CLTallMask;
}
}
- (void)setRich:(BOOL)rich {
if(rich) {
_tallRichHandsome.bits |= CLRichMask;
} else {
_tallRichHandsome.bits &= ~CLRichMask;
}
}
- (void)setHandsome:(BOOL)handsome {
if(handsome) {
_tallRichHandsome.bits |= CLHandsomeMask;
} else {
_tallRichHandsome.bits &= ~CLHandsomeMask;
}
}
@end
*************************main.m***************************
int main(int argc, const char * argv[]) {
@autoreleasepool {
CLPerson *person = [[CLPerson alloc] init];
person.tall = NO;
person.rich = YES;
person.handsome = NO;
NSLog(@"\ntall = %d, \nrich = %d, \nhandsome = %d", person.isTall, person.isRich, person.isHandsome);
}
return 0;
}
************************輸出結果**************************
2019-08-02 17:20:07.157392+0800 OC底層Runtime[1673:197854]
tall = 0,
rich = 1,
handsome = 0
Program ended with exit code: 0
複製代碼
能夠看到,成功實現了getter/setter
的需求。實際上和咱們第一種方案裏面的char _tallRichHandsome
的使用是徹底相同的,只不過這裏換成了_tallRichHandsome.bits
,不一樣的是這裏咱們經過union
中的struct
來加強代碼的可讀性,其實用下面的寫法,省略掉struct
定義,獲得的結果徹底相同
@interface CLPerson()
{
union {
char bits;
} _tallRichHandsome;
}
@end
複製代碼
須要注意的是,第一種方案裏面,對於成員信息(
tall
、rich
、handsome
)在內存裏面的位置,咱們是經過結構體來定義的,而蘋果的方案裏面,則實際上依靠mask碼來控制的,mask碼的位移數就表明了成員信息的位置,而union
裏面的那個struct
最重要的做用就是解釋說明bits
內部的成員信息,就是爲了加強可讀性,就是爲了讓人容易看懂。因此之後在閱讀源碼的時候再看到這種union
,不再用懼怕了,就那麼回事。
如今回到開篇的有關isa的源碼
struct objc_object {
private:
isa_t isa;
public:
複製代碼
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;
};
#endif
};
複製代碼
這裏,精減掉了一些兼容性代碼,只保留針對iOS部分的代碼,根據本文研究的一些話題,咱們能夠將蘋果對isa的優化歸納爲
經過位運算和位域以及聯合體技術,更加充分的利用了isa的內存空間,將對象的真正的地址存放在了isa內存的其中33位上面,其他的31位被用來存放對象相關的其餘信息。下面是isa其餘位上的做用說明
nonpointer
—— 0,表明普通指針,存儲着class、meta-class對象的內存地址;1,表明優化過,使用位域存儲更多信息has_assoc
—— 是否設置過關聯對象,若是沒有,施放時會速度更快has_cxx_dtor
—— 是否有C++的稀構函數,若是沒有,施放時會更快shiftcls
—— 這個部分存儲的是真正的Class、Meta-Class對象的內存地址信息,所以要經過isa & ISA_MASK
才能取出這裏33位的值,獲得對象的真正地址。magic
—— 用於在調試的時候分辨對象是否完成了初始化weekly_referenced
—— 是否被弱飲用指針指向過,若是沒有,釋放時會更快extra_rc
—— 裏面存儲的值是 引用計數 - 1deallocating
——對象是否正在被釋放has_sidtable_rc
——引用計數器是否過大沒法存儲在isa中,若果是,這裏就爲1,引用計數就會被存儲在一個叫SideTable的類的屬性中。
爲何上面的has_assoc
、has_cxx_dtor
、weekly_referenced
會影響對象釋放的速度呢?objc源碼裏面有答案:對象在釋放的時候,會調用void *objc_destructInstance(id obj)
方法
/*********************************************************************** * objc_destructInstance * Destroys an instance without freeing memory. * Calls C++ destructors. * Calls ARC ivar cleanup. * Removes associative references. * Returns `obj`. Does nothing if `obj` is nil. **********************************************************************/
void *objc_destructInstance(id obj) {
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
複製代碼
從源碼的註釋以及實現邏輯,很容易看出,程序會
obj->hasCxxDtor()
來決定是否調用object_cxxDestruct(obj)
進行C++析構,obj->hasAssociatedObjects()
來決定是否調用_object_remove_assocations(obj)
進行關聯對象引用的移除。在obj->clearDeallocating();
裏面isa.weakly_referenced
和isa.has_sidetable_rc
會決定是否進行 weak_clear_no_lock(&table.weak_table, (id)this);
和table.refcnts.erase(this);
操做。 所以isa中上述的這幾個值會影響到對象釋放的速度。
我在詳解isa&superclass指針中有過以下總結 而本文開篇的iOS源碼裏面中有以下規定,在iOS下(也就是arm64),
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
複製代碼
上面的ISA_MASK
是經過16進製表示的,不太方便看,咱們經過科學計算器轉換一下
這樣能夠清晰的看到,經過 isa & ISA_MASK
取出來的究竟是哪幾位上面的值。同時還能夠發現一個小細節,最終得出來的對象的地址值,會獲得36個有效二進制位,而最後的四位,只多是 1000
或者 0000
,也就是十六進制下的 8
或0
,所以對象的地址最後一位(十六進制下),必定是8
或0
。體會一下,而後經過代碼走一波
#import "ViewController.h"
#import <objc/runtime.h>
#import "CLPerson.h"
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"ViewController類對象地址:%p", [ViewController class]);
NSLog(@"ViewController元類對象地址:%p", object_getClass([ViewController class]));
NSLog(@"CLPerson類對象地址:%p", [CLPerson class]);
NSLog(@"CLPerson元類對象地址:%p", object_getClass([CLPerson class]));
}
@end
*************************************************************打印輸出
2019-08-05 10:49:42.408303+0800 iOS-Runtime[1276:57991] ViewController類對象地址:0x103590dc8
2019-08-05 10:49:42.408405+0800 iOS-Runtime[1276:57991] ViewController元類對象地址:0x103590df0
2019-08-05 10:49:42.408481+0800 iOS-Runtime[1276:57991] CLPerson類對象地址:0x103590e90
2019-08-05 10:49:42.408565+0800 iOS-Runtime[1276:57991] CLPerson元類對象地址:0x103590e68
(lldb)
複製代碼
有關isa的探討到這裏就結束了。
Runtime原理探究(一)—— isa的深刻體會(蘋果對isa的優化)