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 <objc/runtime.h>,然后直接打印出当前的类也能够验证,这里就不打印了,直接去查看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查看源码。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352