iOS之深入解析对象isa的底层原理

对象本质

一、NSObject 本质

OC 代码的底层实现实质是 C/C++代码 ,继而编译成汇编代码,最终变成机器语言。

① clang C/C++ 编译器
  • 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关键字
  • Clang 是⼀个 C++ 编写,基于 LLVM 发布于 LLVM BSD 许可证下的 C/C++/Objective-C/Objective-C++ 编译器。它与 GNU C 语⾔规范⼏乎完全兼容(当然也有部分不兼容的内容,包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,⽐如 C 函数重载(通过 attribute((overloadable) )来修饰函数),其⽬标(之⼀)就是超越 GCC 。
  • clang -rewrite-objc main.m -o main.cpp 把⽬标⽂件编译成c++⽂件。
  • UIKit报错问题:Xcode安装的时候顺带安装了 xcrun 命令, xcrun 命令在 clang 的基础上进⾏了⼀些封装。
	clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /
	Applications/Xcode.app/Contents/Developer/Platforms/
	iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk main.m 
  • 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 mainarm64.cpp (⼿机)
② 运用 clang 将目标文件编译成 cpp(C++文件)
  • 在main.m中添加以下代码:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

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

@implementation YDWHandsomeBoy

@end

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

  • 打开终端,进入main.m所在的文件夹,通过 clang rewirte-objc main.m -o main.cpp xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 命令,生成cpp文件。

在这里插入图片描述

  • 然后回到main.m的文件中,打开cpp文件,搜索“YDWHandsomeBoy”,即可看到,对象YDWHandsomeBoy在底层被编译成了一个结构体:
	#ifndef _REWRITER_typedef_YDWHandsomeBoy
	#define _REWRITER_typedef_YDWHandsomeBoy
	typedef struct objc_object YDWHandsomeBoy;
	typedef struct {} _objc_exc_YDWHandsomeBoy;
	#endif
	
	extern "C" unsigned long OBJC_IVAR_$_YDWHandsomeBoy$_name;
	struct YDWHandsomeBoy_IMPL {
		struct NSObject_IMPL NSObject_IVARS;
		NSString *_name;
	};
	
	// @property (nonatomic, copy) NSString *name;
	/* @end */
	
	
	// @implementation YDWHandsomeBoy
  • 可以看到上面的c++代码中有个NSObject_IMPL NSObject_IVARS这个东西,然后搜索NSObject_IMPL可以发现:
	struct NSObject_IMPL {
		Class isa;
	};
  • 由此可以得出:
    • NSObject的底层实现实质是一个 结构体 ,而结构体中的成员 isa是Class类型
    • 通过源码 typedef struct objc_class *Class 可知它是一个指针,在 64 位环境下指针占 8 个字节,而在 32 位环境下占 4 个字节,因此该结构体占 8 个字节(因为该结构体只有一个成员)。
二、NSObject 对象内存
  • 初始化一个NSObject并打印:
	NSObject *obj = [[NSObject alloc] init];
	NSLog(@"%zd",malloc_size((__bridge const void *)obj));
  • 可知NSObject对象占16个字节。那么与上文中NSObject结构体中占8个字节是否冲突?再次打印:
	NSLog(@"%zd",class_getInstanceSize([NSObject class]))
  • 不难发现:获取NSObject类的实例对象的成员变量所占用的(内存对齐之后)大小,显示确实为8个字节。
  • 在objc的源码中找到 class_getInstanceSize方法,发现它返回的是 cls->alignedInstanceSize() ,对它的描述为Class’s ivar size rounded up to a pointer-size boundary意指 返回成员变量占据的大小 。因此创建一个NSObject对象需要分配16个字节,只是真正利用的只有8个字节,即isa这个成员的大小。
  • 查看allocWithZone的源码发现它最底层的创建实例的方法实际上是调用了C语言的 calloc方法 ,在该方法中,规定若 分配的字节不满16将把它分配为16个字节
三、若一个YDWHandsomeBoy类继承自NSObject类,那么YDWHandsomeBoy类的对象占多少内存?
  • 新建YDWHandsomeBoy类,添加成员变量,在main中实现以下代码:
	#import <Foundation/Foundation.h>
	#import <objc/runtime.h>
	#import <malloc/malloc.h>
	
	@interface YDWHandsomeBoy : NSObject
	
	@property (nonatomic, copy) NSString *name;
	@property (nonatomic, copy) NSString *nickName;
	@property (nonatomic, assign) int age;
	@property (nonatomic, assign) int address;
	@property (nonatomic, assign) int number;
	
	@end
	
	@implementation YDWHandsomeBoy
	
	@end
	
	int main(int argc, const char * argv[]) {
	    @autoreleasepool {
	        // insert code here...
	        YDWHandsomeBoy *boy = [[YDWHandsomeBoy alloc] init];
	        boy.name = @"Y";
	        boy.nickName = @"D";
	        boy.age = 18;
	        boy.address = 10;
	        boy.number = 11;
	        NSLog(@"%zd",malloc_size((__bridge const void *)boy));
	    }
	    return 0;
	}
	 打印结果如下:
	 2020-09-02 15:44:27.345192+0800 iOS之对象isa[53982:3621024] 48
  • 通过以上代码可以看出:
  • 若一个类继承自另一个类,则它的底层会 将父类的成员变量放在结构体的最前面,此后依次放置本类的成员变量
  • 而从之前的分析可知,NSObject_IMPL的本质就是一个 装有成员变量isa的结构体 ,因此,YDWHandsomeBoy类对象所占的内存为isa的内存8加上YDWHandsomeBoy类成员变量所占的空间,若不满16个字节,会强制分配到16个字节。
  • 由于 内存对齐 的规定,结构体的最终大小必须是 最大成员的倍数

isa

一、isa 简介
  • alloc初始化时不仅 创建对象并且分配内存 ,同时 初始化 isa 指针属性
  • Objective-C 对象在底层本质上是 结构体 ,所有的对象里面都会包含有一个 isa ,isa 的定义是一个 联合体 isa_t ,isa_t 包含了 当前对象指向类的信息
	union isa_t {
	    isa_t() { }
	    isa_t(uintptr_t value) : bits(value) { }
	
	    Class cls;
	    uintptr_t bits;
	#if defined(ISA_BITFIELD)
	    struct {
	        ISA_BITFIELD;  // defined in isa.h
	    };
	#endif
	};
  • isa 是一个 联合体,而这其实是从内存管理层面来设计的,因为联合体是 所有成员共享一个内存,联合体 内存的大小取决于内部成员内存大小最大的那个元素
  • 对于 isa 指针来说,就不用额外声明很多的属性,直接在 内部的 ISA_BITFIELD 保存信息
  • 由于联合体 属性间互斥 ,所以 cls 和 bits 在 isa 初始化流程时是在 两个分支 中被赋值的。
	union isa_t {
	    isa_t() { }
	    isa_t(uintptr_t value) : bits(value) { }
	
	    Class cls;
	    uintptr_t bits;
	#if defined(ISA_BITFIELD)
	    struct {
	        ISA_BITFIELD;  // defined in isa.h
	    };
	#endif
	};
	
	#   define ISA_MASK        0x00007ffffffffff8ULL
	#   define ISA_MAGIC_MASK  0x001f800000000001ULL
	#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
	#   define ISA_BITFIELD                                                        \
	      uintptr_t nonpointer        : 1;                                         \
	      uintptr_t has_assoc         : 1;                                         \
	      uintptr_t has_cxx_dtor      : 1;                                         \
	      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
	      uintptr_t magic             : 6;                                         \
	      uintptr_t weakly_referenced : 1;                                         \
	      uintptr_t deallocating      : 1;                                         \
	      uintptr_t has_sidetable_rc  : 1;                                         \
	      uintptr_t extra_rc          : 8
	#   define RC_ONE   (1ULL<<56)
	#   define RC_HALF  (1ULL<<7)
  • isa_t 是一个联合体,联合体的特性就是 内部所有的成员共用一块内存地址空间 ,也就是说isa_t、cls、bits会共用同一块内存地址空间,这块 内存地址空间大小取决于最大长度内部成员的大小 ,即64位8字节,由此可以知道isa 的所占的内存空间大小为8字节。isa_t联合体如下:
	struct {
	   uintptr_t indexed           : 1;
	   uintptr_t has_assoc         : 1;
	   uintptr_t has_cxx_dtor      : 1;
	   uintptr_t shiftcls          : 44;
	   uintptr_t magic             : 6;
	   uintptr_t weakly_referenced : 1;
	   uintptr_t deallocating      : 1;
	   uintptr_t has_sidetable_rc  : 1;
	   uintptr_t extra_rc          : 8;
	};
二、isa 结构

isa 作为一个联合体,有一个结构体属性为 ISA_BITFIELD ,其大小为 8 个字节,也就是 64 位。基于__arm64__ 和 x86 64 架构如下:

	# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   define ISA_BITFIELD                                                      \
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t deallocating      : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

# else
#   error unknown architecture for packed isa
# endif
  • union-isa_t 存储分配如下:

在这里插入图片描述

  • nonpointer: 表示是否对 isa 指针 开启指针优化
    0: 纯 isa 指针;
    1: 不止是类对象地址, isa 中包含了类信息、对象的引用计数等。
  • has_assoc: 关联对象标志位 ,0 没有,1 存在。
  • has_cxx_dtor: 该对象 是否有 C++ 或者 Objc 的析构器 ,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象。
  • shiftcls: 存储类指针的值 ,开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
  • magic: 用于 调试器判断当前对象是真的对象还是没有初始化的空间
  • weakly_referenced: 标志对象是否被指向或者曾经 指向一个 ARC 的弱变量 ,没有弱引用的对象可以更快释放。
  • deallocating: 标志对象 是否正在释放内存
  • has_sidetable_rc: 当对象引用技术大于 10 时,则需要借用该变量 存储进位
  • extra_rc: 当表示该 对象的引用计数值 ,实际上是引用计数值减 1。 例如,如果对象的引用计数为 10,那么 extra_rc 为 9;如果引用计数大于 10, 则需要使用到has_sidetable_rc。
三、isa 初始化
① isa 源码实现
  • 在objc的源码中有isa的初始化方法:
	inline void 
	objc_object::initInstanceIsa(Class cls, bool hasCxxDtor) {
	    assert(!cls->instancesRequireRawIsa());
	    assert(hasCxxDtor == cls->hasCxxDtor());
	
	    initIsa(cls, true, hasCxxDtor);
	}
	
	inline void 
	objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) { 
	    assert(!isTaggedPointer()); 
	    
	    if (!nonpointer) {
	        isa.cls = cls;
	    } else {
	        assert(!DisableNonpointerIsa);
	        assert(!cls->instancesRequireRawIsa());
	
	        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;
	    }
	}
  • 由于nonpointer传入的是true,SUPPORT_INDEXED_ISA定义为0,所以可以对这段代码简化一下:
	inline void 
	objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) { 
	   isa_t newisa(0);
	   newisa.bits = ISA_MAGIC_VALUE;
	   newisa.has_cxx_dtor = hasCxxDtor;
	   newisa.shiftcls = (uintptr_t)cls >> 3;
	   isa = newisa;
	}
