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]