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

前言

对象我们几乎每天都在说的词,不管是生活中,还是工作中。生活中你如果没有对象,那么兄弟你得加油了,实在不行我给你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
@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;
	NSString *_LWname;
	NSInteger _age;
};

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

// @implementation LWPerson
复制代码

总结:LWPerson底层是结构体LWPerson_IMPL结构体中有3个变量,其中_LWname _age是自定义的属性,NSObject_IVARS就是 NSObjectisaLWPerson 继承于NSObject,意味着LWPerson也有NSObject所有的成员变量。

对象本质总结

  • 对象的本质是结构体
  • 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);

    if (!nonpointer) {
        newisa.setClass(cls, this);
    } else { 
       ...
       newisa.bits = ISA_MAGIC_VALUE;
       ...
    }

    // 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;
}
复制代码

我们发现了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原因,isa指针的变量是按位域存储的,shiftcls从第3位开始存储,为了不影响前3位的数据 ,只有把newCls前3位右移抹零

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位就是shiftcls1ISA_MASK 就像一个面具把露出来的显示,其它的全部抹掉。

initnew探究

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

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享