04-深入了解isa指针以及内部结构

简介: 04-深入了解isa指针以及内部结构

isa地址指向验证

结合下面这段代码进行分析:

@interface Person : NSObject
@property(assign, nonatomic) int height;
@end
@implementation Person
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [Person new];
        Class personCls = [person class];
        Class personMetaClass = object_getClass(personCls);
        NSLog(@"%p - %p - %p", person, personCls, personMetaClass);
    }
    return 0;
}

上文(03-isa指针 & superclass指针)我们分析到实例对象的isa指向类对象,接下来我们调试一下:

(lldb) p/x (long)person->isa
(long) $0 = 0x011d800100008579
(lldb) p/x personCls
(Class) $1 = 0x0000000100008578 Person
(lldb)

我们会发现person的isa地址和类对象的地址并不相同,具体是什么原因产生的?我们直接分析objc源码,我们会看到下面这样一段获取真实的isa,真实的isa要对象的地址需要&上ISA_MASK的值,才是真正的isa的值;

ISA_MASK的值:

# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
#endif

再来分析一下isa的值,会发现结论和我们前文所分析的一致,直接看图:

位运算 & 结构体位域 & 联合体

当我们直接声明一个属性的时候,我们知道每一个属性的内存空间都是独立的,比如下面这样,show和hidden分别占用了一个字节的空间;

@interface Person : NSObject
@property(assign, nonatomic, getter=isShow) BOOL show;
@property(assign, nonatomic, getter=isHidden) BOOL hidden;
@end

对于BOOL类型来说,占用1个字节,1个字节占用8位,那么我们可否利用1个字节来存储他们两个的值呢?一个字节占用8位,我们可以利用最后两位来分别存储show和hidden,例如0b00000011,代表两个都为YES,0b00000001代表show为YES,hidden为NO,那么我们可以这样设计;

@interface Person : NSObject {
    char _showHidden;
}
- (void)setShow:(BOOL)isShow;
- (void)setHidden:(BOOL)isHidden;
- (BOOL)isShow;
- (BOOL)isHidden;
@end

如何取出特定位的值?

假设我们刚开始是0b00000001,如何取出倒数第二位的0,这个时候我们可以使用 & 上指定位进行计算,比如0b01 & 0b10,结果就为 0b00,再比如0b11 & 0b10,结果为0b10,会发现,我们想取出哪一位就 & 上一个对应位为1的值就可以,比如我们想取出0b00000001倒数第二位则 & 上0b00000010就可以了;

那么它的代码实现可以这样做;showHidden & 1要么有值,要么没值,第一个是将他转为BOOL类型并取反,第二个是再取反,相当于回到原来并转换为了BOOL类型,当然也可以直接强制转换为BOOL类型;

- (BOOL)isShow {
    return !!(_showHidden & 1);
}
- (BOOL)isHidden {
    return !!(_showHidden & 2);
}

进一步优化 & 1、 & 2,使用宏:

#define ShowMask (1 << 0) // 1
#define HiddenMask (1 << 1) // 2
- (BOOL)isShow {
    return !!(_showHidden & ShowMask);
}
- (BOOL)isHidden {
    return !!(_showHidden & HiddenMask);
}

如何给特定位设置值?

假设外面传进来hidden传为YES,我们开始为0b01,我想将倒数第二位设置为1,变为0b11,这个时候我们使用 | 上对应位为1就可以了,0b01 | 0b10 结果为0b11,倒数第二位则变为了1,其他位不变;

假设外面传进来hidden传为NO,我们开始为0b00000011,我想将倒数第二位设置为0,变为0b00000001,这个时候我们要保持其他为不变,我们可以这样将0b00000011 & 0b00000001,这样结果为0b00000001,其他位则保持不变,另外会发现我们 & 上的值0b00000001,实际为Mask取反以后的值;我们可以修改代码如下:

- (void)setShow:(BOOL)isShow {
    if (isShow) {
        _showHidden |= ShowMask;
    }else {
        _showHidden &= ~ShowMask;
    }
}
- (void)setHidden:(BOOL)isHidden {
    if (isHidden) {
        _showHidden |= HiddenMask;
    }else {
        _showHidden &= ~HiddenMask;
    }
}

测试一下:

Person *person = [Person new];
person->_showHidden = 0b00000010;
NSLog(@"person.isShow - %d, person.isHidden - %d", person.isShow, person.isHidden);
person.show = YES;
person.hidden = NO;
NSLog(@"person.isShow - %d, person.isHidden - %d", person.isShow, person.isHidden);

打印结果:

