一、涉及知识点
1.共用体(联合体)
定义
在进行某些算法的C语言编程的时候,需要使几种不同类型的变量存放到同一段内存单元中,也就是使用覆盖技术,几个变量互相覆盖,以达到节省空间的目的,这种几个不同的变量共同占用一段内存的结构叫共用体,又称联合体。
特点:
共用体和结构体有下列区别:
a.结构体的成员之间是共存的:各个成员占用不同的内存,它们互相之间没有影响。
b.联合体的成员之间是互斥的:所有成员共用同一段内存,修改一个成员的值,会影响其余所有成员。
c.结构体占用的内存:大于等于所有成员占用内存的总和(需要内存对齐)
d.联合体占用的内存:等于最大的成员占用的内存,同一时刻只能保存一个成员的值
声明
#pragma mark 共用体
union WJUnion {
int a;
float b;
char c;
};
2.位域
定义
C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称位域( bit field) 。利用位段能够用较少的位数存储数据。
特点:
a.位段成员的类型必须指定为unsigned或int类型;
b.若某一位段要从另一个字开始存放,用:0长度为0的空位段,作用就是使下一个位段从下一个存储单位(视不同编译系统而异)开始存放;
c.一个位段必须存储在同一存储单元中,不能跨两个单元;
d.可以定义无名字段例如":2";
e.位段的长度不能大于存储单元的长度,也不能定义位段数组;
f.位段可以用整形格式符输出;
g.位段可以在数值表达式中引用,它会被系统自动地转换成整形数;
声明
#pragma mark 结构体
struct WJCarStruct {
BOOL front;
BOOL back;
BOOL left;
BOOL right;
};
#pragma mark 位域
struct WJCarStructBit {
BOOL front : 1;
BOOL back : 1;
BOOL left : 1;
BOOL right : 1;
};
分析:
结构体WJCarStruct占4个字节,因为每个BOOL各占1个字节;
位域WJCarStructBit占1个字节,对比WJCarStruct结构体节省3个字节。
二、OC对象底层分析辅助工具(clang编译源码成底层代码)
1.clang定义
clang简而言之是一个C语言、C++、Objective-C语言的轻量级编译器,源代码发布于BSD协议下。Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字
2.在终端使用clang相关命令编译WJPerson.m生成WJPerson.cpp底层文件
2.1 使用clang命令
# clang -rewrite-objc WJPerson.m -o WJPerson.cpp
2.2 使用Xcode中的xcrun命令
# xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc WJPerson.m -o WJPerson.cpp
3.打开WJPerson.cpp并找到WJPerson类
typedef struct objc_object WJPerson;
typedef struct {} _objc_exc_WJPerson;
extern "C" unsigned long OBJC_IVAR_$_WJPerson$_name;
extern "C" unsigned long OBJC_IVAR_$_WJPerson$_nickName;
struct WJPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS; //就是isa指针
NSString * _Nonnull _name; //属性name对应的成员变量
NSString * _Nonnull _nickName; //属性nickName对应的成员变量
};
// @property (nonatomic, copy)NSString *name;
// @property (nonatomic, copy)NSString *nickName;
/* @end */
从编译之后的文件找到WJPerson_IMPL,可以看出WJPerson类实际上是一个结构体;由此可见,在OC中类的本质就是结构体(struct)。
3.1 来看结构体中的成员变量_name和_nickName,其实就是WJPerson头文件中声明的两个属性name和nickName所对应的。
@interface WJPerson : NSObject
@property (nonatomic, copy)NSString *name;
@property (nonatomic, copy)NSString *nickName;
@end
3.2 再来看结构体NSObject_IMPL是什么呢?
在WJPerson.cpp中搜索NSObject_IMPL,发现其实就是isa指针,
struct NSObject_IMPL {
Class isa;
};
在OC中基本上所有的对象都是继承NSObject,但是真正的底层实现是objc_object的结构体类型
3.3 对象的本质拓展
在WJPerson.cpp文件中全局搜索*Class时,发现有这样几行代码如下:
typedef struct objc_class *Class;
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
typedef struct objc_object *id;
typedef struct objc_selector *SEL;
源码分析:
常用的id原来是也是一个objc_object结构体指针别称,这就解释了id修饰变量和作为返回值的时候为什么不加*了,此外还有Class、SEL等也是结构体指针。
三、nonPointerIsa分析
对象alloc流程核心三步曲
a.instanceSize,计算开辟内存需要的大小。
b.calloc,向系统申请开辟内存,返回地址指针。
c.initInstanceIsa,初始化指针和类关联起来。
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
进入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 {
......
}
isa = newisa;
}
进入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_BITFIELD分析
在表现一个类的地址时,会出现一个词:nonPointerIsa。就比如一个类,也就可以作为一个指针,类上面是可以有很多内容是能够被存储的。类的指针是8字节,8字节 * 8 bit = 64 bit(64位)。那么如果只是用来存储一个指针,就会造成大大的浪费,因为每个类都有一个isa指针。苹果就对这个isa做了优化,就把和类息息相关的一些内容存在里面,比如:是否正在释放、引用计数、weak、关联对象、析构函数等等(所以,OC在底层,就是C++,像OC的释放,并不是真正的释放,而是其下层的C++释放,才是真正的释放),这些都和类先关,所以,可以把这些内容存储到那64位里面去。那么就出现了nonPointerIsa。nonPointerIsa也不是一个简单的地址。我们可以通过查看isa_t的位域,来了解里面存的是什么。
在arm64中:
# 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
字段说明如下:
nonpointer:
表示是否对isa指针开启指针优化 0:纯isa指针,1:不⽌是类对象地址,isa中包含了类信息、对象的引⽤计数等;
has_assoc:
关联对象标志位,0没有,1存在;
has_cxx_dtor:
该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象;
shiftcls:
存储类指针的值,开启指针优化的情况下,在arm64架构中有33位⽤来存储类指针;
magic:
⽤于调试器判断当前对象是真的对象还是没有初始化的空间;
weakly_referenced:志对象是否被指向或者曾经指向⼀个ARC的弱变量,没有弱引⽤的对象可以更快释放;
deallocating:
标志对象是否正在释放内存;
has_sidetable_rc:
当对象引⽤技术⼤于10时,则需要借⽤该变量存储进位;
extra_rc:
当表示该对象的引⽤计数值,实际上是引⽤计数值减1,例如,如果对象的引⽤计数为10,那么extra_rc为9。如果引⽤计数⼤于10,则需要使⽤到下⾯的has_sidetable_rc。
以arm64为例,堆中,8字节对齐排列,8字节 * 8 bit = 64 bit(64位)。
那么,nonpointer占[1]号位置,has_assoc占[2]号位置,has_cxx_dtor占[3]号位置,shiftcls占[4 ~ 36]号位置,magic占[37~42]号位置,weakly_referenced占[43]号位置,deallocating占[44]号位置,has_sidetable_rc占[45]号位置,extra_rc占[46 ~ 64]号位置。
总结:
1.OC中类的本质就是结构体。
2.ISA是通过共用体(联合体)互斥的特性,确定ISA是纯的ISA还是NONPOINTER_ISA,并通过位域来实现节约空间。