Runtime原理探究(一)—— isa的深刻體會(蘋果對isa的優化)


Runtime系列文章

Runtime原理探究(一)—— isa的深刻體會(蘋果對isa的優化)面試

Runtime原理探究(二)—— Class結構的深刻分析編程

Runtime原理探究(三)—— OC Class的方法緩存cache_t緩存

Runtime原理探究(四)—— 刨根問底消息機制markdown

Runtime原理探究(五)—— super的本質架構

Runtime原理探究(六)—— 面試題中的Runtime編程語言


☕️☕️本文篇幅比較長,創做的目的並非爲了在簡書上刷贊和閱讀量,而是爲了本身往後溫習知識所用。若是有幸被你發現這篇文章,而且引發了你的閱讀興趣,請休息充分,靜下心來,精力充足地開始閱讀,但願這篇文章能對你有所幫助。如發現任何有誤之處,肯請留言糾正,謝謝。☕️☕️ide

如何理解Objective-C的動態特性?

不少靜態編程語言,編寫完代碼後,通過編譯鏈接生成可執行文件,最後就能夠在電腦上運行起來。函數

以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];這句代碼,在運行階段,能夠調用CLPersontest方法,也能夠經過OC的動態特性,使其最終調用別的方法,例如abc方法,甚至,還能夠調用另一個類的方法。除此以外,OC還能夠在程序運行階段,給類增長方法等,這就是所謂的動態特性

Runtime簡介

  • Objective-C是一門動態性比較強的編程語言,根C、C++等語言有很大不一樣
  • Objective-C的動態性是由Runtime API來支撐的
  • Runtime API提供的接口基本都是C語言的,源碼由C/C++/彙編語言編寫

isa詳解

深刻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個字節
  • 由於有內存對齊和bucketSized的因素,因此類對象佔用16個字節的內存空間。

🐞🐞🐞可是_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的後三位來分別存放tallrichhandsome這三個信息,有什麼方法能夠辦到呢?

取值

首先咱們來解決getter方法,也就是取值問題。如何從特定的位裏面取出值呢?沒錯,——&(按位與運算)

假設咱們規定

  • tall_tallRichHandsome的右起第1位表示,
  • rich_tallRichHandsome的右起第2位表示,
  • handsome_tallRichHandsome的右起第3位表示,
  • 而且tall=YESrich=NOhandsome=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 << 11<< 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;表達了三個信息——tallrichhandsome,若是須要表示10個信息,可想而知這裏的命名會很是長,顯然擴展性和可讀性都很是差。

如今來看一下下面這段代碼

@interface CLPerson()
{
    struct {
        char tall : 1;
        char rich : 1;
        char handsome : 1;
        
    }_tallRichHandsome;
    
// char _tallRichHandsome;
}

@end
複製代碼

代碼中,使用結構體struct取代以前的char _tallRichHandsome,結構體內有三個成員——tallrichhandsome。每一個成員後面的: 1表明這個成員佔用1個位。成員前面的類型關鍵字不產生實際做用,只不過定義變量的語法規定須要有類型關鍵字,這裏爲了統一都寫成char,成員實際佔用內存的大小由後面的這個: X來表示,X就表示佔用的位數。這個就是位域,關於這個概念的具體內容,能夠自行查看C語言相關基礎知識。由於struct做爲一個總體單元,分配內存的最小單位是一個字節,那麼tallrichhandsome這三個成員會按照前後定義的順序,在這一個字節的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) 
複製代碼

結果很清晰的顯示了,三個成員tallrichhandsome的值確實是被正確設置了。 此外,還能夠經過直接查看_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,三個成員tallrichhandsome在其中對應的位上的值分別是010,這樣就和咱們的設定吻合了,證實了咱們的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碼。缺點是在取值的時候因爲存在補位轉換,致使最終取值不夠精準(第一種方案經過位運算取值的方式不存在這個問題)。

d共用體

接下來,咱們來研究一下蘋果採用的優化方案。蘋果其實是基於上面第一種方案中的位運算方法,結合聯合體/共用體(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定義成一個unionunion中的定義的成員是共享內存空間的,按照上面的寫法,咱們在實際進行位運算實現getter/setter的時候,使用char bits;bits就是不少位的意思,具體要多少位,靠它前面的類型關鍵字來肯定,這裏咱們須要8位就夠,因此經過char來定義。由於下面的structchar 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
複製代碼

須要注意的是,第一種方案裏面,對於成員信息(tallrichhandsome)在內存裏面的位置,咱們是經過結構體來定義的,而蘋果的方案裏面,則實際上依靠mask碼來控制的,mask碼的位移數就表明了成員信息的位置,而union裏面的那個struct最重要的做用就是解釋說明bits內部的成員信息,就是爲了加強可讀性,就是爲了讓人容易看懂。因此之後在閱讀源碼的時候再看到這種union,不再用懼怕了,就那麼回事。


蘋果isa優化總結

如今回到開篇的有關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—— 裏面存儲的值是 引用計數 - 1
  • deallocating——對象是否正在被釋放
  • has_sidtable_rc——引用計數器是否過大沒法存儲在isa中,若果是,這裏就爲1,引用計數就會被存儲在一個叫SideTable的類的屬性中。

爲何上面的has_assochas_cxx_dtorweekly_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_referencedisa.has_sidetable_rc會決定是否進行 weak_clear_no_lock(&table.weak_table, (id)this);table.refcnts.erase(this);操做。 所以isa中上述的這幾個值會影響到對象釋放的速度。


ISA_MASK的細節

我在詳解isa&superclass指針中有過以下總結 而本文開篇的iOS源碼裏面中有以下規定,在iOS下(也就是arm64),

# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
複製代碼

上面的ISA_MASK 是經過16進製表示的,不太方便看,咱們經過科學計算器轉換一下

這樣能夠清晰的看到,經過 isa & ISA_MASK 取出來的究竟是哪幾位上面的值。同時還能夠發現一個小細節,最終得出來的對象的地址值,會獲得36個有效二進制位,而最後的四位,只多是 1000 或者 0000,也就是十六進制下的 80,所以對象的地址最後一位(十六進制下),必定是80。體會一下,而後經過代碼走一波

#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的優化)

Runtime原理探究(二)—— Class結構的深刻分析

Runtime原理探究(三)—— OC Class的方法緩存cache_t

Runtime原理探究(四)—— 刨根問底消息機制

Runtime原理探究(五)—— super的本質

Runtime原理探究(六)—— 面試題中的Runtime

相關文章
相關標籤/搜索