iOS的OC的isa的底层原理

2,413 阅读8分钟

前言

一步一个脚印地探索iOS的OC底层原理,通过前面的文章可以大概了解了OC对象创建的alloc原理OC对象的内存字节对齐,但是这也只是知道了对象创建的底层过程和开辟内存空间的,这篇文章将介绍对象的本质和对象与类的关联---isa

1.isa的初始化

isa指针:在Objective-C中,任何类的定义都是对象。类和类的实例(对象)没有任何本质上的区别。任何对象都有isa指针。也就是说在对象创建的时候就会有isa指针初始化了。为了搞清楚还是需要用到OC对象创建的alloc原理里面源码的_class_createInstanceFromZone的方法的部分源码,然后跟着流程进去得到如下的部分源码

//_class_createInstanceFromZone的部分代码
//初始化实例的isa指针
 obj->initInstanceIsa(cls, hasCxxDtor);
 
 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);

#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字面的意思是没有指针的,一般情况下nonpointer是为true的,只有在例如实现了allocwithzone方法,retain,release等的时候会是false。如果为false是直接将传进来的cls为isa的关联的cls赋值。

其他的剩下的部分就是对isa的初始化赋值了。但是具体的isa内部是怎样的还是不知道的,从源码中isa_t点击进去可以查看。

2.isa的内部结构

通过源码可以知道isaisa_t类型的内部结构

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

#ifndef _UINTPTR_T
#define _UINTPTR_T
typedef unsigned long           uintptr_t;
#endif /* _UINTPTR_T */

从中可以知道,isa是一个联合体(union),里面有关联的类cls和long型的bits。

什么是联合体(union)呢?联合体是一种特殊的类,也是一种构造类型的数据结构。完全就是共用一个内存首地址,并且各种变量名都可以同时使用,操作也是共同生效。所以也叫共用体。并且联合体(union)中是各变量是“互斥”的,但是内存使用更为精细灵活,也节省了内存空间。

由上面的概念可以知道,clsbits之间是互斥的,即有cls就没有bits,有bits就没有cls。这就很好地解释了为什么上面的源码在初始化isa的时候会用nonpointer来区分开。所以isa的大小占8个字节,64位。其中这64位中分别存储了什么呢?通过ISA_BITFIELD位域源码:

# 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

# 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

这两种是分别在arm64x86系统架构下的,但是都是64位的,本文的说明是在x86下介绍的。

  • nonpointer:占1位,表示是否对isa指针开启指针优化,0:表示纯指针;1:表示不止是类对象地址,isa中包含了类信息、对象的引用计数等。
  • has_assoc:占1位,表示关联对象标志位,0没有,1存在。
  • has_cxx_dtor:占1位,表示该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有则可以更快的释放对象。
  • shiftcls:占44位,存储类指针的值。开启指针优化的情况下,在arm64架构中有33位用来存储类指针。(由上面的初始化源码也可以很好的说明,关联的类指针向右移3位)
newisa.shiftcls = (uintptr_t)cls >> 3;
  • magic:占6位,用于调试器判断当前对象是真的对象还是没有初始化的空间。
  • weakly_referenced:占1位,表示对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放。
  • deallocating:占1位,表示对象是否正在释放内存。
  • has_sidetable_rc:占1位,表示当对象引用技术大于10时,则需要借用该变量存储进位。
  • extra_rc:占8位,表示该对象的引用计数值,实际上是引用计数总值减1。例如,如果对象的引用计数为10,那么extra_rc为9,如果引用计数大于10,则需要使用到下面的has_sidetable_rc。

3.isa是对象与类的连接

为了方便介绍下面的内容需要定义一个什么属性都没有的TestJason类,然后通过objc4-756.2苹果官方的源码。通过object_getClass这个方法可以获取到类。

TestJason *test2 = [TestJason alloc];
Class testClass = object_getClass(test2);
NSLog(@"%@",test2);

通过源码找到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;
}

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

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
}

#   define SUPPORT_INDEXED_ISA 0
#   define ISA_MASK        0x00007ffffffffff8ULL

从源码中可以知道返回的isa最终是(Class)(isa.bits & ISA_MASK)。 其中源码有一个判断isTaggedPointer(),其中苹果对于Tagged Pointer的概念引入,是为了节省内存和提高执行效率,对于 64 位程序,相关逻辑能减少一半的内存占用,以及 3 倍的访问速度提升,100 倍的创建、销毁速度提升。如果想了解这部分的内容可以看看深入理解 Tagged Pointer

下面就是用lldb的指令来验证一下的。首先用x/4gx test2来打印出地址

然后用p/x TestJason.class打印出TestJason类的内存值

由源码知道类Class的最终返回是(Class)(isa.bits & ISA_MASK)的,所以将x/4gx test2打印出来的0x001d800100001749 & ISA_MASK的值得到如下:

最终发现$3$4的内存值是一样的,所以isa是关联着对象与类的。由前面的文章知道由于内存的优化对象的其他属性的位置实际会发生变化的,所以对象的第一个属性就是isa

4.isa的走位原理

通过上面的介绍,可以知道了isa是关联着对象与类的,并且对象的isa指向类,因为万物皆对象,那么类的isa指向的是谁呢?可以通过苹果官方的isa的走位流程图 isa流程图.png

其中虚线是代表isa的走位,实线代表的是继承关系的走位。图中有一个meta class元类的概念。

什么是元类?在OC中,对象的方法并没有存储于对象的结构体中(如果每一个对象都保存了自己能执行的方法,那么对内存的占用有极大的影响)。 当对象的实例方法被调用时,它通过自己的isa来查找对应的类,然后在所属类的 class_data_bits_t结构体中查找对应方法的实现。同时,每一个objc_class 也有一个指向自己的父类的指针superclass用来查找继承的方法。 而当调用 类方法 时,它的查找流程是怎样的呢?对此OC的解决方案就是引入元类,来保证类方法也能通过相同的机制查找到。对于元类的解释转自OC源码分析之isa

是不是看着这张图的各种箭头指向一脸懵逼呢?下面就是来对这张图的isa的走位验证一下。

4.1 isa的走位

还是用TestJason这个什么属性都没有的类来介绍。通过lldb的指令来验证。由上面的图可以知道,对象的isa指向类,类的isa指向元类。

由图可以知道,先用x/4gx test2来打印出isa的内存值,然后用isa的内存值&ISA_MASK得到$9,然后po $9此时得到的是类。然后再用x/4gx来打印$9的值,此时就是相当于打印出类的isa的内存值了。最后可以看到两个TestJason,但是这两个的内存值是不一样的,分别是类和元类。但是中可以看到元类里面还有isa值,那么就继续打印。

从中可以看到,元类的isa指向了NSObject,但是这个NSObject到底是类呢?还是元类呢?为了搞清楚,可以用x/4gx NSObject.class来打印类的isa来验证。

从中可以看到$13打印的内存值是等于$16的,那么就可以知道元类isa指向的是根元类isa。那么根元类isa指向谁呢?继续打印

从中可以看到根元类isa指向了它的本身,这就形成了一个闭环。所以整体来说的话,就是

对象的isa-->类的isa-->元类的isa-->根元类的isa-->根元类的isa(根元类本身)

这就很好地验证了苹果的官网的isa走位图。

4.2 对象,类之间的继承的走位

为了介绍方便,添加多一个TestSuperJason的类,并且让TestJason是继承TestSuperJason的。还是通过lldb的指令来的打印class_getSuperclass方法。

这是类的继承关系的打印

这是元类的继承关系打印

所以它们的继承关系是

类:
TestJason-->TestSuperJason-->NSObject-->nil
元类:
TestJason元类-->TestSuperJason元类-->NSObject元类-->NSObject类-->nil

5.对象的本质

通过上面的知识可以大概了解了isa的原理,但是对象的本质是什么还不是很了解的,可以通过clang编译成cpp文件来查看。实现的代码如下:

@interface Jason : NSObject{
    NSString *nickName;
}
@property (nonatomic, copy) NSString *name;
@end

@implementation Jason

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"123");
    }
    return 0;
}

在文件的路径下,可以终端输入命令,就可以查看main.cpp文件

clang -rewrite-objc main.m -o main.cpp

从中可以看到

#ifndef _REWRITER_typedef_Jason
#define _REWRITER_typedef_Jason
typedef struct objc_object Jason;
typedef struct {} _objc_exc_Jason;
#endif

extern "C" unsigned long OBJC_IVAR_$_Jason$_name;
struct Jason_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *nickName;
	NSString *_name;
};

// @property (nonatomic, copy) NSString *name;
/* @end */


// @implementation Jason


static NSString * _I_Jason_name(Jason * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Jason$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_Jason_setName_(Jason * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Jason, _name), (id)name, 0, 1); }
// @end


struct NSObject_IMPL {
	Class isa;
};

从中可以看到,对象最终会被编译成结构体struct,NSObject_IMPL里面包含着isa,在类里面定义的属性name和成员变量nickName也是在Jason_IMPL结构体里面,但是属性变量name是有getter方法_I_Jason_namesetter方法_I_Jason_setName_的,而成员变量nickName是没有的。并且这些方法里面都有默认带有两个参数id selfSEL _cmd,这样就很好解释了我们在方法中可以直接调用self

5.最后

通过上面的内容可以了解到isa是一个isa_t类型的联合体(union),并且里面的属性是互斥的,isa的大小占8字节。一般情况下都是在bits下的位域来存储内容,其中ISA_BITFIELDx86arm64架构下都是64位,但是里面的属性的占位有点区别的。isa是关联着对象与类的,并且对象的本质就是一个结构体。至此有关isa的原理介绍到此结束。