iOS-Runtime1-isa存儲信息分析

Objective-C是一門動態(tài)性比較強的編程語言,跟C、C++等語言有著很大的不同。
什么叫動態(tài)性?
一般應(yīng)用程序運行需要經(jīng)過三步:編寫代碼 -> 編譯 -> 運行,如果是C語言,編譯之后方法就確定了,運行的時候就會調(diào)用那個方法。但是對于OC,由于其動態(tài)性,就算編譯完了,也可以在程序運行時決定調(diào)用哪個方法,而且就算沒有實現(xiàn)某個方法,也可在運行的時候動態(tài)生成某個方法。

Objective-C的動態(tài)性是由Runtime API來支撐的,Runtime API基本是開源的,源碼在https://opensource.apple.com/tarballs/搜索objc,點擊objc4文件夾進去,下載一個最新的(數(shù)字最大的)。

Runtime API提供的接口基本都是C語言的,源碼由C\C++\匯編語言編寫。

一. isa類型

isa指針和superclass指針簡單講了,實例對象的isa&ISA_MASK得到類對象的地址值,類對象的isa&ISA_MASK得到元類對象的地址值。

要想學(xué)習(xí)Runtime,首先要了解它底層的一些常用數(shù)據(jù)結(jié)構(gòu),比如isa指針。在arm64架構(gòu)之前,isa就是一個普通的指針,的確存儲著類對象、元類對象的內(nèi)存地址,從arm64架構(gòu)開始,對isa進行了優(yōu)化,變成了一個共用體(union)結(jié)構(gòu),還使用位域來存儲更多的信息。

在objc4搜索“objc_object {”

struct objc_object {
private:
    isa_t isa; //64位之前是Class isa
......
}

發(fā)現(xiàn),64位之后isa是isa_t類型的,isa_t是共用體(在isa指針和superclass指針中,我們知道,64位之前isa是Class類型的)進入isa_t:

union isa_t
{
    Class cls;
    uintptr_t bits;
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33;
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    };
};

二. 為什么要設(shè)計成共用體?

創(chuàng)建一個MJPerson對象,里面有三個屬性,創(chuàng)建對象,并且打印對象占用內(nèi)存大小。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MJPerson *person = [[MJPerson alloc] init];
        person.rich = YES; // 1字節(jié)
        person.tall = NO; // 1字節(jié)
        person.handsome = NO; // 1字節(jié)
        
        NSLog(@"tall:%d rich:%d hansome:%d", person.isTall, person.isRich, person.isHandsome);
        NSLog(@"%zd", class_getInstanceSize([MJPerson class]));  //16字節(jié)
    }
    return 0;
}

打印為16字節(jié)(isa指針占8字節(jié),BOOL值一共占用3字節(jié),一共11字節(jié),再加上內(nèi)存對齊,所以是16字節(jié))

有沒有占用內(nèi)存更小的方式實現(xiàn)給MJPerson添加三個BOOL屬性呢?

方式一:char類型的位運算

8位二進制是一個字節(jié)(0b0000 0000),BOOL值只有0和1兩個值,所以其實我們可以使用一個字節(jié)(最后3位)來表示這三個值。

原理很簡單,先使用一個char類型的成員變量保存這三個屬性:

  1. 取值:
    使用掩碼的按位與運算,想取出哪一位的值,就把哪一位置為1,其他為0。
  2. 設(shè)值:
    使用掩碼按位或運算,可以將某一位置為1,而不改變其他位,實現(xiàn)設(shè)置YES。
    使用掩碼取反的按位與運算,將某一位置為0,而不改變其他位,實現(xiàn)設(shè)置NO。

詳細(xì)見代碼和注釋:
MJPerson.m

#import <Foundation/Foundation.h>

@interface MJPerson : NSObject
//@property (assign, nonatomic, getter=isTall) BOOL tall;
//@property (assign, nonatomic, getter=isRich) BOOL rich;
//@property (assign, nonatomic, getter=isHansome) BOOL handsome;

- (void)setTall:(BOOL)tall;
- (void)setRich:(BOOL)rich;
- (void)setHandsome:(BOOL)handsome;

- (BOOL)isTall;
- (BOOL)isRich;
- (BOOL)isHandsome;

@end

MJPerson.h

#import "MJPerson.h"

//什么是位運算?與或非運算 (& | ~)  感嘆號!是給BOOL值取反的

//按位“與”,都是1結(jié)果才是1,其他結(jié)果都是0

//開發(fā)中使用掩碼的按位與運算來取出特定位的值,想取出哪一位的值,就把哪一位置為1,其他為0
// 0000 0111
//&0000 0100
//------
// 0000 0100

//掩碼按位或運算,可以將某一位置為1,而不改變其他位
//掩碼取反的按位與運算,將某一位置為0,而不改變其他位

// 掩碼,一般用來做按位與(&)運算的
//#define MJTallMask 0b00000001
//#define MJRichMask 0b00000010
//#define MJHandsomeMask 0b00000100

#define MJTallMask (1<<0) //1往左位移0位
#define MJRichMask (1<<1) //1往左位移1位
#define MJHandsomeMask (1<<2) //1往左位移2位

@interface MJPerson()
{
    char _tallRichHansome; //只占一個字節(jié) 0b 0000 0000
}
@end

@implementation MJPerson

- (instancetype)init
{
    if (self = [super init]) {
        _tallRichHansome = 0b00000100;
    }
    return self;
}

- (void)setTall:(BOOL)tall
{
    if (tall) {
        //掩碼的按位或運算,將某一位置為1
        _tallRichHansome |= MJTallMask;
    } else {
        //掩碼取反的按位與運算,將某一位置為0
        _tallRichHansome &= ~MJTallMask;
    }
}

- (BOOL)isTall
{
    //與運算之后,結(jié)果要么是0要么有值,但是我們需要的是BOOL值,怎么轉(zhuǎn)換呢?
    //取反,再取反
    return !!(_tallRichHansome & MJTallMask);
}

- (void)setRich:(BOOL)rich
{
    if (rich) {
        _tallRichHansome |= MJRichMask;
    } else {
        _tallRichHansome &= ~MJRichMask;
    }
}

- (BOOL)isRich
{
    return !!(_tallRichHansome & MJRichMask);
}

- (void)setHandsome:(BOOL)handsome
{
    if (handsome) {
        _tallRichHansome |= MJHandsomeMask;
    } else {
        _tallRichHansome &= ~MJHandsomeMask;
    }
}

- (BOOL)isHandsome
{
    return !!(_tallRichHansome & MJHandsomeMask);
}

@end

上面即可實現(xiàn)使用一個字節(jié)給MJPerson對象添加三個BOOL屬性。

方式二:結(jié)構(gòu)體的位域

上面使用了一個char類型的位運算來實現(xiàn),其實還可以使用結(jié)構(gòu)體的位域來實現(xiàn),也是占一個字節(jié)。

#import "MJPerson.h"

@interface MJPerson()
{
    //char占一個字節(jié),8位
    //結(jié)構(gòu)體支持位域,使用:1代表只占一位,這時候就不看左邊的char了
    //這時候整個結(jié)構(gòu)體就占3位,系統(tǒng)不可能分配3位給它,所以至少占用一個字節(jié)
    struct {
        char tall : 1;
        char rich : 1;
        char handsome : 1;
    } _tallRichHandsome; // 0b0000 0000
    //最后三位放上面的值,而且前面的成員放在最右邊(tall最后一位,rich倒數(shù)第二位,handsome倒數(shù)第三位)
}
@end

@implementation MJPerson

- (void)setTall:(BOOL)tall
{
    _tallRichHandsome.tall = tall;
}

- (BOOL)isTall
{
    //將0b1 -> 0b0000 0000
    //BOOL值是8位二進制,但是我們存下來的是一位(0b1),如何將一位轉(zhuǎn)成8位二進制呢?取反再取反
    //如果不做兩次取反操作,系統(tǒng)會自動把所有的0都b賦值為1,就是0b1 -> 0b1111 1111,就不對了
    return !!_tallRichHandsome.tall;
}

- (void)setRich:(BOOL)rich
{
    _tallRichHandsome.rich = rich;
}

- (BOOL)isRich
{
    return !!_tallRichHandsome.rich;
}

- (void)setHandsome:(BOOL)handsome
{
    _tallRichHandsome.handsome = handsome;
}

