IOS 底层原理之对象的本质&isa关联类

1,361 阅读12分钟

前言

对象我们几乎每天都在说的词,不管是生活中,还是工作中。生活中你如果没有对象,那么兄弟你得加油了,实在不行我给你new一个。在这愉快的玩笑中我们走入工作中的对象,说到对象不得不提到isa,因为isa告诉我们这个对象是属于谁的。下面就探究下对象的本质以及isa

准备工作

联合体(union)

联合体和结构体类型也是由不同类型的数据组成,下面通过代码探究下联合体

union LWPerson {
    int a;      //4
    short b;    //2
    char c;     //1
};

int main(int argc, char * argv[]) {
  
    @autoreleasepool {
        union LWPerson    person;
        person.a = 8;
        NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
        person.b = 2;
        NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
        person.c = 'd';
        NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
        
        NSLog(@"%lu---%lu",sizeof(person),sizeof(union LWPerson));
        
    }
    return 0;
}
2021-06-10 15:50:45.789591+0800 isa探究[78582:10551210] a=8---b=8---c=
2021-06-10 15:50:45.790087+0800 isa探究[78582:10551210] a=2---b=2---c=
2021-06-10 15:50:45.790107+0800 isa探究[78582:10551210] a=100---b=100---c=d
2021-06-10 15:50:45.790125+0800 isa探究[78582:10551210] 4---4

总结:

  • 联合体可以定义多个不同类型的成员,联合体的内存大小由其中最大的成员的大小决定。
  • 联合体中修改其中的某个变量会覆盖其他变量的值。
  • 联合体所有的变量公用一块内存,变量之间互斥

联合体优缺点

  • 优点:内存使用更为灵活,节省内存。
  • 缺点:不够包容。

位域(Bit field)

有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个只有01两种状态成员, 用一位二进位即可,目的是节省存储空间,处理简便。下面通过代码探究位域

struct OldCar {
    BOOL front;
    BOOL back;
    BOOL left;
    BOOL right;
};

struct NewCar {
    BOOL front: 1;
    BOOL back : 1;
    BOOL left : 1;
    BOOL right: 1;
};

int main(int argc, char * argv[]) {
  
    @autoreleasepool {
        
    struct OldCar oldCar;
    struct NewCar newCar;
    NSLog(@"----%lu----%lu",sizeof(oldCar),sizeof(newCar));
        
    }
    return 0;
}
2021-06-10 16:25:04.266985+0800 isa探究[78608:10560097] ----4----1

总结:oldCar的内存大小是4字节newCar的内存大小是1字节1字节包含8位(bit)newCar中所有变量都是按 位(bit)存在这1字节中,具体的存放格式是从右往左往 0000 1111 依次存放frontbackleftright 没有45°仰望天空哈。

对象的本质

探究对象本质之前,先了解编辑器Clang

Clang

  • Clang 是一个C语言C++Objective-C语言的轻量级编译器,是由Apple主导编写的
  • Clang 主要用于把源文件编译成底层文件,比如把main.m 文件编译成main.cppmain.o或者可执行文件。便于观察底层的逻辑结构,便于我们探究底层。

Clang 终端编译命令

clang -rewrite-objc main.m -o main.cpp 
//UIKit报错
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
// xcrun命令基于clang基础上进行了封装更好用
//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 

对象的本质

下面探究对象的本质,通过实例进行探究代码如下

@interface LWPerson : NSObject
{
    NSString * height;
}
@property(nonatomic,  copy)NSString   *LWname;
@property(nonatomic,assign)NSInteger  age;
@end

@implementation LWPerson

@end

int main(int argc, char * argv[]) {
  
    @autoreleasepool {
    }
    return 0;
}

通过clang命令把 main.m 文件编译成 main.cpp部分代码如下

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

struct NSObject_IMPL {
	Class isa;
};

extern "C" unsigned long OBJC_IVAR_$_LWPerson$_LWname;
extern "C" unsigned long OBJC_IVAR_$_LWPerson$_age;
struct LWPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	double  height;
	NSString *_LWname;
	NSInteger _age;
};

// @property(nonatomic, copy)NSString *LWname;
// @property(nonatomic,assign)NSInteger age;
/* @end */

// @implementation LWPerson

源码分析:LWPerson底层是结构体LWPerson_IMPL结构体中有4个变量,其中height_LWname_age是自定义的属性,NSObject_IVARS就是 NSObjectisaLWPerson 继承于NSObject,意味着LWPerson也有NSObject所有的成员变量。

NSObject的底层是NSObject_IMPL里面只有一个成员变量是Class isa。通常叫isa叫做isa指针,那么这里的Class应该是个指针类型,在main.cpp文件中全局搜索*Class。代码如下

