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