作为一名iOS开发人员,每天都是面向对象开发,可以说,对象纵横交错穿插在我们的工程项目中,跳跃在指尖的敲击下。平时十分不经意,突然之间,来了个灵魂的拷问,什么是对象?
脑瓜子突然嗡嗡的。。。。
想起刚开始学iOS时,创建的Person对象。。。。好像没啥印象了,?,那么要探究什么是对象,也就是得探究对象的本质
对象的本质
在将对象之前,我们先了解一个工具—–clang(用来看底层的源码结构)
1 对象的底层是结构体
1.1 什么是clang
Clang是⼀个C语⾔、C++、Objective-C语⾔的轻量级编译器。源代码发布于BSD协议下。Clang将⽀持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。
也就是说,Clang是⼀个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器。
好,clang就介绍到这里~~biu ~~ biu ~~ biu ~~
1.2 如何获得.cpp文件
下面我们进入主题。老规矩,代码走一波?
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface TestPerson : NSObject
@property (nonatomic, copy) NSString *test_nickName;
@end
@implementation TestPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
复制代码
建立一个TestPerson对象,然后给这个对象一个成员变量test_nickName
创建好之后,对这个工程show in finder,接着打开终端,如下如:

clang的终端命令:clang-rewrite-objc main.m -o main.cpp把⽬标⽂件编译成c++⽂件,(这里的目标文件是main.m编译到main.cpp中)
注:如果在我们自己创建的iOS工程中,要想把一个viewcontroller.m文件编译成c++文件,就会报UIKit错误
如果遇到UIKit报错问题,就执行下面这条命令,需要更改iOS的版本,还有最后的文件名(main.m)
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0-isysroot/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk main.m
或者是直接使用 xcrun 命令
xcode 安装的时候顺带安装了 xcrun 命令, xcrun 命令在 clang 的基础上进⾏了⼀些封装,要更好⽤⼀些
下面的两种命令更加的简单
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp(模拟器)
xcrun -sdk iphoneosclang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp(⼿机)
复制代码
那么执行clang命令之后,就能得到一个main.cpp文件

1.3 对象的底层是结构体
打开main.cpp文件,全文索引之前创建的TestPerson对象

