OC底层原理-objc 818(二)isa&superclass

309 阅读8分钟

struct objc_classobjc_class与objc_object

在学习isa之前我么先研究一下objc_class与objc_object之间的关系。

objc_object

我们先看objc_object结构体

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
typedef struct objc_class *Class;

我们看到objc_object结构体中只有一个Class类型的isa,我们在看Class的定义是一个objc_class的结构体指针。

objc_class

我们在来看objc_class的结构体

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

这里我们需要注意这个结构体已经被标注OBJC2_UNAVAILABLE目前是一个已经废弃了的结构体。

其实在当前的objc项目中在用的objc_class的结构体是我们下边的这个结构体

struct objc_class : objc_object {
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;  
}

在C++中结构体是可以继承的,我们可以看到objc_class继承自objc_object结构体。

由于objc_object中只有一个isa指针,那么objc_class的结构体简化后如下:

struct objc_class : objc_object {
    // Class ISA;
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;  
}

Class _Nonnull isa:当前类对象的isa指针

Class superclass:父类

cache_t cache:类的缓存信息(后续会讲到)

class_data_bits_t bits:类的重要信息都存储在里面class_data_rw_t和class_data_ro_t中

class_data_rw_t:主要存储类的属性、实例方法、协议信息

class_data_ro_t:主要存储成员变量信息

综述:对象会被定义为objc_object结构体,类会被定义为objc_class对象,而objc_class继承自objc_objec所以类也是一个特殊的对象。

isa到底是什么

我们知道OC对象的本质其实就是结构体,OC对象对应的结构体就是struct objc_object,我们可以通过clang来讲我们的OC代码转换成C++代码,来求证一下。

Clang

  • Clang是一个C语言、C++、Objective-C语言的轻量级编译器。源代码发布于BSD协议下。 Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。
  • Clang是一个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器
  • 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。
  • 使用XCode自带 clang 可以将OC代码编译成C++代码
  • xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m(源文件路径) -o main-arm64.cpp(目标文件路径)
  • Xcrun: Xcode Run
  • -sdk iphoneos: iOS系统
  • clang:XCode自带编译环境
  • -arch arm64:编译对应架构的成果物
  • -rewrite-objc:将文件便以为目标C++文件
  • -fobjc-arc:内存管理方式
  • -fobjc-runtime=ios-8.0.0:runtime版本 当需要用到runtime时需要使用该参数

Clang转换

@interface YJCar : NSObject

@property (copy, nonatomic) NSString *name;

- (void) sayNB;

@end

@implementation YJCar

- (void) sayNB
{
    
}

@end

转换为C++的代码如下:

#ifndef _REWRITER_typedef_YJCar
#define _REWRITER_typedef_YJCar
typedef struct objc_object YJCar;
typedef struct {} _objc_exc_YJCar;
#endif

extern "C" unsigned long OBJC_IVAR_$_YJCar$_name;
struct YJCar_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *__strong _name;
};


// @property (copy, nonatomic) NSString *name;

// - (void) sayNB;

/* @end */


// @implementation YJCar


static void _I_YJCar_sayNB(YJCar * self, SEL _cmd) {

}


