四、isa与类关联的原理

在分析isa与类关联的原理之前先了解clang编译器

Clang

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

图片.png

clang输出一下(clang -rewrite-objc main.m -o main.cpp)

图片.png

也可以用一下指令

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

//2、将 ViewController.m 编译成  ViewController.cpp
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 

输出之后的.mm太长了, 截取重要的信息:

//NSObject
#ifndef _REWRITER_typedef_NSObject
#define _REWRITER_typedef_NSObject
typedef struct objc_object NSObject;
typedef struct {} _objc_exc_NSObject;
#endif

struct NSObject_IMPL {
    Class isa;
};

//Person
typedef struct objc_object LGPerson;
typedef struct {} _objc_exc_LGPerson;
#endif

extern "C" unsigned long OBJC_IVAR_$_LGPerson$_name;
struct LGPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;//isa 继承至nsobject
    NSString *_name;
};

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


// @implementation LGPerson

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

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

可以看出底层实现为结构体(struct)有一个属性name;
_I_LGPerson_name_I_LGPerson_setName_为属性namesetget方法

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);//新值 retain
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot]; //原子性, 加锁
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);// 释放旧值
}

isa

根据源跟踪, 我们可以到这一步

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

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

提供了两个成员,cls 和 bits,由联合体的定义所知,这两个成员是互斥的,也就意味着,当初始化isa指针时,有两种初始化方式
1. 通过cls初始化,bits无默认值

-2. 通过bits初始化,cls有默认值
还提供了一个结构体定义的位域,用于存储类信息其他信息结构体的成员ISA_BITFIELD,这是一个宏定义,有两个版本 __arm64__(对应ios 移动端) 和 __x86_64__(对应macOS),以下是它们的一些宏定义,如下图所示

图片.png
  • nonpointer有两个值,表示自定义的类等,占1位

    • 0:纯isa指针
    • 1:不只是类对象地址,isa中包含了类信息、对象的引用计数等
  • has_assoc表示关联对象标志位,占1位

    • 0:没有关联对象
    • 1:存在关联对象
  • has_cxx_dtor 表示该对象是否有C++/OC的析构器(类似于dealloc),占1位

    • 如果有析构函数,则需要做析构逻辑
    • 如果没有,则可以更快的释放对象
  • shiftclx表示存储类的指针的值(类的地址), 即类信息

    • arm64中占 33位,开启指针优化的情况下,在arm64架构中有33位用来存储类指针
    • x86_64中占 44位
  • magic 用于调试器判断当前对象是真的对象 还是 没有初始化的空间,占6位

  • weakly_refrenced是 指对象是否被指向 或者 曾经指向一个ARC的弱变量
    没有弱引用的对象可以更快释放

  • deallocating 标志对象是是否正在释放内存

  • has_sidetable_rc表示 当对象引用计数大于10时,则需要借用该变量存储进位

  • extra_rc(额外的引用计数) --- 导尿管表示该对象的引用计数值,实际上是引用计数值减1
    如果对象的引用计数为10,那么extra_rc为9

# 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
图片.png

调试initIsa 方法的时候犯了错误
想查看newisa信息,

 po newisa

什么信息都没有, 因这部分源码是c++, 并没有description方法, 不能用po查看
应该用 p newisa

图片.png

magic = 59 ?
magic59是由于将isa指针地址转换为二进制,从47(因为前面有4个位域,共占用47位,地址是从0开始)位开始读取6位,再转换为十进制,如下图所示

image

isa 与 类 的关联

clsisa 关联原理就是isa指针中的shiftcls位域中存储了类信息,其中initInstanceIsa的过程是将 calloc 指针 和当前的 类cls 关联起来,有以下几种验证方式:

  • 【方式一】通过initIsa方法中的newisa.shiftcls = (uintptr_t)cls >> 3;验证

  • 【方式二】通过isa指针地址ISA_MSAK 的值 & 来验证

  • 【方式三】通过runtime的方法object_getClass验证

  • 【方式四】通过位运算验证

方式一:通过 initIsa 方法

  • 运行至newisa.shiftcls = (uintptr_t)cls >> 3;前一步,其中 shiftcls存储当前类的值信息

    • 此时查看cls,是LGPerson

    • shiftcls赋值的逻辑是将 LGPerson进行编码后,右移3

      image
  • 执行lldb命令p (uintptr_t)cls,结果为(uintptr_t) $2 = 4294975720,再右移三位,有以下两种方式(任选其一),将得到536871965存储到newisashiftcls

    • p (uintptr_t)cls >> 3

    • 通过上一步的结果$2,执行lldb命令p $2 >> 3

      image
  • 继续执行程序到isa = newisa;部分,此时执行p newisa

    image

    bits赋值结果的对比,bits的位域中有两处变化

    • cls默认值,变成了LGPerson,将isa与cls完美关联

    • shiftcls0变成了536871965

      image

所以isa中通过初始化后的成员值变化过程,如下图所示

image

为什么在shiftcls赋值时需要类型强转?

因为内存的存储不能存储字符串机器码只能识别 0 、1这两种数字,所以需要将其转换为uintptr_t数据类型,这样shiftcls中存储的类信息才能被机器码理解, 其中uintptr_tlong

为什么需要右移3位?

主要是由于shiftcls处于isa指针地址的中间部分,前面还有3个位域,为了不影响前面的3个位域的数据,需要右移将其抹零

方式二:通过 isa & ISA_MSAK

  • 在方式一后,继续执行,回到_class_createInstanceFromZone方法,此时cls 与 isa已经关联完成,执行po objc

  • 执行x/4gx obj,得到isa指针的地址0x001d8001000020e9

  • isa指针地址 & ISA_MASK (处于macOS,使用x86_64中的定义),即 po 0x001d8001000020e9 & 0x00007ffffffffff8 ,得出LGPerson

    • arm64中,ISA_MASK 宏定义的值为0x0000000ffffffff8ULL

    • x86_64中,ISA_MASK 宏定义的值为0x00007ffffffffff8ULL

      image

方式三:通过 object_getClass

通过查看object_getClass的源码实现,同样可以验证isa与类关联的原理,有以下几步:

  • main中导入#import

  • 通过runtime的api,即object_getClass函数获取类信息

你可能感兴趣的:(四、isa与类关联的原理)