typedef struct objc_class *Class;

//对象的底层实现
struct objc_object {
    Class _Nonnull isa __attribute__((deprecated));
};

//NSObject的底层实现
struct NSObject_IMPL {
	Class isa;
};

源码分析:

  • Class类型实际是一个 objc_class类型的结构体指针,objc_class是所有类的底层实现。由此我们猜测isa可能跟类信息存在着重要的关联,具体的关联下面会进行探究。
  • NSObject的底层实现和对象的底层实现有什么区别。两者成员变量的结构体都是Class isa,那么就必然存在继承关系。所有对象的底层都是继承objc_object,在OC中基本上所有的对象都是继承NSObject,但是真正的底层实现是objc_object的结构体类型。

对象的本质拓展

main.cpp文件中全局搜索*Class时,发现有这样几行代码如下

typedef struct objc_object *id;
typedef struct objc_selector *SEL;

源码分析:熟悉的idSEL。常用的id原来是一个objc_object结构体指针,这就解释了id修饰变量和作为返回值的时候为什么不加*,意料之外的是SEL也是结构体指针。

main.cpp中,发现了方法的底层实现代码如下

image.png

图中显示:

  • 属性的getset方法中都有获取当前变量的位置代码,怎么去获取当前的变量呢?底层实现就是当前对象的首地址+变量的偏移值
  • 代码中我自定义了一个局部变量height,但是底层代码仅添加了一个变量。而定义的属性,底层自动添加了带_变量以及getset方法的实现。哎呀,这不是我们常见的面试题嘛。.cpp文件还是很不错的啊。

对象本质总结

  • 对象的本质是结构体
  • LWPersonisa是继承NSObject中的isa
  • NSObject中只有一个成员变量那就是isa

isa关联类

对象本质探究让我们知道对象的第一个变量就是isa,在探究 IOS 底层原理之 alloc 探究 中我们发现alloc一个对象最核心的三个方法cls->instanceSize 计算内存大小 , (id)calloc(1, size)开辟内存返回地址指针, obj->initInstanceIsa初始化isa关联类。既然这么多地方都指向isa,那么就来探究下isa的结构以及isa是如何关联类的。

isa 结构

通过alloc流程 alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone,断点在 obj->initInstanceIsa,进入obj->initInstanceIsa

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

断点进入initIsa

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); 
    
    isa_t newisa(0); // isa初始化

    if (!nonpointer) {
        newisa.setClass(cls, this);//如果是纯指针 isa被直接cls赋值
    } else { 
       ASSERT(!DisableNonpointerIsa);
       ASSERT(!cls->instancesRequireRawIsa());
#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else   
        newisa.bits = ISA_MAGIC_VALUE;
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
#endif
        newisa.extra_rc = 1;
    }
    isa = newisa;
}

我们发现了isa的结构类型是isa_t,进入isa_t

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

    uintptr_t bits;

private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};

源码分析:isa_t联合体isa_t有两个变量 一个是bits,一个是cls。通过上面分析联合体互斥的,那就意味着初始化isa有两种方式:

  • bits被赋值,cls 没有值或者被覆盖
  • cls 被赋值,bits没有值或者被覆盖

isa_t中还有一个结构体成员变量ISA_BITFIELD。这是一个对应有两个端,一个是__arm64__(iOS),一个是__x86_64__(macOS)。ISA_BITFIELD通过位域存储信息,那就具体看下有哪些信息

image.png

图中的省略了iOS的模拟器数据只留下了真机macOS

bits64位存储分布图

image.png

各变量的含义:

  • nonpointer:表示是否对isa指针进行优化,0表示纯指针,1表示不止是类对象的地址,isa中包含了类信息、对象、引用计数等
  • has_assoc:关联对象标志位,0表示未关联,1表示关联
  • has_cxx_dtor:该对象是否C ++ 或者Objc的析构器,如果有析构函数,则需要做析构逻辑,没有,则释放对象
  • shiftcls:储存类指针的值,开启指针优化的情况下,在arm64架构中有33位用来存储类指针,x86_64架构中占44
  • magic:用于调试器判断当前对象是真的对象还是没有初始化的空间
  • weakly_referenced:指对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放
  • deallocating:标志对象是否正在释放
  • has_sidetable_rc:当对象引用计数大于10时,则需要借用该变量存储进位
  • hextra_rc:表示该对象的引用计数值,实际上引用计数值减1,例如,如果对象的引用计数为10,那么extra_rc9,如果大于10,就需要用到上面的has_sidetable_rc

