一、对象的本质
1、clang的介绍
Clang是⼀个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器。
clang具有如下优点:
- 编译速度更快:在某些平台上,clang的编译速度要明显快于gcc。
- 占用内存更小:clang生成的AST所占用的内存通常是gcc的五分之一左右。
- 模块化的设计:clang采用基于库的模块化设计,更易于IDE的集成及其他用途的重用。
- 诊断信息可读性强:在编译过程中,clang会创建并保留大量详细的元数据 (metadata),这将更有利于调试和错误报告。
- 设计更清晰简单,容易理解,易于扩展加强。与代码基础较为古老的gcc相比,学习曲线会显得更为平缓。
我们可以通过clang的还原,看到OC代码在底层的实现!
2、clang的简单使用
我们新建一个工程,选择macOS
下的Command Line Tool
,同时创建一个HPerson
对象:
然后在终端
进行编译:
clang -rewrite-objc main.m -o main.cpp
编译成功,得到一个main.cpp
:
但是如果我们创建一个app工程
,引用了系统的动态库,再编译的话,就会报错:
那么可以使用带链接
的编译方法:
// 命令
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk 编译文件名称.后缀
// 示例
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
编译成功,得到一个main.cpp
:
更多的指令可以通过-help
去探索:
clang -help
3、查看c++源码
-
对象
打开第一次生成的main.cpp
,搜索HPerson
:
#ifndef _REWRITER_typedef_HPerson
#define _REWRITER_typedef_HPerson
typedef struct objc_object HPerson;
typedef struct {} _objc_exc_HPerson;
#endif
extern "C" unsigned long OBJC_IVAR_$_HPerson$_HName;
struct HPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_HName;
};
发现对象在底层的本质就是结构体!
那么NSObject_IVARS
是什么呢?
跟进去看一看:
struct NSObject_IMPL {
Class isa;
};
发现NSObject_IVARS就是isa!
同时:
typedef struct objc_object HPerson;
也印证了之前《OC对象原理探究之alloc探索》探索底层的时候得到的结论:
NSObject在底层就是objc_object。
也可以看看Class
的底层:
typedef struct objc_class *Class;
意味着Class
是一个结构体指针
!
id
也和Class
类似:
typedef struct objc_object *id;
所以id
可以不带*
使用:
id person;
-
属性
我们继续搜索HName
:
//getter方法
static NSString * _I_HPerson_HName(HPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_HPerson$_HName)); }
//setter方法
static void _I_HPerson_setHName_(HPerson * self, SEL _cmd, NSString *HName) { (*(NSString **)((char *)self + OBJC_IVAR_$_HPerson$_HName)) = HName; }
就会找到属性的getter
和setter
方法,发现方法有2个隐藏参数
:
HPerson * self //对象自身的指针
SEL _cmd //方法的指针
再来看看getter
的return
:
return (*(NSString **)((char *)self + OBJC_IVAR_$_HPerson$_HName));
这里就是对象的首地址
加上成员变量的指针地址
,得到成员变量的内存地址
!
setter
的return
也是一样,拿到成员变量的内存地址
然后赋值
!
二、位域和联合体
1、位域
先来看看这个结构体
:
struct HCar1 {
BOOL front;
BOOL back;
BOOL left;
BOOL right;
};
BOOL
类型占用的内存为1字节
,根据内存对齐原则可得出HCar1
的内存大小为4字节
。
1字节
为8位
,4字节
则为32位
。
一辆车的前后左右,每个方向使用0
和1
表示即可,那么我们使用4位
即可满足要求,最多也就是1字节
!如下图所示:
如果使用4字节
,则浪费了3字节
的空间!
那么怎么优化呢?
我们可以使用位域
:
struct HCar2 {
BOOL front: 1;
BOOL back : 1;
BOOL left : 1;
BOOL right: 1;
};
冒号后面的数字
代表该成员变量占用了多少位
!
所以HCar2
大小为1字节
!如下图所示:
这样就大大的优化了内存
!
2、联合体
在一个结构体
里面:
struct HTeacher1 {
char * name;
int age;
double height;
};
所有的成员变量
都有自己的内存空间
,都有自己的值
!如下图所示:
但是如果是一个联合体
呢?
就会发现只有1
个成员变量
的值
是正确
的,为什么呢?
打印成员变量
的内存地址
:
(lldb) p &teacher2.name
(char **) $10 = 0x000000030412b3e8
(lldb) p &teacher2.age
(int *) $11 = 0x000000030412b3e8
(lldb) p &teacher2.height
(double *) $12 = 0x000000030412b3e8
就会发现内存地址
都是一样
的!
所以联合体
的所有成员变量都是使用同一个
内存地址,每次只有一个值是正确的!
3、总结
结构体(struct)中所有变量是“共存”的!
优点是“有容乃⼤”,全⾯;
缺点是struct内存空间的分配是粗放的,不管⽤不⽤,全分配。
联合体(union)中是各变量是“互斥”的!
缺点就是不够“包容”;
但优点是内存使⽤更为精细灵活,也节省了内存空间
三、nonPointerIsa的分析
1、什么是nonPointerIsa
在《OC对象原理探究之alloc探索》文章中,我们探索了alloc
。
在alloc
中对象分配内存
后,进行类的绑定
,即创建isa
:
obj->initIsa(cls);
继续跟进:
inline void
objc_object::initIsa(Class cls)
{
initIsa(cls, false, false);
}
还只是中转
方法,继续跟进到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
的创建方法!
根据最后isa
的赋值newisa
,看看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
为union
,即联合体
!
按照我们之前的思路,一个类
为指针
,8字节
,64位
,如果只用来存指针
是不是太浪费
了!
在OC
里面,可以发现类
里面除了指针
其实还存有其他
的很多东西,比如是否正在释放、引用计数、weak、关联对象、析构函数等等,这种就叫做nonPointerIsa
,不再是一个简简单单的指针地址isa
。
2、isa内存分布
那么isa
里面到底存了什么呢?
我们看看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_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
# else
# 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
# endif
# 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
# else
# error unknown architecture for packed isa
# endif
发现ISA_BITFIELD
是位域!
以arm64
为例:
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推导class
1、通过掩码得到class
打印一个对象
的内存
,将isa
转为2进制
:
(lldb) x/4gx p
0x10895e9f0: 0x011d800100008275 0x0000000000000000
0x10895ea00: 0x0000000000000000 0x0000000000000000
(lldb) p/t 0x011d800100008275
(long) $2 = 0b0000000100011101100000000000000100000000000000001000001001110101
发现64位
并没有存满,还存在很多空位
!
我们打印类
的时候发现和isa
并不一样:
p/x p.class
(Class) $3 = 0x0000000100008270 LGPerson
那么isa
怎么转成类
的呢?
在《OC对象原理探究之内存对齐》文章中有说明,需要isa & ISA_MASK
即可!
p/x 0x011d800100008275 & 0x00007ffffffffff8ULL
(unsigned long long) $4 = 0x0000000100008270
为什么呢?
是因为isa
中不止是类
的信息,还有很多其他
信息,需要通过掩码
来获取类的信息!
如下图所示:
把掩码
盖在isa
上面的时候就类
信息会暴露出来了!
2、通过位运算得到class
在initIsa
方法里面可以发现:
如果不是nonpointer
,则直接setClass
;否则进行位域赋值
!
那么我们可以根据位域
来获取class
!
按照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
shiftcls
是存储class
信息的成员变量,前面有3位
,后面有17位
,即:
那么我们可以先右移3位
,将前3位归零,再左移20位
,将后17位归零,最后再右移17位
,回归原本位置,即可得到class
信息:
(lldb) x/4gx p
0x108a09980: 0x011d800100008275 0x0000000000000000
0x108a09990: 0x0000000000000000 0x0000000000000000
(lldb) p/x 0x011d800100008275 >> 3
(long) $1 = 0x0023b0002000104e
(lldb) p/x 0x0023b0002000104e << 20
(long) $2 = 0x0002000104e00000
(lldb) p/x 0x0002000104e00000 >> 17
(long) $3 = 0x0000000100008270
(lldb) p/x p.class
(Class) $4 = 0x0000000100008270 LGPerson