可以看到,TestPerson对象是有一个结构体组成的。
由这里,就能得出,对象在底层的本质就是一个结构体。
当前的结构体里面,又嵌套了一个结构体,相当于一个伪继承(不是真正的继承,但是在c++里面是可以的),它继承了NSObject_IMPL,那么NSObject_IVARS就是成员变量的isa(这里可以全文索引 NSObject_IMPL { ,就能找到这个结构体struct)

到这里,就能确定TestPerson对象里面有一个isa指针,还有自己声明的成员变量。
再根据TestPerson是结构体的这张截图,在114395行,根据这行代码:typedef struct objc_object TestPerson;可以看出TestPerson它本质的类的类型是objc_object,而我们OC层面上看到的,TestPerson是继承NSObject,但是在底层上面,其实就是 objc_object 。
与此同理,就像class类型,我们可以索引class { 就能找到

他的底层竟然也是带着objc_ 的,那么再接着索引objc_ class

从这里就可看到class类型,其实就是objc_class * 类型,意味着当前的class是一个结构体指针,而Class只是一个别名而已
再比如id类型,使用id引用的时候,是不需要加*的,为什么了?

因为它本身就是objc_object *
那么到了这里,是不是就很清晰了。

1.4 setter方法和getter方法的底层
来,再回到我们的TestPerson上面来,在main.cpp文件中,还能找到成员变量test_nickName的getter方法和setter方法。

像113514行和113517行,其实就是test_nickName的getter方法和setter方法。
就比如,113514行这个getter方法,我们在上层进行调用的时候,是不显示(TestPerson * self, SEL _cmd)这些参数的,那么这些参数,其实就是这个方法的隐藏参数。从这个getter方法的返回值的方式 return (*(NSString **)((char *)self + OBJC_IVAR_$_TestPerson$_test_nickName)); 是通过self(TestPerson)加上一个test_nickName指针地址的偏移值,来获取test_nickName的值,再通过(*(NSString **)还原为string类型。下面这个草图,就更加形象明确了:

取值的过程就是:先拿到当前成员变变量的地址,再去取这个地址里面所存的值。
像113517行的setter方法,也是一样的方式进行存值。
分析到这里,是不是有种恍然的感觉,心底想呼喊一下:

嗯哼、嗯哼、嗯哼。。。稍安、勿躁、静坐 ~ ~ ~ ~ ?,精彩继续 biu ~
我们从刚刚的分析,知道TestPerson对象结构体中,除了变量test_nickName外,还有个isa指针。这个isa指针里面有什么了?,接下来就开始isa的表演(无广告连接?)
2 ISA指针
2.1 结构体、联合体、位域
结构体我们比较熟悉,那么联合体和位域是啥子嘞?嘿嘿,直接上代码:

从打印中可以看出,这个TestCar1结构体的内存大小为4,因为每个布尔值的内存大小为1,所以总内存为4。
注:这里和成员变量字节内部对齐是有区别的,像通常说的8字节对齐,是关于成员变量在OC层面对齐。而在结构体里面,是最大的成员变量。所以,结构体和成员变量是不一样的。
结构体TestCar1,共4字节,每字节8位,共32位(bit),如:0000 0000 0000 0000 0000 0000 0000 1111,开辟了32位(bit),然而只使用了4bit用来存储,那么就剩余了28bit空着,这样就造成了浪费。只需要半个字节的空间,就足够了,也就是:0000 1111。那么就需要进行优化,就引出了一个词:位域。
2.1.1 位域
既然系统是自动分配了这的大的内存出来,假如,给结构体里面的布尔值变量指定位置大小了?会不会有所优化?根据下图来看下:

从打印结果可以看出,相比于结构体TestCar1,结构体TestCar2占用的内存空间更小,就是:0000 1111,这样就大大的优化了内存空间。
再进行测试下,把TestCar2改成

打印的结果就为:

结构体TestCar2占用了2字节内存,我们在通过二进制码看一下4个布尔值分别占据的位置:

注:单个变量的最大位置数只能是8(bit)。
举个实例:
在使用代理的时候:
@protocol TestProtocol <NSObject>
@optional
- (void)methodA;
- (void)methodB;
- (void)methodC;
@end
复制代码
我们在进行调用的时候通常都要判断代理对象是否已经实现该方法,代码如下:
- (void)methodA_CallBack {
if (_delegate && [_delegate respondsToSelector:@selector(methodA)]) {
[_delegate performSelector:@selector(methodA)];
}
}
复制代码
这样会存在一个性能问题,就是每次都要通过消息机制去确认delegate是否已经实现该方法,虽然OC的消息机制中会将方法实现缓存到类对象的方法缓存中,但如果调用的比较频繁的时候,还是会影响性能。
所以我们可以通过位域进行改进。
改进后的代码:
@interface TestClass ()
{
struct {
unsigned int methodAFlag : 1;
unsigned int methodBFlag : 1;
unsigned int methodCFlag : 1;
} _delegateFlags;
}
@end
@implementation TestClass
- (void)setDelegate:(id<TestProtocol>)delegate {
_delegate = delegate;
_delegateFlags.methodAFlag = [_delegate respondsToSelector:@selector(methodA)];
_delegateFlags.methodBFlag = [_delegate respondsToSelector:@selector(methodB)];
_delegateFlags.methodCFlag = [_delegate respondsToSelector:@selector(methodC)];
}
- (void)methodA_CallBack {
if (_delegateFlags.methodAFlag) {
[_delegate performSelector:@selector(methodA)];
}
}
- (void)methodB_CallBack {
if (_delegateFlags.methodBFlag) {
[_delegate performSelector:@selector(methodB)];
}
}
- (void)methodC_CallBack {
if (_delegateFlags.methodCFlag) {
[_delegate performSelector:@selector(methodC)];
}
}
@end
复制代码
这样的话,方法判断只会在设置代理的时候进行一次,并将值保存在了缓存(位域)中,省去了很多次的方法查询,提高了效率。
2.1.2 联合体
讲完位域,接着引入另一个知识点:联合体。创建一个TestTeacher1结构体:

通过3个断点,还有断点处的打印来看,最开始,都是值为0的,然后再进行赋值。
接着,使用联合体创建TestTeacher2

同样的3个断点,当执行到断点①时,和TestTeacher1的情况是差不多的,但是从断点②开始,就有了比较大的差别。此时只是给name赋了值,age和height都没有,但是这两者却有值存在,那么这些值,就是我们通常说的脏数据了(也就是占位符)。然而执行到断点③,age赋值了,但是name的值,却被置空了。
这个就是联合体的特性:变量之间是互斥的。
结构体(struct)中:所有变量是“共存”的
优点是“有容乃⼤”,全⾯;
缺点是struct内存空间的分配是粗放的,不管⽤不⽤,全分配。
联合体(union)中:是各变量是“互斥”的
缺点就是不够“包容”;
但优点是内存使⽤更为精细灵活,也节省了内存空间;
复制代码
看到这里,也许会有老铁问:不是isa的表演吗?咋还跟我们扯啥联合体、位域。这不是挂羊头卖狗肉吗?(抵制黑心商??)
商家郑重承诺:并不是的啊,那是因为isa里面涉及到了这些。

2.2 isa指针
2.2.1 nonPointerIsa的分析
讲nonPointerlsa的分析,就得到objc的源码中去(源码下载地址,在iOS的底层探究——–alloc文章里面)。前面,我们讲的alloc的底层源码的时候,alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone这么一个流程。
在_class_createInstanceFromZone还有一个重要的点,那就是 obj->initIsa(cls);,这句代码,它把从堆内存申请的结构体指针和当前的cls绑定在一起。看底层代码:

进入initIsa方法里面

在这里,就出现了isa,在进入isa_t

可以看出,isa是一个联合体。
在普遍的表现一个类的地址时,会出现一个词:nonPointerIsa。就比如一个类,也就可以作为一个指针,类上面是可以有很多内容是能够被存储的。类的指针是8字节,8字节 * 8 bit = 64 bit(64位)。那么如果只是用来存储一个指针,就会造成大大的浪费,因为每个类都有一个isa指针。苹果就对这个isa做了优化,就把和类息息相关的一些内容存在里面,比如:是否正在释放、引用计数、weak、关联对象、析构函数等等(所以,OC在底层,就是C++,像OC的释放,并不是真正的释放,而是其下层的C++释放,才是真正的释放)。这些都和类先关,所以,可以把这些内容存储到那64位里面去。那么就出现了nonPointerIsa。nonPointerIsa也不是一个简单的地址。我们可以通过查看isa_t的位域,来了解里面存的是什么。
在X86_64中:

在arm64中:
#if__arm64__
#define ISA_MASK 0x0000000ffffffff8ULL
#define ISA_MAGIC_MASK 0x000003f000000001ULL
#define ISA_MAGIC_VALUE 0x000001a000000001ULL
#define ISA_BITFIELD
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
#define RC_ONE (1ULL<<45)
#define RC_HALF (1ULL<<18)
复制代码
nonpointer:表示是否对isa指针开启指针优化 0:纯isa指针,1:不⽌是类对象地址,isa中包含了类信息、对象的引⽤计数等;
has_assoc:关联对象标志位,0没有,1存在;
has_cxx_dtor:该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象;
shiftcls:存储类指针的值。开启指针优化的情况下,在arm64架构中有33位⽤来存储类指针;
magic:⽤于调试器判断当前对象是真的对象还是没有初始化的空间;
weakly_referenced:志对象是否被指向或者曾经指向⼀个ARC的弱变量,没有弱引⽤的对象可以更快释放;
deallocating:标志对象是否正在释放内存;
has_sidetable_rc:当对象引⽤技术⼤于10时,则需要借⽤该变量存储进位;
extra_rc:当表示该对象的引⽤计数值,实际上是引⽤计数值减1,例如,如果对象的引⽤计数为10,那么extra_rc为9。如果引⽤计数⼤于10,则需要使⽤到下⾯的has_sidetable_rc。
以arm64为例,堆中,8字节对齐排列,8字节 * 8 bit = 64 bit(64位)。
那么,nonpointer占[1]号位置,has_assoc占[2]号位置,has_cxx_dtor占[3]号位置,shiftcls占[4 ~ 36]号位置,magic占[37~42]号位置,weakly_referenced占[43]号位置,deallocating占[44]号位置,has_sidetable_rc占[45]号位置,extra_rc占[46 ~ 64]号位置。
2.2.2 isa的位运算
以X86_64为参照:

根据isa的位域存储内容,类的主体部分内容存储在shiftcls位置上,那么在shiftcls位置段的一边是3个位置,另外一边是17个位置。想要查看shiftcls里面是什么,通过平移可以得到,下面通过一个图来说明下:

那么根据这个步骤,在工程中进行操作:

可以得出,shiftcls存放的是对象的TestMan.class的全部信息了。
3 init和new
3.1 init方法底层
在objc的源码中,main文件里面,初始化一个TestPerson类:TestPerson *p = [[TestPerson alloc] init] ;,那么可以通过点击init进入到其底层源码中。因为已经进过alloc了,所以是一个对象了

接着进入_objc_rootInit方法

看上去,好像什么都没做,直接返回了自己。
那么init具体做了什么了?
init他是一个初始化方法、工厂设计模式、构造函数,用来给子类进行重写。在我们平常的开发过程中,经常会重写init方法。使得初始化方法可以根据不同情况进行重新构造。其实就是提供接口,便于扩展。
3.2 new方法的底层
同样的方式方法,或者直接在源码中搜索new {,就能找到new的底层实现,如图:

其实,new就是alloc + init;
就比如,创建一个TestPerson类:

在main.m文件里面,分别用alloc + init方式和new方式初始化TestPerson。

打印的结果,无论是地址,还是赋值,都是一样的,new = alloc + init。
通过汇编调试,也是一样的:

在objc源码中搜索objc_opt_new

也是一样的。
到了此处,欧耶,大功告成,对象的本质的探究,就完成了,有木有点收获啊,(不许没有啊<( ̄▽ ̄)/)
感谢各位的光临~ ~ ~ ~ ~ ~ 





















![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)