isa结构分析总结

  • isa分为nonpointer类型和非nonpointer。非nonpointer类型只是一个纯指针,nonpointer还包含了类的信息
  • isa联合体+位域的方式存储信息的。采用这种方式的有点就是节省大量内存。万物皆对象,只要是对象就有isa指针,大量的isa就占用了很多内存,联合体公用一块内存节省了部分内存,而位域更是在节省内存的基础上存储了信息,可以说isa指针的内存得到了充分的利用。

isa关联类探究

首先定义一个LWPerson类,初始化[LWPerson alloc],流程如下:alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone--> obj->initInstanceIsa -->initIsa,现在探究isa到底怎么和类关联起来

image.png

图中显示 newisa(0)bits进行赋值,bits里面所有的变量都是0

image.png

图中显示 newisa.bits = ISA_MAGIC_VALUEISA_MAGIC_VALUE 是一个宏 ISA_MAGIC_VALUE = 0x001d800000000001,变量值改变的有bits = 8303511812964353cls = 0x001d800000000001nonpointer = 1margic = 59。我们恢复下赋值的过程。如下图

image.png

  • 0x001d800000000001 转换成10进制 等于8303511812964353
  • cls = 0x001d800000000001是因为给bits赋值的时候覆盖了clsisa_t联合体
  • 0位的1等于 nonpointer = 1
  • margic的值怎么算呢,按位计算从47位开始,长度是6位 结果就是111011,从第0个位置开始算11101110进制就是59.(对比图中的第一张计算图和 第三张计算图)

断点进入setClass

image.png

图中显示shiftcls = 536873007,下面来验证下结果是否正确

image.png

图中显示

  • shiftcls = 536873007 与上面的算法shiftcls=(uintptr_t)newCls >> 3 得到的结果是一样的。
  • LWPerson的类地址>>3进行10进制转换赋值给shiftcls。此时isa已经关联LWPerson类 ,cls变量被覆盖 cls = LWPerson

问题:(uintptr_t)newCls >> 3 为什么需要右移3

  • MACH_VM_MAX_ADDRESS表示虚拟内存最大寻址空间,在__arm64__MACH_VM_MAX_ADDRESS = 0x1000000000 虚拟内存最大寻址空间是36位。在__x86_64__MACH_VM_MAX_ADDRESS = 0x7fffffe00000 虚拟内存最大寻址空间是47位。
  • 字节对齐是8字节对齐,也就是说指针的地址只能是8的倍数,那么指针地址的后3位只能是0,比如0x80x180x30转换成二进制后3位都是0

image.png

基于以上两条,为了节省内存空间,把后3位0抹去。在__arm64__shiftcls33位,在__x86_64__shiftcls44位。因此需要将类地址右移3位,即(uintptr_t)newCls >> 3。可以说isa的优化算是做到了极致。

isa关联类总结

clsisa 关联类的原理就是isa指针中的shiftcls存储了类信息。

总结

通过对isa对象的本质的探究,认识到对层次探究的必要性和重要性,虽然探究的过程是复杂的繁琐的,但是结果却令人兴奋,这就是底层探究的魅力。

补充

isa关联类的几种方式

  • shiftcls =(uintptr_t)newCls >> 3
  • isa位运算
  • isa&ISA_MASK

shiftcls =(uintptr_t)newCls >> 3 上面已经验证过了

isa位运算

类信息是存储在isa指针中,shiftclsisa是按存储,macOS从第3位开始存储,大小是44位。位运算是目的是只保留shiftcls信息,其它位的信息抹零位运算过程shiftcls的相对位置要保持不变。如下图

image.png

  • isa的值是0x011d8001000041a10x011d8001000041a1 >> 3 结果等于 0x0023b00020000834
  • 0x0023b00020000834 << 20 结果等于 0x0002000083400000
  • 0x0002000083400000 >> 17 结果等于 0x00000001000041a0
  • po 0x00000001000041a0的结果是LWPerson,说明isa已经关联了类

isa位运算过程图

image.png

isa&ISA_MASK

ISA_MASK是一个__x86_64__ 的值等于 0x00007ffffffffff8ULL__arm64__ 的值等于0x0000000ffffffff8ULLisa的值是0x011d8001000041a1,验证下0x011d8001000041a1&0x00007ffffffffff8ULL结果。如下图

image.png

图中显示 po 0x011d8001000041a1 & 0x00007ffffffffff8ULL的结果是LWPerson。说明isa已经关联了类。二进制方式打印ISA_MASK p/t 0x00007ffffffffff8ULL,结果显示高17位0低3位0,中间的44位1,用来显示isa中的shiftclsISA_MASK 就像一个面具把露出来的显示,其它的全部抹掉。

initnew探究

initnew的探究已经更新到 IOS 底层原理之 alloc 探究 博客中