② isa 初始化数据
  • 可以看到对bits的赋值ISA_MAGIC_VALUE = 0x001d800000000001ULL,将此转为二进制,在结合isa_t的结构得出如下的isa_t的初始数据图:

在这里插入图片描述

  • 对 isa 赋值ISA_MAGIC_VALUE初始化实际上只是设置了indexed和magic两部分的数据:
    • indexed表示 isa_t 的类型 :0表示 raw isa,也就是没有结构体的部分,访问对象的 isa 会直接返回一个指向 cls 的指针,也就是在 iPhone 迁移到 64 位系统之前时 isa 的类型;1则表示当前 isa 不是指针,但是其中也有 cls 的信息,只是其中关于类的指针都是保存在 shiftcls 中。
    • magic 用于 调试器判断当前对象是否有初始化空间
  • 在设置indexed和magic的值后会对has_cxx_dtor进行设值。has_cxx_dtor表示该对象是否有 C++ 或者 Objc 的析构器 ,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象。
	newisa.has_cxx_dtor = hasCxxDtor;
  • 将当前对象的类指针存放在 shiftcls 中:
	newisa.shiftcls = (uintptr_t)cls >> 3;
  • 对cls的地址右移动3位的目的是 为了减少内存的消耗 ,因为类的指针需要按照8字节对齐,也就是说类的指针的大小必定是8的倍数,其二进制后三位为0 ,右移三位抹除后面的3位0并不会产生影响。
