初识isa

939 阅读8分钟

提到runtime很多人第一反应就是每个NSObject对象都拥有一个isa,那么isa又是什么呢?isa又有什么用呢?

在阅读objc源码时候可以发现以下定义

struct objc_object {
    isa_t isa;
}

那么isa_t又是什么?

union isa_t 
{
    Class cls;
    uintptr_t bits;
    struct {
         uintptr_t nonpointer        : 1;//->表示使用优化的isa指针
         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;//1->在extra_rc存储引用计数将要溢出的时候,借助Sidetable(散列表)存储引用计数,has_sidetable_rc设置成1
        uintptr_t extra_rc          : 19;  //->存储引用计数
    };
   };

isa_t是一个联合体,它里面包含了一个结构体,这个结构体的各个成员的作用已经在上面标注了

isa_t里面包含了arm64以及x86_64架构的内容,上述只是获取了arm64的内容

那么既然已经提到了isa那我们就不得不说下Class

typedef struct objc_class *Class;

可以看到Class是一个objc_class结构体的指针,那么objc_class是什么?

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache; 
    class_data_bits_t bits;   
}

因为objc_class继承自objc_object所以objc_class也拥有isa指针,由此可以看出Class也是个对象,OC里面万物皆对象

回到上面的问题,那么isa有什么用呢?说到这个问题又得引入另一个概念元类(Meta Class),先简单的了解下什么是元类:

大家都知道,当实例化一个对象后NSObject *objc = [[NSObject alloc] init];,这个对象objc里面包含了isa指针,而根据isa指针可以找到这个对象的类,而我们在上面也看到了,类也包含了一个isa指针,这个isa指针正是指向元类的,网上有个图特别经典,我就直接偷来贴上

感觉上图表述的已经很清晰了,不在做多余的陈述

因为在OC中,对象方法并没有存储在没个对象的实例当中(如果每个对象都存储了实例方法,那么内存将会一直增加),所以所有的对象方法都存储在类中,而我们调用实例方法的时候其实就是根据isa找到自己的类,从类中取到实例方法,如果类中没有实例方法,就调用superclass继续查找;那么类方法又是存储在哪呢?没错,就是存储在刚刚所说的元类中

  • 实例方法调用时,通过对象的isa在类中获取方法的实现
  • 类方法调用时, 通过类的isa在元类中获取方法的实现

我们都知道[NSObject alloc]最终被转换成objc_msgSend(NSObject,@selector(alloc)),这时候系统会根据isa指针去找类或者元类,在缓存中(cache_t)先去寻找,找不到就往class_rw_t中的方法列表中寻找,如果在找不到,就使用superclass指针往父类中寻找,如果找到顶部还是没找到,会调用消息转发的四个方法(这里只给列出来)

第一步:+(BOOL)resolveInstanceMethod:(SEL)sel;或者+ (BOOL)resolveClassMethod:(SEL)sel
第二步:- (id)forwardingTargetForSelector:(SEL)aSelector;
第三步:调用- (void)forwardInvocation:(NSInvocation *)anInvocation,在调用forwardInvocation:之前会调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法来获取这个选择子的方法签名
第四步:- (void)doesNotRecognizeSelector:(SEL)aSelector这个方法,我们也可以在这个方法中做些文章,避免掉crash

isa的初始化

inline void 
objc_object::initClassIsa(Class cls)
{
    if (DisableNonpointerIsa  ||  cls->instancesRequireRawIsa()) {
        initIsa(cls, false/*not nonpointer*/, false);
    } else {
        initIsa(cls, true/*nonpointer*/, false);
    }
}
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, bool nonpointer, bool hasCxxDtor) 
{ 
    assert(!isTaggedPointer()); 
    
    if (!nonpointer) {//没有优化
        isa.cls = cls;
    } else {
        assert(!DisableNonpointerIsa);
        assert(!cls->instancesRequireRawIsa());
        isa_t newisa(0);
        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; //yty 拿手机举例子:shiftcls:33;(shiftcls会分配到33位),在64位的手机上拿33位保存类的地址
        ,但因为位对齐的缘故,所有地址都是8的倍数,所有地址书写的成二进制数最后3位全是0,所以才如上见到:cls >> 3;(消除了3个没有影响的0)
	/*
	 0000001000011101100000000000000100000000000000000010010101100 001 isa地址
		                 00000000000100000000000000000010010101100 000 class地址

	 */
        isa = newisa;
    }
}

