前两篇文章重点分析了OC创建对象时的alloc流程分析和内存对齐,这篇文章继续探索OC创建对象的过程中是如何与类做关联的?对象的本质是什么?
基础知识介绍
- 在分析重点过程前,我们先一起了解下一些基础知识,这更有利于我们接下来的探索和分析
联合体
- 联合体(union):也可以叫做共用体
- 在前面的文章我们介绍过结构体的内存对齐,这里不熟悉的可以参考前面的文章(OC对象原理之内存对齐)
- 联合体的语法和结构体非常类似,但是它们占用内存的情况却有很大的不同,主要表现在:
- 结构体的成员之间是共存的:各个成员占用不同的内存,它们互相之间没有影响。
- 联合体的成员之间是互斥的:所有成员共用同一段内存,修改一个成员的值,会影响其余所有成员。
- 结构体占用的内存:大于等于所有成员占用内存的总和(需要内存对齐)
- 联合体占用的内存:等于最大的成员占用的内存,同一时刻只能保存一个成员的值
- 示例:
union LGUnion {
char a;
int b;
double c;
} union1;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
union1.a = 'a';
union1.b = 10;
union1.c = 5.5;
}
复制代码
- 未赋值时内存情况:
- p union1:输出union1的值
- p/t union1:以二进制形式输出union1的值
- 对
a
赋值后内存情况:
- 对b赋值后内存情况:
- 对c赋值后内存情况:
- 总结: 从上面的示例可以看出,联合体
union1
占用的内存大小即为最大成员double c
占用的内存大小,占8字节,成员char a
占总内存的低8位,成员int b
占总内存的低32位;
位域
- 有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可,例如开关只有通电和断电两种状态,用 0 和 1 表示足以,也就是用一个二进位。正是基于这种考虑,C语言又提供了一种叫做位域的数据结构。
- 定义:在定义结构体或联合体时,成员变量后面加
: 数字
,用来限定成员变量占用的位数,这就是位域;例如:
struct LGScruct {
bool a: 1;
bool b: 1;
bool c: 1;
bool d: 1;
}struct1;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
struct1.a = 1;
struct1.b = 0;
struct1.c = 1;
struct1.d = 1;
NSLog(@"%ld", sizeof(struct LGScruct));
}
复制代码
- 从上面的打印结果可以看出:
- 首先打印的是结构体变量
struct1
占用的内存空间大小,为1字节;如果没有位域的限制,此结构的大小为4字节 x/1bt &struct1
:以单字节二进制形式打印变量struct1内存地址中存放的数据- 低4位(从低到高,即从右到左)分别对应的是
a
、b
、c
、d
的值;共占4个二进制位,即0.5个字节大小,再进行结构体的内存对齐,总大小为最大成员变量的整数倍,即bool
类型(占1字节)的整数倍,为1字节;
- 首先打印的是结构体变量
- 位域还有一些其他规则:(这个比较简单,有兴趣的同学可以自己验证一下)
- 位域的宽度不能超过它所依附的数据类型的宽度,否则编译时会报错;
- 当相邻的成员类型相同时,如果他们的位宽之和小于类型的
sizeof
大小,那么后面的成员紧邻前面的成员存储,否则从新的存储单元开始存储,偏移为类型大小的整数倍。
Clang介绍
Clang
是C
语言、C++
、Objective-C
语言的轻量级编译器Clang
可以将目标文件编译成C++
文件:clang -rewrite-objc mian.m -o main.cpp
Clang
编译UIKit
报错(fatal error: 'UIKit/UIKit.h' file not found
)问题解决办法:- 指定
SDK
路径: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 -o main.cpp
- 使用
xcrun
:- 模拟器:
xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m -o main-arm64.cpp
- 手机:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
- 模拟器:
- 指定
Clang分析对象的本质
main.m
定义:
#import <Foundation/Foundation.h>
@interface LGPerson : NSObject
@property (nonatomic) NSString *lgName;
@end
@implementation LGPerson
@end
int main(int argc, const char * argv[]) {
LGPerson *p = [LGPerson alloc];
NSLog(@"p:%@", p);
return 0;
}
复制代码
- 使用
clang
将main.m
编译成main.cpp
:clang -rewrite-objc main.m -o main.cpp
main.cpp
中关键代码:
#ifndef _REWRITER_typedef_LGPerson
#define _REWRITER_typedef_LGPerson
typedef struct objc_object LGPerson;
typedef struct {} _objc_exc_LGPerson;
#endif
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_lgName;
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_lgName;
};
// @property (nonatomic) NSString *lgName;
/* @end */
// @implementation LGPerson
static NSString * _I_LGPerson_lgName(LGPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_lgName)); }
static void _I_LGPerson_setLgName_(LGPerson * self, SEL _cmd, NSString *lgName) { (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_lgName)) = lgName; }
// @end
int main(int argc, const char * argv[]) {
LGPerson *p = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_1d__07nngks0bl536_p0nvqhjkm0000gn_T_main_06b9c6_mi_0, p);
return 0;
}
复制代码
- 可以看到,
LGPerson
实际上是struct objc_object
类型,objc_object
的定义如下,默认就有一个Class
类型的isa
变量
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
复制代码
- 从
LGPerson
的实现struct LGPerson_IMPL
中也可以看出,第一个成员变量为struct NSObject_IMPL
类型,定义如下:
struct NSObject_IMPL {
Class isa;
};
复制代码
- 而
LGPerson_IMPL
中的另一个成员变量即为我们自定义的属性lgName
:NSString *_lgName;
OC
中的属性会自动生成对应的getter
和setter
方法,在这里对应的分别是_I_LGPerson_lgName
和_I_LGPerson_setLgName_
- 这两个方法的前两个参数都是
self
和_cmd
,这两个参数是默认的隐式参数 - 在
getter
方法和setter
方法中都是是通过self + OBJC_IVAR_$_LGPerson$_lgName
去获取或改变变量lgName
的值的,OBJC_IVAR_$_LGPerson$_lgName
的定义:extern"C"unsignedlongintOBJC_IVAR_$_LGPerson$_lgName__attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct LGPerson, _lgName);
,这里是通过位移找到变量的内存地址,然后进行取值或赋值的
- 这两个方法的前两个参数都是
- 总结:
- 类的定义在底层会变编译成结构体,第一个成员变量为
isa
; - 对象的本质为
struct objc_object
类型的结构体指针; OC
定义的属性,会自动生成成员变量、getter
方法和setter
方法;
- 类的定义在底层会变编译成结构体,第一个成员变量为
objc源码分析isa初始化流程
- 前两篇文章介绍到了创建对象最终调用的方法为
_class_createInstanceFromZone
,其中初始化isa
会调用obj->initInstanceIsa(cls, hasCxxDtor);
或obj->initIsa(cls);
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)
{
ASSERT(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
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);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
复制代码
- 接下来就探究一下初始化
isa
过程做了什么
动态调试
- 在
initInstanceIsa
和initIsa
分别添加断点,运行调试 - 在
obj
赋值前打印:
- 在
obj
赋值后打印:
- 从两次的打印结果可以看出,
obj
赋值后,内存地址0x000000010065dc30
的类型从id
变成了LGPerson *
,即此过程中内存地址和类LGPerson
做了关联,所以可以打印出内存地址对应的对象类型为LGPerson *
调用流程分析
- 不同的架构处理调用的流程可能会有所差别,但是最终都会调用到
initInstanceIsa
或initIsa
中,而这两个方法最终都会调用objc_object::initIsa
,具体流程如下:
- 从上述流程可以看出最终都会调用
objc_object::initIsa
,并在此方法中对isa
进行赋值,下面重点分析下该方法 - 在分析该方法前,先看下这里涉及到的几个重点知识:
ISA_BITFIELD
isa_t
ISA_BITFIELD分析
- 这里进行了架构的判断,不同架构对应的宏定义是不一样的,调试
objc
源码过程中,是运行在MacOS
系统上,使用的是x86_64
架构,下面是对应的定义:(其他架构的定义类似,在补充中会详细分析)
# 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)
复制代码
- 在
x86_64
架构中各个字段含义:nonpointer
:表示是否对isa
指针开启指针优化;0:纯isa
指针,1:isa
中包含了类信息、对象的引用计数等信息;has_assoc
:关联对象标志;0:没有,1:存在has_cxx_dtor
:该对象是否有C++
或者Objc
的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象shiftcls
:存储类指针的值magic
:用于调试器判断当前对象是真的对象,还是没有初始化的空间weakly_referenced
:标志对象是否被指向或者曾经指向一个ARC
的弱变量,没有弱引用的对象可以更快释放unused
:标志是否未被使用过has_sidetable_rc
:当对象引用计数大于10时,则需要借用该变量存储进位extra_rc
:表示该对象的引用计数值,实际上是引用计数值减1。 例如,如果对象的引用计数为10,那么extra_rc
为9。如果引用计数大于10, 则需要使用到has_sidetable_rc
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
是一个联合体,而我们前面介绍过了,联合体的成员变量存储是互斥的,成员变量bits
和结构体使用同一块内存;- 最上面是两个构造函数,第一个构造函数没有做任何处理,第二个构造函数使用传入的参数
value
对成员变量bits
进行赋值; - 下面结构体中的
ISA_BITFIELD
为宏定义,实际上这里使用了位域,此结构体和成员变量bits
共用同一块内存空间;
objc_object::initIsa分析
objc_object::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;
}
isa = newisa;
}
复制代码
- 查看
isa
变量类型:
struct objc_object {
private:
isa_t isa;
// ...省略
}
复制代码
- 这里的
isa
就是isa_t
类型的变量,isa
是objc_object
结构体的第一个成员,而类在底层编译时会自动转化成struct objc_object
类型 - 这里对
isa
赋值使用的是变量newisa
- 从最上面可以看出,
newisa
是isa_t
类型,newisa(0)
即为调用isa_t
中的第二个构造函数进行初始化,这里赋值为0的作用是对isa_t
中的成员变量bits
进行赋值,避免脏数据的影响。 - 判断是否是
nonpointer
,即非纯isa
指针:- 否:调用
newisa.setClass(cls, this);
,直接赋值 - 是:开启
isa
指针优化,对isa
指针中的bits
和位域对应的变量赋值;
- 否:调用
总结
isa
是isa_t
联合体类型,他们的成员bits
和结构体位域共用同一块内存- 对
isa
进行初始化时,先判断是否是nonpointer
,即是否开启了指针优化- 否:直接赋值
- 是:对联合体中的位域进行赋值
补充: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
复制代码
- 从上面的源码中可以看出,这里首先对架构进行了判断,主要分为
arm64
和x86_64
两种架构 x86_64
架构中的内容很清晰,前面也已经分析过了,下面重点看下arm64
架构的处理arm64
架构:- 这里又做了条件判断,主要是
__has_feature(ptrauth_calls)
和TARGET_OS_SIMULATOR
- 其中
TARGET_OS_SIMULATOR
是模拟器,在这里代表arm64
架构的模拟器设备 - 那么
__has_feature(ptrauth_calls)
是什么呢? - 下面重点介绍下
__has_feature(ptrauth_calls)
的作用
- 这里又做了条件判断,主要是
__has_feature(ptrauth_calls)介绍
__has_feature
:此函数的功能是判断编译器是否支持某个功能ptrauth_calls
:指针身份验证,针对arm64e
架构;使用Apple A12
或更高版本A
系列处理器的设备(如iPhone XS
、iPhone XS Max
和iPhone XR
或更新的设备)支持arm64e
架构- 参考链接:developer.apple.com/documentati…
- 下面分别使用真机
iPhone 12
和iPhone 8
进行验证
验证方法一:通过isa存储数据验证
- 因为
arm64
架构中if分支和else分支存储的数据结构是不一样的,这里根据weakly_referenced
值做验证 - 测试代码
LGPerson *p = [LGPerson alloc];
__weak typeof(p) weakP = p;
NSLog(@"p:%@", p);
复制代码
-
在
__weak typeof(p) weakP = p;
处添加断点,这行代码执行前weakly_referenced
的值为0,执行后会变为1 -
测试
iPhone 8
- 执行前
- 执行后:
-
测试
iPhone 12
- 执行前
- 执行后
-
验证结果: 从上面两个设备的验证结果可以看出,执行
__weak typeof(p) weakP = p;
前后,iPhone 8
修改的是第43位(从右往左),而iPhone 12
修改的是第3位(从右往左),分别对应的是arm64
架构中的else
分支和if
分支的存储结构
验证方法二:通过断点和汇编验证
- 在
[LGPerson alloc]
处设置断点 - 执行到断点处后,添加符号断点
_objc_rootAllocWithZone
,并继续运行 iPhone 8
iPhone 12
- 验证结果:
iPhone 8
中使用了0xffffffff8
,iPhone 12
中使用了0x7ffffffffffff8
,在objc
源码的isa.h
中搜索发现,这两个值分别对应的是arm64
架构中else
分支的ISA_MASK
值和if
分支的ISA_MASK
值
总结
-
__has_feature(ptrauth_calls)
的作用是判断编译器是否支持指针身份验证功能 -
iPhone X
系列以上的设备(arm64e
架构,使用Apple A12
或更高版本A
系列处理器的设备)支持指针身份验证 -
对于
arm64
架构- iPhone X系列以上(包含)设备使用如下结构:
# 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) 复制代码
- iPhone X以下(不包含)设备使用如下结构:
# 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) 复制代码
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END