前言
对象我们几乎每天都在说的词,不管是生活中,还是工作中。生活中你如果没有对象,那么兄弟你得加油了,实在不行我给你new一个。在这愉快的玩笑中我们走入工作中的对象,说到对象不得不提到isa,因为isa告诉我们这个对象是属于谁的。下面就探究下对象的本质以及isa。
准备工作
联合体(union)
联合体和结构体类型也是由不同类型的数据组成,下面通过代码探究下联合体
union LWPerson {
int a; //4
short b; //2
char c; //1
};
int main(int argc, char * argv[]) {
@autoreleasepool {
union LWPerson person;
person.a = 8;
NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
person.b = 2;
NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
person.c = 'd';
NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
NSLog(@"%lu---%lu",sizeof(person),sizeof(union LWPerson));
}
return 0;
}
2021-06-10 15:50:45.789591+0800 isa探究[78582:10551210] a=8---b=8---c=
2021-06-10 15:50:45.790087+0800 isa探究[78582:10551210] a=2---b=2---c=
2021-06-10 15:50:45.790107+0800 isa探究[78582:10551210] a=100---b=100---c=d
2021-06-10 15:50:45.790125+0800 isa探究[78582:10551210] 4---4
总结:
- 联合体可以定义多个不同类型的成员,联合体的
内存大小由其中最大的成员的大小决定。 - 联合体中修改其中的某个变量会
覆盖其他变量的值。 - 联合体所有的变量
公用一块内存,变量之间互斥。
联合体优缺点
- 优点:内存使用更为灵活,节省内存。
- 缺点:不够包容。
位域(Bit field)
有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个只有0和1两种状态成员, 用一位二进位即可,目的是节省存储空间,处理简便。下面通过代码探究位域
struct OldCar {
BOOL front;
BOOL back;
BOOL left;
BOOL right;
};
struct NewCar {
BOOL front: 1;
BOOL back : 1;
BOOL left : 1;
BOOL right: 1;
};
int main(int argc, char * argv[]) {
@autoreleasepool {
struct OldCar oldCar;
struct NewCar newCar;
NSLog(@"----%lu----%lu",sizeof(oldCar),sizeof(newCar));
}
return 0;
}
2021-06-10 16:25:04.266985+0800 isa探究[78608:10560097] ----4----1
总结:oldCar的内存大小是4字节,newCar的内存大小是1字节。1字节包含8位(bit),newCar中所有变量都是按 位(bit)存在这1字节中,具体的存放格式是从右往左往 0000 1111 依次存放front 、back、left 、right 没有45°仰望天空哈。
对象的本质
探究对象本质之前,先了解编辑器Clang
Clang
Clang是一个C语言、C++、Objective-C语言的轻量级编译器,是由Apple主导编写的Clang主要用于把源文件编译成底层文件,比如把main.m文件编译成main.cpp、main.o或者可执行文件。便于观察底层的逻辑结构,便于我们探究底层。
Clang 终端编译命令
clang -rewrite-objc main.m -o main.cpp
//UIKit报错
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
// xcrun命令基于clang基础上进行了封装更好用
//3、模拟器编译
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
//4、真机编译
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
对象的本质
下面探究对象的本质,通过实例进行探究代码如下
@interface LWPerson : NSObject
{
NSString * height;
}
@property(nonatomic, copy)NSString *LWname;
@property(nonatomic,assign)NSInteger age;
@end
@implementation LWPerson
@end
int main(int argc, char * argv[]) {
@autoreleasepool {
}
return 0;
}
通过clang命令把 main.m 文件编译成 main.cpp部分代码如下
#ifndef _REWRITER_typedef_NSObject
#define _REWRITER_typedef_NSObject
typedef struct objc_object NSObject;
typedef struct {} _objc_exc_NSObject;
#endif
struct NSObject_IMPL {
Class isa;
};
extern "C" unsigned long OBJC_IVAR_$_LWPerson$_LWname;
extern "C" unsigned long OBJC_IVAR_$_LWPerson$_age;
struct LWPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
double height;
NSString *_LWname;
NSInteger _age;
};
// @property(nonatomic, copy)NSString *LWname;
// @property(nonatomic,assign)NSInteger age;
/* @end */
// @implementation LWPerson
源码分析:LWPerson底层是结构体,LWPerson_IMPL结构体中有4个变量,其中height,_LWname ,_age是自定义的属性,NSObject_IVARS就是 NSObject中isa。LWPerson 继承于NSObject,意味着LWPerson也有NSObject所有的成员变量。
NSObject的底层是NSObject_IMPL里面只有一个成员变量是Class isa。通常叫isa叫做isa指针,那么这里的Class应该是个指针类型,在main.cpp文件中全局搜索*Class。代码如下
typedef struct objc_class *Class;
//对象的底层实现
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
//NSObject的底层实现
struct NSObject_IMPL {
Class isa;
};
源码分析:
Class类型实际是一个objc_class类型的结构体指针,objc_class是所有类的底层实现。由此我们猜测isa可能跟类信息存在着重要的关联,具体的关联下面会进行探究。NSObject的底层实现和对象的底层实现有什么区别。两者成员变量的结构体都是Class isa,那么就必然存在继承关系。所有对象的底层都是继承objc_object,在OC中基本上所有的对象都是继承NSObject,但是真正的底层实现是objc_object的结构体类型。
对象的本质拓展
在main.cpp文件中全局搜索*Class时,发现有这样几行代码如下
typedef struct objc_object *id;
typedef struct objc_selector *SEL;
源码分析:熟悉的id和SEL。常用的id原来是一个objc_object结构体指针,这就解释了id修饰变量和作为返回值的时候为什么不加*,意料之外的是SEL也是结构体指针。
在main.cpp中,发现了方法的底层实现代码如下
图中显示:
- 属性的
get和set方法中都有获取当前变量的位置代码,怎么去获取当前的变量呢?底层实现就是当前对象的首地址+变量的偏移值。 - 代码中我自定义了一个局部变量
height,但是底层代码仅添加了一个变量。而定义的属性,底层自动添加了带_变量以及get和set方法的实现。哎呀,这不是我们常见的面试题嘛。.cpp文件还是很不错的啊。
对象本质总结
- 对象的本质是
结构体 LWPerson的isa是继承NSObject中的isaNSObject中只有一个成员变量那就是isa
isa关联类
对象本质探究让我们知道对象的第一个变量就是isa,在探究 IOS 底层原理之 alloc 探究 中我们发现alloc一个对象最核心的三个方法cls->instanceSize 计算内存大小 , (id)calloc(1, size)开辟内存返回地址指针, obj->initInstanceIsa初始化isa关联类。既然这么多地方都指向isa,那么就来探究下isa的结构以及isa是如何关联类的。
isa 结构
通过alloc流程 alloc –> _objc_rootAlloc –> callAlloc –> _objc_rootAllocWithZone –> _class_createInstanceFromZone,断点在 obj->initInstanceIsa,进入obj->initInstanceIsa
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
断点进入initIsa
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
isa_t newisa(0); // isa初始化
if (!nonpointer) {
newisa.setClass(cls, this);//如果是纯指针 isa被直接cls赋值
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
newisa.extra_rc = 1;
}
isa = newisa;
}
我们发现了isa的结构类型是isa_t,进入isa_t
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private:
// Accessing the class requires custom ptrauth operations, so
// force clients to go through setClass/getClass by making this
// private.
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
bool isDeallocating() {
return extra_rc == 0 && has_sidetable_rc == 0;
}
void setDeallocating() {
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif
void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
Class getDecodedClass(bool authenticated);
};
源码分析:isa_t是联合体。isa_t有两个变量 一个是bits,一个是cls。通过上面分析联合体是互斥的,那就意味着初始化isa有两种方式:
bits被赋值,cls没有值或者被覆盖cls被赋值,bits没有值或者被覆盖
isa_t中还有一个结构体成员变量ISA_BITFIELD。这是一个宏对应有两个端,一个是__arm64__(iOS),一个是__x86_64__(macOS)。ISA_BITFIELD通过位域存储信息,那就具体看下有哪些信息
图中的省略了iOS的模拟器数据只留下了真机和macOS的宏
bits的64位存储分布图
各变量的含义:
nonpointer:表示是否对isa指针进行优化,0表示纯指针,1表示不止是类对象的地址,isa中包含了类信息、对象、引用计数等has_assoc:关联对象标志位,0表示未关联,1表示关联has_cxx_dtor:该对象是否C ++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,没有,则释放对象shiftcls:储存类指针的值,开启指针优化的情况下,在arm64架构中有33位用来存储类指针,x86_64架构中占44位magic:用于调试器判断当前对象是真的对象还是没有初始化的空间weakly_referenced:指对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放deallocating:标志对象是否正在释放has_sidetable_rc:当对象引用计数大于10时,则需要借用该变量存储进位hextra_rc:表示该对象的引用计数值,实际上引用计数值减1,例如,如果对象的引用计数为10,那么extra_rc为9,如果大于10,就需要用到上面的has_sidetable_rc
isa结构分析总结
isa分为nonpointer类型和非nonpointer。非nonpointer类型只是一个纯指针,nonpointer还包含了类的信息isa是联合体+位域的方式存储信息的。采用这种方式的有点就是节省大量内存。万物皆对象,只要是对象就有isa指针,大量的isa就占用了很多内存,联合体公用一块内存节省了部分内存,而位域更是在节省内存的基础上存储了信息,可以说isa指针的内存得到了充分的利用。
isa关联类探究
首先定义一个LWPerson类,初始化[LWPerson alloc],流程如下:alloc –> _objc_rootAlloc –> callAlloc –> _objc_rootAllocWithZone –> _class_createInstanceFromZone–> obj->initInstanceIsa –>initIsa,现在探究isa到底怎么和类关联起来
图中显示 newisa(0)对bits进行赋值,bits里面所有的变量都是0
图中显示 newisa.bits = ISA_MAGIC_VALUE, ISA_MAGIC_VALUE 是一个宏 ISA_MAGIC_VALUE = 0x001d800000000001,变量值改变的有bits = 8303511812964353 ,cls = 0x001d800000000001,nonpointer = 1,margic = 59。我们恢复下赋值的过程。如下图
0x001d800000000001转换成10进制等于8303511812964353cls=0x001d800000000001是因为给bits赋值的时候覆盖了cls,isa_t是联合体- 第
0位的1等于nonpointer=1 margic的值怎么算呢,按位计算从47位开始,长度是6位结果就是111011,从第0个位置开始算111011的10进制就是59.(对比图中的第一张计算图和 第三张计算图)
断点进入setClass
图中显示shiftcls = 536873007,下面来验证下结果是否正确
图中显示
shiftcls=536873007与上面的算法shiftcls=(uintptr_t)newCls >> 3得到的结果是一样的。LWPerson的类地址>>3进行10进制转换赋值给shiftcls。此时isa已经关联LWPerson类 ,cls变量被覆盖,cls=LWPerson。
问题:(uintptr_t)newCls >> 3 为什么需要右移3位
MACH_VM_MAX_ADDRESS表示虚拟内存最大寻址空间,在__arm64__中MACH_VM_MAX_ADDRESS=0x1000000000虚拟内存最大寻址空间是36位。在__x86_64__中MACH_VM_MAX_ADDRESS=0x7fffffe00000虚拟内存最大寻址空间是47位。- 字节对齐是
8字节对齐,也就是说指针的地址只能是8的倍数,那么指针地址的后3位只能是0,比如0x8,0x18,0x30转换成二进制后3位都是0。
基于以上两条,为了节省内存空间,把后3位是0抹去。在__arm64__中shiftcls占33位,在__x86_64__中shiftcls占44位。因此需要将类地址右移3位,即(uintptr_t)newCls >> 3。可以说isa的优化算是做到了极致。
isa关联类总结
cls 与 isa 关联类的原理就是isa指针中的shiftcls存储了类信息。
总结
通过对isa和对象的本质的探究,认识到对层次探究的必要性和重要性,虽然探究的过程是复杂的繁琐的,但是结果却令人兴奋,这就是底层探究的魅力。
补充
isa关联类的几种方式
shiftcls=(uintptr_t)newCls >> 3isa位运算isa&ISA_MASK
shiftcls =(uintptr_t)newCls >> 3 上面已经验证过了
isa位运算
类信息是存储在isa指针中,shiftcls在isa是按位存储,macOS从第3位开始存储,大小是44位。位运算是目的是只保留shiftcls信息,其它位的信息抹零。位运算过程shiftcls的相对位置要保持不变。如下图
isa的值是0x011d8001000041a1,0x011d8001000041a1 >> 3结果等于0x0023b000200008340x0023b00020000834 << 20结果等于0x00020000834000000x0002000083400000 >> 17结果等于0x00000001000041a0po 0x00000001000041a0的结果是LWPerson,说明isa已经关联了类
isa位运算过程图
isa&ISA_MASK
ISA_MASK是一个宏。__x86_64__ 的值等于 0x00007ffffffffff8ULL,__arm64__ 的值等于0x0000000ffffffff8ULL。isa的值是0x011d8001000041a1,验证下0x011d8001000041a1&0x00007ffffffffff8ULL结果。如下图
图中显示 po 0x011d8001000041a1 & 0x00007ffffffffff8ULL的结果是LWPerson。说明isa已经关联了类。二进制方式打印ISA_MASK p/t 0x00007ffffffffff8ULL,结果显示高17位是0,低3位是0,中间的44位是1,用来显示isa中的shiftcls。ISA_MASK 就像一个面具把露出来的显示,其它的全部抹掉。
init和new探究
init和new的探究已经更新到 IOS 底层原理之 alloc 探究 博客中
今天的文章IOS 底层原理之对象的本质&isa关联类分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/23340.html