nonpointer是一个标记当前isa指针是否被优化(未优化时isa是一个指向类的指针),因为在initInstanceIsa调用initIsa方法时nonpointer固定是true所以可以看出在上述方法中应该走的是else分支,可以看到在初始化的时候对整个isa.bits赋值了一个ISA_MAGIC_VALUE

# if __arm64__
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
# elif __x86_64__
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#endif

arm64架构的宏值转换二进制为

第64位
0000 0000 0000 0000 0000 0001 1010 0000
0000 0000 0000 0000 0000 0000 0000 0001
                                      第1位

因为isa_t是一个联合体,所以给bits赋值相当于给isa_t其中的结构体赋值

nonpointer 与 magic

在图中可以看出ISA_MAGIC_VALUE就是给nonpointer以及magic赋值。nonpointer等于1表示是已经优化过的isa,不是一个指针;0表示当前的isa是一个指针,指向classmagic 的值为 0x1a(arm64下,x86_64环境下为0x3b) 用于调试器判断当前对象是真的对象还是没有初始化的空间

has_cxx_dtor

在设置完bits(nonpointer 与 magic)后设置has_cxx_dtor,这一位表示当前对象有 C++ 或者 ObjC 的析构器(destructor),如果没有析构器就会快速释放内存。

在当前对象拥有对象属性/成员变量的时候系统会自动为我们插入一个- (void).cxx_destruct;方法

newisa.has_cxx_dtor = hasCxxDtor;

shiftcls

设置完nonpointer 与 magic以及has_cxx_dtor,就开始设置shiftcls

newisa.shiftcls = (uintptr_t)cls >> 3;

拿arm64举例子:shiftcls:33(shiftcls会分配到33位),在64位的手机上拿33位保存类的地址,但因为位对齐的缘故,所有地址都是8的倍数,所有地址书写的成二进制数最后3位全是0,所以才如上见到:cls >> 3;

下面我们测试一下

可以看到类地址的最后一位都是80,在16进制中80转换为2进制最后三位都是000,所以向右偏移3位不会对类地址照成影响

我们在来打印一下对象的地址,与Class的地址进行一下对比

可以看到对象的isa内容为0x000005a104379c45转换为二进制为

 0000 0000 0000 0000 0000 0101 1010 0001
 0000 0100 0011 0111 1001 1100 0100 0101

在图中表示为

类的地址为0x0000000104379c40转换为二进制为

 0000 0000 0000 0000 0000 0000 0000 0001
 0000 0100 0011 0111 1001 1100 0100 0000

可以看出shiftcls 值为 0001 0000 0100 0011 0111 1001 1100 0100 0,class地址为1 0000 0100 0011 0111 1001 1100 0100 0000class地址右移三位刚好就是shiftcls的值

因为我们的isa已经不是一个指向类的指针了,我们如果想要获取到类就可以使用

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#endif
inline Class 
objc_object::ISA() 
{
    return (Class)(isa.bits & ISA_MASK);
}

其本质就是让isa.bits与一个数进行与运算获取

其他

isa_t中我们还有一些没有说到的字段,他们都是在实际编码的时候会进行更改的

  • has_assoc

对象是否曾有或者有关联对象,如果没有可以更快速释放

  • weakly_referenced

对象是否曾被指向或被指向一个ARC的弱变量,如果没有可以更快速释放

  • deallocating

对象是否正在释放

  • has_sidetable_rc

对象的引用计数如果超出extra_rc上线的标记,将会将多余的引用计数存储在一个全局的Hash表中(SideTable),后面会讲到

  • extra_rc

对象的引用计数超过1,会存在这个这个里面,所以如果extra_rc没有超出上限的真实的returnCount 为 extra_rc+1


文章参考:
从 NSObject 的初始化了解 isa