person.isShow - 0, person.isHidden - 1
person.isShow - 1, person.isHidden - 0

位运算补充对于KVO的使用,看一下传递的NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld,这样传递进去,内部是如何取得呢?

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[person addObserver:self forKeyPath:@"show" options:options context:NULL];
Options的定义:
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,

0x01=0b01、0x02=0b10,当0b01 | 0b10的时候结果为0b11,当内部判断是否为NSKeyValueObservingOptionNew的时候,我们可以直接拿0b11 & 0b01,然后判断它是否为0就可以知道了;

为什么要判断它是否为0?假如我们传递进来的参数只有一个NSKeyValueObservingOptionOld,那么 0b10 & 0x01,它的结果为0x00,如果拿0b10 & 0b11,那么它的结果为0b10,结果不为0,故包含NSKeyValueObservingOptionOld,不包含NSKeyValueObservingOptionNew;

我们自己构造一个示例:

typedef NS_ENUM(NSUInteger, LTEnumOptions) {
    LTEnumOptionsA = 1 << 0, //0x1
    LTEnumOptionsB = 1 << 2, //0x2
    LTEnumOptionsC = 1 << 3, //0x4
};
@interface Person : NSObject
@property(assign, nonatomic) LTEnumOptions options;
@end
@implementation Person
- (void)setOptions:(LTEnumOptions)options {
    if (options & LTEnumOptionsA) {
        NSLog(@"LTEnumOptionsA");
    }
    if (options & LTEnumOptionsB) {
        NSLog(@"LTEnumOptionsB");
    }
    if (options & LTEnumOptionsC) {
        NSLog(@"LTEnumOptionsC");
    }
}
@end

测试代码:

Person *person = [Person new];
person.options = LTEnumOptionsA | LTEnumOptionsB | LTEnumOptionsC;
运行结果:
LTEnumOptionsA
LTEnumOptionsB
LTEnumOptionsC

结构体位域

我们可以结构体位域的形式实现上述功能,这样也能做到showHidden占用1个字节(占用的字节数为位域的最大值)

@interface Person : NSObject {
    @public
// char _showHidden;
    struct {
        char show : 1;
        char hidden : 1;
    }showHidden;
}

那么我们可以修改代码如下:

- (void)setShow:(BOOL)isShow {
    showHidden.show = isShow;
}
- (void)setHidden:(BOOL)isHidden {
    showHidden.hidden = isHidden;
}
- (BOOL)isShow {
    return showHidden.show;
}
- (BOOL)isHidden {
    return showHidden.hidden;
}

测试代码:

Person *person = [Person new];
person.show = YES;
person.hidden = NO;
NSLog(@"person.isShow - %d, person.isHidden - %d", person.isShow, person.isHidden);

打印结果:

person.isShow - -1, person.isHidden - 0

会发现isShow的打印结果为-1,产生原因:

(lldb) p/x person->showHidden
((anonymous struct)) $1 = (show = 0x01, hidden = 0x00)
(lldb) p/x &(person->showHidden)
((anonymous struct) *) $2 = 0x00000001052099b8
(lldb) x 0x00000001052099b8
0x1052099b8: 01 00 00 00 00 00 00 00 ca 1b ea 7c ff 7f 00 00  ...........|....
0x1052099c8: f0 bc 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

相当于我们拿着0x01最后一位为1,把它强制转换为了BOOL类型,BOOL类型占用1个字节,把0x1转换为BOOL则变成了0x11111111,也就是十进制的-1;所以我们可以这样直接把showHidden.isShow做类似最上面的操作,结果为非0即为真,直接!!showHidden.isShow即可,修改下面这两个方法,会发现打印结果正常,这里不再贴测试代码;

- (BOOL)isShow {
    return !!showHidden.show;
}
- (BOOL)isHidden {
    return !!showHidden.hidden;
}

联合体联合体也叫共用体,是一种特殊的类,也是一种构造类型的数据结构;在一个联合体内能够定义多种不同的数据类型,这些数据共享同一段内存,以达到节省空间的目的;联合体有两个特性:

  • 在union中,分配内存空间的大小,等于占内存最大的数据类型字节大小;
  • 共享同一段内存;

我们可以修改之前的实现为如下代码,运行结果符合预期结果:

#define ShowMask (1 << 0) // 1
#define HiddenMask (1 << 1) // 2
@interface Person : NSObject {
    @public
    union {//占用1个字节
        char bits;
        struct { //此处代码仅为了增加可读性,可以删除此处代码
            char show : 1;
            char hidden : 1;
        };
    }_showHidden;
}
- (void)setShow:(BOOL)isShow;
- (void)setHidden:(BOOL)isHidden;
- (BOOL)isShow;
- (BOOL)isHidden;
@end
@implementation Person
- (void)setShow:(BOOL)isShow {
    if (isShow) {
        _showHidden.bits |= ShowMask;
    }else {
        _showHidden.bits &= ~ShowMask;
    }
}
- (void)setHidden:(BOOL)isHidden {
    if (isHidden) {
        _showHidden.bits |= HiddenMask;
    }else {
        _showHidden.bits &= ~HiddenMask;
    }
}
- (BOOL)isShow {
    return !!(_showHidden.bits & ShowMask);
}
- (BOOL)isHidden {
    return !!(_showHidden.bits & HiddenMask);
}
@end

isa_t

通过查看OC的源码,可以发现isa的结构为isa_t的一个共用体:

  • 在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址
  • 从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息

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;
    };
};

结合上面我们对联合体的分析,我们可以很清楚的知道bits占8个字节64位,使用这64位存储更多的信息,每一位代表的含义如下:

  • nonpointer:0:代表普通的指针,存储着Class、Meta-Class对象的内存地址;1:代表优化过,使用位域存储更多的信息;
  • has_assoc:是否有设置过关联对象,如果没有,释放时会更快;
  • has_cxx_dtor:是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快;
  • shiftcls:存储着Class、Meta-Class对象的内存地址信息;
  • magic:用于在调试时分辨对象是否未完成初始化;
  • weakly_referenced:是否有被弱引用指向过,如果没有,释放时会更快;
  • deallocating:对象是否正在释放;
  • extra_rc:里面存储的值是引用计数器减1;
  • has_sidetable_rc:引用计数器是否过大无法存储在isa中,如果为1,那么引用计数会存储在一个叫SideTable的类的属性中;

Person *person = [Person new];
objc_setAssociatedObject(person, "name", @"zs", OBJC_ASSOCIATION_COPY_NONATOMIC);

再回过头来看实例对象的isa指针地址(真机调试):

(lldb) p/x (long)person->isa
(long) $0 = 0x0100000104b70d83

(lldb)根据二进制位,对应的位置如图,其他位依次往前对应就可以了;

本文部分内容可能来源于网络,发布的内容如果侵犯了您的权益,请联系我们尽快删除!


收藏划线


相关文章
|
10月前
|
编译器 数据库 C语言
4.1 C/C++ 使用结构与指针
C/C++语言是一种通用的编程语言,具有高效、灵活和可移植等特点。C语言主要用于系统编程,如操作系统、编译器、数据库等;C语言是C语言的扩展,增加了面向对象编程的特性,适用于大型软件系统、图形用户界面、嵌入式系统等。C/C++语言具有很高的效率和控制能力,但也需要开发人员自行管理内存等底层资源,对于初学者来说可能会有一定的难度。
39 0
|
11月前
|
存储
03-isa指针 & superclass指针
03-isa指针 & superclass指针
46 0
|
存储 C语言
《C和指针》读书笔记(第十章 结构和联合)(下)
《C和指针》读书笔记(第十章 结构和联合)(下)
|
存储 C语言 C++
《C和指针》读书笔记(第十章 结构和联合)(上)
《C和指针》读书笔记(第十章 结构和联合)(上)
每日一题—— 判断一个链表是否为回文结构(快慢指针,对撞指针)
每日一题—— 判断一个链表是否为回文结构(快慢指针,对撞指针)
|
存储 XML JSON
【C语言】结构体——我就是秩序的创建者!(结构体数组、结构体指针、嵌套、匿名、字面量、伸缩型数组、链式结构)
【C语言】结构体——我就是秩序的创建者!(结构体数组、结构体指针、嵌套、匿名、字面量、伸缩型数组、链式结构)
指向结构指针(C Primer Plus 第六版)
指向结构指针(C Primer Plus 第六版)
63 0
|
存储 缓存 Java
Java GC详解 - 1. 最全面的理解Java对象结构 - 对象指针 OOPs(下)
Java GC详解 - 1. 最全面的理解Java对象结构 - 对象指针 OOPs(下)
Java GC详解 - 1. 最全面的理解Java对象结构 - 对象指针 OOPs(下)
|
存储 缓存 Java
Java GC详解 - 1. 最全面的理解Java对象结构 - 对象指针 OOPs(上)
Java GC详解 - 1. 最全面的理解Java对象结构 - 对象指针 OOPs(上)
Java GC详解 - 1. 最全面的理解Java对象结构 - 对象指针 OOPs(上)