- (BOOL)isHandsome
{
    return !!_tallRichHandsome.handsome;
}

@end

代碼比較簡單,可自行看注釋。下面驗證,執(zhí)行代碼:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MJPerson *person = [[MJPerson alloc] init];
        person.rich = YES;
        person.tall = YES;
        person.handsome = YES;
        
        NSLog(@"tall:%d rich:%d hansome:%d", person.isTall, person.isRich, person.isHandsome);
    }
    return 0;
}

打斷點,打印內(nèi)存:

(lldb) p/x &(person->_tallRichHandsome)
((anonymous struct) *) $0 = 0x00000001005236a8
(lldb) x 0x00000001005236a8
0x1005236a8: 07 00 00 00 00 00 00 00 b1 41 ec ae ff ff 1d 00  .........A......
0x1005236b8: 8c 07 00 00 01 00 00 00 33 73 75 69 74 65 2f 49  ........3suite/I

打印_tallRichHandsome地址,查看內(nèi)存,可以看出這個地址值為7,轉(zhuǎn)成二進制就是0b0000 0111,說明代碼沒問題。

方式三:共同體

isa就是使用了共同體,共用體里面的成員共用一塊內(nèi)存。

如下,結(jié)構(gòu)體:

struct Date {
    int year; //4字節(jié)
    int month; //4字節(jié)
    int day; //4字節(jié)
};

它在內(nèi)存中結(jié)構(gòu)如下:

結(jié)構(gòu)體.png

如果是共用體:

union Date {
    int year; //4字節(jié)
    int month; //4字節(jié)
    int day; //4字節(jié)
};

在內(nèi)存中結(jié)構(gòu)如下:

共用體.png

先看如下代碼,再看代碼后的解釋:

#import "MJPerson.h"

#define MJTallMask (1<<0)
#define MJRichMask (1<<1)
#define MJHandsomeMask (1<<2)
#define MJThinMask (1<<3)

@interface MJPerson()
{
    union { //共用體
        char bits; //1字節(jié)
        
        struct {
            char tall : 1; //1位
            char rich : 1; //1位
            char handsome : 1; //1位
            char thin : 1; //1位
        };//這個結(jié)構(gòu)體僅僅是為了可讀性,而且自始至終一直都在操作bits,刪除這個結(jié)構(gòu)體也不影響
    } _tallRichHandsome;
}
@end

@implementation MJPerson

- (void)setTall:(BOOL)tall
{
    if (tall) {
        _tallRichHandsome.bits |= MJTallMask;
    } else {
        _tallRichHandsome.bits &= ~MJTallMask;
    }
}

- (BOOL)isTall
{
    return !!(_tallRichHandsome.bits & MJTallMask);
}

- (void)setRich:(BOOL)rich
{
    if (rich) {
        _tallRichHandsome.bits |= MJRichMask;
    } else {
        _tallRichHandsome.bits &= ~MJRichMask;
    }
}

- (BOOL)isRich
{
    return !!(_tallRichHandsome.bits & MJRichMask);
}

- (void)setHandsome:(BOOL)handsome
{
    if (handsome) {
        _tallRichHandsome.bits |= MJHandsomeMask;
    } else {
        _tallRichHandsome.bits &= ~MJHandsomeMask;
    }
}

- (BOOL)isHandsome
{
    return !!(_tallRichHandsome.bits & MJHandsomeMask);
}

- (void)setThin:(BOOL)thin
{
    if (thin) {
        _tallRichHandsome.bits |= MJThinMask;
    } else {
        _tallRichHandsome.bits &= ~MJThinMask;
    }
}

- (BOOL)isThin
{
    return !!(_tallRichHandsome.bits & MJThinMask);
}

@end

上面的代碼使用了方式一的位運算來設(shè)值和取值,使用了方式二的結(jié)構(gòu)體位域來實現(xiàn)代碼的可讀性。通過共用體將bits和結(jié)構(gòu)體結(jié)合起來,而且自始至終一直都在操作bits,沒有動結(jié)構(gòu)體,結(jié)構(gòu)體僅僅是為了可讀性,所以不會影響bits里面的值,刪除這個結(jié)構(gòu)體也不影響。

