iOS Objective-C底层 part2:born

96
破弓
0.3 2017.05.08 20:53* 字数 1936

以下内容以至少你已经理解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
C_memory_map.jpeg
//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
OC_memory_map.jpeg

以上是C语言的一个栈上的结构体指针指向堆上的结构体实例的代码+内存示意图和OC的一个栈上的对象指针指向堆上的对象实例的代码+内存示意图.
因为OC的对象说到底还是个结构体实例,所以OC的对象生成的结果和C语言生成结构体指针指向结构体实例的结果是一样的.当然OC的对象生成过程会比较复杂,因为OC可是优雅的动态语言诶!以下就是曲折的诞生过程:

alloc调用堆栈.png

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取决于类的中成员变量的个数:

instanceSize.png

再看看最后的调用:calloc或者malloc_zone_calloc就和C语言在堆中申请空间如出一辙了.

2.2 isa init
  • isa没那么简单,因为优化了

在part1内已经提过了多遍的isa,当然只要知道OC内万物皆对象,也肯定知道类实例->类->元类用isa串联起来的关系:

simple_isa.png

但具体到真实的应用场景下,isa的串联会比上图描绘更复杂更具体一些,特别是在64位系统上.所有用了64位系统的电子产品都没有用全64位来表示地址.
因为这不现实:32位==>4G内存,64位==>你算算看.

64位不全拿来表示地址,这就给64位的isa留下了很大的优化空间(32位时对象的isa只是指向类而已).
我们截取类实例到类的过程来说明优化的isa

instance_point_to_class_simple.png

先找到关于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就存储了类实例归属的类的地址.如图:

instance_point_to_class_real.png

当然类对象指向元类对象也是一样的道理.
除了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.png

当然这样的工作不在init内部也能完成.

born.png

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.

日记本
Web note ad 1