static NSString * _I_YJCar_name(YJCar * self, SEL _cmd) { return (*(NSString *__strong *)((char *)self + OBJC_IVAR_$_YJCar$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_YJCar_setName_(YJCar * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct YJCar, _name), (id)name, 0, 1); }

我们可以看到YJCar被转换为了typedef struct objc_object YJCar,objc_object结构体。

我们在往下看struct NSObject_IMPL NSObject_IVARS,其实是isa指针

struct NSObject_IMPL {
	__unsafe_unretained Class isa;
};

联合体和位域

在探索ISA之前我们需要先了解一下联合体与联合体位域知识

联合体与结构体

联合体与结构体都是基础的构造数据类型

结构体(struct)

结构体: 把不同的数据整合成一个整体, 其变量是共存的, 变量是否使用都会为其开辟内存空间。

  • 优点: 储存容量大, 包容性强, 并且成员与成员之间不会互相影响
  • 缺点: 因为结构体里面成员都会给分配内存(不管用没用到), 没有用到的会造成内存浪费
联合体(union)

联合体: 也叫共用体, 把不同的数据整合成一个整体,其变量是互斥的, 所有成员同一片内存。联合体采用内存覆盖技术, 同一时刻只能保存一个成员值, 即:

  • 优点: 所有成员共用一段内存节省内存空间
  • 缺点: 包容性弱, 成员互斥, 同一时刻只能保存一个成员的值
两者的区别
  • 内存占用
    • 结构体: 各个成员占用不同内存, 相互没有影响
    • 联合体: 各个内存占用同一内存, 修改一个成员影响其他所有成员
  • 内存分配
    • 结构体: 开辟的内存大于等于所有成员总和(对齐可能导致有空余)
    • 联合体: 内存占用为最大成员占用的内存

ISA来了

  • 在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址
  • 在arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息
  • 需要将isa的内容&ISA_MASK位运算后得到具体的Class、Meta-Class的内存地址
    • 共用体类似结构体,共用体内的元素公用同一块内存空间
    • 共用体的内存大小至少为公用体内最大元素的内存空间大小
    • 共用体内可通过位域的方式为不同的元素分配制定的内存空间大小
    • 共用体内内存的排放为从后向前排
    • isa共用体结构
      • nonpointer:
        • 0:代表普通的指针,存储着Class、Meta-Class对象的内存地址
        • 1:代表优化过,使用位域存储更多的信息
      • has_assoc:
        • 是否有设置过关联对象,如果没有,释放时会更快
      • has_cxx_dtor:

是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快

  • shiftcls:
    • 存储着Class、Meta-Class对象的内存地址信息
  • magic:
    • 用于在调试时分辨对象是否未完成初始化
  • weakly_referenced:
    • 是否有被指向或者曾经指向过ARC的若变量,如果没有,释放时会更快
  • deallocating:
    • 对象是否正在释放内存
  • extra_rc:
    • 里边存储的值时引用计数器减1
  • has_sidetable_rc:
    • 引用计数器是否过大无法存储在isa中
    • 如果为1,那么引用计数器会存储在一个叫SideTable的类的属性中

Shiftcls在ARM和X86平台是有一定区别的

# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     define ISA_MASK        0x007ffffffffffff8ULL
#     define ISA_MAGIC_MASK  0x0000000000000001ULL
#     define ISA_MAGIC_VALUE 0x0000000000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 0
#     define ISA_BITFIELD                                                      \
        uintptr_t nonpointer        : 1;                                       \
        uintptr_t has_assoc         : 1;                                       \
        uintptr_t weakly_referenced : 1;                                       \
        uintptr_t shiftcls_and_sig  : 52;                                      \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 8
#     define RC_ONE   (1ULL<<56)
#     define RC_HALF  (1ULL<<7)
#   else
#     define ISA_MASK        0x0000000ffffffff8ULL
#     define ISA_MAGIC_MASK  0x000003f000000001ULL
#     define ISA_MAGIC_VALUE 0x000001a000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 1
#     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 unused            : 1;                                       \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 19
#     define RC_ONE   (1ULL<<45)
#     define RC_HALF  (1ULL<<18)
#   endif

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_HAS_CXX_DTOR_BIT 1
#   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 unused            : 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

我们可以看到在ARM64平台占用的是3~36字节,共占33字节

uintptr_t shiftcls : 33; /MACH_VM_MAX_ADDRESS 0x1000000000/

而在X86也就是我们的模拟上占用的是3~47字节,共占44字节

uintptr_t shiftcls : 44; /MACH_VM_MAX_ADDRESS 0x7fffffe00000/

通过Shitcls获取Mate元类

我们以X86平台为例

方式一:位运算移动

其实我们要想获取Shitcls的信息,只需要将isa_t中的值向右移动3字节,在向左移动20字节,在向右移动17字节即可

我们可以通过位运算左移右移来处理

示例:

我们在继续查看一下元类的isa是什么

方式二:与运算

通过上述lldb的方式分析来看,我们已经知道了isa的结构体,并且也已经了解到了,实例对象的isa指向关系。

总结:

实例对象isa-->类对象isa-->元类isa-->根原类isa-->根原类isa-->根原类isa

isa和实例对象关系的建立

在上一节我们分析alloc的过程,在createInstanceFromZone函数中,通过initIsa函数将我们创建的objc对象与类进行关联。

我们先来看一下initIsa的结构体代码

objc_object::initIsa(Class cls)
{
    initIsa(cls, false, false);
}

我们发现结构内部调用了initIsa()方法

SuperClass

superclass存储的就是当前类的的父类信息,实例对象没有supperclass,只有类对象才有superclass。这也就是objc_object结构体只有isa的原因。

总结:

Class对象SupperClass-->父Class-->父Class-->……-->NSobject-->nil

方法调用流程

方法调用分为实例方法调用个类方法调用,在研究调用之前我们先了解一下这心方法的存储位置。

instance对象(实例对象)

  • instance对象的isa指向class
  • 当调用对象方法时,先通过isa找到class对象,然后在找到对象方法进行调用
  • class的isa指向元类的class,也就是mate_class
  • 当调用类方法时,需要通过isa找到class,在通过class的isa找到元类,然后在找到类方法进行调用

class对象(类对象)

  • 当Student的instance对象要调用Person对象的实例方法时,会先通过instance的isa找到Student的class,然后在Student的class的superclass找到Person,最后找到方法进行调用。

mate_class对象(元类对象)

  • 当Student的class对象要调用Person的对象方法时,会先通过Student的isa找到Student的mate_class,在通过Student的mate_class的superclass找到Person的mate_class,最后找到Person的类方法进行调用
  • 此处特殊:当通过superclass查找父类的方法时,一直找到NSObject的时候都没找到方法,系统会想前查找一次,也就是会找到根元类的类对象的实例方法,如果找到了也会进行调用,在找不到就会报错。

最后我们在来看isa和superclass的大神图,就全都一目了然了

万能图

KC老师拆分图

\