iOS探索 isa初始化&指向分析

4,982 阅读12分钟

欢迎阅读iOS探索系列(按序阅读食用效果更加)

写在前面

在介绍isa之前,先介绍一个位域和结构体的知识点

一、位域

1.定义

有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1两种状态,用1位二进位即可。为了节省存储空间并使处理简便,C语言提供了一种数据结构,称为位域位段

所谓位域就是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作——这样就可以把几个不同的对象用一个字节的二进制位域来表示

2.与结构体比较

位域的使用与结构体相仿,它本身也是结构体的一种

// 结构体
struct FXStruct {
    // (类型说明符 元素);
    char a;
    int b;
} FXStr;

// 位域
struct FXBitArea {
    // (类型说明符 位域名: 位域长度);
    char a: 1;
    int b: 3;
} FXBit;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Struct:%lu——BitArea:%lu", sizeof(FXStr), sizeof(FXBit));
    }
    return 0;
}

输出Struct:8——BitArea:4

位域有兴趣的可以看下struct中位域的定义

二、联合体

1.定义

当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体(union)

  • 联合体是一个结构
  • 它的所有成员相对于基地址的偏移量都为0
  • 此结构空间要大到足够容纳最"宽"的成员
  • 各变量是“互斥”的——共用一个内存首地址,联合变量可被赋予任一成员值,但每次只能赋一种值, 赋入新值则冲去旧值

2.与结构体比较

结构体每个成员依次存储,联合体中所有成员的偏移地址都是0,也就是所有成员是叠在一起的,所以在联合体中在某一时刻,只有一个成员有效——结构体内存大小取决于所有元素,联合体取决于最大那个

三、isa结构/流程分析

1.isa初始化

在之前的iOS探索 alloc流程中轻描淡写的提了一句obj->initInstanceIsa(cls, hasCxxDtor)——只知道内部调用initIsa(cls, true, hasCxxDtor)初始化isa,并没有对isa进行细说

2.initIsa分析

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;
    }
}

①创建对象跟着断点不难发现nonpointer为true

if-else跳转了else流程——SUPPORT_INDEXED_ISA表示isa_t中存放的 Class信息是Class 的地址,还是一个索引(根据该索引可在类信息表中查找该类结构地址)

isa_t newisa(0)相当于初始化isa这个东西,new.相当于给isa赋值属性

1.SUPPORT_INDEXED_ISA适用于WatchOS 2.isa作为联合体具有互斥性,而cls、bits是isa的元素,所以当!nonpointer=true时对cls进行赋值操作,为false是对bits进行赋值操作(反正都是一家人,共用一块内存地址)

3.isa结构

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

①首先isa是个联合体,拥有两个初始化方法

isa内部有个Class cls——Classisa有绑定关系——isa指向类的结构

③isa采用联合体+位域的形式来优化内存(ISA_BITFIELD是个位域宏定义)

先初始化bits决定联合体的长度,再对联合体内的位域ISA_BITFIELD进行赋值

联合体所有属性共用内存,内存长度等于其最长成员的长度,使代码存储数据高效率的同时,有较强的可读性;而位域可以容纳更多类型

4.ISA_BITFIELD宏定义

#if SUPPORT_PACKED_ISA

    // extra_rc must be the MSB-most field (so it matches carry/overflow flags)
    // nonpointer must be the LSB (fixme or get rid of it)
    // shiftcls must occupy the same bits that a real class pointer would
    // bits + RC_ONE is equivalent to extra_rc + 1
    // RC_HALF is the high bit of extra_rc (i.e. half of its range)

    // future expansion:
    // uintptr_t fast_rr : 1;     // no r/r overrides
    // uintptr_t lock : 2;        // lock for atomic property, @synch
    // uintptr_t extraBytes : 1;  // allocated with extra bytes

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   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 deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   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 deallocating      : 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

// SUPPORT_PACKED_ISA
#endif

不同架构下isa所占内存均为8字节——64位,但内部分布有所不同,arm64架构isa内部成员分布如下图

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

shiftcls之外了解即可

5.shiftcls关联类

@interface FXPerson : NSObject
@end
@implementation FXPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FXPerson *p = [[FXPerson alloc] init];
        NSLog(@"%@",p);
    }
    return 0;
}

父类NSObject结构中可以有个isa属性

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

但是内存中属性的位置是会因为优化发生改变的,下面就来证实下内存中第一位一定是isa

5.1 反证法(不推荐)

假设实例对象第一位内存是isa

①打印第一位内存

②二进制打印第一位内存的内存值

③因为模拟器是x86架构的,由isa位域结构可知,shiftcls前面有3位——右移3位——抹去isa前3位

shiftcls后面有17位——左移17位——抹去isa后17位

⑤因为末尾的0都是我们添加的——右移17位——得到shiftcls

⑥根据newisa.shiftcls = (uintptr_t)cls >> 3;——shiftcls等于class地址右移3位

比对两组shiftcls二进制,发现它们二进制一样(前面不一样是因为没抹掉)

如果你听得云里雾里的话,请看第二种方法

5.2 通过object_getClass(推荐)

①在<objc/runtime.h>下使用object_getClass方法

#import <objc/runtime.h>

@interface FXPerson : NSObject
@end
@implementation FXPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FXPerson *p = [[FXPerson alloc] init];
        object_getClass(p);
    }
    return 0;
}

②跟进object_getClass方法

/***********************************************************************
* object_getClass.
* Locking: None. If you add locking, tell gdb (rdar://7516456).
**********************************************************************/
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

③跟进getIsa()

#if SUPPORT_TAGGED_POINTERS

inline Class 
objc_object::getIsa() 
{
    if (!isTaggedPointer()) return ISA();

    uintptr_t ptr = (uintptr_t)this;
    if (isExtTaggedPointer()) {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        return objc_tag_ext_classes[slot];
    } else {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
        return objc_tag_classes[slot];
    }
}

④一般isTaggedPointer都为false,跟进ISA()

#if SUPPORT_NONPOINTER_ISA

inline Class 
objc_object::ISA() 
{
    assert(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    return (Class)(isa.bits & ISA_MASK);
#endif
}

⑤已知SUPPORT_INDEXED_ISA适用于WatchOS,那么走return (Class)(isa.bits & ISA_MASK);

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
# endif

⑥检验

打印出isa & mask的值,与class相比较(mask取x86架构)

从上述两种方法都能得出实例对象首地址一定 是isa

6.isa初始化流程图

四、isa走位

1.类在内存中只会存在一份

@interface FXPerson : NSObject
@end
@implementation FXPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Class class1 = [FXPerson class];
        Class class2 = [FXPerson alloc].class;
        Class class3 = object_getClass([FXPerson alloc]);
        Class class4 = [FXPerson alloc].class;
        NSLog(@"\n%p\n%p\n%p\n%p",class1,class2,class3,class4);
    }
    return 0;
}
0x1000024a0
0x1000024a0
0x1000024a0
0x1000024a0

输出证明类在内存中只会存在一个,而实例对象可以存在多个(自行证明)

2.1 通过对象/类查看isa走向

其实和实例对象一样,都是由上级实例化出来的——类的上级叫做元类

我们先用p/x打印类的内存地址,再用x/4gx打印内存结构取到对应的isa,再用mask进行偏移得到isa指向的上级(等同于object_getClass)依次循环

①打印FXPerson类取得isa

②由FXPerson类进行偏移得到FXPerson元类指针,打印FXPerson元类取得isa

③由FXPerson元类进行偏移得到NSObject根元类指针,打印NSObject根元类取得isa

④由NSObject根元类进行偏移得到NSObject根元类本身指针

⑤打印NSObject根类取得isa

⑥由NSObject根类进行偏移得到NSObject根元类指针

结论:

实例对象-> 类对象 -> 元类 -> 根元类 -> 根元类(本身)

NSObject(根类) -> 根元类 -> 根元类(本身)

指向根元类的isa都是一样的

2.2.通过NSObject查看isa走向

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // NSObject实例对象
        NSObject *object1 = [NSObject alloc];
        // NSObject类
        Class class = object_getClass(object1);
        // NSObject元类
        Class metaClass = object_getClass(class);
        // NSObject根元类
        Class rootMetaClass = object_getClass(metaClass);
        // NSObject根根元类
        Class rootRootMetaClass = object_getClass(rootMetaClass);
        NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
    }
    return 0;
}
0x100660ba0 实例对象
0x7fffacd3d140 类
0x7fffacd3d0f0 元类
0x7fffacd3d0f0 根元类
0x7fffacd3d0f0 根根元类

因为是NSObject(根类)它的元类就是根元类——输出可得根元类指向自己

2.3 证明类、元类是系统创建的

①运行时伪证法

main之前FXPerson类FXPerson元类已经存在在内存中,不过此时程序已经在运行了,并没有什么说服力

②查看MachO文件法

FXPerson已经存在在MachO文件中

结论:
对象是程序猿根据类实例化的
类是代码编写的,内存中只有一份,是系统创建的
元类是系统编译时,系统编译器创建的,便于方法的编译

3.isa走位图

isa走位(虚线):实例对象-> 类对象 -> 元类 -> 根元类 -> 根元类(本身)

继承关系(实线):NSObject父类为nil,根元类的父类为NSObject

小彩蛋——编译器优化

  • None[-O0]: 不优化。在这种设置下, 编译器的目标是降低编译消耗,保证调试时输出期望的结果。程序的语句之间是独立的:如果在程序的停在某一行的断点出,我们可以给任何变量赋新值抑或是将程序计数器指向方法中的任何一个语句,并且能得到一个和源码完全一致的运行结果。
  • Fast[-O1]: 大函数所需的编译时间和内存消耗都会稍微增加。在这种设置下,编译器会尝试减小代码文件的大小,减少执行时间,但并不执行需要大量编译时间的优化。在苹果的编译器中,在优化过程中,严格别名,块重排和块间的调度都会被默认禁止掉。
  • Faster[-O2]: 编译器执行所有不涉及时间空间交换的所有的支持的优化选项。在这种设置下,编译器不会进行循环展开、函数内联或寄存器重命名。和'Fast[-O1]'项相比,此设置会增加编译时间和生成代码的性能。
  • Fastest[-O3]: 在开启'Fast[-O1]'项支持的所有优化项的同时,开启函数内联和寄存器重命名选项。这个设置有可能会导致二进制文件变大。
  • Fastest, Smallest[-Os]: 优化大小。这个设置开启了'Fast[-O1]'项中的所有不增加代码大小的优化选项,并会进一步的执行可以减小代码大小的优化。
  • Fastest, Aggressive Optimizations[-Ofast]: 这个设置开启了'Fastest[-O3]'中的所有优化选项,同时也开启了可能会打破严格编译标准的积极优化,但并不会影响运行良好的代码。

Debug默认不优化,Relese默认Fastest, Smallest[-Os]