写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。
目录如下:
以上内容的总结专栏
写在前面
之前我们分析了alloc底层流程和结构体的内存对齐原理。那么,今天我们来分析下对象的本质是什么。
我们都知道,OC语言是基于C和C++语言增加了一层面向对象,那么,我们就从OC的对象,在C和C++中底层的实现代码来开始今天的探索之路。
准备
1、Clang
Clang是一个C语言、C++、Objective-C语言的轻量级编译器。源代码发布于BSD协议下。 Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。Clang是一个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器。 其常用命令如下:
把目标文件编译成c++文件
clang -rewrite-objc main.m -o main.cpp
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 (手机)
2、union
联合体
我们通过一个例子来认识和了解 union 以及 union与struct的区别
struct SMTeacher3 {
char *name;
int age;
double height ;
};
union SMTeacher4 {
char *name;
int age;
double height ;
};
简单总结一下,
联合体 中各变量是“互斥的”,缺点是不够“包容”;优点是内存使用更为精细灵活,也节省来内存空间。 (通过打印的t4占用了8字节内存空间比结构体的24字节节省了3倍,随着第二次第三次打印 t4 的内容,可以看到每次赋值后,其他的属性就变成了脏数据,不再可读);
结构体 则是所有的变量都是“共存的”,优点是足够“包容”全面,缺点是内存的管理是粗放的,不管用不用全分配。
位域
有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有 0 和 1 两种状态,用 1 位二进位即可。为了节省存储空间,并使处理简便,C 语言又提供了一种数据结构,称为"位域"或"位段"。
所谓"位域"是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。
开始
首先,我们在新建的项目中 main.h 文件中,我们添加一个SMPerson对象 :
@interface SMPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) NSInteger height;
@property (nonatomic, copy) NSString *like;
@end
@implementation SMPerson
@end
接着,我们通过命令行工具,将 main.h 文件编译为c++文件 main.cpp ,命令为:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
可以看到 在当前的项目路径下生成来一个 main.cpp 文件。直接找到我们的SMPerson 类在底层是一个结构体,实现如下 :
struct SMPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
NSInteger _age;
NSInteger _height;
NSString *_like;
};
多出来的这个 struct NSObject_IMPL NSObject_IVARS; 是什么呢?
没错他就是 isa,
typedef struct objc_class *Class;
struct NSObject_IMPL {
Class isa;
};
下面是SMPerson 属性的set、get方法的底层实现
static NSString * _I_SMPerson_name(SMPerson * self, SEL _cmd) {
return (*(NSString **)((char *)self + OBJC_IVAR_$_SMPerson$_name));
}
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_SMPerson_setName_(SMPerson * self, SEL _cmd, NSString *name) {
objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct SMPerson, _name), (id)name, 0, 1);
}
static NSInteger _I_SMPerson_age(SMPerson * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_SMPerson$_age)); }
static void _I_SMPerson_setAge_(SMPerson * self, SEL _cmd, NSInteger age) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_SMPerson$_age)) = age; }
static NSInteger _I_SMPerson_height(SMPerson * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_SMPerson$_height)); }
static void _I_SMPerson_setHeight_(SMPerson * self, SEL _cmd, NSInteger height) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_SMPerson$_height)) = height; }
static NSString * _I_SMPerson_like(SMPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_SMPerson$_like)); }
static void _I_SMPerson_setLike_(SMPerson * self, SEL _cmd, NSString *like) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct SMPerson, _like), (id)like, 0, 1); }
我们通过调试台将 p1 对象的内存信息打印出来,
由此可见,p1 对象在内存中,其属性是如下所示布局的:
所以,在set、get方法的底层实现中 OBJC_IVAR_$_SMPerson$_name 就是属性对应的内存地址的偏移量, 系统通过每个属性的偏移量,来实现对其赋值和取值。
对象的本质:
对象在底层就是一个结构体,其内部存储了类的实例变量。
对象的 NSObject_IMPL 继承自 NSObject 的 isa。
NSObject 只有一个成员变量 isa。
isa 的底层实现是什么呢?
iOS 底层原理探索之 alloc中我们探寻类 alloc 方法的底层流程。 其中在
/// 将类和指针做绑定
obj->initInstanceIsa(cls, hasCxxDtor);
中,此过程包含了 isa 的初始过程,要探究 isa 的底层实现,我们就从 isa 的创建开始:
initIsa 内容如下:
我们可以看到 方法内创建了一个 isa_t 类型的 newisa 实例, 做了 赋值操作后,返回了 newisa。 那么,接着我们就来详细的看一下这个 isa_t 的底层实现。
isa_t 内容如下:
所以 isa_t 是一个联合体, 有两个成员变量一个是 bits, 还有一个 是 cls。我们知道 联合体 中各变量是互斥的, 它的优点是内存使用更为精细灵活。 所以,也就是说, isa_t 有两种初始化方式:
bits被赋值,cls没有值或者值被覆盖;cls被赋值,bits没有值或者值被覆盖。
isa_t 中还有一个成员变量 是 结构体 ISA_BITFIELD, 这个宏定义对应 __arm64__ 和 __x86_64__ 即 iOS 和 MacOS 两个端的实现。
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
为来更直观的理解上面 位域 ISA_BITFIELD存储的信息, 我们画个图来解析一下以上这段很长的代码。
arm64架构下的isa:
x86_64架构下的isa:
各变量代表的意思是:
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。
isa底层实现总结
isa有两种类型nonpointer类型和 非nonpointer类型,nonpointer类型包含来类信息、对象引用计数等数据; 非nonpointer类型只是一个纯指针。isa使用联合体加位域来实现。 采用这种方式节省了大量的内存(由此可见,苹果在底层实现是做了很多的优化工作的);开发中,大量使用的对象都会有一个isa指针, 这么多的isa会占用大量的内存,联合体中成员变量的互斥特性,节省了部分的内存空间。再加上位域的使用,更是在节省内存空间的同时,将对象类的信息等信息作了优化存储,这样,使得isa指针 所占用的内存空间得到最大化的使用。
扩展
# if __arm64__
# define ISA_MASK 0x007ffffffffffff8ULL
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
以上四行代码,是我取自 结构体 ISA_BITFIELD 中的内容。
这里的 ISA_MASK 是一个掩码。 我们知道 isa 的 8字节 - 64位存储空间中 在 arm64 也就是手机端 shiftcls 存储类指针的值 占用的是第4位开始33位长度的一段内存空间; 在 x86_64 也就是电脑端, shiftcls 存储类指针的值 占用的是第4位开始44位长度的一段内存空间。我们调试台打印出来一个对象的内存地址后,取第一位存储的isa地址&上对应的掩码后,就是对象的类指针的值。
下面,验证一下: