iOS底层系列09 -- isa的底层探索

OC对象在C/C++底层的本质

  • 本文使用的 iOS底层系列01-- objc4-781源码的编译与调试文章中的源码工程,创建继承自NSObject的YYPerson类,创建YYStudent其继承自YYPerson类;
@interface YYPerson : NSObject

@property(nonatomic,copy)NSString *name;

@end
@interface YYStudent : YYPerson

@property(nonatomic,assign)NSInteger age;

@end
Snip20210209_114.png
  • 首先了解一下Clang编译器,详细内容见 iOS逆向04 -- 编译过程

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

    • clang -rewrite-objc main.m -o main.cpp 将main.m文件编译成main.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 将ViewController.m文件编译成ViewController.cpp文件 因为涉及UIKit框架SDK 所以要输入SDK的路径;
    • xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 使用xcode工具 xcrun 模拟器文件编译 main.m文件编译成main.cpp文件;
    • xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp 使用xcode工具 xcrun 真机文件编译 main.m文件编译成main.cpp文件;
  • 打开终端 cd 指定文件路径 执行 clang -rewrite-objc YYPerson.m -o YYPerson.cpp
    YYPerson.m文件编译成YYPerson.cpp文件,可以看到本地生成了一个YYPerson.cpp文件,双击打开YYPerson.cpp文件,全局搜索YYPerson定位如下所示:

Snip20210209_115.png
  • NSObject的C++底层如下:
Snip20210209_116.png
  • 可以看出YYPerson在C++底层被转化成 YYPerson_IMPL结构体,其内部有两个成员分别是 struct NSObject_IMPL_name,其中struct NSObject_IMPL是NSObject在C++底层的结构体,因为YYPerson继承自NSObject,所以其YYPerson_IMPL结构体成员会包含NSObject的底层结构体struct NSObject_IMPL,并且是放在第一个成员的位置;
  • 终端执行 clang -rewrite-objc YYStudent.m -o YYStudent.cpp
    YYStudent.m文件编译成YYStudent.cpp文件;
Snip20210209_118.png
  • YYStudent继承自YYPerson,其C++底层结构体struct YYStudent_IMPL中的数据成员包含了YYPerson的底层结构体YYPerson_IMPL,其是放在数据成员的第一个位置;

  • 总结:

    • OC类Class在C++底层的本质就是结构体
    • OC类Class之间的继承关系,在C++底层表现为子类的结构体成员包含父类的结构体并将父类的结构体放在数据成员的第一个位置
    • NSObject的C++底层结构体有一个Class类型的isa数据成员,那么所有OC对象都会存在一个isa属性成员;

isa的结构分析

  • 在Arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class的内存地址;
  • 从Arm64架构开始,对isa进行了优化,如果要获取到Class、Meta-Class的内存地址,需要将isa与其掩码ISA_MASK 作位与运算 即 (isa & ISA_MASK) 才能得到Class、Meta-Class的内存地址;
  • isa变成了一个共用体(union),并且使用位域来存储更多的信息
  • 在 iOS底层系列03 -- alloc init new方法的探索这片文章提到OC对象在alloc时底层调用_class_createInstanceFromZone函数,在最后会创建一个isa_t共用体变量,最后赋值给实例对象的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对象
        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;
        //isa_t属性与class的绑定关系建立
        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_t赋值给对象的isa属性,实现了对象isa的初始化,即实现了对象与class的绑定
        isa = newisa;
    }
}
  • 首先我们来分析一下isa_t这个共用体的结构,定位到其结构代码如下:
Snip20210209_119.png
  • 共用体的特性是数据成员之间是互斥的,能极大的节约内存;
  • isa_t有两个数据成员分别为clsbits,两者之间是互斥的;
  • 还提供了一个利用结构体定义的位域,用于存储类的信息,结构体的成员ISA_BITFIELD,这是一个宏定义,存在两个版本 arm64(对应iOS移动端)和 x86_64(对应macOS),以下是它们的一些宏定义,如下图所示(这里引用的是他人的解析图片)
  • uintptr_t bits 就是ISA的存储数据占8个字节,64个二进制位,用它的二进制位来表示类的相关信息,实现原理可见最后面自定义位域实现;
Snip20210209_120.png
  • uintptr_t等价于 unsigned long 即无符号长整型,占8个字节,64个二进制位;
  • ISA_BITFIELD位域总共有64个二进制位,不同的二进制位或者二进制位区域表示不同的含义,下面详细解释每一个位域参数的含义;
  • nonpointer:表示是否对isa开启指针优化,0代表是纯isa指针,1代表除了地址外,还包含了类的其他信息,占一个二进制位
  • has_assoc:表示关联对象标志位,0表示没有关联对象,1表示关联对象,占一个二进制位
  • has_cxx_dtor:表示该对象是否有C++/OC的析构器(类似于dealloc)如果有析构函数,则需要做一些析构的逻辑处理,如果没有,则可以更快的释放对象,占一个二进制位
  • shiftcls:表示类的地址,在开启指针优化的情况下,在arm64中占33位,在x86_64中占44位
  • magic:判断当前对象是真的对象还是一段没有初始化的空间,占6个二进制位
  • weakly_refrenced:表示是否被弱引用指向,占1个二进制位
  • deallocating:表示对象是是否正在释放内存,占1个二进制位
  • has_sidetable_rc:表示是否使用 sidetable散列表来存储对象的引用计数,占1个二进制位
  • extra_rc:存储对象的引用计数值,若extra_rc存储已满,则开始使用sidetable散列表来存储对象的引用计数,占19个二进制位,详细原理可见 iOS内存管理07 -- retain, release, dealloc与retainCount的源码分析
  • isa位域图如下所示:
