OC底层原理 06: isa结构分析

主动已经是我对热爱东西表达的极限了

对象的本质?
联合体位域的简析?
isa的结构信息?
isa如何关联类?
通过位运算验证关联类
总结。

  • 什么是对象?
    对象在底层变成了什么呢?

  • 什么是Clang?

Clang是一个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器

  • 用Clang做些什么?

Clang 通过底层编译,将一些m文件编译为cpp。 因为OC为C++或者C的超集,通过Clang底层编译,可以更多的看到底层的实现原理与逻辑和底层的架构

  • 如何使用Clang?

Clang终端操作四种命令如下:

clang -rewrite-objc main.m -o main.cpp 把目标文件编译成c++文件

`xcode`安装的时候顺带安装了`xcrun`命令,`xcrun`命令在`clang`的基础上进行了 一些封装,要更好用一些

xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp (模拟器)

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp (手机)

Clang 具体操作请查看:后续

通过Clang编译查看的底层是如何实现的,首先定义如下:

  @interface LGPerson : NSObject
  @property (nonatomic, copy) NSString *name;
  @end

  @implementation LGPerson
  @end

  int main(int argc, const char * argv[]) {
      @autoreleasepool {
          // insert code here...
          NSLog(@"Hello, World!");    }
      return 0;
  }

找到已经编译好的main.cpp文件,搜索LGPerson,得到我想要的信息如下:

  #ifndef _REWRITER_typedef_LGPerson
  #define _REWRITER_typedef_LGPerson
  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;
      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

对象是如何被编译为结构体的呢?

我们知道结构体在C++可以继承,C可以伪继承

伪继承方式:
直接将NSObject结构体定义为LGPerson中的第一个属性,意味着LGPerson 拥有 NSObject中的所有成员变量
LGPerson中的第一个属性 NSObject_IVARS 等效于 NSObject中isa

这里将LGPerson_IMPL结构体中的第一属性命名为当前结构体,即LGPerson_IMPL拥有 NSObject中的所有成员变量。而NSObject_IVARSisa

LGPerson类中的nameget, set方法也是一一对应的:

main.cpp中类对应的get,set方法截图

set方法调用了objc_setProperty,通过分析得出objc_setProperty采用工厂模式:return newValue, remove oldValue ;意味着任何类中的set方法都会执行了如下操作:

objc4_781源码中找到objc_setProperty查看如何执行操作get , set

继续分析isa结构,OC底层原理 结构体&联合体

  • isa结构分析:

在之前探索 OC底层原理 alloc & init & new 篇 时提到过initInstanceIsa方法,通过initInstanceIsa为切入点来分析isa是如何初始化的,可以看到isa指针的类型isa_t定义是通过联合体(union)来定义联合体不清楚的小伙伴请参考:OC底层原理 结构体&联合体

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

    Class cls; //这里返回的cls 为什么会是一个class类型?请查看后续
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

这里的isa_t为什么要定义成联合体呢?是为了优化内存,而isa指针占用的内存大小为8字节,即64位,已经能够存储大量信息了,这样可以节省内存空间,以提高性能的目的

isa_t的定义中可以看出,
通过cls初始化,bits无默认值
通过bits初始化,cls有默认值
体现了联合体互斥性

通过宏ISA_BITFIELD可以看出isaiOS( __arm64__)macOS(__x86_64__)的计算是存在差异

isa不同环境的差异化截图
  • isa的结构信息对应如下:
    • nonpointer:表示是否对 isa 指针开启指针优化
      0:纯isa指针;
      1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等

    • has_assoc:关联对象标志位,
      0没有关联对象
      1存在关联对象

    • has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器(类似于dealloc
      如果析构函数,则需要做析构逻辑
      如果没有,则可以更快的释放对象

    • shiftcls:存储类指针的值(类的地址), 即类信息开启指针优化的情况下,
      __arm64__ 架构中有 33 位用来存储类指针。
      __x86_64__ 架构中有 44 位用来存储类指针。

    • magic:用于调试器判断当前对象是真的对象还是没有初始化的空间

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

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

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

    • extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc9。如果引用计数大于 10, 则需要使用 has_sidetable_rc

更直观的isa的结构信息图如下:

isa结构对照表

通过 initIsa方法,查看 isa指针是如何被初始化

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    ASSERT(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa = isa_t((uintptr_t)cls);//isa初始化
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());

        isa_t newisa(0);  //isa初始化
#if SUPPORT_INDEXED_ISA  // !nonpinter 执行,即isa通过cls定义
        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 // bits 时执行的流程
        newisa.bits = ISA_MAGIC_VALUE; // bits进行赋值
        // 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; //isa与类关联
#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;
    }
}
  • initIsa方法主要使用:cls初始化isa,和bits初始化isa

