Runtime系列文章
Runtime原理探究(一)—— isa的深入體會(huì)(蘋果對(duì)isa的優(yōu)化)
Runtime原理探究(二)—— Class結(jié)構(gòu)的深入分析
Runtime原理探究(三)—— OC Class的方法緩存cache_t
Runtime原理探究(四)—— 刨根問底消息機(jī)制
Runtime原理探究(五)—— super的本質(zhì)
[Runtime原理探究(六)—— Runtime的應(yīng)用...待續(xù)]-()
[Runtime原理探究(七)—— Runtime的API...待續(xù)]-()
Runtime原理探究(八)—— 面試題中的Runtime
????本文篇幅比較長(zhǎng)哟绊,創(chuàng)作的目的并不是為了在簡(jiǎn)書上刷贊和閱讀量椿胯,而是為了自己日后溫習(xí)知識(shí)所用硼砰。如果有幸被你發(fā)現(xiàn)這篇文章,并且引起了你的閱讀興趣顶掉,請(qǐng)休息充分,靜下心來,精力充足地開始閱讀幽邓,希望這篇文章能對(duì)你有所幫助拐揭。如發(fā)現(xiàn)任何有誤之處撤蟆,肯請(qǐng)留言糾正,謝謝堂污。????
如何理解Objective-C的動(dòng)態(tài)特性家肯?
很多靜態(tài)編程語言,編寫完代碼后盟猖,經(jīng)過編譯連接生成可執(zhí)行文件讨衣,最后就可以在電腦上運(yùn)行起來。
以C語言為例
void test() {
printf("Hello World");
}
int main() {
test();
}
以上代碼經(jīng)過編譯之后式镐,main
函數(shù)里面就一定會(huì)調(diào)用test()
反镇,而test()
的實(shí)現(xiàn)也一定會(huì)是和代碼中寫的一樣,這些在編譯完成那一刻就決定了娘汞,運(yùn)行過程中不會(huì)發(fā)生改變的歹茶。C可以說就是典型的靜態(tài)語言。
與之相比,Objective-C就可以在運(yùn)行階段修改之前編譯階段確定好的一些函數(shù)和方法辆亏。
************************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];
這句代碼,在運(yùn)行階段扮叨,可以調(diào)用CLPerson
的test
方法缤弦,也可以通過OC的動(dòng)態(tài)特性,使其最終調(diào)用別的方法彻磁,例如abc
方法碍沐,甚至,還可以調(diào)用另外一個(gè)類的方法衷蜓。除此之外累提,OC還可以在程序運(yùn)行階段,給類增加方法等磁浇,這就是所謂的動(dòng)態(tài)特性斋陪。
Runtime簡(jiǎn)介
- Objective-C是一門動(dòng)態(tài)性比較強(qiáng)的編程語言,根C置吓、C++等語言有很大不同
- Objective-C的動(dòng)態(tài)性是由Runtime API來支撐的
- Runtime API提供的接口基本都是C語言的无虚,源碼由C/C++/匯編語言編寫
isa詳解
深入Runtime之前,先要解決一個(gè)比較重要的概念——isa
衍锚。在早期的Runtime里面友题,isa
指針直接指向class/meta-class
對(duì)象的地址,isa
就是一個(gè)普通的指針戴质。
后來度宦,蘋果從ARM64位架構(gòu)開始,對(duì)isa
進(jìn)行了優(yōu)化告匠,將其定義成一個(gè)共用體(union
)結(jié)構(gòu)戈抄,結(jié)合 位域 的概念以及 位運(yùn)算 的方式來存儲(chǔ)更多類相關(guān)信息。isa
指針需要通過與一個(gè)叫ISA_MASK的值(掩碼)進(jìn)行二進(jìn)制&
運(yùn)算后专,才能得到真實(shí)的class/meta-class
對(duì)象的地址呛凶。接下來,就具體探究一下蘋果究竟是怎么優(yōu)化的行贪。
首先從源碼角度漾稀,對(duì)比一下變化isa
優(yōu)化前后的變化
***************************************
typedef struct objc_class *Class;
typedef struct objc_object {
Class isa;
} *id;
上面是64位之前,objc_object
的定義如上建瘫,isa直接指向objc_class
崭捍。
再看看優(yōu)化后objc_object
的定義
struct objc_object {
private:
isa_t isa;
public:
arm64開始,isa
的類型變成了isa_t
啰脚,這是什么鬼殷蛇?這個(gè)就是接下來討論的重點(diǎn)实夹,先看一下它的源碼
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
};
上面的代碼就是蘋果對(duì)于isa優(yōu)化的精華所在,為了看懂上面的代碼粒梦,首先需要從一些基礎(chǔ)知識(shí)開始說亮航。
場(chǎng)景需求分析
首先定義一個(gè)類CLPerson,首先給CLPerson增加幾個(gè)屬性以及成員變量
@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;
對(duì)于它們的使用匀们,無需多說缴淋,如下
#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類對(duì)象的內(nèi)存占用情況
2019-07-16 13:15:04.083828+0800 OC底層Runtime[2509:80387] 16
Program ended with exit code: 0
通過我之前對(duì)與對(duì)象內(nèi)存布局的分析的文章泄朴,這里可以得出如下結(jié)論:
-
isa
占用了8個(gè)字節(jié) -
_rich
重抖、_tall
、_handsome
這三個(gè)成員變量個(gè)占用1個(gè)字節(jié) - 因?yàn)橛袃?nèi)存對(duì)齊和bucketSized的因素祖灰,所以類對(duì)象占用16個(gè)字節(jié)的內(nèi)存空間钟沛。
??????但是
_rich
、_tall
局扶、_handsome
實(shí)際上只可能有2個(gè)值恨统,YES
/NO
,也就是0和1三妈,它們完全可以用一個(gè)二進(jìn)制位來表示延欠,三個(gè)加在一起也就只需要占用3個(gè)二進(jìn)制位,連半個(gè)字節(jié)都用不了沈跨。有什么方法可以實(shí)現(xiàn)這種節(jié)約內(nèi)存的需求呢???????
如果直接用屬性的話兔综,肯定就會(huì)自動(dòng)生成帶下劃線的成員變量饿凛,這樣就無法精簡(jiǎn)內(nèi)存。所以需要手動(dòng)實(shí)現(xiàn)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文件里面涧窒,用一個(gè)char _tallRichHandsome;
(一個(gè)字節(jié))來存儲(chǔ)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
這三個(gè)信息,有什么方法可以辦到呢慧瘤?
取值
首先我們來解決getter
方法戴已,也就是取值問題。如何從特定的位里面取出值呢锅减?沒錯(cuò)糖儡,——&
(按位與運(yùn)算)。
假設(shè)我們規(guī)定
-
tall用
_tallRichHandsome
的右起第1
位表示怔匣, -
rich用
_tallRichHandsome
的右起第2
位表示握联, -
handsome用
_tallRichHandsome
的右起第3
位表示, - 并且tall=
YES
,rich=NO
金闽, handsome=YES
纯露,
那么_tallRichHandsome
的值應(yīng)該是 0000 0101
tall (YES) | rich (NO) | handsome (YES) | |
---|---|---|---|
_tallRichHandsome | 0000 0101 | 0000 0101 | 0000 0101 |
mask碼(用來取值) | &0000 0001 | &0000 0010 | &0000 0100 |
通過&運(yùn)算得到結(jié)果 | 0000 0001 | 0000 0000 | 0000 0100 |
根據(jù)&
運(yùn)算的特點(diǎn),想要取出特定位上面的值代芜,只需將mask
碼中對(duì)應(yīng)位設(shè)置為1埠褪,因?yàn)?原來值 & 1 = 原來值
,將mask碼中其他位的設(shè)置為0蜒犯,就可以屏蔽出特定位之外其余位上面的值组橄,因?yàn)?原來值 & 0 = 0
,這個(gè)應(yīng)該很好理解罚随。至于取出來的值如何轉(zhuǎn)化成我們所需要的值(在這里我們需要的是YES/NO
)蝌借,就有很多辦法了。好了怠益,現(xiàn)在去代碼里面實(shí)現(xiàn)一下霜幼。如下所示
*************************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;//設(shè)定一個(gè)初值
}
return self;
}
/*
mask碼
tall的mask碼:二進(jìn)制 0b 00000001 ---> 十進(jìn)制 1
rich的mask碼:二進(jìn)制 0b 00000010 ---> 十進(jìn)制 2
handsome的mask碼:二進(jìn)制 0b 00000100 ---> 十進(jìn)制 4
*/
- (BOOL)isTall {
return !!(_tallRichHandsome & 1);
}
- (BOOL)isRich {
return !!(_tallRichHandsome & 2);
}
- (BOOL)isHandsome {
return !!(_tallRichHandsome & 4);
}
@end
**************************運(yùn)行結(jié)果**************************
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值);
來轉(zhuǎn)換成BOOL值的潮改,因?yàn)?code>_tallRichHandsome & mask值得出的結(jié)果狭郑,要么是0,要么是一個(gè)大于0的整數(shù)汇在,因此通過兩次!
運(yùn)算翰萨,可以得到對(duì)應(yīng)的BOOL值,0對(duì)應(yīng)NO
糕殉,大于0的數(shù)對(duì)應(yīng)YES
亩鬼。
mask碼的值可以用二進(jìn)制表示,也可以用十進(jìn)制表示阿蝶,但是在具體的使用中雳锋,需要大量注釋代碼說明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);
}
但是還有一個(gè)問題筑煮,從宏定義里面辛蚊,我們不容易看出到底mask碼是要取出哪一位的值,所以真仲,改成二進(jìn)制表示更好嚼隘,如下
#define CLTallMask 0b00000001
#define CLRichMask 0b00000010
#define CLHandsomeMask 0b00000100
但是仍然不完美,做開發(fā)的哪個(gè)沒有點(diǎn)強(qiáng)迫癥袒餐,寫這么一大串二進(jìn)制飞蛹,太麻煩了谤狡,所以我們有更犀利的方法,沒錯(cuò)卧檐,通過位移運(yùn)算符來表示墓懂,如下
#define CLTallMask (1 << 0)
#define CLRichMask (1 << 1)
#define CLHandsomeMask (1 << 2)
1代表0b00000001,也就是二進(jìn)制的1霉囚,
1 << 0
表示左移0位捕仔,也就是不移動(dòng),那么就代表去右邊最低位上的值盈罐,同理榜跌,1 << 1
,1<< 2
就分別表示取右起第二位和第三位上的值盅粪,這樣就清晰易懂了钓葫。
為什么叫MASK?
剛接觸編程的時(shí)候票顾,我曾經(jīng)很困惑础浮,用來獲取特定位上的內(nèi)容的這一串二進(jìn)制碼為什么在英文里叫mask
,這個(gè)mask
為什么要翻譯成掩碼?不知道大家有沒有困惑過奠骄。后來想著想著豆同,突然開竅了了,這個(gè)mask
是用來拿到特定位上的值含鳞,也就是查看你想要看到的部位影锈。mask
這個(gè)單詞的含義里面有 面具 的意思,面具總知道吧蝉绷,就下面這個(gè)面具上的幾個(gè)洞鸭廷,分別是眼睛和嘴,因?yàn)槟闳⒓用婢遬arty的時(shí)候潜必,只想讓人看見眼鏡和嘴巴,其他地方都遮掩起來沃但。我們?cè)谔囟ㄎ簧厦娴娜≈荡殴觯皇歉@個(gè)一樣嗎,因此老外給這個(gè)東西取名叫mask碼宵晚,其實(shí)就是為了形象生動(dòng)垂攘,根本不是啥高大上的東西。只不過中文翻譯我個(gè)人覺得太生硬了淤刃,翻譯成 面具碼 豈不是更好晒他。小感慨一下,英文技術(shù)文檔里面有挺多這種翻譯過來很奇怪的名詞逸贾,其實(shí)就是文化差異陨仅,老外從他們的文化角度去給一些概念進(jìn)行了生動(dòng)形象的命名津滞,但到了我們這邊的確是翻譯的慘不忍睹,簡(jiǎn)直就是量產(chǎn)羅玉鳳白粕恕4バ臁!所以學(xué)好英文還是很重要的狐赡,有些翻譯真是害死人撞鹉。
設(shè)值
接下來看一看如何把外部設(shè)定的值保存到對(duì)應(yīng)的位上面去,而且不能影響到其他位上面的值颖侄。
(1)設(shè)值為1鸟雏。正好按位或運(yùn)算(|
)就能實(shí)現(xiàn)這里的要求。來回顧一下或運(yùn)算的規(guī)則
- 0 | 0 = 0
- 0 | 1 = 1
- 1 | 0 = 1
- 1 | 1 = 1
因此根據(jù)上面的特點(diǎn)览祖,跟mask
碼進(jìn)行或運(yùn)算(|
)就可以將特定值設(shè)置到目標(biāo)位中孝鹊。因?yàn)?code>mask碼中,對(duì)應(yīng)目標(biāo)位的就是1穴墅,對(duì)應(yīng)非目標(biāo)位的就是0惶室。
(2)設(shè)值為0。上面還介紹了通過按位與運(yùn)算(&
)取值玄货,結(jié)合這里的需求皇钞,可以發(fā)現(xiàn),只需要將mask
碼按位取反之后松捉,在與目標(biāo)對(duì)象進(jìn)行與運(yùn)算(&
)夹界,便可以將指定位設(shè)置為0。
對(duì)飲實(shí)現(xiàn)代碼如下
#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);
}
//設(shè)定值操作
- (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
調(diào)用及打印結(jié)果
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
可以看到設(shè)定成功隘世。通過以上的嘗試可柿,就將原本需要3個(gè)字節(jié)來表示的信息,存儲(chǔ)到了一個(gè)字節(jié)里面丙者,以達(dá)到節(jié)省空間的目的复斥。
位域
上面的篇幅,我們通過|
和&
兩種位運(yùn)算械媒,實(shí)現(xiàn)節(jié)約內(nèi)存的目標(biāo)目锭,請(qǐng)思考一下,這樣是否完美了呢纷捞?
細(xì)細(xì)分析一下痢虹,會(huì)發(fā)現(xiàn)有如下不足:
- 后期的維護(hù)時(shí),假如我們有需要新增一個(gè)新的屬性主儡,那么就需要 增加一個(gè)對(duì)應(yīng)的
mask
碼奖唯,增加對(duì)應(yīng)的set
方法, 增加對(duì)應(yīng)的getter
方法糜值,還是相對(duì)麻煩的丰捷,而且代碼體積也會(huì)迅速增加坯墨。- 我們通過
char _tallRichHandsome;
表達(dá)了三個(gè)信息——tall
、rich
瓢阴、handsome
畅蹂,如果需要表示10個(gè)信息,可想而知這里的命名會(huì)非常長(zhǎng)荣恐,顯然擴(kuò)展性和可讀性都非常差液斜。
現(xiàn)在來看一下下面這段代碼
@interface CLPerson()
{
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
}_tallRichHandsome;
// char _tallRichHandsome;
}
@end
代碼中,使用結(jié)構(gòu)體struct取代之前的char _tallRichHandsome
叠穆,結(jié)構(gòu)體內(nèi)有三個(gè)成員——tall
少漆、rich
、handsome
硼被。每個(gè)成員后面的: 1
代表這個(gè)成員占用1個(gè)位示损。成員前面的類型關(guān)鍵字不產(chǎn)生實(shí)際作用,只不過定義變量的語法規(guī)定需要有類型關(guān)鍵字嚷硫,這里為了統(tǒng)一都寫成char
检访,成員實(shí)際占用內(nèi)存的大小由后面的這個(gè): X
來表示,X就表示占用的位數(shù)仔掸。這個(gè)就是位域脆贵,關(guān)于這個(gè)概念的具體內(nèi)容,可以自行查看C語言相關(guān)基礎(chǔ)知識(shí)起暮。因?yàn)?code>struct作為一個(gè)整體單元卖氨,分配內(nèi)存的最小單位是一個(gè)字節(jié),那么tall
负懦、rich
筒捺、handsome
這三個(gè)成員會(huì)按照先后定義的順序,在這一個(gè)字節(jié)的8位空間里面纸厉,從右至左排布系吭。
相應(yīng)地,下面需要調(diào)整一下對(duì)應(yīng)的
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
從上面的輸出結(jié)果可以看出颗品,貌似getter
/setter
像是生效了肯尺,但是好像rich
有點(diǎn)問題,設(shè)置成YES
抛猫,最后打印出來了是-1
蟆盹,應(yīng)該是1
才符合預(yù)期的孩灯,這個(gè)問題先不管后面來解決闺金,我們可以加一個(gè)斷點(diǎn),開看一下結(jié)構(gòu)體_tallRichHandsome
情況
也可以在lldb窗口通過命令得到
lldb) p/x person->_tallRichHandsome
((anonymous struct)) $0 = (tall = 0x00, rich = 0x01, handsome = 0x00)
(lldb)
結(jié)果很清晰的顯示了峰档,三個(gè)成員tall
败匹、rich
寨昙、handsome
的值確實(shí)是被正確設(shè)置了。
此外掀亩,還可以通過直接查看_tallRichHandsome
的內(nèi)存中的情況舔哪,來說明結(jié)果。
首先通過下面命令拿到_tallRichHandsome
的內(nèi)存地址
(lldb) p/x &(person->_tallRichHandsome)
((anonymous struct) *) $1 = 0x00000001033025c8
然后通過命令查看該地址所對(duì)應(yīng)內(nèi)存的情況
(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 ................
這個(gè)結(jié)果怎么看呢槽棍,首先要知道捉蚤,這種打印方式,是按照16進(jìn)制來顯示的炼七,那么每2個(gè)數(shù)字就代表一個(gè)字節(jié)缆巧,上面我們說了_tallRichHandsome
實(shí)際占用1個(gè)字節(jié)大小,所以它對(duì)應(yīng)的值應(yīng)該是打印結(jié)果中的最開始的2個(gè)數(shù)字 02
豌拙,而這個(gè)值轉(zhuǎn)換成二進(jìn)制是0000 0010
陕悬,三個(gè)成員tall
、rich
按傅、handsome
在其中對(duì)應(yīng)的位上的值分別是0
捉超、1
、0
唯绍,這樣就和我們的設(shè)定吻合了拼岳,證明了我們的getter
/setter
方法生效了。
回到上面我們遺留的問題推捐,為什么被設(shè)置成YES
的成員裂问,內(nèi)存里面驗(yàn)證了沒有問題,為何最終被打印出來的卻是-1
呢牛柒?原因在于堪簿,getter
方法中返回值的時(shí)候,做了一次強(qiáng)制轉(zhuǎn)換皮壁。如何理解呢
我們通過下面的方法驗(yàn)證椭更,將rich的getter方法調(diào)整如下,并在返回的地方加上斷點(diǎn)
- (BOOL)isRich {
BOOL ret = _tallRichHandsome.rich;
return ret;
}
通過lldb打印ret的內(nèi)存結(jié)果如下
(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內(nèi)存里面是ff蛾魄,也就是二進(jìn)制的11111111虑瀑,確實(shí)如我們上面所說,結(jié)果在強(qiáng)轉(zhuǎn)是有這個(gè)問題滴须,
實(shí)際上舌狗,在轉(zhuǎn)換的時(shí)候,是根據(jù)對(duì)象值的最左邊位上的值進(jìn)行補(bǔ)值填充操作的扔水,因?yàn)镹O對(duì)應(yīng)的是0痛侍,一位二進(jìn)制的0轉(zhuǎn)換成BOOL,其余位上都補(bǔ)0魔市,所以不會(huì)影響最終結(jié)果主届。
至于這里為什么一個(gè)字節(jié)上的11111111被輸出的時(shí)候顯示-1赵哲,有疑問的話請(qǐng)復(fù)習(xí)一下有符號(hào)數(shù)的表達(dá)方式,這里不做贅述君丁。
對(duì)于當(dāng)前的這個(gè)問題枫夺,解決辦法也不少,我們可以用之前進(jìn)行兩次绘闷!運(yùn)算橡庞,就可以得到1了
- (BOOL)isRich {
return !!_tallRichHandsome.rich;;
}
或者,可以擴(kuò)充一下成員信息所需要的位數(shù)
@interface CLPerson()
{
struct {
char tall : 2;
char rich : 2;
char handsome : 2;
}_tallRichHandsome;
// char _tallRichHandsome;
}
@end
這樣印蔗,如果誰需要設(shè)置成YES
毙死,因?yàn)檎加昧?位,所以結(jié)果會(huì)是0b01
喻鳄,按照補(bǔ)位填充的規(guī)則扼倘,應(yīng)該是0b0000 0001
,不會(huì)影響最終值除呵。
小結(jié):使用上面的優(yōu)化方案再菊,我們精減了getter/setter的代碼實(shí)現(xiàn),還省去了mask碼颜曾。缺點(diǎn)是在取值的時(shí)候由于存在補(bǔ)位轉(zhuǎn)換纠拔,導(dǎo)致最終取值不夠精準(zhǔn)(第一種方案通過位運(yùn)算取值的方式不存在這個(gè)問題)。
共用體
接下來泛豪,我們來研究一下蘋果采用的優(yōu)化方案稠诲。蘋果實(shí)際上是基于上面第一種方案中的位運(yùn)算方法,結(jié)合聯(lián)合體/共用體(union
)這個(gè)技術(shù)來實(shí)現(xiàn)的诡曙。
首先來回顧一下union
這個(gè)概念臀叙,
union Person {
char * name;//占用8個(gè)字節(jié)
int age; // 占用 4個(gè)字節(jié)
bool isMale ; //占用1個(gè)字節(jié)
}; //
系統(tǒng)會(huì)為union Person
分配8個(gè)字節(jié)空間,它的3個(gè)成員共用這一段8字節(jié)的空間价卤。對(duì)比一下struct
的定義
struct Person {
char * name;//占用8個(gè)字節(jié)
int age; // 占用 4個(gè)字節(jié)
bool isMale ; //占用1個(gè)字節(jié)
}; //
根據(jù)內(nèi)存對(duì)其原則劝萤,系統(tǒng)為struct Person
分配16字節(jié)內(nèi)存,其3個(gè)成員會(huì)擁有各自獨(dú)立使用的內(nèi)存空間慎璧。
用一張圖來總結(jié)如下
回到關(guān)于蘋果優(yōu)化的問題床嫌,首先看如下代碼
@interface CLPerson()
{
union {
char bits;
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
};
} _tallRichHandsome;
}
@end
將_tallRichHandsome
定義成一個(gè)union
,union
中的定義的成員是共享內(nèi)存空間的胸私,按照上面的寫法厌处,我們?cè)趯?shí)際進(jìn)行位運(yùn)算實(shí)現(xiàn)getter/setter
的時(shí)候,使用char bits;
岁疼,bits
就是很多位的意思阔涉,具體要多少位,靠它前面的類型關(guān)鍵字來確定,這里我們需要8位就夠洒敏,所以通過char
來定義。因?yàn)橄旅娴?code>struct和char bits;
是共享內(nèi)存的疙驾,實(shí)際使用中不會(huì)用到這個(gè)struct
凶伙,但是可以借助它來解釋bits
里面各個(gè)位所代表的含義,體會(huì)一下它碎。那么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;
}
************************輸出結(jié)果**************************
2019-08-02 17:20:07.157392+0800 OC底層Runtime[1673:197854]
tall = 0,
rich = 1,
handsome = 0
Program ended with exit code: 0
可以看到函荣,成功實(shí)現(xiàn)了getter/setter
的需求。實(shí)際上和我們第一種方案里面的char _tallRichHandsome
的使用是完全相同的扳肛,只不過這里換成了_tallRichHandsome.bits
傻挂,不同的是這里我們通過union
中的struct
來增強(qiáng)代碼的可讀性,其實(shí)用下面的寫法挖息,省略掉struct
定義金拒,得到的結(jié)果完全相同
@interface CLPerson()
{
union {
char bits;
} _tallRichHandsome;
}
@end
需要注意的是,第一種方案里面套腹,對(duì)于成員信息(
tall
绪抛、rich
、handsome
)在內(nèi)存里面的位置电禀,我們是通過結(jié)構(gòu)體來定義的幢码,而蘋果的方案里面,則實(shí)際上依靠mask碼來控制的尖飞,mask碼的位移數(shù)就代表了成員信息的位置症副,而union
里面的那個(gè)struct
最重要的作用就是解釋說明bits
內(nèi)部的成員信息,就是為了增強(qiáng)可讀性政基,就是為了讓人容易看懂贞铣。所以以后在閱讀源碼的時(shí)候再看到這種union
,再也不用害怕了沮明,就那么回事咕娄。
蘋果isa優(yōu)化總結(jié)
現(xiàn)在回到開篇的有關(guān)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
};
這里,精減掉了一些兼容性代碼珊擂,只保留針對(duì)iOS部分的代碼圣勒,根據(jù)本文研究的一些話題,我們可以將蘋果對(duì)isa的優(yōu)化概括為
通過位運(yùn)算和位域以及聯(lián)合體技術(shù)摧扇,更加充分的利用了isa的內(nèi)存空間圣贸,將對(duì)象的真正的地址存放在了isa內(nèi)存的其中33位上面,其余的31位被用來存放對(duì)象相關(guān)的其他信息扛稽。下面是isa其他位上的作用說明
nonpointer
—— 0吁峻,代表普通指針,存儲(chǔ)著class、meta-class對(duì)象的內(nèi)存地址用含;1矮慕,代表優(yōu)化過,使用位域存儲(chǔ)更多信息has_assoc
—— 是否設(shè)置過關(guān)聯(lián)對(duì)象啄骇,如果沒有痴鳄,施放時(shí)會(huì)速度更快has_cxx_dtor
—— 是否有C++的稀構(gòu)函數(shù),如果沒有缸夹,施放時(shí)會(huì)更快shiftcls
—— 這個(gè)部分存儲(chǔ)的是真正的Class痪寻、Meta-Class對(duì)象的內(nèi)存地址信息,因此要通過isa & ISA_MASK
才能取出這里33位的值虽惭,得到對(duì)象的真正地址橡类。magic
—— 用于在調(diào)試的時(shí)候分辨對(duì)象是否完成了初始化weekly_referenced
—— 是否被弱飲用指針指向過,如果沒有芽唇,釋放時(shí)會(huì)更快extra_rc
—— 里面存儲(chǔ)的值是 引用計(jì)數(shù) - 1deallocating
——對(duì)象是否正在被釋放has_sidtable_rc
——引用計(jì)數(shù)器是否過大無法存儲(chǔ)在isa中顾画,若果是,這里就為1匆笤,引用計(jì)數(shù)就會(huì)被存儲(chǔ)在一個(gè)叫SideTable的類的屬性中亲雪。
為什么上面的has_assoc
、has_cxx_dtor
疚膊、weekly_referenced
會(huì)影響對(duì)象釋放的速度呢义辕?objc源碼里面有答案:對(duì)象在釋放的時(shí)候,會(huì)調(diào)用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;
}
從源碼的注釋以及實(shí)現(xiàn)邏輯寓盗,很容易看出灌砖,程序會(huì)
- 根據(jù)
obj->hasCxxDtor()
來決定是否調(diào)用object_cxxDestruct(obj)
進(jìn)行C++析構(gòu), - 根據(jù)
obj->hasAssociatedObjects()
來決定是否調(diào)用_object_remove_assocations(obj)
進(jìn)行關(guān)聯(lián)對(duì)象引用的移除傀蚌。
在obj->clearDeallocating();
里面
isa.weakly_referenced
和isa.has_sidetable_rc
會(huì)決定是否進(jìn)行weak_clear_no_lock(&table.weak_table, (id)this);
和table.refcnts.erase(this);
操作基显。因此isa中上述的這幾個(gè)值會(huì)影響到對(duì)象釋放的速度。
ISA_MASK的細(xì)節(jié)
我在詳解isa&superclass指針中有過如下總結(jié)
而本文開篇的iOS源碼里面中有如下規(guī)定善炫,在iOS下(也就是arm64)撩幽,
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
上面的ISA_MASK
是通過16進(jìn)制表示的,不太方便看箩艺,我們通過科學(xué)計(jì)算器轉(zhuǎn)換一下
isa & ISA_MASK
取出來的到底是哪幾位上面的值。同時(shí)還可以發(fā)現(xiàn)一個(gè)小細(xì)節(jié)艺谆,最終得出來的對(duì)象的地址值榨惰,會(huì)得到36個(gè)有效二進(jìn)制位,而最后的四位静汤,只可能是 1000
或者 0000
琅催,也就是十六進(jìn)制下的 8
或0
居凶,因此對(duì)象的地址最后一位(十六進(jìn)制下),一定是8
或0
藤抡。體會(huì)一下侠碧,然后通過代碼走一波
#import "ViewController.h"
#import <objc/runtime.h>
#import "CLPerson.h"
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"ViewController類對(duì)象地址:%p", [ViewController class]);
NSLog(@"ViewController元類對(duì)象地址:%p", object_getClass([ViewController class]));
NSLog(@"CLPerson類對(duì)象地址:%p", [CLPerson class]);
NSLog(@"CLPerson元類對(duì)象地址:%p", object_getClass([CLPerson class]));
}
@end
*************************************************************打印輸出
2019-08-05 10:49:42.408303+0800 iOS-Runtime[1276:57991] ViewController類對(duì)象地址:0x103590dc8
2019-08-05 10:49:42.408405+0800 iOS-Runtime[1276:57991] ViewController元類對(duì)象地址:0x103590df0
2019-08-05 10:49:42.408481+0800 iOS-Runtime[1276:57991] CLPerson類對(duì)象地址:0x103590e90
2019-08-05 10:49:42.408565+0800 iOS-Runtime[1276:57991] CLPerson元類對(duì)象地址:0x103590e68
(lldb)
有關(guān)isa的探討到這里就結(jié)束了。
Runtime系列文章
Runtime原理探究(一)—— isa的深入體會(huì)(蘋果對(duì)isa的優(yōu)化)
Runtime原理探究(二)—— Class結(jié)構(gòu)的深入分析
Runtime原理探究(三)—— OC Class的方法緩存cache_t
Runtime原理探究(四)—— 刨根問底消息機(jī)制
Runtime原理探究(五)—— super的本質(zhì)
[Runtime原理探究(六)—— Runtime的應(yīng)用...待續(xù)]-()
[Runtime原理探究(七)—— Runtime的API...待續(xù)]-()
Runtime原理探究(八)—— 面試題中的Runtime