我们已经学习了对象的初始化
、内存对齐
等内容。这篇文章将深入学习探究对象的本质
、对isa进行分析
。
学习对象本质之前,先引入一个工具clang
。
一.clang
1.什么是clang
Clang是⼀个C语⾔、C++、Objective-C语⾔的轻量级编译器。
源代码发布于BSD协议下。
Clang将⽀持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。
lang是⼀个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器。
2013年4⽉,Clang已经全⾯⽀持C++11标准,并开始实现C++1y特性(也就是C++14,这是
C++的下⼀个⼩更新版本)。Clang将⽀持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。
Clang是⼀个C++编写、基于LLVM、发布于LLVM BSD许可证下的C/C++/Objective-C/
Objective-C++编译器。它与GNU C语⾔规范⼏乎完全兼容(当然,也有部分不兼容的内容,包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,⽐如C函数重载(通过__attribute__((overloadable))来修饰函数),其⽬标(之⼀)就是超越GCC。
2.clang的作用
那么clang
应该学习什么呢?做什么呢?
因为OC是C、C++的超集,通过clang可以将m文件编译成cpp文件,这样我们可以了解更多的关于底层的实现原理。
3.clang的使用方式
- clang -rewrite-objc
main.m
-omain.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
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
(⼿机)
如果代码有使用runtime
的内容,如weak
,可以使用一下指令:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
二.探索对象的本质
1.使用clang编译生成cpp文件
直接上代码!引入一个案例,在main.m文件
中添加GFPerson类的声明和实现
,如下图所示:
打开系统终端,进入main.m文件
所在目录,运行命令clang -rewrite-objc main.m -o main.cpp
执行指令后,即可编译生成一个main.cpp
文件。
2.cpp文件解读
打开运行指令后编译生成的cpp文件
,文件很长,从我们自定义的类GFPerson
开始寻找!
1.GFPerson对象
全局搜索GFPerson
,获得以下核心代码:
// GFPerson结构体声明
typedef struct objc_object GFPerson;
typedef struct {} _objc_exc_GFPerson;
#endif
// GFPerson_IMPL结构体实现
struct GFPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
/* @end */
// @implementation GFPerson
// @end
解读代码:
- 定义了一个
别名GFPerson
,该别名指向struct objc_object类型
; - 在结构体实现
GFPerson_IMPL
中,有一个成员变量NSObject_IVARS
,来自所继承的结构体,也就是isa
;另一个成员变量是_name
,也就是GFPerson
的属性,和OC
层面定义是一致的。
2.NSObjec对象
根据NSObject_IMPL
进行搜索,获取NSObject类
的声明和实现等相关内容。
// NSObject结构体声明
typedef struct objc_object NSObject;
typedef struct {} _objc_exc_NSObject;
#endif
// NSObject实现-对象
struct NSObject_IMPL {
Class isa;
};
解读代码:
- 定义
别名NSObject
,同样指向struct objc_object类型
。 - 在
NSObject
结构体实现中,有一个Class类型
的成员变量isa
;
3.底层结构关系
进一步搜索Class
的定义和objc_object
的定义,见下面代码:
// Class定义 - 指向objc_class的指针
typedef struct objc_class *Class;
// objc_object定义 - 根类定义
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
// id的定义指向objc_object的指针
typedef struct objc_object *id;
// SEL 方法编号,方法选择器指针
typedef struct objc_selector *SEL;
解读代码:
OC层面
的NSObject
,在底层对应objc_object结构体
;- 子类的
isa
均继承自NSObject
,也就是来自objc_object
结构体; Objective-C
中NSObject
是大多数类的根类
,而objc_object
可以理解为就是c\c++
层面的根类
。isa的类型为Class,被定义为指向objc_class的指针
。- 在开发中可以
用id来表示任意对象
,根本原因就是id被定义为指向objc_object的指针,也就指向NSObject的指针
。 SEL
方法选择器指针,方法编号。
通过上面的分析,可以得出以下结构关系:
4.get\set方法
GFPerson类
的属性,自动添加get\set方法
。见下面代码:
static NSString * _I_GFPerson_name(GFPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_GFPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_GFPerson_setName_(GFPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct GFPerson, _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。
5.cpp内容补充
除了我们最关心的对象的定义外,在cpp文件
中,还可一看到ro
、rw
、协议
、分类
、方法
等内容的定义。
分类
的定义。包括分类名称
、关联的类
、实例方法列表
、类方法列表
等信息。
struct _category_t {
const char *name; // 名称
struct _class_t *cls; // 关联的类
const struct _method_list_t *instance_methods; // 实例方法
const struct _method_list_t *class_methods; // 类方法
const struct _protocol_list_t *protocols; // 协议
const struct _prop_list_t *properties; // 属性
};
- 方法或函数的定义。
Method
是一个objc_method结构体
,包括方法编号selector
、type encoding
、方法实现地址
。
struct _objc_method {
struct objc_selector * _cmd; // 方法编号
const char *method_type; // type encodings
void *_imp; // 方法实现地址
};
其中_cmd
和_imp
比较熟悉,方法编号和方法实现,那么method_type
是什么呢?在苹果开发者官网,可以找到对应的type encodings
对照表。
如本例中GFPerson
的方法列表:
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[4];
} _OBJC_$_INSTANCE_METHODS_GFPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
4,
{{(struct objc_selector *)"name", "@16@0:8", (void *)_I_GFPerson_name},
{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_GFPerson_setName_},
{(struct objc_selector *)"name", "@16@0:8", (void *)_I_GFPerson_name},
{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_GFPerson_setName_}}
};
以name属性
的get方法
为例:@16@0:8
@
:表示返回值,一个object对象
,这里是NSString *
类型;16
:函数的传入的所有参数的字节数之和为16
,因为有两个隐藏参数GFPerson * self
和SEL _cmd
,所以一共16个字节
;@
:第一个参数类型为id型
,GFPerson * self
;0
:前面的参数起始的字节位置(从0开始)
;:
:第二个参数类型为sel
,SEL _cmd
;8
:前面的参数起始的字节位置(从8开始)
。
3.对象本质总结
通过工具clang
,编译生成的cpp文件
,我们可以发现,对象实质是一个结构体
。在OC层,NSObject是大多数类的根类,而objc_object可以理解为就是c\c++层面的根类
。NSObject
仅有一个实例变量Class isa
,Class实质上是指向objc_class的指针
。objc_class
的定义见下面的代码:
struct objc_class : objc_object {
// 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_rw_t *data() const {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
…… 省略
}
objc_class
继承自objc_object
,所以万物皆对象!
三.联合体位域简述
在学习isa
之前,先了解一下联合体以及位域。
1.位域
引入一个案例,定义一个结构体Car
,体现车的运动方向,见下面代码:
这样看上去是可以满足的的业务需求的,但是这里有个问题,这个结构体需要占用4
个字节32
位,使用4
个字节去体现一个单一功能有些浪费空间。理论上,一个字节就可以体现车的运动状态,改进一下,见下图:
这里使用了位域
,用位
来体现一个功能,比如有值就是用1,没有值就是用0
。BOOL front : 1;
表示front
占用一位,这样体现一个车辆的状态只需要4位
即可,这样整体需要一个字节即可满足要求!
2.结构体特点
同样引入一个案例!见下图:
上面的案例中定义了一个结构体Person1
,char * name
占用8
个字节,int age
占用4
个字节,double height
占8
个字节,结合8字节对齐
,该结构体共占用24
个字节。同时,运行代码,给结构体赋值过程中,结构体中各个属性之间并无冲突,处于共存的状态。
结构体(struct)
特点总结如下:
- 优点:
共存
,有容乃⼤,全⾯; - 缺点:
struct
内存空间的分配是粗放的,不管⽤不⽤,全分配。
3.联合体特点
同样引入一个案例,来区分结构体和联合体的区别!见下图:
上面的案例中定义了一个联合体Person2
,char * name
占用8
个字节,int age
占用4
个字节,double height
占8
个字节,而这三个属性是互斥的,该联合体实际占用空间是8
个字节。同时,运行代码,给联合体赋值过程中,联合体中各个属性之间处于互斥的状态,并且联合体实际大小与最大的属性值大小相等
。
联合体(union)
特点总结如下:
- 优点:内存使⽤更为精细灵活,也节省了内存空间;
- 缺点:不够包容,各变量是
互斥
的。
四.isa探索
1.isa_t联合体
通过上面的案例,认识到了联合体与结构体的区别,同时了解到位域在节省内存方面的优势。而isa
,就是采用联合体结合位域,对数据进行了封装。见下面源码:
// isa 联合体
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
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
2.nonpointer isa各位含义
nonpointer
:1位,表示是否对isa
指针开启指针优化,0:纯isa指针
,1:不⽌是类对象地址
,isa
中包含了类信息、对象的引⽤计数等。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
。
3.nonpointer isa初始化
在对象进行初始化过程中,_class_createInstanceFromZone
中三个重要的初始化流程:
cls->instanceSize
,计算要开辟的内存大小,16字节对齐原则
;obj = (id)calloc(1, size);
,内存空间开辟;obj->initInstanceIsa
,isa
初始化过程。
- 本篇重点学习
nonpointer isa
的初始化流程!
设置断点,运行程序,过滤出我们所需要研究的GFPerson类
的初始化流程。见下图所示:
isa_t为联合体
,初始化nonpointer isa
,则cls
属性为空,bits结构体
会被初始化(互斥)
,8字节共64位,默认都为0
。继续运行代码,bits
赋值ISA_MAGIC_VALUE
,赋值后,各位域的值见下图:
第一位值为1
,即对 isa
指针开启指针优化。从47
到53
位,也就是magic
赋值为59
,非0
,表示当前对象已被初始化
。通过计算器可以验证,59
的二进制就是0011 1011
,如下图所示:
继续运行代码,将类的地址右移3位
,赋值给shiftcls
,见下图。为何要右移三位呢?因为shiftcls
前面还有3位存储着nonpointer
、has_assoc
、has_cxx_dtor
。
isa
初始化流程结束,我们可以通过创建的gf对象
反推isa
指向的是否为GFPerson类
。见下图:
获取gf对象
的内存结构,右移3位
,左移20位
,再右移17位
,获取类地址,成功指向GFPerson类
。
4.对象获取类
平常获取对象的类
会直接调用class方法
,那么class方法
内部实现是怎样的?见下面源码:
- (Class)class {
return object_getClass(self);
}
// getIsa()方法;
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
// 当前不是taggedPointer,而是nonpointor isa, 直接返回ISA()
inline Class
objc_object::getIsa()
{
if (fastpath(!isTaggedPointer())) return ISA();
extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
uintptr_t slot, ptr = (uintptr_t)this;
Class cls;
slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
cls = objc_tag_classes[slot];
if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
cls = objc_tag_ext_classes[slot];
}
return cls;
}
// ISA——返回:return (Class)(isa.bits & ISA_MASK);
inline Class
objc_object::ISA()
{
ASSERT(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
return (Class)(isa.bits & ISA_MASK);
#endif
}
通过解读上面的代码,发现获取对象的类,最终是实现代码是(Class)(isa.bits & ISA_MASK);
,是通过对象isa & ISA_MASK
。ISA_MASK
是什么呢?见下图:
在计算器中可以发现,该掩码低三位
和高17位
全部是0
,通过对象isa & ISA_MASK
运算,会将对象isa
的低三位
和高17位
全部抹零
,等价于上面的右移3位
,左移20位
,再右移17位
操作流程。
ISA_MASK
也即是ISA
的一个面具!验证一下: