iOS中类的isa

446 阅读8分钟

对象的本质

准备工作

首先我们在main.m文件中来定义一个简单的类

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *sex;
@property (nonatomic, assign) char test;
@end

@implementation Person
@end

然后我们通过clang来编译成main.cpp文件,这里简单介绍下clang,了解clang直接跳过

Clang

clang是一个由Apple主导编写,基于LLVM的C/C++/OC的编译器。主要是用于底层编译,将一些文件输出成c++文件,例如main.m 输出成main.cpp,其目的是为了更好的观察底层的一些结构及实现的逻辑,方便理解底层原理。

编译命令有以下几种

//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp

//2、将 ViewController.m 编译成  ViewController.cpp(注意iPhoneSimulator13。sdk路径一定和本地对的上)
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m

//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 

//4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp 

Clang 编译

  • 执行clang -rewrite-objc main.m -o main.cpp完之后在main.m同级目录下会看到一个新的main.cpp文件。打开这个文件,发现里面有很多代码,我们直接查找我们新添加的Person类的代码
#ifndef _REWRITER_typedef_Person
#define _REWRITER_typedef_Person
typedef struct objc_object Person;
typedef struct {} _objc_exc_Person;
#endif

extern "C" unsigned long OBJC_IVAR_$_Person$_name;
extern "C" unsigned long OBJC_IVAR_$_Person$_age;
extern "C" unsigned long OBJC_IVAR_$_Person$_sex;
extern "C" unsigned long OBJC_IVAR_$_Person$_test;
struct Person_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	char _test;
	int _age;
	NSString *_name;
	NSString *_sex;
};

// @property (nonatomic, copy) NSString *name;
// @property (nonatomic, assign) int age;
// @property (nonatomic, copy) NSString *sex;
// @property (nonatomic, assign) char test;

/* @end */


// @implementation Person

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

static void _I_Person_setName_(Person * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _name), (id)name, 0, 1); }

static int _I_Person_age(Person * self, SEL _cmd) { return (*(int *)((char *)self + OBJC_IVAR_$_Person$_age)); }
static void _I_Person_setAge_(Person * self, SEL _cmd, int age) { (*(int *)((char *)self + OBJC_IVAR_$_Person$_age)) = age; }

static NSString * _I_Person_sex(Person * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_sex)); }
static void _I_Person_setSex_(Person * self, SEL _cmd, NSString *sex) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _sex), (id)sex, 0, 1); }

static char _I_Person_test(Person * self, SEL _cmd) { return (*(char *)((char *)self + OBJC_IVAR_$_Person$_test)); }
static void _I_Person_setTest_(Person * self, SEL _cmd, char test) { (*(char *)((char *)self + OBJC_IVAR_$_Person$_test)) = test; }
// @end

通过上面的代码我们发现我们定义的声明和实现变成了一个结构体和一些方法 在仔细看结构体发现这个结构体变量的顺序和我们定义属性的顺序还不一样,这里顺序不一样的原因是因为系统在底层做了内存的优化,具体的可以看内存对齐那些事这篇文章

在往下看get/set方法,我们发现NSString 类型的变量,set方法都调用了一个objc_setProperty的方法,而intchar类型的变量都没有调用objc_setProperty,我们带着疑问往下看

objc_setProperty

我们通过objc-781的源码跟到objc_setProperty具体实现

void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}

在接着往下看reallySetProperty的实现

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

我们看到reallySetProperty里面除了对newValue进行set,还需要对新值进行objc_retain,最后把oldValue进行objc_release 看到这里我们明白了为什么int/char这写类型都不需要调用objc_setProperty,因为它们都是assign进行修饰,根本不需要进行引用计数管理。

从这里可以看出系统底层所有需要引用计数管理的类型都调用objc_setProperty这个方法。这么做的好处就是做到了接口隔离

isa

我们在回到上面的结构体,stuct中发现除了我们定义的变量,最上面还多出来一个 struct

struct NSObject_IMPL NSObject_IVARS;

接着往下看NSObject_IMPL是什么

struct NSObject_IMPL {
	Class isa;
};

到这里我们终于看到isa,接下来通过源码继续去深入分析isa,之前的iOS中Object-C的 alloc、init执行流程文章中我们知道初始化的时候会执行initInstanceIsa把类地址和isa 进行关联。我们来看看是怎么进行关联的 initInstanceIsa源码

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

initInstanceIsa中调用了initIsa源码,如下

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    ASSERT(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa = isa_t((uintptr_t)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;
    }
}

通过断点,执行这行代码

isa = isa_t((uintptr_t)cls)

isa: 是 objc_object的私有属性 isa_t:是一个联合体,具体实现 在了解isa_t之前,我们先了解下联合体

union(联合体/共用体)

结构体:是指把不同的数据组合成一个整体,其变量是共存的,变量不管是否使用,都会分配内存

  • 缺点:所有属性都分配内存,比较浪费内存,假设有4个int成员,一共分配了16字节的内存,但是在使用时,你只使用了4字节,剩余的12字节就是属于内存的浪费

  • 优点:存储容量较大,包容性强,且成员之间不会相互影响

联合体:联合体也是由不同的数据类型组成,但其变量是互斥的,所有的成员共占一段内存。而且共用体采用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会将原来成员的值覆盖掉

  • 缺点:可读性差,包容性弱

  • 优点:所有成员共用一段内存,使内存的使用更为精细灵活,同时也节省了内存空间

isa_t

我们从代码中看出isa 是一个isa_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
};

这里的isa_t就是一个联合体,对内存做了一个优化,主要是通过isa指针进行位域(即二进制中每一位均可表示不同的信息)的原理实现。通常来说,isa指针占用的内存大小是8字节,即64位,已经足够存储很多的信息了,这样可以极大的节省内存,以提高性能

从isa_t的结构源码我们可以看出有两个成员,clsbits,由联合体的定义所知,这两个成员是互斥的,也就意味着,当初始化isa指针时,有两种初始化方式:

  • 通过cls初始化,bits无默认值
  • 通过bits初始化,cls有默认值 除了这两个成员,还有个结构体,结构体中是个ISA_BITFIELDISA_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
#   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

这里x86架构和arm64下宏定义还不一样,这里以x86为例具体分析下: 内存布局如下: 内容的含义:

nonpointer:(存储在第0字节) : 是否为优化isa标志。0代表是优化前的isa,一个纯指向类或元类的指针;1表示优化后的isa,不止是一个指针,isa中包含类信息、对象的引用计数等。现在基本上都是优化后的isa。

has_assoc :(存储在第1个字节): 关联对象标志位。对象含有或者曾经含有关联引用,0表示没有,1表示有,没有关联引用的可以更快地释放内存(dealloc的底层代码有体现)。

has_cxx_dtor:(存储在第2个字节): 析构函数标志位,如果有析构函数,则需进行析构逻辑,如果没有,则可以更快速地释放对象(dealloc的底层代码有体现)。

shiftcls :(存储在第3-35字节)存储类的指针,其实就是优化之前 isa 指向的内容。在arm64架构中有33位用来存储类指针。x86_64架构有44位。

magic:(存储在第36-41字节):判断对象是否初始化完成, 是调试器判断当前对象是真的对象还是没有初始化的空间。

weakly_referenced:(存储在第42字节):对象被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放(dealloc的底层代码有体现)。

deallocating:(存储在第43字节):标志对象是否正在释放内存。

has_sidetable_rc:(存储在第44字节):判断该对象的引用计数是否过大,如果过大则需要其他散列表来进行存储。

extra_rc:(存储在第45-63字节。):存放该对象的引用计数值减1后的结果。对象的引用计数超过 1,会存在这个里面,如果引用计数为 10,extra_rc 的值就为9。

实战验证

验证方式一:

首先创建一个Person

Person *p = [[Person alloc]init];

首先我们读取下p的信息

x/4gx p

打印结果

0x101204490: 0x001d8001000020e9 0x0000000000000000
0x1012044a0: 0x75636f44534e5b2d 0x69766552746e656d

根据我们之前的信息得到0x001d8001000020e9 就是person isa 的地址,然后根据上面的存储结构,我们先把右移3位把右3位抹零

po 0x001d8001000020e9 >> 3

得到

1037939513492509a

在左移20位

po 1037939513492509 << 20

结果

576461882953564160

在右移17还原之前对应的地址,这样左右两边都已经抹零,只剩下中间44位,我们再来看看

po 576461882953564160 >> 17

结果

Person

是不是很神奇,确实拿到了Person这个对象,也验证了isa的中间44位确实存储了Person的信息

验证方式二:

我们看系统是怎么拿到Person的信息的呢:

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
}

这里可以看到是通过isa.bits & ISA_MASK ,ISA_MASK是

#   define ISA_MASK        0x00007ffffffffff8ULL

我们把0x00007ffffffffff8ULL转成二进制 除了中间44位是1其他都是0,系统把isa.bits& ISA_MASK之后,得到的结果和我们上面第一种方式得到的结果相同

po 0x001d8001000020e9 & 0x00007ffffffffff8

结果

Person

initIsa实现源码

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    ASSERT(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa = isa_t((uintptr_t)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;
    }
}

这里可以看到首先对bits 进行赋值ISA_MAGIC_VALUE,x86下0x001d800000000001ULL,转成二进制对应到对应 isa_t 中内存布局的位置,可以看出对 bits 赋值 就是对 nonpintermagic 赋值的过程

然后是对has_cxx_dtor赋值

最后对shiftcls赋值,这里我们看到对shiftclscls赋值的时候把cls右移了3位,主要的原因是减小内存消耗,因为指针是要按照8字节对齐的,实际后三位是没有意义的。这和 isa_t 中的内存布局没有关系,因为类可不是按照isa_t进行内存布局的

总结

到这里我们就大概搞清楚isa 与cls 的关联原理就是isa这个指针中shiftcls位域中存储了类信息(x86下44位,arm64下33位),initInstanceIsa的过程就是calloc指针和当前的的类cls关联起来。 这里还有个细节就是继承,类是可以集成的,那类转成stuct之后是怎么实现继承的呢。

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_name;
};

通过源码额可以看到把isa 这个成员放在了stuct 的首位,实现了一个伪继承,达到继承的效果。