③ isa的初始化流程示意

在这里插入图片描述

四、isa 关联对象和类

isa 是对象中的第一个属性,这是在继承的时候发生的,要早于对象的成员变量,属性列表,方法列表以及所遵循的协议列表。在 alloc 底层,有一个方法叫做 initIsa ,这个方法的作用就是 初始化 isa 联合体位域 。上文中我们已经看到了这个方法:

	newisa.shiftcls = (uintptr_t)cls >> 3;
① cls 存储到 isa
  • isa 刚初始化时,还没有被赋值,bits 全为空值,p newisa 如下:

在这里插入图片描述

  • 继续向下执行,当断点执行到如下位置的时候,bits 会被赋上默认值(nonpointer = 1,magic = 59),继续p newisa如下:

在这里插入图片描述

  • 为什么 magic = 59 呢?其实,通过计算器可以转换算出:0x001d800000000001 = 59,上面我们已经 po 0x001d800000000001ULL 了,可以看到这个默认值。
  • 继续输出 bits 二进制、输出 cls 指针二进制可以看到:在未设置 shiftcls 时,bits 从右到左 [3, 46] 位都是0。如下:
	(lldb) p/t 8303511812964353
	(long) $3 = 0b0000000000011101100000000000000000000000000000000000000000000001
	(lldb) p/t (uintptr_t)cls
	(uintptr_t) $4 = 0b0000000000000000000000000000000100000000010001110100000000111000
	(lldb) p/t (uintptr_t)cls >> 3
	(uintptr_t) $5 = 0b0000000000000000000000000000000000100000000010001110100000000111
	(lldb) 
  • 为什么要右移三位?在 Objective-C 中,类的指针是按照字节(8 bits)对齐的,也就是说类指针地址转化成十进制后,都是8的倍数,也就是说,类指针地址转化成二进制后,后三位都是0。既然是没有意义的0,那么在存储时就可以省略,用节省下来的空间存储一些其他信息。
  • 当 bits 被赋值之后,如下:

在这里插入图片描述

  • 可以看到,现在的 bits 的 [3, 46] 位正好是之前 cls 指针右移三位的内容。
② isa 关联对象和类
  • 通过 LLDB 进行调试打印,就可以知道一个对象的 isa 会关联到这个对象所属的类:

在这里插入图片描述

  • LLDB 调试的时候左移右移操作其实很好理解,先观察 isa 的 ISA_BITFIELD 位域的结构:ISA_BITFIELD 的前 3 位是 nonpointer、has_assoc、has_cxx_dtor ,中间 44 位是 shiftcls ,后面 17 位是剩余的内容,同时因为 iOS 是 小端模式 ,那么就需要去掉右边的 3 位和左边的 17位,所以就会采用 >> 3 << 3 然后 << 13 >> 13 的操作。
五、isa 走位分析
① class object(类对象)/ metaclass(元类)
  • Object-C的对象其本质就是结构体,前面也分析了每一个对象都会有一个isa。同时类的本质也是一个结构体,而且是继承自objc_object的。
	struct objc_object {
	private:
	    isa_t isa;
	...
	};
	
	struct objc_class : objc_object {
	    // Class ISA;
	    Class superclass;
	    // 方法缓存
	    cache_t cache;             // formerly cache pointer and vtable
	    // 用于获取具体的类信息
	    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
	...
	};
  • 在objc_class中也有isa:
	struct objc_class : objc_object {
	    isa_t isa;
	    Class superclass;
	    cache_t cache;             // formerly cache pointer and vtable
	    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
	...
	};
  • class_isMetaClass用于判断Class对象是否为元类,object_getClass用于获取对象的isa指针指向的对象。
OBJC_EXPORT BOOL class_isMetaClass(Class cls) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

OBJC_EXPORT Class object_getClass(id obj) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
  • 我们知道:对象可以创建多个,但是类是否可以创建多个呢?其实答案是否定的,类在内存中只会存在一份。
 	Class class1 = [Boy class];
    Class class2 = [Boy alloc].class;
    Class class3 = object_getClass([Boy alloc]);
    Class class4 = [Boy alloc].class;
    NSLog(@"\n%p-\n%p-\n%p-\n%p",class1, class2, class3, class4);

 	// 打印如下:
 	0x10edbedc8
	0x10edbedc8
	0x10edbedc8
	0x10edbedc8
  • 通过 LLDB 调试打印,其实可以发现:类的内存结构里面的第一个结构打印出来还是 Boy,那么是不是就意味着 对象 ->类->类 这样的死循环呢?这里的第二个类其实是 元类,是由系统创建的,这个元类无法被我们实例化。

在这里插入图片描述

  • 一个实例对象通过class方法获取的Class就是它的isa指针指向的类对象,而类对象不是元类,类对象的isa指针指向的对象是元类。关系如下:
    在这里插入图片描述
② isa 走位
  • 官方的经典 isa 走位图:
    • 实例对象的isa指向的是类;
    • 类的isa指向的元类;
    • 元类指向根元类;
    • 根元类指向自己;
    • NSObject的父类是nil,根元类的父类是NSObject。

在这里插入图片描述

  • LLDB 调试打印:

在这里插入图片描述

六、对象的本质 isa
  • OC 对象的本质就是一个结构体,在 libObjc 源码的 objc-private.h 源文件中可以看到:
	struct objc_object {
	private:
	    isa_t isa;
	
	public:
	
	    // ISA() assumes this is NOT a tagged pointer object
	    Class ISA();
	
	    // getIsa() allows this to be a tagged pointer object
	    Class getIsa();
	
	    ......
	}
  • 对于对象所属的类来说,也可以在 objc-runtime-new.h 源文件中找到(即 objc_class 内存中第一个位置是 isa,第二个位置是 superclass):
	struct objc_class : objc_object {
	    // Class ISA;
	    Class superclass;
	    cache_t cache;             // formerly cache pointer and vtable
	    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
	    
	    ......
	}
  • 对象在底层其实是一个结构体 objc_object ,而Class 在底层也是一个结构体 objc_class 。
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

╰つ栺尖篴夢ゞ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值