以下内容以至少你已经理解OC内万物皆对象
的概念为基础,当然你还得有一份可以跑得objc源码
1. Obj before born
在我们还没有书写代码创建对象时,内存内已经满是对象(类,元类)了.
2. Obj 诞生 alloc
2.1 申请堆空间
//C code
typedef struct{
char name[21];
char age;
}CustomStruct;
typedef CustomStruct * CustomStructPointer;
int main(int argc, const char * argv[]) {
CustomStructPointer stu = (CustomStructPointer)malloc(sizeof(CustomStruct));
stu->age = 10;
strcpy(stu->name, "pogong");
printf("stack address %p\n",&stu);
printf("heap address %p\n",stu);
free(stu);
return 0;
}
打印:
stack address 0x7fff5fbff708
heap address 0x100403ff0
//OC code
//PGCustomClass.h
@interface PGCustomClass : NSObject
@property(nonatomic,copy)NSString * name;
@property(nonatomic,assign)int age;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
PGCustomClass * obj = [[PGCustomClass alloc]init];
NSLog(@"stack address %p",&obj);
NSLog(@"heap address %p",obj);
}
return 0;
}
打印:
stack address 0x7fff5fbff728
heap address 0x101a02bc0
以上是C语言的一个栈上的结构体指针
指向堆上的结构体实例
的代码+内存示意图和OC的一个栈上的对象指针
指向堆上的对象实例
的代码+内存示意图.
因为OC的对象说到底还是个结构体实例,所以OC的对象生成的结果和C语言生成结构体指针指向结构体实例
的结果是一样的.当然OC的对象生成过程会比较复杂,因为OC可是优雅的动态语言诶!以下就是曲折的诞生过程:
alloc
像内的调用栈大概如上图所示,看代码的捋很久,也不需要全都记住,主要知道几个关键参数,关键条件和关键实现就可以了.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
if (slowpath(checkNil && !cls)) return nil;
#if __OBJC2__
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// No alloc/allocWithZone implementation. Go straight to the allocator.
// fixme store hasCustomAWZ in the non-meta class and
// add it to canAllocFast's summary
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
// Has ctor or raw isa or something. Use the slower path.
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif
// No shortcuts available.
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
1.hasCustomAWZ
存在于类的元类中,标识这个类有没有复写alloc/allocWithZone:
;
2.canAllocFast
是否支持快速创建.
可以看出最终都调用了_class_createInstanceFromZone
static __attribute__((always_inline))
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
if (!cls) return nil;
assert(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (!obj) return nil;
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
几大判断条件:
hasCxxCtor
:类及父类是否有自己的构造函数;
hasCxxDtor
:类及父类是否有自己的析构函数(这个条件在后面讲对象dealloc
的时候也会说到,与对象是否有实例变量有关,这条件会记录在对象的isa
内);
fast:类是否用了是优化的isa
;
canAllocNonpointer and SUPPORT_NONPOINTER_ISA
两个都带nonpointer,
SUPPORT_NONPOINTER_ISA是来标识当前平台是否支持优化的isa
,但即使平台支持,具体到某一个类却是不一定的,要具体的去验证类的元类的信息.不过可以放心大多系统的类的isa
都是支持优化的,我们自定义的类的isa
也是支持优化的.
canAllocNonpointer
则是具体标记某个类是否支持优化的isa
.
在阅读源码时还有会各种带nonpointer
字样的针对优化isa的标记,除SUPPORT_NONPOINTER_ISA外,全是针对某个类而言的.
优化的isa
是什么?接下来会说.
zone:老版本中要先去看看zone是否有空间,在OBJC2下,忽略zone参数.
size:这由外围传入,size存储在类的元类内
// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() {
assert(isRealized());
return data()->ro->instanceSize;
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
size_t size = cls->instanceSize(extraBytes);
data()->ro->instanceSize;
上方的注解May be unaligned depending on class's ivars.
.类实例的instanceSize
取决于类的中成员变量的个数:
再看看最后的调用:calloc
或者malloc_zone_calloc
就和C语言在堆中申请空间如出一辙了.
2.2 isa init
-
isa
没那么简单,因为优化了
在part1内已经提过了多遍的isa
,当然只要知道OC内万物皆对象
,也肯定知道类实例->类->元类
用isa串联起来的关系:
但具体到真实的应用场景下,isa
的串联会比上图描绘更复杂更具体一些,特别是在64位系统上.所有用了64位系统的电子产品都没有用全64位来表示地址.
因为这不现实:32位==>4G内存,64位==>你算算看.
64位不全拿来表示地址,这就给64位的isa
留下了很大的优化空间(32位时对象的isa
只是指向类而已).
我们截取类实例到类的过程来说明优化的isa
先找到关于
id
的定义:
typedef struct objc_object *id;
然后objc_object又是什么:
struct objc_object {
private:
isa_t isa;
}
然后再看isa_t是什么(这里只看arm64的):
union isa_t
{
Class cls;
uintptr_t bits;
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
}
这个union isa_t
新奇了,联合体
少见吧!更奇怪的是联合体还嵌套了结构体
,有不明白的请戳.
简单的说就是:Class cls
+uintptr_t bits
+struct{......}
共用一块64位的内存空间,当然只有一个有效,在SUPPORT_NONPOINTER_ISA
为1的情况下,仍然有一些类不支持优化的isa
,所以这样的union isa_t
就支持多用:
Class cls->为未优化版的isa指向一个类
uintptr_t bit+struct{......}
uintptr_t bit用于对64位统一赋值,
struct{......}做细化读取与细化赋值
请注意这联合体内结构体内的这个字段shiftcls
,shiftcls
=shift class
,短的类地址.union isa_t
共计64位,shiftcls
占33位.这就是一个操作系统地址变量优化的细节.在64位iPhone上只拿33位表示地址的,也就是说这的shiftcls
就存储了类实例归属的类的地址.如图:
当然类对象
指向元类对象
也是一样的道理.
除了shiftcls
之外,isa_t
内的各个字段均有用处,这些也就是64位的isa
具体优化的地方:
nonpointer:1->表示使用优化的isa指针
has_assoc:1->是否包含关联对象
has_cxx_dtor:1->是否包含析构函数
shiftcls:33->类的指针
magic:6->固定值,用于判断是否完成初始化
weakly_referenced:1->对象是否指向一个弱引用对象
deallocating:1->对象是否正在销毁
has_sidetable_rc:1->在extra_rc存储引用计数将要溢出的时候,借助sidetable(散列表)存储引用计数,has_sidetable_rc设置成1
extra_rc:19->存储引用计数
后面章节的文章会细说关于这些字段所实现和优化的功能.
- 初始化对象的isa
初始化对象的isa
要么initInstanceIsa->initIsa
,要么直接调用initIsa->initIsa
.
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
assert(!cls->instancesRequireRawIsa());
assert(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
inline void
objc_object::initIsa(Class cls)
{
initIsa(cls, false, false);
}
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
assert(!isTaggedPointer());
if (!nonpointer) {
isa.cls = cls;
} else {
assert(!DisableNonpointerIsa);
assert(!cls->instancesRequireRawIsa());
isa_t newisa(0);
#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
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
// 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;
}
}
SUPPORT_NONPOINTER_ISA
前面已经说过,而SUPPORT_INDEXED_ISA 为 1
是另外一种优化,用isa内indexcls存储着类在类列表内的索引,这个用在watch
上,手机和电脑上没有这么用.
所以再看objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
的实现就简单多了.
不支持nonpointer
的,isa.cls = cls;
支持nonpointer
的,
对newisa.bits
赋值,即对isa
的64位统一初始化赋值,(统一初始化赋值)
newisa.has_cxx_dtor
记录传入的has_cxx_dtor
,(细化赋值)
newisa.shiftcls
记录下cls
的地址.(细化赋值)
newisa.shiftcls = (uintptr_t)cls >> 3;(为什么右移3位?)
拿手机举例子:shiftcls:33;(shiftcls会分配到33位),在64位的手机上拿33位保存类的地址,但因为位对齐的缘故,所有地址都是8的倍数,所有地址书写的成二进制数最后3位全是0,所以才如上见到:cls >> 3;(消除了3个没有影响的0)
3. Obj 装扮 init
- (id)init {
return _objc_rootInit(self);
}
id
_objc_rootInit(id obj)
{
return obj;
}
在没有复写init
方法的情况下,init
的实现特别简单.
- (instancetype)init
{
self = [super init];
if (self) {
_name = @"pogong";
_age = 28;
}
return self;
}
复写init
的情况下能做的也只是对类实例
的成员变量
的初始化装扮
.
当然这样的工作不在init
内部也能完成.
4. Obj 怪胎 Tagged Pointer
事情是是要从32位系统转向64系统说起.
32位系统下:
NSNumber * num = [[NSNumber alloc]initWithInt:1];
栈上
4个字节的对象指针
指向堆上
8个字节(存储isa4个字节+存储值4个字节)的对象实例
,共计12个字节.
64位系统下:
NSNumber * num = [[NSNumber alloc]initWithInt:1];
栈上
8个字节的对象指针
指向堆上
16个字节(存储isa8个字节+存储值8个字节)的对象实例
,共计24个字节.
保存一个int
要用8个字节,包装成对象要24字节,有点太浪费了.
所以Tagged Pointer
应运而生,
NSNumber *number1 = @1;
NSNumber *number2 = @2;
NSNumber *number3 = @3;
NSNumber *numberFFFF = @(0xFFFF);
NSLog(@"number1 pointer is %p", number1);
NSLog(@"number2 pointer is %p", number2);
NSLog(@"number3 pointer is %p", number3);
NSLog(@"numberffff pointer is %p", numberFFFF);
打印:
number1 pointer is 0xb000000000000012
number2 pointer is 0xb000000000000022
number3 pointer is 0xb000000000000032
numberffff pointer is 0xb0000000000ffff2
我们前面已经讲过,因为64位系统上8位对齐
,16进制打印出的地址最后一位不是8就是0(2进制打印后三位全是0),而这里最后一位是2,很怪异,这就是对Tagged Pointer
的标记.再将标记位的前面的数值和对象本身的值进行比较一模一样.
Tagged Pointer
就是将值
与Tagged Pointer的标记
混在一块64位的内存内.看上去是对象,但却没有isa
(一个没有灵魂的对象==>Tagged Pointer
).但索性现在的isa
也不能直接被调用,所以不会造成什么不便.
栈上
8个字节的对象指针
指向堆上
8个字节(Tagged Pointer)的对象实例
,共计16个字节.
Tagged Pointer
的引入,节约了64位系统的内存,提高了运行效率.
除NSNumber
外,NSDate
,NSString
都应用到Tagged Pointer
.