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)根据二进制位,对应的位置如图,其他位依次往前对应就可以了;
本文部分内容可能来源于网络,发布的内容如果侵犯了您的权益,请联系我们尽快删除!
享收藏划线