执行代码,开始验证,以LGPerson为例:

对赋值newisa.bits赋值之后打印出newisa查看LLDB结果

p newisa 打印结果

  • 对赋值newisa.bits赋值会对cls进行追加;对cls赋值不会同时对bits进行赋值(联合体互斥性

通过LLDB打印结果,在进行赋值的时候,bits中的magic会存在一个59呢?
使用计算器查看宏ISA_MAGIC_VALUE592进制

为什么magic为59

magic的占位为6,从47~52表示magic的占位,所以magic的默认值为59二进制表示;也可以理解为isa指针将magic的占位为6,转换为2进制存储

  • isa与类关联

当执行完如下代码后,LLDB打印出当前newisa
clsisa 关联原理就是isa指针中的shiftcls位域中存储了类信息,其中initInstanceIsa的过程是将 calloc 指针 和当前的 类cls 关联起来。而newisa.shiftcls = (uintptr_t)cls >> 3 是将当前cls 强转为系统能够编译的01识别码。然后右移3位。之所以要移动3位,要知道shiftcls才是我们需要存储的类信息,从上面isa对照表中可以看出,前面三位并不是我们需要的类内容,需要右移3位进行抹0操作。

newisa.shiftcls = (uintptr_t)cls >> 3; //isa与类关联
isa与类关联结果图

这个时候我们已经知道isa 进行了关联。

  • 开始验证isa中的shiftcls是否真正的存储了当前类信息

initInstanceIsa方法返回出去,然后开始验证:

  • 方式1. 通过isa指针地址与宏ISA_MSAK 的值 通过位运算& 来验证
      #   define ISA_MASK        0x00007ffffffffff8ULL    //__x86_64__ 
      #   define ISA_MASK        0x0000000ffffffff8ULL    //__arm64__
    
    LLDB 操作:po obj ,然后x/4gx LGPerson的地址 或者直接x/4gx,然后取LGPersonisa地址指针 &ISA_MASK,结果如下:
    位运算验证
  • 方式2. 使用位运算验证
    shiftcls存储在当前3~46位,
    向右移动3位,抹除0~2号位数据,
    向左移动20位,移除47~64号位的数据,
    向右移动17位,回到原来的shiftcls所在的存储位,直接取出,即LGPerson 类信息
    位运算操作截图
  • 方式3. 通过runtimeobject_getClass来验证
    main.m导入头文件#import ,然后直接打印出当前的类也能够验证,这里就不打印了,直接去查看object_getClass的实现,
    object_getClass方法如下:
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

我们进入getIsa方法继续查看,

inline Class 
objc_object::getIsa() 
{
    if (fastpath(!isTaggedPointer())) return ISA(); //直接return了当前isa

    extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
    uintptr_t slot, ptr = (uintptr_t)this;
    Class cls;

    slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
    cls = objc_tag_classes[slot];
    if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
        slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        cls = objc_tag_ext_classes[slot];
    }
    return cls;
}

继续查看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); //这里与我们方式1做的操作是一样的
#endif
}

我们可以看出object_getClass的内部实现,也是采用了位运算&的方式对当前类进行处理。

验证总结:验证isa与类是否关联还有其他方式,这里不一一阐述,有兴趣的同学可以自己探索。

  • isa结构分析总结:
    1.isa通过isa_t方法进行初始化,其中使用到了联合体位域isa关联的同时,做了强转的操作和位运算计算存储,其中newisa.shiftcls = (uintptr_t)cls >> 3;uintptr_t表示long类型。
    2.x86_64arm64环境不同,存在部分计算差异
    3.掌握如何对isa的关联验证
    4.如何使用Clang查看源码。

你可能感兴趣的:(OC底层原理 06: isa结构分析)