如何探究对象的本质?
因为oc的底层是c和c++实现的,clang可以将oc还原为c或者c++的代码,所以通过clang可以看到一个对象的c或者c++的基层实现。
Clang的扩展,什么是Clang?
Clang: a C language family frontend for LLVM。Clang 项目为LLVM 项目的 C 语言家族(C、C++、Objective C/C++、OpenCL、CUDA 和 RenderScript)中的语言提供了语言前端和工具基础结构。提供了 GCC 兼容的编译器驱动程序和 MSVC 兼容的编译器驱动程序 。
Clang编译oc文件
模拟器版cpp文件生成命令:
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
生成了main-arm64.cpp文件:
301623835223_.pic.jpg查看文件的内容,我在main.m中声明和实现了NNPerson这个类,对应的main.cpp文件的代码如下:
WechatIMG33.jpeg
可以看到NNPerson生成了一个结构体:
struct NNPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _myAge;
};
其中,有一个NSObject_IMPL,还一个我在oc中定义的myAge。进一步查看NSObject_IMPL是什么,全局搜索可以看到如下代码块:
struct NSObject_IMPL {
Class isa;
};
typedef struct objc_class *Class;
typedef struct objc_object *id;
typedef struct classref *classref_t;
由此可见,一个对象的本质是一个结构体,里面主要有一个结构体NSObject_IMPL,也就是isa,是一个objc_class指针。
在这里我们也看到了关于id的定义,就是一个objc_object指针,所以我们通常用id来声明对象的时,直接id声明就好,不用再加对象的星号了。
然后进一步查找objc_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
通过这里可以看到objc_class继承自objc_object,我们点进去objc_object看到了objc_class的结构:
#if !OBJC_TYPES_DEFINED
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;
/// An opaque type that represents a category.
typedef struct objc_category *Category;
/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
#endif
看到这个,但是在现在的oc版本中这段代码已经废弃了,最前面有宏定义#if !OBJC_TYPES_DEFINED,然后查看这个OBJC_TYPES_DEFINED发现定义如下:
#ifdef _OBJC_OBJC_H_
#error include objc-private.h before other headers
#endif
#define OBJC_TYPES_DEFINED 1
#undef OBJC_OLD_DISPATCH_PROTOTYPES
#define OBJC_OLD_DISPATCH_PROTOTYPES 0
始终为1,取非的话就是0就不会走这个地方了。
再搜索看到可以看大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
//此处省略下面的一堆方法等等等
}
objc_class继承自objc_object,所以我们来看objc2在使用的objc_object的结构,查找如下:
struct objc_object {
private:
isa_t isa;
//此处省略下面的一堆定义 主要可以看到一个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);
};
isa_t是一个联合体,所以下面说下结构体和共用体的区别。
结构体和共用体
-结构体:结构体(Struct)是一种集合,它里面包含了多个变量或数组,它们的类型可以相同,也可以不同,每个这样的变量或数组都称为结构体的成员(Member)。定义格式如下:
struct 结构体名{
结构体所包含的变量或数组
};
-共用体:共用体(Union)有时也被称为联合或者联合体。定义格式如下:
union 共用体名{
成员列表
};
结构体和共用体的区别在于:结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。
举一个共用体的例子🌰:
union NNTestUnion{
int a;
char b;
double c;
}testUnion;
NSLog(@"占用的大小是:%lu",sizeof(testUnion));
testUnion.a = 5;
NSLog(@"a=%d,b=%c,c=%f",testUnion.a,testUnion.b,testUnion.c);
testUnion.b = 'a';
NSLog(@"a=%d,b=%c,c=%f",testUnion.a,testUnion.b,testUnion.c);
testUnion.c = 8.0;
NSLog(@"a=%d,b=%c,c=%f",testUnion.a,testUnion.b,testUnion.c);
打印如下:
351624008206_.pic.jpg
可以根据上面的结果看到,同一时刻只能保存一个成员的值。成员之间是互斥的
如果union的成员定义改成如下:
union NNTestUnion{
int a;
char b;
float c;
}testUnion;
那么占用的大小第一个打印变成:
2021-06-18 17:23:57.350435+0800 KCObjcBuild[48834:10108220] 占用的大小是:4
所以共用体占用的内存等于最长的成员占用的内存。
位域
位域:有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可。例如开关只有通电和断电两种状态,用 0 和 1 表示足以,也就是用一个二进位。正是基于这种考虑,C语言又提供了一种叫做位域的数据结构。
在结构体定义时,我们可以指定某个成员变量所占用的二进制位数(Bit),这就是位域。
举一个位域的例子🌰:
struct direction{
bool up:1;
bool down:1;
bool left:1;
bool right:1;
}direction1;
struct Direction{
bool up;
bool down;
bool left;
bool right;
}Direction1;
NSLog(@"direction1占用的大小是:%lu",sizeof(direction1));
NSLog(@"Direction1占用的大小是:%lu",sizeof(Direction1));
打印结果如下:
361624009262_.pic.jpg
就很清楚的可以看到结果,指定完毕之后就节省了很多内存
ISA_BITFIELD
在union isa_t 中有一个ISA_BITFIELD这个,点进去看一下这个的定义:
typedef unsigned long uintptr_t;
# if __arm64__
# 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
# elif __x86_64__
# 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
可以看到ISA_BITFIELD是一个宏定义,所以那个isa_t中:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
可以替换为:
typedef unsigned long uintptr_t;
#if defined(ISA_BITFIELD)
struct {
// defined in isa.h
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;
};
可以了解到,isa的定义,它里面定义了一个位域。并且可以看到,x86_64和arm64下的位域定义是不一样的,不过都是占满了所有的64位(1+1+1+33+6+1+1+1+19 = 64,x86_64同理),下面来说明一下每一个位域参数的含义:
-nonpointer:表示是否对isa开启指针优化 。0代表是纯isa指针,1代表除了地址外,还包含了类的一些信息、对象的引用计数等。
-has_assoc:关联对象标志位。
-has_cxx_dtor:该对象是否有C++或Objc的析构器,如果有析构函数,则需要做一些析构的逻辑处理,如果没有,则可以更快的释放对象。
-shiftcls:存在类指针的值,开启指针优化的情况下,arm64位中有33位来存储类的指针。
-magic:判断当前对象是真的对象还是一段没有初始化的空间。
-weakly_referenced:是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象释放的更快。
-unused:标志是否未被使用过。
-has_sidetable_rc:当对象引用计数大于10时,则需要进位。
-extra_rc:表示该对象的引用计数值,实际上是引用计数减一。例如:如果引用计数为10,那么extra_rc为9。如果引用计数大于10,则需要使用has_sidetable_rc。
( 说明:以上参数含义摘抄自网络资料,不是官方文档解释,我自己想查官方的解释没找到哇ε=(´ο`))) )*
类和isa是如何绑定的?
之前学习alloc的流程时,最后在方法_class_createInstanceFromZone中,做了三件事情,一个是计算所需要的空间大小,一个是开辟内存空间,另外一个是isa的的初始化,和相关类进行绑定。方法是initIsa,那么具体来看看这个方法里做了些什么。
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_BITFIELD各个位域的意义,可以了解到shiftcls是用来存放类指针的值,magic是用来判断这个类有没有被初始化完毕。
那么上面的代码可以看到,会先初始化一个isa_t,根据nonpointer,也就是是不是纯的ISA指针进行不同分支的初始化后续操作。
我这个走的是纯指针的流程:
371624250036_.pic.jpg
然后会来到setClass方法:
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
}
我这个进到setClass方法以后直接走的这句
shiftcls = (uintptr_t)newCls >> 3;
shiftcls这个的值是newCls右移三位得到,我查看相关资料说这里是为了内存对齐,指针占8字节所以是八字节对齐。
此时我打印右移之前的和右移之后的内存地址如下:
381624250375_.pic.jpg
过掉断点走到这个地方:
391624250523_.pic_hd.jpg
此时打印下newisa为:
401624250583_.pic.jpg
根据显示的地址,此时isa中的cls已经关联上了类,shiftcls也保存上了相关的类信息。
(虽然我还是有点迷糊,再有新的理解会更新的,太难了哇😭)
网友评论