通过源码分析isa

251 阅读6分钟

通篇以一下代码为例objc4-781源码中验证对应方法

#import <Foundation/Foundation.h>
#import <objc/runtime.h>


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

@implementation TDPerson
@end

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

  • clang

    • 概念

    Clang是⼀个C语⾔、C++、Objective-C语⾔的轻量级编译器。源代码发布于BSD协议下。 Clang将⽀持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。
    **Clang是⼀个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器 **
    2013年4⽉,Clang已经全⾯⽀持C++11标准,并开始实现C++1y特性(也就是C++14,这是 C++的下⼀个⼩更新版本)Clang将⽀持其普通lambda表达式、返回类型的简化处理以及更 好的处理constexpr关键字。 [2] Clang是⼀个C++编写、基于LLVM、发布于LLVM BSD许可证下的C/C++/Objective-C/ Objective-C++编译器。它与GNU C语⾔规范⼏乎完全兼容(当然,也有部分不兼容的内容, 包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,⽐如C函数重载 (通过_ attribute_((overloadable))来修饰函数),其⽬标(之⼀)就是超越GCC。

    • 将oc文件编译成c++文件的编译指令
      1. clang -rewrite-objc main.m -o main.cpp
      2. 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 main.m
      3. xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m -o main-x86_64.cpp(模拟器)
      4. xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp(手机)
  • 对象的本质

    1. 将.m文件编译成.cpp文件并全局搜索TDPerson可得到 发现对象在底层会被编译成结构体
    2. NSObject_IMPL探索
      从上述编译后的代码可以发现结构体继承NSObject_IMPL(这里的继承是伪继承,这种写法可以达到继承的效果,TDPerson_IMPL结构体可以拥有NSObject_IMPL的所有属性)NSObject_IMPL里面又是什么呢继续在编译后的文件中找 所以TDPerson_IMPL中的第一个属性 NSObject_IVARS 其实就是NSObject中的isa
  • isa

    通过上述论证发现对象的本质就是结构体,并且该结构的第一个属性是isa,isa又是什么
    1. 根据源码查找isa
      根据alloc & init & new 源码分析文章中看alloc过程会发现initIsa的方法发现isa,是一个isa_t类型,继续跟源码看一下isa_t又是什么发现是一个联合体位域,所以isa其实就是一个联合体位域
    2. isa为什么是一个联合体位域
      • 联合体属性公用一片内存,成员之间是互斥的,这里有两个属性cls、bits,这样写增加了isa_t的灵活性,可以通过cls初始化isa,同事也可以通过bits初始化isa
      • 位域:我们知道主要是用来节省空间的,就是有些类信息其实不需要那么多位数去存储可能只需要一位就够了,使用位域我们就可以节省不少空间,下图是ISA_BITFIELD属性占用内存情况: 可以看到很多属性只需要1位就够了
    3. isa中的属性分别代表什么
      • 属性含义
      • 属性内存占用情况图解
    4. 透过源码查看isa初始化流程(initIsa)
      通过alloc & init & new 源码分析文章可以知道alloc方法会走到一个加initIsa的方法
    5. 通过源码验证isa的内存分配情况
      1. initIsa源码
      inline void 
       objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
       { 
         ASSERT(!isTaggedPointer()); 
      
         //从这里就可以验证isa为什么用联合体
         //如果仅仅就是一个存isa指针我们只需要通过赋值cls初始化就好
         //如果含有一些类信息、引用计数等,就需要赋值bits来初始化isa
         if (!nonpointer) {
             //如果是纯指针,直接设置到cls
             isa = isa_t((uintptr_t)cls);
         } else {
             ASSERT(!DisableNonpointerIsa);
             ASSERT(!cls->instancesRequireRawIsa());
             //初始化isa
             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;
         }
       }
      
      
      1. **isa_t newisa(0);**之后打印看结果
      2. newisa.bits = ISA_MAGIC_VALUE之后再看结果
      3. 对比第二三步得出结论
        1. ISA_MAGIC_VALUE是就是给nonpointer以及magic赋值的
        2. 怎么赋值的为什么nonpointer是1,magic是59
          首先透过源码仔细看一下ISA_MAGIC_VALUE到底是什么发现就是一个宏,然后这个宏是一个16进制的值0x001d800000000001,下一步再将这个16进制的值转换成二进制从上面isa的属性内存分布图不难看出nonpointer存储在第0位置的所以是1毋庸置疑,然后我们项目是跑在mac上的不是真机,所以CPU架构师x86_64,所以呢shiftcls就占用了44位,加上前面三位(nonpointer、has_assoc、has_cxx_dtor各占用1位)然后又是从0位置开始得出magic从47位开始读,然后magic又是占用6位所以得到magic=111011,然后再转换成10进制得到是59
        3. 验证了联合体的特性bits、cls互斥公用一段内存上图中8303511812964353转换成16进制其实就是0x001d800000000001,所以这两个属性值一样
      4. 细节分析newisa.shiftcls = (uintptr_t)cls >> 3;
        应为赋值会从第0位置开始读取数据,然而cls中的shiftcls信息是从第三位开始存储的,所以要右移三位,还一个就是shiftcls赋值为什么要强转:因为内存的存储不能存储字符串,机器码只能识别 0 、1这两种数字,所以需要将其转换为uintptr_t数据类型,这样shiftcls中存储的类信息才能被机器码理解, 其中uintptr_t是long
    6. isa与类的关联
      上面的代码继续往下执行**newisa.shiftcls = (uintptr_t)cls >> 3;**之后在打印newisa看结果 发现此时cls就是TDPerson,这个时候再将newisa赋值给isa那么isa就和类关联起来了
  • 通过isa获取类信息

    1. 通过x/4gx 查看内存分配情况 应为isa是所有对象的第一个参数,所以就可以得到isa的指针地址0x001d8001000020e9
    2. 第一种方法通过isa & ISA_MASK得到类其实& ISA_MASK就是一个小算法的到shiftcls值,应为isa里面只有shiftcls存储的才是类的指针
    3. 通过位运算
      想要获取类信息其实就是获取isa里面的shiftcls信息,所以要想获取到shiftcls信息肯定要将其他位置的信息剔除掉,上文说过shiftcls不管在arm_64还是x86_64的架构中都是冲第3各位值存储的只不过shiftcls占用的值不一样,所以在arm_64位的系统中需要剔除前三位和后面28位的信息,而在x86_64的架构中需要剔除前三位和后面17位的信息,最重要的已不是要将shiftcls相对位置还原到初始的位置,然后在打印大概流程如下
    4. 通过object_getClass获取
      其实object_getClass源码实现就是isa & ISA_MASK