Snip20210209_121.png
  • 根据上面创建isa_t共用体的函数objc_object::initIsa() ,当代码执行到 newisa.bits = ISA_MAGIC_VALUE; 这里时,断点停下,在控制台上输入命令p newisa 可以看到以下的结果:
Snip20210210_122.png
  • 当过掉当前断点,完成bits成员的赋值,此时bits赋值的是一个默认值为ISA_MAGIC_VALUE = 0x001d800000000001ULL 然后控制台再次输入p newisa,看到结果如下:
Snip20210210_123.png
  • 看到newisa的成员已经被初始化了,并且位域上的有些位也被赋值了;
Snip20210210_127.png
  • bits赋值默认值后,位域magic的值为59;首先cls = 0x001d800000000001 是16进制,计算位域时需要转成二进制,再者此代码调试都是在Mac x86_64平台上进行的,由上面的位域图可知magic的位域为[47,52]占6个二进制位,具体换算如下所示:
Snip20210210_130.png
  • 断点接着往下走,我们来继续分析,如下图所示:
Snip20210210_132.png
  • 首先我们已经知道在isa指针中的shiftcls位域中存储的是类信息;
  • newisa.shiftcls = (uintptr_t)cls >> 3 其中cls就是当前类YYPerson,从这里就可以看出newisa开始与Class类建立关联了;
  • newisa.shiftcls = (uintptr_t)cls >> 3赋值完成之后,看到newisa的cls成员已经被赋值为YYPerson,说明newisa已经与Class类建立了关联;
  • 在控制台上输入p (uintptr_t)cls ;然后在输入 p (uintptr_t)cls >> 3 如下所示:
    Snip20210210_133.png
  • 这就解释了shiftcls成员为什么等于536872031;
  • 在计算shiftcls成员时,cls右移了三位是因为由上面的位域图知道在shiftcls之前还存在三个二进制;
  • 下面附一张newisa内部成员变化流程图:
Snip20210210_135.png
  • 上面的代码流程执行完,那么实例对象isa指针完成初始化并且与类Class建立了关联;

  • 下面提供几种方式来证明实例对象的isa是指向实例对象的类的:

  • 第一种方式:通过isa指针地址与ISA_MSAK 的值 & 来验证
    当实例对象的isa完成初始化并绑定类之后,会接着执行_class_createInstanceFromZone函数下面的逻辑,在下面的逻辑代码中打下断点如下所示:

    Snip20210210_136.png

  • ISA_MASK isa指针的掩码在x86_64架构平台上其值为:0x00007ffffffffff8ULL,通过isa指针的值与isa指针的掩码做位与运算,最后得到isa所指向的类YYPerson;

  • 第二种方式:通过object_getClass() 函数
    object_getClass() 函数`底层实现就是采用第一种方式,通过isa指针的值与isa指针的掩码做位与运算,最后得到isa所指向的类;

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
objc_object::getIsa() 
{
    if (fastpath(!isTaggedPointer())) 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;
}
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
}
  • 最终的代码执行:(Class)(isa.bits & ISA_MASK)

自定义实现系统的ISA指针类型 -- 位域

第一个版本
  • 代码实现如下:
#import 

@interface YYPerson : NSObject

@property(nonatomic,assign)BOOL tall;
@property(nonatomic,assign)BOOL rich;
@property(nonatomic,assign)BOOL handsome;

@end
#import "YYPerson.h"

#define YYTallMask (1<<0)
#define YYRichMask (1<<1)
#define YYHandsomeMask (1<<2)

@interface YYPerson ()
{
    char _tallRichHandsome;
}
//0000 0001
//倒数第一位  表示tall
//倒数第二位  表示rich
//倒数第三位  表示handsome
@end

@implementation YYPerson

- (instancetype)init{
    self = [super init];
    if (self) {
        _tallRichHandsome = 0b00000011;
    }
    return self;
}

- (void)setTall:(BOOL)tall{
    if (tall) {
        _tallRichHandsome |= YYTallMask;
    }else{
        _tallRichHandsome &= ~YYTallMask;
    }
}

- (BOOL)tall{
    return !!(_tallRichHandsome & YYTallMask);
}

- (void)setRich:(BOOL)rich{
    if (rich) {
        _tallRichHandsome |= YYRichMask;
    }else{
        _tallRichHandsome &= ~YYRichMask;
    }
}

- (BOOL)rich{
    return !!(_tallRichHandsome & YYRichMask);
}

- (void)setHandsome:(BOOL)handsome{
    if (handsome) {
        _tallRichHandsome |= YYHandsomeMask;
    }else{
        _tallRichHandsome &= ~YYHandsomeMask;
    }
}

- (BOOL)handsome{
    return !!(_tallRichHandsome & YYHandsomeMask);
}
@end
  • 测试代码:
#import 
#import "YYPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        YYPerson *person = [[YYPerson alloc]init];
        
        NSLog(@" tall = %d -- rich = %d -- handsome =  %d",person.tall,person.rich,person.handsome);
        
        person.handsome = YES;
        
        NSLog(@" tall = %d -- rich = %d -- handsome =  %d",person.tall,person.rich,person.handsome);
    }
    return 0;
}
  • 调试结果如下:
Snip20210629_19.png
  • 看到确实能实现用一个二进制位表示一个BOOL属性,能达到节约内存的目的;
第二个版本
  • YYPerson.h保持不变
#import "YYPerson.h"

@interface YYPerson ()
{
    //结构体 + 位域
    struct{
        char tall : 1;
        char rich : 1;
        char handsome : 1;
    }_tallRichHandsome;
}
@end

@implementation YYPerson

- (instancetype)init{
    self = [super init];
    if (self) {
        
    }
    return self;
}

- (void)setTall:(BOOL)tall{
    _tallRichHandsome.tall = tall;
}

- (BOOL)tall{
    return !!_tallRichHandsome.tall;
}

- (void)setRich:(BOOL)rich{
    _tallRichHandsome.rich = rich;
}

- (BOOL)rich{
    return !!_tallRichHandsome.rich;
}

- (void)setHandsome:(BOOL)handsome{
    _tallRichHandsome.handsome = handsome;
}

- (BOOL)handsome{
    return !!_tallRichHandsome.handsome;
}
@end
  • 测试代码:
#import 
#import "YYPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        YYPerson *person = [[YYPerson alloc]init];
        person.tall = YES;
        person.rich = NO;
        person.handsome = NO;
        
        NSLog(@" tall = %d -- rich = %d -- handsome =  %d",person.tall,person.rich,person.handsome);
    }
    return 0;
}
  • 调试结果如下:
Snip20210629_20.png
  • 使用结构体 + 位域 的技术 实现一个二进制位表示一个BOOL属性;
第三个版本
#import 

@interface YYPerson : NSObject

- (void)setTall:(BOOL)tall;
- (BOOL)tall;

- (void)setRich:(BOOL)rich;
- (BOOL)rich;

- (void)setHandsome:(BOOL)handsome;
- (BOOL)handsome;

@end
#define YYTallMask (1<<0)
#define YYRichMask (1<<1)
#define YYHandsomeMask (1<<2)

#import "YYPerson.h"

@interface YYPerson ()
{
    //共用体
    union{
        char bits;
        struct{
            char tall : 1;
            char rich : 1;
            char handsome : 1;
        };
    }_tallRichHandsome;
}
//0000 0001
//倒数第一位  表示tall
//倒数第二位  表示rich
//倒数第三位  表示handsome
@end

@implementation YYPerson

- (instancetype)init{
    self = [super init];
    if (self) {
        
    }
    return self;
}

- (void)setTall:(BOOL)tall{
    if (tall) {
        _tallRichHandsome.bits |= YYTallMask;
    }else{
        _tallRichHandsome.bits &= ~YYTallMask;
    }
    
}

- (BOOL)tall{
    return !!(_tallRichHandsome.bits & YYTallMask);
}

- (void)setRich:(BOOL)rich{
    if (rich) {
        _tallRichHandsome.bits |= YYRichMask;
    }else{
        _tallRichHandsome.bits &= ~YYRichMask;
    }
}

- (BOOL)rich{
    return !!(_tallRichHandsome.bits & YYRichMask);
}

- (void)setHandsome:(BOOL)handsome{
    if (handsome) {
        _tallRichHandsome.bits |= YYHandsomeMask;
    }else{
        _tallRichHandsome.bits &= ~YYHandsomeMask;
    }
}

- (BOOL)handsome{
    return !!(_tallRichHandsome.bits & YYHandsomeMask);
}
@end
  • 测试代码如下:
#import 
#import "YYPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        YYPerson *person = [[YYPerson alloc]init];
        person.tall = YES;
        person.rich = NO;
        person.handsome = NO;
        
        NSLog(@" tall = %d -- rich = %d -- handsome =  %d",person.tall,person.rich,person.handsome);
    }
    return 0;
}
  • 调试结果如下:
Snip20210629_21.png
  • 使用 共用体 + 位域 的技术 实现一个二进制位表示一个BOOL属性;
  • 结构体struct已经形同虚设,主要作用在于展示不同位所表示的信息而已,增加可读性;
  • char bits是共用的数据对象,占一个字节8个二进制位,不同的二进制位表示不同的信息数据;

你可能感兴趣的:(iOS底层系列09 -- isa的底层探索)