通过C++源码分析OC对象、类的本质
1. OC编译生成C++代码的方法的两种方法
这里我们有在main里写一些代码声明一个LGPerson
类看一下编译之后的结果
1.1 clang
clang -rewrite-objc main.m -o main.cpp //把⽬标⽂件编译成c++⽂件
这种方式比较简单直接但是如果引用到OC
的系统库像UIKit
之类的会报错,解决报错需要加一坨参数
UIKit
报错问题
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
通过clang
编译之后会生成一个.cpp文件
![image.png](https://upload-images.jianshu.io/upload_images/3910976-6d896d9ea365e18e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
1.2 xcrun
xcode
安装的时候顺带安装了xcrun
命令,xcrun
命令在clang
的基础上进⾏了
⼀些封装,要更好⽤⼀些
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o
main-arm64.cpp //(模拟器)
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main�arm64.cpp //(⼿机)
xcrun
编译之后也是生成.cpp
产物
下面我们来看下这个cpp
文件
2.对象的本质
搜索一下
LGPerson
可以看到一下几点
-
OC
中的LGPerson
类底层是struct LGPerson_IMPL
结构体 -
OC
中@interface LGPerson : NSObject
LGPerson
继承NSObject
底层是typedef struct objc_object LGPerson;
这样体现的
到这个对象的本质就显而易见了 对象的本质是结构体
struct NSObject_IMPL NSObject_IVARS;
LGPerson_IMPL中的这个成员变量是isa
struct NSObject_IMPL {
Class isa;
};
typedef struct objc_class *Class;
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
typedef struct objc_object *id;
typedef struct objc_selector *SEL;
typedef void (*IMP)(void );
-
Class
底层是struct objc_class *
类型,NSObject
底层是struct objc_object
结构体,id
底层是struct objc_object *
类型 -
struct objc_object
的实现是struct objc_object { Class _Nonnull isa __attribute__((deprecated)); };
也就是说NSObject底层实现的结构体里只要一个成员变量isa -
id
底层实现是struct objc_object *
类型,怪不得声明id类型变量时 后面不用再加"*"了 -
SEL
是struct objc_selector *
类型 -
IMP
是void (*)(void )
函数指针类型
接下来看下KCName
属性的getter
、setter
方法的底层实现
static NSString * _I_LGPerson_KCName(LGPerson * self, SEL _cmd) {
return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_KCName));
}
static void _I_LGPerson_setKCName_(LGPerson * self, SEL _cmd, NSString *KCName) {
(*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_KCName)) = KCName;
}
- 首先数入参
LGPerson * self
,SEL _cmd
,所有OC
方法都有这两个隐藏参数,所以在OC
的方法中我们可以使用self
- 然后可以看到
getter
和setter
里是通过首地址+偏移量的方式取和存的
2. 联合体位域
联合体跟结构体对比着来会好理解一点
//结构体
struct LGCar1 {
BOOL front; // 0 1
BOOL back;
BOOL left;
BOOL right;
};
先来看这个结构体,它包含4个BOOL
型的成员变量,占4个字节的内存,BOOL
类型用0和1就能标识也就是说用一个二进制位就足够了,4个BOOL
类型用4位就可以表示也就是半个字节就够了(一个字节包含8个二进制位),由此该位域出场了
// 位域
struct LGCar2 {
BOOL front: 1;
BOOL back : 1;
BOOL left : 1;
BOOL right: 1;
};
这个结构体中的每个变量都只占用一个二进制位
输出
car1
和car2
占用的内存,car1
占用了4字节,car2
只占用1个字节,虽然car2
只需要4位(半个字节)就够了,但是内存是以字节为单位的,所以分配了1个字节,这样用了位域之后一下子就节省了3/4的内存,car2
中会使用一个二进制位标识一个属性,了解了位域再来了解一下联合体(也叫共用体)
teacher1
和teacher2
包含相同的变量,但是前者开辟24字节的内存,后者只开辟了8字节内存,这是啥情况呢
结构体(struct)中所有变量是“共存”的——优点是“有容乃⼤”,
全⾯;缺点是struct内存空间的分配是粗放的,不管⽤不⽤,全分配。
联合体(union)中是各变量是“互斥”的——缺点就是不够“包容”;
但优点是内存使⽤更为精细灵活,也节省了内存空间
也就是说结构体会给所有的成员变量都分配内存,所以分配了24字节的内存,联合体里的变量内存是公用的所以只需要开辟一个最大变量需要的内存就够了,最大的是double
类型的所以分配了8个字节。这时候你可能还是有点蒙蔽,接下来我们来看下联合体的内存情况
刚创建的就是内存都初始化为了
0x0000000000000000
这是正常的
当给name
属性复制之后,age
和height
变成了0x0000000100003e68(age是int型的四个字节所以只展示了0x00003e68)
,p (char *)0x0000000100003e68
当我们以字符串的类型输出0x0000000100003e68
发现也是"Cooci",所有的属性公用了相同的内存,到这里对联合体公用内存的意思,应该有个清晰的认识了吧,最后再来看下联合体位域
#import
NS_ASSUME_NONNULL_BEGIN
@interface LGCar : NSObject
@property (nonatomic, assign) BOOL front;
@property (nonatomic, assign) BOOL back;
@property (nonatomic, assign) BOOL left;
@property (nonatomic, assign) BOOL right;
@end
NS_ASSUME_NONNULL_END
定义一个LGCar
类,有向前,向后,向左,向右四个属性
#import "LGCar.h"
#define LGDirectionFrontMask (1 << 0)
#define LGDirectionBackMask (1 << 1)
#define LGDirectionLeftMask (1 << 2)
#define LGDirectionRightMask (1 << 3)
@interface LGCar(){
// 联合体
union {
char bits;
// 位域
struct {
char front : 1;
char back : 1;
char left : 1;
char right : 1;
};
} _direction;
}
@end
.m里加了个_direction
的联合体实现前后左右属性
- (void)setFront:(BOOL)isFront {
if (isFront) {
_direction.bits |= LGDirectionFrontMask;
} else {
_direction.bits &= ~LGDirectionFrontMask;
}
NSLog(@"%s",__func__);
}
- (BOOL)isFront{
return _direction.front;
}
- (void)setBack:(BOOL)isBack {
_direction.back = isBack;
NSLog(@"%s",__func__);
}
- (BOOL)isBack{
return _direction.back;
}
分别通过操作_direction.bits
和 _direction.back
的方式改变值
我们知道对象类型的起始位置存的是
isa
后面存属性(也就是0x100705760 + 8)就是_direction
的位置为了避免干扰只看_direction
这1个字节的内存情况
执行完
setFront
后的结果
setFront:
是直接操作bits
的
setBack:
同样改变的是这片内存,这样有一个好处用联合体位域存放数据_direction.front
由于联合体公用内存其实就是取bits
的第一位(从右向左),_direction.back
就是去的bits
的第二位 ,赋值的时候不用分别赋值,可以直接给bits
赋值,取值的时候通过.front
、.back
等由可以很方便的单独取
3.isa分析
前面讲了那么多就是为了引出isa_t
,那么这个isa_t
又是从哪儿冒出来的呢,还记的我们在iOS底层原理探究01-alloc底层原理里我们说过OC对象
在创建的过程中是通过initIsa
对isa
进行初始化的,当时也说了对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);
};
这么大一坨看着是不是有点懵,来简化一下
union isa_t {
uintptr_t bits;
Class cls;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
这样看着简单多了吧,接下来通过位域看下isa
里存了些啥
define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
这个是arm64下isa_t的位域
- nonpointer: 表示是否对 isa 指针开启指针优化
0:纯isa指针,1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等 - has_assoc: 关联对象标志位,0没有,1存在
- has_cxx_dtor: 该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑,
如果没有,则可以更快的释放对象 - shiftcls: 存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤来存储类指针。
- magic: ⽤于调试器判断当前对象是真的对象还是没有初始化的空间
- weakly_referenced: 志对象是否被指向或者曾经指向⼀个 ARC 的弱变量,
没有弱引⽤的对象可以更快释放。 - has_sidetable_rc: 当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位
- extra_rc: 当表示该对象的引⽤计数值,实际上是引⽤计数值减 1,
例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,
则需要使⽤到下⾯的 has_sidetable_rc。
nonpointerisa 需要跟ISA_MASK
做&运算才能得到class
下面放上isa的走位图
最后补充一个知识点
我们前面那么多内容都是对alloc
的探究,那init
呢,init
方法做了什么
alloc
之后已经生成了对象,所以对调用- (id)init
这个对象方法,而不是调用类方法,init
方法里调用了_objc_rootInit
,进去看一下
什么都没做直接返回了传进来的
obj
,这不是多此一举吗?你是不是会有这个疑问,其实不是的,苹果的工程师又不傻,这样做肯定是有他的道理,这是工厂设计模式的体现,目的是定义工厂方法让子类重写这个方法,根据不同子类的重写做不同的操作。
我们有时候会使用
new
而不是alloc
、init
,来看下new
的实现
new
方法是直接调用callAlloc
,然后再调用init
方法,其实跟alloc
、init
是一样的,但是前面alloc
流程我们知道LLVM
会hook
了alloc
方法,那它会不会也hook
了new
呢,咱们运行起来看下
我们分别来个
alloc
和new
看下运行时的汇编
alloc
被替换成了objc_alloc
这个我们之前已经知道了
new
被替换成了objc_opt_new
接下来看下这个方法
如果是
OBJC2
的情况下还是[callAlloc(cls, false/*checkNil*/) init];
这样调用,因为我们现在都是用的OBJC2
所以流程还是没有变的,最终的结论是new
跟alloc+init
是一样的