OC是一门面向对象的语言,那什么是对象呢,今天我们一起探索一下对象的本质
认识clang
Clang是一个C语言、C++、Objective-C语言的轻量级编译器。源代码发布于BSD协议下。Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。
Clang是一个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器。
Clang是一个C++编写、基于LLVM、发布于LLVM BSD许可证下的C/C++/Objective-C/Objective-C++编译器。它与GNU C语言规范几乎完全兼容(当然,也有部分不兼容的内容,包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,比如C函数重载(通过__attribute__((overloadable))来修饰函数),其目标(之一)就是超越GCC。 clang官方文档
查看源码,探索类的本质
生成cpp文件
创建一个Person类
@interface Person : NSObject
@end
@implementation Person
@end
通过
clang -rewrite-objc main.m -o main.cpp 把目标文件编译成c++文件
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
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
然后我们就得到了一个cpp文件
查看cpp文件
搜索我们创建的Person文件
查看创建的Person类
#ifndef _REWRITER_typedef_Person
#define _REWRITER_typedef_Person
typedef struct objc_object Person;
typedef struct {} _objc_exc_Person;
#endif
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
我们发现Person在底层是一个结构体,并且有一个元素
struct NSObject_IMPL NSObject_IVARS;
这个是结构体的一个伪继承
这个NSObject_IMPL是什么呢,在这个cpp文件中搜索一下发现
struct NSObject_IMPL {
Class isa;
};
总结: 定义了一个别名
Person,该别名指向struct objc_object类型; 在结构体实现Person_IMPL中,有一个成员变量NSObject_IVARS,来自所继承的结构体,也就是isa; 我们的Person继承了NSObject,在底层就是objc_object
疑问:🤔️为什么isa的类型是Class?
- 在OC底层探索-alloc底层原理 源码分析文章中,提及过alloc方法的核心之一的initInstanceIsa方法,通过查看这个方法的源码实现,我们发现,在初始化isa指针时,是通过isa_t类型初始化的,
- 而在NSObject定义中isa的类型是Class,其根本原因是由于isa 对外反馈的是类信息,为了让开发人员更加清晰明确,需要在isa返回时做了一个类型强制转换,类似于swift中的 as 的强转。
底层探索
typedef struct objc_class *Class;
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
typedef struct objc_object *id;
typedef struct objc_selector *SEL;
OC层面的NSObject,在底层对应objc_object结构体;- 子类的
isa均继承自NSObject,也就是来自objc_object结构体; Objective-C中NSObject是大多数类的根类,而objc_object可以理解为就是c\c++层面的根类。isa的类型为Class,被定义为指向objc_class的指针。Class也是一个结构体的指针- 在开发中可以用
id来表示任意对象,根本原因就是id被定义为指向objc_object的指针,也就指向NSObject的指针。 SEL方法选择器指针,方法编号。
get/set方法
将上面的Person类写个name属性,生成cpp文件在查看一下
static NSString * _I_Person_name(Person * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_Person_setName_(Person * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _name), (id)name, 0, 1); }
通过以上代码可以发现,无论是get方法还是set方法,都会有两个隐藏参数,self和_cmd,也就是方法接收者和方法编号。在获取属性时,采用指针平移的方式,获取成员变量所在地址,转换后返回对应的数值。
objc_setProperty,在对实例变量进行设置时,会自动调用objc_setProperty方法。该方法可以理解为set方法的底层适配器,通过统一的封装,实现set方法的统一入口。
在runtime源码中,搜索objc_setProperty,可以找到最终实现方法,见下段代码:
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue); // retain新值
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
// 释放旧值
objc_release(oldValue);
}
本质是通过指针平移找到成员变量位置,然后进行新值的retain,旧值的release。
总结
通过对objc_setProperty的底层源码探索,有以下几点说明:
-
objc_setProperty方法的目的适用于关联上层的set方法以及底层的set方法,其本质就是一个接口 -
这么设计的原因是,
上层的set方法有很多,如果直接调用底层的set方法中,会产生很多的临时变量,当你想查找一个sel时,会非常麻烦 -
基于上述原因,苹果采用了
适配器设计模式(即将底层接口适配为客户端需要的接口),对外提供一个接口,供上层的set方法使用,对内调用底层的set方法,使其相互不受影响,即无论上层怎么变,下层都是不变的,或者下层的变化也无法影响上层,主要是达到上下层接口隔离的目的
对象的大小
空对象
空对象,大小为8字节,从上面可以看出空对象有个
isa
使用
class_getInstanceSize()需要导入头文件#import <objc/runtime.h>
添加属性
如果根据结构体的大小计算,应该是48个,打印出大小是40。
发现有内存优化,在一个8字节中存了两个字段。
添加成员变量
所占内存增加了
添加方法
所占内存没有变化
结论: 1.对象的大小跟成员变量和属性有关,跟方法无关,也就是对象的大小跟成员变量有关 2.在添加成员变量的过程中,由于成员变量的数据类型是不一致的,向最大数据类型的成员变量对齐。继承自NSObject对象的类,默认字节对齐方式是8字节。 3.底层虽然是结构体,但它的大小进行了优化
附上x /nuf 的说明
x /nuf x以十六字节打印 ————— n表示要显示的内存单元的个数 ————— u表示一个地址单元的长度: b表示单字节 h表示双字节 w表示四字节 g表示八字节 ————— f表示显示方式,可取如下值: x按十六进制格式显示变量 d按十进制格式显示变量 u按十进制格式显示无符号整型 o按八进制格式显示变量 t按二进制格式显示变量 a按十六进制格式显示变量 i指令地址格式 c按字符格式显示变量 f按浮点数格式显示变量 事例:x/4gx 以十六进制打印,4个单元,一个单元长度是8字节,以16进制显示
对象本质
通过工具clang,编译生成的cpp文件,我们可以发现,对象实质是一个结构体。在OC层,NSObject是大多数类的根类,而objc_object可以理解为就是c\c++层面的根类。NSObject仅有一个实例变量Class isa,Class实质上是指向objc_class的指针。
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
Class getSuperclass() const {
#if __has_feature(ptrauth_calls)
# if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
if (superclass == Nil)
return Nil;
#if SUPERCLASS_SIGNING_TREAT_UNSIGNED_AS_NIL
void *stripped = ptrauth_strip((void *)superclass, ISA_SIGNING_KEY);
if ((void *)superclass == stripped) {
void *resigned = ptrauth_sign_unauthenticated(stripped, ISA_SIGNING_KEY, ptrauth_blend_discriminator(&superclass, ISA_SIGNING_DISCRIMINATOR_CLASS_SUPERCLASS));
if ((void *)superclass != resigned)
return Nil;
}
#endif
void *result = ptrauth_auth_data((void *)superclass, ISA_SIGNING_KEY, ptrauth_blend_discriminator(&superclass, ISA_SIGNING_DISCRIMINATOR_CLASS_SUPERCLASS));
return (Class)result;
# else
return (Class)ptrauth_strip((void *)superclass, ISA_SIGNING_KEY);
# endif
#else
return superclass;
#endif
}
探索isa
位域
先看一个结构体
输出car size 4
结构体car里面的元素是BOOL类型的,只需要4位,现在却占了4个字节32位浪费了很多空间。
使用位域优化
输出:
car size 4
car1 size 1
BOOL front:1; 1表示占1位,0000 0000从后向前依次是front、back、left、right
联合体
先看一个结构体
struct Teacher {
char name;
int age;
double height;
};
给结构体赋值
1.声明
2.
name赋值
3.
age赋值
证明当前的结构体里的元素能够同时赋值
联合体(共用体)
union Teacher1 {
char name;
int age;
double height;
};
联合体里面的内容与上面的结构体一模一样
给联合体赋值
- 声明
name赋值
在给name赋值以后,age和height都有值,但是这是脏数据age赋值
在给age赋值以后,name的数据没有了,联合体是互斥的
isa就是联合体
我们看源码中,alloc一个对象的时候会将isa与类绑定,会使用initIsa(),initIsa()里有一个对象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是一个联合体,有两属性Class cls 和uintptr_t bits,这两个属性时互斥的,该联合体占用8个字节内存空间。
Class cls,非nonpointer isa,没有对指针进行优化,直接指向类,
typedef struct objc_class *Class;
uintptr_t bits;
nonpointer isa,使用了结构体位域,针对arm64架构和x86架构提供了不同的位域设置规则。
#if SUPPORT_PACKED_ISA
// ios真机环境
# 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; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
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)
// mac、模拟器环境
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# error unknown architecture for packed isa
# endif
// SUPPORT_PACKED_ISA
#endif
针对两种不同平台,其isa的存储情况如图所示
isa各位含义
nonpointer:1位,表示是否对isa指针开启指针优化,- 0:纯
isa指针, - 1:不⽌是类对象地址,
isa中包含了类信息、对象的引⽤计数等。
- 0:纯
has_assoc:1位,关联对象标志位,- 0没有,
- 1存在。
has_cxx_dtor:1位,该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑;如果没有,则可以更快的释放对象。shiftcls:存储类指针的值。开启指针优化的情况下,在arm64架构中有33位⽤来存储类指针,在x86架构中有44位⽤来存储类指针。magic:6位,⽤于调试器判断当前对象是真的对象还是没有初始化的空间。weakly_referenced:1位,指对象是否被指向或者曾经指向⼀个ARC的弱变量,没有弱引⽤的对象可以更快释放。deallocating:1位,标志对象是否正在释放内存。has_sidetable_rc:1位,当对象引⽤计数⼤于10时,则需要借⽤该变量存储进位。extra_rc:表示该对象的引⽤计数值,实际上是引⽤计数值减1,例如,如果对象的引⽤计数为10,那么 extra_rc 为9。如果引⽤计数⼤于10,则需要使⽤到下⾯的has_sidetable_rc。
探索isa
通过alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone方法路径,查找到initInstanceIsa,并进入其原理实现
进入initIsa方法的源码实现,主要是初始化isa指针
该方法的逻辑主要分为两部分
- 通过
cls初始化isa - 通过
bits初始化isa
isa 与 类 的关联
cls 与 isa 关联原理就是isa指针中的shiftcls位域中存储了类信息,其中initInstanceIsa的过程是将 calloc 指针 和当前的 类cls 关联起来
通过isa指针地址与ISA_MSAK 的值 & 来验证
创建一个macOS下的Command Line Tool项目
这里看到
isa``64位存储情况
这里的
isa怎么到Person.class这个地址呢
源码中
isa掩码
person的isa & 掩码,得到的就是Person.class
我们从上面isa结构中可以看到x86_64环境下shiftcls占44位从后往前放的,所以它是在[17 60]