這種方式就是巧妙的利用共用體,達到了代碼可讀性的目的。

三. isa_t的共用體

現(xiàn)在看isa_t的共用體你應(yīng)該就很容易理解了。

union isa_t
{
    Class cls;
    uintptr_t bits;
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        //0,代表普通的指針,存儲著Class、Meta-Class對象的內(nèi)存地址
        //1,代表優(yōu)化過,使用位域存儲更多的信息
        uintptr_t nonpointer        : 1; 
        //是否有設(shè)置過關(guān)聯(lián)對象,如果沒有,釋放時會更快
        uintptr_t has_assoc         : 1;
        //是否有C++的析構(gòu)函數(shù)(.cxx_destruct),如果沒有,釋放時會更快
        uintptr_t has_cxx_dtor      : 1;
        //存儲著Class、Meta-Class對象的內(nèi)存地址信息
        uintptr_t shiftcls          : 33;
        //用于在調(diào)試時分辨對象是否未完成初始化
        uintptr_t magic             : 6;
        //是否有被弱引用指向過,如果沒有,釋放時會更快
        uintptr_t weakly_referenced : 1;
        //對象是否正在釋放
        uintptr_t deallocating      : 1;
        //引用計數(shù)是否過大無法存儲在isa中
        //如果為1,那么引用計數(shù)會存儲在一個叫SideTable的結(jié)構(gòu)體的refcnts成員中,refcnts是個散列表
        uintptr_t has_sidetable_rc  : 1;
        //里面存儲的值是引用計數(shù)減1
        uintptr_t extra_rc          : 19;
    };
};
  1. 可以看出64位之后,isa不僅僅用來存放地址值了,還用來存放更多的東西,所有的值都存放在bits里面。
  2. 冒號后面的數(shù)字就是占用多少位,加起來之后一共是64位,8個字節(jié),這也和以前我們說的isa指針占用8字節(jié)相吻合。
  3. 上面的shiftcls就是存放類對象、元類對象的內(nèi)存地址信息,可以發(fā)現(xiàn)有33位。

怎么取出地址值呢?
找到上面的“define ISA_MASK 0x0000000ffffffff8ULL”用計算器轉(zhuǎn)換成二進制就是“0b111111111111111111111111111111111000”,發(fā)現(xiàn)就是33個1,就是用這個ISA_MASK來取出地址值的,詳細(xì)操作請參考:isa指針和superclass指針

我們發(fā)現(xiàn),ISA_MASK最后三位都是0,這就導(dǎo)致:二進制下,類對象、元類對象的地址值打印出來最后三位一定都是0

真機上才是arm64架構(gòu),我們將項目跑在真機上,打印驗證一下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%p", [ViewController class]);
    NSLog(@"%p", object_getClass([ViewController class]));
    NSLog(@"%p", [MJPerson class]);
    NSLog(@"%p", object_getClass([MJPerson class]));
}

打?。?/p>

0x107f6ed88
0x107f6edb0
0x107f6ee50
0x107f6ee28

上面是16進制,1位16進制相當(dāng)于4位二進制,2位16進制相當(dāng)于8位二進制,8位二進制是1個字節(jié),所以2位16進制就是1個字節(jié)。觀察上面,最后結(jié)尾不是0就是8,如果是0那末尾肯定是0b0000,如果是8那就是01000,所以驗證了上面的結(jié)論。

簡單驗證:

關(guān)于isa里面存放的其他更多信息,這里簡單驗證下:

創(chuàng)建對象,打斷點:

 MJPerson *person = [[MJPerson alloc] init];

獲取對象的isa

(lldb) p/x person->isa
(Class) $0 = 0x000000010231bf40 MJPerson

將16進制轉(zhuǎn)換成二進制:

16->2.png

可以發(fā)現(xiàn),一共64位,從上圖的右下到左上,分別對應(yīng)結(jié)構(gòu)體中從上往下存儲的值。
比如:右下最后一位0代表現(xiàn)在是普通的指針沒有經(jīng)過優(yōu)化過(因為我沒跑真機),右下倒數(shù)第二位0表示沒有設(shè)置過關(guān)聯(lián)對象,以此類推。

Demo地址:isa-ISA_MASK

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容