第三节课 OC对象原理(下)
全篇开始之前我们想一个问题,研究了这么久对象,究竟什么是对象
呢??
对象本质以及拓展
Clang
探索对象的本质前,我们先了解一个编译器:clang
Clang是一个C语言、C++、OC语言的轻量级编译器。源代码发布于BSD协议下。Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。
clang是一个由Apple主导编写,基于LLVM的C/C++/OC的编译器
探索本质
1、在main
中自定义一个类LGPerson
,有一个属性name
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation LGPerson
@end
2、通过终端,利用clang
将main.m
编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp
3、打开编译好的main.cpp
,找到LGPerson
的定义,发现LGPerson
在底层会被编译成struct 结构体
我们可以看到,这是一个结构体,里面嵌套了一个结构体,结构体能够继承嘛?其实是可以的,但是是属于
伪继承
,伪继承的方式是直接将NSObject
结构体定义为LGPerson
中的第一个属性
,意味着LGPerson拥有NSObject中的所有成员变量。
LGPerson_IMPL
中的第一个属性,其实就是isa
,是继承自NSObject
,
通过上图我们可以看到成员变量是Class isa
。通常叫isa
叫做isa指针
,那么这里的Class
应该是个指针类型
,在main.cpp文件中全局搜索*Class。代码如下
LGPerson
的类型是objc_object
,在OC层面 我们的LGPerson是继承NSObject
,其实在下层真正的实现就是objc_object
。
对象的本质拓展
在刚才看class的过程中,还发现了两个东西
熟悉的id
和SEL
。常用的id
原来是一个objc_object
结构体指针,这就解释了id
修饰变量和作为返回值的时候为什么不加*
,下面的SEL
也是结构体指针这个也是之前不知道的呢,就稍微了解下吧~
在稍下面一点我们看到一些奇奇怪怪的一些东西
这其实就是我们的
Get方法
和Set方法
,但是参数部分呢?我们可在OC中没有看到,这其实就 是我们的隐藏参数。这个
Get
方法中的参数self
+OBJC_IVAR_$_LGPerson$_name
这一步就是我们之前讲过的,要获取一个类的对象的地址,是通过获取类的首地址
+对象的偏移量
的方法来最终取得对象的。
小结
所以从上述探索过程中可以得出:
-
OC对象的本质
其实就是结构体
-
LGPerson
中的isa
是继承
自NSObject中的isa
联合体位域拓展补充
位域(位段)
位域:在C语言中允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单元的成员称为位域。
struct HZMCar1 {
BOOL front; // 0 1
BOOL back;
BOOL left;
BOOL right;
};
struct HZMCar1 car1;
NSLog(@"%ld",sizeof(car1));
<--打印输出-->
4
在开发当中遇到这种情况,其实BOOL就只有两种情况,但是却使用了4个字节32位
,其实我们只用4位就可以
了,这样我们就只使用了1字节,还有3字节其实是浪费的。接下来我们进行改进一下
struct HZMCar2 {
BOOL front: 1;
BOOL back : 1;
BOOL left : 1;
BOOL right: 1;
};
struct HZMCar2 car2;
NSLog(@"%ld",sizeof(car2));
<--打印输出-->
1
其实就是指定对象所占内存长度
联合体(共用体)
struct LGStudent {
char *name;
int age;
double height ;
};
union LGStudent2 {
char *name;
int age;
double height ;
};
struct LGStudent student;
student.name = "HZM";
student.age = 18;
union LGStudent2 student2;
student2.name = "HZM";
student2.age = 18;
NSLog(@"%ld-%ld",sizeof(student),sizeof(student2));
<--打印输出-->
24-8
首先我们能观察到,同样的成员变量,内存大小天差地别,这是为什么呢?我们来通过断点查看下。
可以看到联合的成员变量在
未赋值的情况下是一块脏数据、脏内存
,当第二个变量被赋值的时候又将第一个变量清理了,所以联合体的各种变量互斥
,每次只能赋值一个,这样就可以接受大幅空间,这也就是我们之前看到的为什么联合体的内存大小比较小的原因。
小结:
位域和联合体对比
位域:优点是所有变量可以共存
,缺点是内存空间粗放
,不管用不用,都分配
联合体:优点是内存更精细灵活,节省空间
,缺点是各种变量互斥
isa的类型 isa_t
以下是isa指针的类型isa_t的定义,从定义中可以看出是通过联合体(union)定义的。
union isa_t { //联合体
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
//提供了cls 和 bits ,两者是互斥关系
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
isa_t
类型使用联合体的原因也是基于内存优化的考虑,这里的内存优化是指在isa指针中通过char + 位域
(即二进制中每一位均可表示不同的信息)的原理实现。通常来说,isa指针
占用的内存大小是8
字节,即64
位,已经足够存储很多的信息了,这样可以极大的节省内存,以提高性能
从isa_t的定义中可以看出:
-
提供了两个成员,
cls
和bits
,由联合体的定义所知,这两个成员是互斥
的,也就意味着,当初始化isa指针时,只有一个变量有值- 通过
cls
初始化,bits无默认值
- 通过
bits
初始化,cls有默认值
- 通过
-
还提供了一个结构体定义的
位域
,用于存储类信息及其他信息,结构体的成员ISA_BITFIELD
,这是一个宏定义
,有两个版本__arm64__
(对应ios 移动端) 和__x86_64__
(对应macOS),以下是它们的一些宏定义,如下图
03-ISA_BITFIELD.png
nonpointer
:表示是否对isa
指针开启指针优化
0:纯isa
指针,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 时
,则需要借用该变量存储进位
extra_rc
:当表示该对象的引用计数值
,实际上是引用计数值减 1
例如,如果对象的引用计数为 10
,那么 extra_rc
为 9
。如果引用计数大于 10
, 则需要使用到上面的 has_sidetable_rc
。
ISA_MASK
ISA_MASK
是一个宏,一个掩码。__x86_64__
的值等于 0x00007ffffffffff8ULL
,__arm64__
的值等于0x0000000ffffffff8ULL
。通过x/4g p
获取到的首位地址就是isa
的值是0x001d800100008275
,验证下0x001d800100008275&0x00007ffffffffff8ULL结果
对象通过 isa & 掩码 得到类的信息
isa的位运算
通过上面的学习我们已经知道了isa
的shiftcls
是用来存储类指针
,所以我们降妖获取类指针其实也可以通过位运算
来获取shiftcls的最终值
。
通过上图我们发现,其实我们要获取的shiftcls
正好在中间,左边有3位,右边有28位,那我们如果要获取的话就可以将shiftcls
整体向左移3位
,将多余部分挤出去,向右移20位
,将左边的多余部分挤出去,最后左移17位恢复原位
,这个时候剩下的不就是我们需要的shiftcls
了。我们来验证下
可以看到位运算结束后的结果与我们输出的class相同。