前言:
这是探究OC对象原理的第三章,也是按照对象的 的底层实现原理顺序来进行的。今天我们探究下对象的本质以及一些拓展内容。在这之前我先介绍一下clang
和xcrun
,因为本文需要用到。请看下图(借鉴前人的总结)
简单了解了clang后我们今天需要几个clang
和xcrun
的命令,帮助我们把常用的.m
文件转换成c++
文件,也就是.cpp
文件。
1,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
2,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`
(⼿机)
利用以上命令可以将我们OC的.m文件转成c++的.cpp文件,我们借助.cpp文件 来查看我们生成的对象在c++层面是以怎样的形式存在的。
初步探究
我们先创建一个项目在main.m
文件里创建一个类ZYPerson
,并且创建一个属性 zyName
。如图:
#import
#import
@interface ZYPerson : NSObject
@property (nonatomic, copy) NSString *zyName;
@end
@implementation ZYPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
接下来我们执行上面的clang
命令 将main.m
文件转成main.cpp
的文件并且双击打开。利用 command+F
搜索ZYPerson
。
#ifndef _REWRITER_typedef_ZYPerson
#define _REWRITER_typedef_ZYPerson
typedef struct objc_object ZYPerson;
typedef struct {} _objc_exc_ZYPerson;
#endif
extern "C" unsigned long OBJC_IVAR_$_ZYPerson$_zyName;
struct ZYPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_zyName;
};
发现:
ZYPerson
是以struct
存在的
回到上面我们的ZYPerson_IMPL
代码块,我们看到结构体内struct NSObject_IMPL NSObject_IVARS
这句,我们根据这句代码全局搜索下NSObject_IMPL
struct NSObject_IMPL {
Class isa;
};
发现:
NSObject_IVARS
其实是成员变量isa.
同时我们在ZYPerson_IMPL
结构体中看到我们的属性zyName
。我们不妨探究下属性在底层的setter和getter是如何实现的。在ZYPerson_IMPL
结构体的正下方紧接着我们看到以下两句代码:
// @property (nonatomic, copy) NSString *zyName;
/* @end */
// @implementation ZYPerson
static NSString * _I_ZYPerson_zyName(ZYPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_ZYPerson$_zyName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_ZYPerson_setZyName_(ZYPerson * self, SEL _cmd, NSString *zyName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct ZYPerson, _zyName), (id)zyName, 0, 1); }
// @end
我们分析代码发现第一句代码
return
了一个值,第二句代码却是objc_setProperty
设置了一个属性。并且两者都携带了我们经常提到的隐藏参数ZYPerson * self
、SEL _cm
,再次根据第一行代码返回值的形式,很明显是在拼接地址的方法。这很符合我们iOS
在内存中查找值的规律。我们基本可以确定第一句代码就是我们属性zyName
的getter
方法,相对的第二句则是它的setter
方法。同时我们也明白了为什么我们可以在每个方法里都能调用到self的原因,因为每个方法属性都携带了self
、SEL _cm
这两个隐藏参数。
回到最初的代码我们还可以看到这样一句代码:typedef struct objc_object ZYPerson
,这个objc_object
让我很好奇,我们接着查看下它是什么东西,仍然command+F
搜索
typedef struct objc_class *Class;
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
typedef struct objc_object *id;
发现:
objc_object
是以struct
存在的,而且我们的ZYPerson
是继承的NSObject
但是在底层其实是objc_object
在这段代码中,我们看到了熟悉的Class
和id
。并且我们看到他们俩的声明跟ZYPerson
的声明有点不一样他们都是以指针形式存在的,看对比:
发现:
typedef struct objc_object ZYPerson;
typedef struct objc_class *Class;
typedef struct objc_object *id;
这也是为什么我们在使用id
声明对象的时候为什么不需要*
的原因。因为他自己本身就是一个objc_object *
结构体指针,他可以定义任何类型的变量。同时我们常用的Class
他的类型也是objc_class *
结构体指针。
结构体、位域、联合体
接下来我们插入一点预备知识,就是结构体、位域、联合体,分别来看看他们的区别。
实例一
struct ZYComputer1 {
BOOL iMac; // 0 1
BOOL macBookPro;
BOOL airBook;
BOOL iPad;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct ZYComputer1 computer1;
NSLog(@"ZYComputer1's size : %ld",sizeof(computer1));
}
return 0;
}
查看NSLog打印出来的结果:
2021-06-16 17:30:17.916212+0800 ZYProjectTree1[59630:1944971] ZYComputer1's size : 4
可以看到
ZYComputer1
的大小为4
字节
示例二
struct ZYComputer2 {
BOOL iMac: 1;
BOOL macBookPro : 1;
BOOL airBook : 1;
BOOL ipad: 1;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct ZYComputer2 computer2;
NSLog(@"ZYComputer2's size : %ld",sizeof(computer2));
}
return 0;
}
查看NSLog打印出来的结果:
021-06-16 17:35:49.180143+0800 ZYProjectTree1[59712:1948611] ZYComputer2's size : 1
可以看到
ZYComputer1
的大小为1
字节,这就是位域互斥
示例三
struct ZYPerson1 {
double height;
char *name;
long age;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct ZYPerson1 person1;
person1.height = 180.0;
person1.name = "Wayne";
person1.age = 20;
NSLog(@"person1's height : %f",person1.height);
NSLog(@"person1's name : %s",person1.name);
NSLog(@"person1's age : %ld",person1.age);
}
return 0;
}
查看NSLog打印出来的结果:
2021-06-16 17:49:01.845235+0800 ZYProjectTree1[60074:1959304] person1's height : 180.000000
2021-06-16 17:49:01.845684+0800 ZYProjectTree1[60074:1959304] person1's name : Wayne
2021-06-16 17:49:01.845719+0800 ZYProjectTree1[60074:1959304] person1's age : 20
可以看到
ZYPerson1
的所有属性都已经被赋值成功了。所以结构体是共存的。
示例四
union ZYPerson2 {
char *name;
double height;
long age;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
union ZYPerson2 person2;
person2.name = "Wayne001";
person2.height = 190.0;
person2.age = 18;
NSLog(@"person2's name : %s",person2.name);
NSLog(@"person2's height : %f",person2.height);
NSLog(@"person2's age : %ld",person2.age);
}
return 0;
}
直接查看NSLog
打印出来的结果你会发现会崩溃(具体原因至今没找到),所以我们用断点在lldb
下打印看看 (每一次p person2
打印对应每一个断点)
(lldb) p person2
(ZYPerson2) $0 = (name = 0x0000000000000000, height = 0, age = 0)
(lldb) p person2
(ZYPerson2) $1 = (name = "Wayne001", height = 2.1220038126150982E-314, age = 4294983532)
(lldb) p person2
(ZYPerson2) $2 = (name = "", height = 190, age = 4640889047261118464)
(lldb) p person2
(ZYPerson2) $3 = (name = "", height = 8.8931816251424378E-323, age = 18)
(lldb)
如图:
可以看到
ZYPerson2
在几次赋值的过程中始终只保存了一个最后赋予的值。并不像实例三一样每一个属性都被赋值。这就是联合体 union
和结构体 struct
直接的区别。联合体也是互斥的。
总结:
1,
结构体 struct
的大小是根据内部成员变量的类型和个数来决定的。可以根据实例一证明。
2,对结构体 struct
内的成员变量指定位域(如示例二
)可以优化结构体 struct
的大小。但是最终的大小还是会以字节
的倍数
来计算,如示例二
中指定位域
为1 位
,四个成员变量加起来应该是4 位
也就是0.5字节
,但是最终会分配1字节
。因为内存中不存在0.5字节
的单位。
3,结构体 struct
中的成员变量是共存
的,存储的值互不影响,可以通过示例三
来证明,每个成员变量都成功赋值。优点是全⾯
;缺点是struct
内存空间的分配是粗放
的,不管⽤不⽤,全分配。
4,联合体 union
中的成员变量是互斥
的,当某一个成员变量被赋值其他成员变量的地址将不被使用,此时其他成员变量存储的将是脏数据
。联合体(union
)的缺点就是不够包容
;优点是内存使⽤更为精细灵活
,也节省了内存空间
initIsa 探索
首先上一段代码 (ps:来源于苹果开源的objc
底层源码):
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
/***删除了前段部分代码,删除的代码是前两章探索的alloc、内存分配的部分***/
//以下判断就是我们要探索的 isa 部分 也就是 对象alloc 以及内存分配后 的步骤
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
/***删除了后段部分代码,删除的代码是关于return的内容***/
}
这段代码我们很熟悉,因为在前两章我们经常要跟踪alloc
代码到这个方法里。这个方法就包含了对象初始化的一些过程,从alloc
到内存分配
再到现在我们探索的 initIsa
和class
的绑定。前两步我们已经在前两章探索过了,这里紧接着第二章探索initIsa
我们根据这个if 判断首先看到initInstanceIsa
这个方法。我们跟踪进去:
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
可以看到这方法 进去后 最终还是调用了initIsa
方法。所以我们着重探索initIsa
,同样利用command+F
可以跟踪到以下方法:
inline void
objc_object::initIsa(Class cls)
{
initIsa(cls, false, false);
}
NONPOINTER_ISA
继续上面的代码跟踪:
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
isa_t newisa(0);
if (!nonpointer) {
newisa.setClass(cls, this);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
newisa.extra_rc = 1;
}
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
在上面这个方法里我们提取一些比较熟悉的东西,从上往下看,我们可以看到isa_t newisa(0)
这个方法,发现是个isa
的初始化方法,我们看看这个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
(ps:这个不是位域里的bits
么)、还有我们的对象cls
,而且我们发现还有一个ISA_BITFIELD
的结构体成员变量。我们看看ISA_BITFIELD
是什么
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_MASK 0x007ffffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 0
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t weakly_referenced : 1; \
uintptr_t shiftcls_and_sig : 52; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# 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
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# endif
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# 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 : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 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
在这里我们看到分别是 arm64
和 x86_64
两个框架的定义,我们发现这个ISA_BITFIELD
原来是位域。而且我们发现ISA_BITFIELD
存的内容有nonpointer
、关联对象(has_assoc)
、析构函数(has_cxx_dtor)
、类的指针地址(shiftcls)
、(magic)
、弱引用(weakly_referenced)
、是否使用(unused)
、散列表(has_sidetable_rc)
、引用计数(extra_rc)
等。这就证明isa_t
并不是简单地isa指针和类的信息,而是存了很多其他东西,这就所谓的NONPOINTER_ISA
。
下面针对这个位域我们看看 arm64
架构下的图解:
0号标志位——
nonpointer
:表示是否对isa
指针开启指针优化
0:纯isa指针,1:不⽌是类对象地址,isa
中包含了类信息、对象的引⽤计数等
1号标志位——
has_assoc
:关联对象标志位,0没有,1存在
2号标志位——
has_cxx_dtor
:该对象是否有C++
或者Objc
的析构器,如果有析构函数,则需要做析构,如果没有,则可以更快的释放对象
3-35号标志位——
shiftcls
: 存储类指针的值。开启指针优化的情况下,在arm64
架构中有33 位
⽤来存储类指针。
36-41号标志位——
magic
:⽤于调试器判断当前对象是真的对象还是没有初始化的空间。
42号标志位——
weakly_referenced
:志对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放。
43号标志位——`unused:标志对象是否正在使用
44号标志位——
has_sidetable_rc
:当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位
45-64号标志位——
extra_rc
:当表示该对象的引⽤计数值,实际上是引⽤计数值减 1。
例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,
则需要使⽤到下⾯的 has_sidetable_rc。
isa关联对象
上面我们花了很长的时间去跟踪了isa_t
一直到位域
的分析。现在我们回到代码去看看
我们创建一个ZYPerson
类,在main
文件里导入头文件,代码如下:
#import
#import
#import "ZYPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
ZYPerson *p = [ZYPerson alloc];
NSLog(@"%@",p);
}
return 0;
}
我们在NSLog(@"%@",p);
这句代码上打断点 利用lldb
来 p
一下 看看这个对象的地址 以及 类的地址:
我们看到这里的对象p
和 类ZYPerson
对应的指针地址 0x011d800100008275
、0x0000000100008270
,0x
后的最高位都是0
这就证明这两个64位
地址的内存根本没有用完。我们下面来看看对象和类到底是怎么关联的。我们回到objc源码
方法
inline void
objc_object::initIsa(Class cls, bool nonpointer,UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor){ }
在这个方法里我们 接着从 isa_t newisa(0)
这句代码往下走,可以看到一个 if else
的判断,如果!nonpointer
就直接newisa.setClass(cls, this);
设置了cls
。如果不是则走else
对上面我们分析的位域进行一些列的赋值 最后也对cls
进行了set
方法。我们跟踪下newisa.setClass(cls, this);
inline void
isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{
// Match the conditional in isa.h.
#if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# if ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_NONE
// No signing, just use the raw pointer.
uintptr_t signedCls = (uintptr_t)newCls;
# elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ONLY_SWIFT
// We're only signing Swift classes. Non-Swift classes just use
// the raw pointer
uintptr_t signedCls = (uintptr_t)newCls;
if (newCls->isSwiftStable())
signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));
# elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ALL
// We're signing everything
uintptr_t signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));
# else
# error Unknown isa signing mode.
# endif
shiftcls_and_sig = signedCls >> 3;
#elif SUPPORT_INDEXED_ISA
// Indexed isa only uses this method to set a raw pointer class.
// Setting an indexed class is handled separately.
cls = newCls;
#else // Nonpointer isa, no ptrauth
shiftcls = (uintptr_t)newCls >> 3;
#endif
}
我们可以看到这方法里先获取到 newClass
uintptr_t signedCls = (uintptr_t)newCls;
然后获取到位域结构:
uintptr_t signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));
然后进行一系列的位移
shiftcls_and_sig = signedCls >> 3;
shiftcls = (uintptr_t)newCls >> 3;
到这里我们不禁有些疑惑为什么要这样位移来处理呢?这里就涉及到另一个我们需要留意的点那就是 ISA_MASK
ISA_MASK (掩码)
在我们上面探究的位域部分,我们可以留意到以下代码
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
这个就是
ISA
的掩码
,他的作用你可以理解为对ISA的一个防护伪装,之前在对象和类绑定的 过程中利用这个ISA_MASK
掩码进行了伪装(因为ISA
不是单纯的nopointer
里面还包含了很多上面我们分析的位域
内容等),所以如果我们想要看到ISA
对应类的地址
就需要经过反掩码
的操作来获取。
我们用代码来演示一下,我们知道对象p
的isa地址
为 0x011d800100008275
,也打印出来了其对应的类的地址
为0x0000000100008270
。我们用掩码来验证一下:(也就是让对象的isa地址
和掩码ISA_MASK
进行 与(&) 操作)
其实我们想要通过对象得到类的信息,还可以通过ISA位运算
直接算出来。下面我们来看看ISA位运算
。
ISA位运算
我们在之前分析isa_t的时候分析到了他包含的位域部分。在位域里有一个关于类的shiftcls
,上面我们分析的是arm64
架构的,但是我们现在电脑跑的代码是在模拟器上所以我们看看shiftcls
在x86
架构上它所占的位是 44
位。所以我们下面计算的时候用 44
位来计算:
图解:
至此,本章内容就算是告一段落了,其中关于对象属性 的setter
、getter
方法的更详细解析,后面再补充。
遇事不决,可问春风。站在巨人的肩膀上学习,如有疏忽或者错误的地方还请多多指教。谢谢!