1.0 对象的本质
我们经常new或者alloc一个对象,那么底层到底是怎么创建这个对象的,这个对象在底层到底是什么呢?
先了解一下Clang编译器,后面我们需要编译源文件
- Clang是一个由Apple主导编写,基于
LLVM的C/C++/Objective-C编译器 - Clang 主要用于把源文件编译成底层文件,比如把main.m 文件编译成main.cpp、main.o或者可执行文件。便于观察底层的逻辑结构,便于我们探究底。
- 终端编译命令:
1、
clang -rewrite-objc main.m -o main.cpp2、clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m//UIKit报错的话就指定路径 3、xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp// xcrun命令基于clang基础上进行了封装更好用,模拟器编译 4、xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp//真机编译
1.0.1 实例探索
写个实例demo,main.m,用clang编译成main.cpp
@interface LGWPerson:NSObject{
NSString* sex;
}
@property(nonatomic,copy)NSString* name;
@property(nonatomic,strong)NSString* nickname;
@property(nonatomic,assign)int age;
@end
@implementation LGWPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
main.cpp部分重要代码如下:
typedef struct objc_object LGWPerson;
typedef struct {} _objc_exc_LGWPerson;
#endif
extern "C" unsigned long OBJC_IVAR_$_LGWPerson$_name;
extern "C" unsigned long OBJC_IVAR_$_LGWPerson$_nickname;
extern "C" unsigned long OBJC_IVAR_$_LGWPerson$_age;
typedef struct objc_object *id;
typedef struct objc_selector *SEL;
typedef struct objc_class *Class;
struct NSObject_IMPL {
Class isa;
};
struct LGWPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *sex;
int _age;
NSString *_name;
NSString *_nickname;
};
// @implementation LGWPerson
static NSString * _I_LGWPerson_name(LGWPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGWPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_LGWPerson_setName_(LGWPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGWPerson, _name), (id)name, 0, 1); }
static NSString * _I_LGWPerson_nickname(LGWPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGWPerson$_nickname)); }
static void _I_LGWPerson_setNickname_(LGWPerson * self, SEL _cmd, NSString *nickname) { (*(NSString **)((char *)self + OBJC_IVAR_$_LGWPerson$_nickname)) = nickname; }
static int _I_LGWPerson_age(LGWPerson * self, SEL _cmd) { return (*(int *)((char *)self + OBJC_IVAR_$_LGWPerson$_age)); }
static void _I_LGWPerson_setAge_(LGWPerson * self, SEL _cmd, int age) { (*(int *)((char *)self + OBJC_IVAR_$_LGWPerson$_age)) = age; }
// @end
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
}
return 0;
}
分析底层源码总结如下:
- LGWPerson对象底层是struct
objc_object结构体,对象的本质是结构体的由来。 - LGWPerson对象有5个变量,其中NSObject_IVARS是一个指向NSObject的isa,由
系统自动创建的变量。 - Class
isa的本质是一个指向structobjc_class *的结构体指针 id是一个指向objc_object的结构体指针,这也是为什么id修饰变量不加*的原因SEL是一个指向objc_selector的结构体指针,SEL _cmd 这样的写法就解释通了- LGWPerson对
象的属性age、name、nickname,底层编译会生成加下划线的变量,并且会生成get、set方法,然后对象的成员变量sex,底层编译是只生成了一个变量,没有get、set方法。 - 仔细观察name和nickname的set方法,发现name的set方法是通过
objc_setProperty方法设置属性值,而nickname的set方法是通过self+OBJC_IVAR__nickname(起始首地址+偏移量)的方法设置属性值。why?这两者为什么会有这个区别,后面的博客我会详细阐述。
2.0 isa
isa这个词应该并不陌生,上面通过实例我们也知道了对象的创建必然会生成isa。那么为什么要创建isa,isa的作用到底是什么?
前面一篇博客“对象的创建过程”详细分析了对象是如何创建的。现在再简单概述一下大致流程:
alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone-->initisa
重点分析下initisa到底干了什么?
2.1 联合体union和结构体struct
在分析initisa之前我们看一下联合体和结构体的区别,这有助于我们分析initisa的源码。
union GyPerson {
int a; //4
short b; //2
char c; //1
};
int main(int argc, char * argv[]) {
@autoreleasepool {
union GyPerson person;
person.a = 8;
NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
person.b = 2;
NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
person.c = 'd';
NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
NSLog(@"%lu---%lu",sizeof(person),sizeof(union GyPerson));
}
return 0;
}
上面的联合体输出如下:
a=8---b=8---c=
a=2---b=2---c=
a=100---b=100---c=d
4---4
再来看一下结构体:
struct GyPerson2 {
int a; //4
short b; //2
char c; //1
};
int main(int argc, char * argv[]) {
@autoreleasepool {
struct GyPerson2 person;
person.a = 8;
NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
person.b = 2;
NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
person.c = 'd';
NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
NSLog(@"%lu---%lu",sizeof(person),sizeof(struct GyPerson2));
}
return 0;
}
结构体输出如下
a=8---b=32766---c=�
a=8---b=2---c=�
a=8---b=2---c=d
8---8
根据上面的输出日志可以总结:
- 联合体的内存大小是由
最大成员的大小决定的 - 联合体中修改其中某个变量会覆盖其他变量的值,也就是
互斥,只能存在一个。 - 结构体的内存大小由结构体中
成员变量决定,具体规则见上一篇博客 - 结构体的成员变量相互独立,
互不影响
两者的优缺点:
- 结构体(struct)中所有变量是“共存”的——优点是“有容乃大”,
全面;缺点是struct内存空间的分配是粗放的,不管用不用,全分配。 - 联合体(union)中是各变量是“
互斥”的——缺点就是不够“包容”; 但优点是内存使用更为精细灵活,也节省了内存空间
2.2 位域
有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如一个布尔值, 用一位二进位0或1表示即可,目的是节省存储空间。
struct Person1 {
BOOL isTall;
BOOL isHandsome;
BOOL isRich;
};
struct Person2 {
BOOL isTall: 1;
BOOL isHandsome : 1;
BOOL isRich : 1;
};
int main(int argc, char * argv[]) {
@autoreleasepool {
struct Person1 person1;
struct Person2 person2;
NSLog(@"内存大小:%lu----%lu",sizeof(person1),sizeof(person2));
}
return 0;
}
输出如下:
内存大小:3----1
总结:Person1占3个字节,变量isTall、isHandsome、isRich分别占一个字节。Person2占1个字节,isTall、isHandsome、isRich分别占一个二进制位,1个字节有8个二进制位。结构体位域好处就体现了出来,极大的节约了内存空间。
2.3 initInstanceIsa
分析一下initInstanceIsa源码:
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
isa_t newisa(0); // isa初始化
if (!nonpointer) {
newisa.setClass(cls, this);//如果是纯指针 isa被直接cls赋值
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = 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;
}
分析:
- 在arm64架构之前,isa就是一个纯指针,存储着class对象或meta-class对象的地址。在arm64架构之后,
苹果用union结构优化了isa指针,让isa能够存储更多的信息,即联合体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:
- 联合体isa_t有两个变量
bits和cls,联合体是互斥的,也就意味着要么bits被赋值,cls没有值或者被覆盖,要么cls被赋值,bits没有值或者被覆盖。 - 联合体位域
ISA_BITFIELD,这是一个宏定义。定义了bits 64位各位域代表的含义,宏定义如下,我们只分析arm64以及x86这两个情况。
#arm_64
define ISA_MASK 0x0000000ffffffff8ULL //isa面具
# 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)
#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
各位域代表的含义 占位以x86_64为例:
nonpointer:表示是否对isa指针进行优化,0表示纯指针,1表示不止是类对象的地址,isa中包含了类信息、对象、引用计数等。占1位[0]has_assoc:关联对象标志位,0表示未关联,1表示关联。占1位[1]has_cxx_dtor:该对象是否C ++ 或者Objc的析构器,如果有析构函数,则需要做析构逻辑,没有,则释放对象。占1位[2]shiftcls:储存类指针的值,开启指针优化的情况下,在arm64架构中有33位用来存储类指针。占44位[3 47]magic:用于调试器判断当前对象是真的对象还是没有初始化的空间。占6位[48 53]weakly_referenced:指对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放。 占1位[54]deallocating:标志对象是否正在释放。占1位[55]has_sidetable_rc:当对象引用计数大于10时,则需要借用该变量存储进位。占1位[56]extra_rc:表示该对象的引用计数值,实际上引用计数值减1,例如,如果对象的引用计数为10,那么extra_rc为9,如果大于10,就需要用到上面的has_sidetable_rc。占8位[57 64]
总结:
- isa_t 是
联合体和位域的方式存储信息。联合体共用一块内存空间,目的是节省内存。 - isa 分为
nonpointer和非nonpointer,非nonpointer只是一个纯指针,而nonpointer还包含了类信息、对象、引用计数等。 shiftcls位域段存储了isa的关联类信息
那么如何获取isa关联类的信息呢?也就是如何获取isa_t中shiftcls的值
2.4 isa关联类的获取
2.4.1 按位运算获取shiftcls
lldb断点调试LGPerson* p=[LGPerson alloc],在macos x86_64中
(lldb) x/4gx p //注释:读取p对象内存
0x10070f660: 0x011d800100008519 0x0000000000000000
0x10070f670: 0x626d6f43534e5b2d 0x646e6957786f426f
(lldb) p/x 0x011d800100008519 >> 3
(long) $2 = 0x0023b000200010a3
(lldb) p/x 0x0023b000200010a3 << 20
(long) $3 = 0x000200010a300000
(lldb) p/x 0x000200010a300000 >> 17
(long) $4 = 0x0000000100008518
(lldb) po 0x0000000100008518
LGPerson
(lldb) p/x LGPerson.class
(Class) $6 = 0x0000000100008518 LGPerson
(lldb)
分析:ios是小端模式,从右往左读。0x011d800100008519是isa,64位,我们要获取isa中第3位到第47位也就是shiftcls的值,就是把前3位抹0,后17位也抹0。0x011d800100008519右移3位清空前3位,再左移20位清空后17位,最后右移17位还原到原来的位置。
2.4.2 isa&ISA_MASK获取shiftcls
(lldb) x/4gx p //注释:读取p对象内存
0x10070f660: 0x011d800100008519 0x0000000000000000
0x10070f670: 0x626d6f43534e5b2d 0x646e6957786f426f
(lldb) p/x 0x011d800100008519 & 0x00007ffffffffff8ULL
(unsigned long long) $8 = 0x0000000100008518
(lldb) po 0x0000000100008518
LGPerson //注释:类对象
(lldb) x/4gx 0x0000000100008518
0x100008518: 0x00000001000084f0 0x000000010036a140
0x100008528: 0x0000000100362390 0x0000801000000000
(lldb) p/x 0x00000001000084f0 & 0x00007ffffffffff8ULL
(unsigned long long) $10 = 0x00000001000084f0
(lldb) po 0x00000001000084f0
LGPerson //注释:元类
(lldb) x/4gx 0x00000001000084f0
0x1000084f0: 0x000000010036a0f0 0x000000010036a0f0
0x100008500: 0x0000000101353650 0x0004e03100000007
(lldb) p/x 0x000000010036a0f0 & 0x00007ffffffffff8ULL
(unsigned long long) $12 = 0x000000010036a0f0
(lldb) po 0x000000010036a0f0
NSObject //注释:根元类
(lldb)
分析:
ISA_MASK是一个宏。x86_64 的值等于 0x00007ffffffffff8ULL,arm64 的值等于0x0000000ffffffff8ULL,- 对象的isa&ISA_MASK得到关联类的信息。LGPerson实例对象isa关联类对象LGPerson,类对象LGPerson的isa关联元类对象LGPerson,元类对象isa关联根元类NSObject。
对象的isa的值0x011d800100008519不等于类的值0x0000000100008518,说明对象isa不仅存储着关联类的信息,还存储着对象的引用计数、是否弱引用等信息。而类的isa0x00000001000084f0等于元类的值0x00000001000084f0,说明类的isa只存储着关联类的信息。 通过上面的分析我们可以理解苹果官方isa的关联图;