前一篇讲解了一下Runtime的底层原理,objc_msgSend的消息发送流程;其实学习Runtime,首先要了解它底层的一些常用数据结构,比如isa指针;在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址;从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。
共用体:
共用体把几种不同数据类型的变量存放在同一块内存里。共用体中的变量共享同一块内存。
union的主要特征有:
- union中可以定义多个成员,union的大小由最大的成员的大小决定;
- union成员共享同一块大小的内存,一次只能使用其中的一个成员;
- 对union某一个成员赋值,会覆盖其他成员的值(但前提是成员所占字节数相同,当成员所占字节数不同时只会覆盖相应字节上的值,比如对char成员赋值就不会把整个int成员覆盖掉,因为char只占一个字节,而int占四个字节);
- union量的存放顺序是所有成员都从低地址开始存放的。
isa的结构如下:
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if SUPPORT_PACKED_ISA
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL //这个很重要后面会讲到🙃
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
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__
...下面代码省略不做重点介绍了🙃
};
isa参数详解
- nonpointer:
0,代表普通的指针,存储着Class、Meta-Class对象的内存地址
1,代表优化过,使用位域存储更多的信息 - has_assoc:是否有设置过关联对象,如果没有,释放时会更快
- has_cxx_dtor:是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
- shiftcls:存储着Class、Meta-Class对象的内存地址信息
- magic:用于在调试时分辨对象是否未完成初始化
- weakly_referenced:是否有被弱引用指向过,如果没有,释放时会更快
- deallocating:对象是否正在释放
- extra_rc:表示该对象的引用计数值,实际上是引用计数值减 1,例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10,则需要使用到下面的 has_sidetable_rc。
- has_sidetable_rc:当对象引用计数大于 10 时,则has_sidetable_rc 的值为 1,那么引用计数会存储在一个叫 SideTable 的类的属性中,这是一个散列表。原文链接
扯了这么多到底什么是isa❓官方介绍是这样的:
Every object is connected to the run-time system through itsisa instance variable, inherited from the NSObject class.isa identifies the object's class; it points to a structurethat's compiled from the class definition. Through isa, anobject can find whatever information it needs at run timesuch asits place in the inheritance hierarchy, the size and structure ofits instance variables, and the location of the methodimplementations it can perform in response to messages.
(每个对象都通过从NSObject类继承的实例变量itsisa连接到运行时系统。isa标识对象的类;它指向一个从类定义编译而来的结构。通过isa,一个对象可以在运行时找到它需要的任何信息,比如它在继承层次结构中的位置、它的实例变量的大小和结构,以及它在响应消息时可以执行的methodimplementations的位置。)
一个对象(Object)的isa指向了这个对象的类(Class),而这个对象的类(Class)的isa指向了metaclass。这样我们就可以找到静态方法和变量了。Objective-C的运行时是动态的,它能让你在运行时为类添加方法或者去除方法以及使用反射(反射什么鬼?传送门)。
讲isa就一定会提到metaclass,这里先提一下什么是metaclass❓
- meta-class是一个类对象的类
- 当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。
- meta-class 是必须的,因为它为一个 Class 存储类方法。每个Class都必须有一个唯一的 meta-class,因为每个Class的类方法基本不可能完全相同。
问题提出:iOS中有静态方法与动态方法,那么两种方法的异同是什么?(问题来源:脚本之家-lqh )
因为每个对象都由相应的数据结构与方法相构成,一个程序可能有多个属于同一个类的对象,而每个对象的数据结构应该是不一的,但方法是相同的,若为每个对象开辟内存空间来存储方法,必然是对内存空间极大的浪费。因此apple是通过类对象与元类来解决这个问题的。
对象的底层实际上就是结构体,其有两个重要的指针,一个是isa指针,一个是super指针。
isa指针:负责指向类对象,用来表明自己是什么类类型,并能调用类对象中的动态方法。
super指针:表示对象的继承关系,指向父类,从而能调用父类的相应方法。
类对象:类对象是由元类生成的对象,负责存储动态方法,动态方法在编译器是不确定的,因此编译器也无法检测与动态方法相关的错误,动态方法的调用是在运行期中通过消息机制来执行的,因此也只有运行期才会报错。
结论:
两者的差异包括:
- 方法列表是区分开的,分别存储在类对象与元类中。
- 动态方法是在运行期中调用,编译器无法检测错误,静态方法是在编译器就确定,编译器能检测错误。
- 动态方法由对象调用,静态方法由类调用,因为调用方法是通过isa和super指针实现的。因此对象只能调用类对象的方法,而类对像能调用元类的方法。
这张图描述如下:
- 类的实例对象的 isa 指向它的类;类的 isa 指向该类的 metaclass;
- 类的 super_class 指向其父类,如果该类为根类则值为 NULL;
- metaclass 的 isa 指向根 metaclass,如果该 metaclass 是根 metaclass则指向自身;
- metaclass 的 super_class 指向父 metaclass,如果该 metaclass 是根 metaclass则指向该 metaclass 对应的类;
- Object-C 为每个类的定义生成两个 objc_class ,一个普通的 class,另一个即metaclass。我们可以在运行期创建这两个 objc_class 数据结构,然后使用 objc_addClass将 class注册到运行时系统中,以此实现动态地创建一个新的类。
为什么这里说生成两个 objc_class ,从前面metaclass就可以了解一二了。讲到这里,大家可能很疑惑isa到底是怎么指向类的,解释如下:isa里面存储各种信息,是一个共用体,其中shiftcls 33位才是用来存放地址,通过&ISA_MASK就可以将33位的地址值取出来,看图分析理解更透彻:
因为下面要用到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;
稍微解释一下各个参数的意思:
- isa:是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass).根元类的isa指针指向本身,这样形成了一个封闭的内循环。
- super_class:父类,如果该类已经是最顶层的根类,那么它为NULL。
- version:类的版本信息,默认为0
- info:供运行期使用的一些位标识。
- instance_size:该类的实例变量大小
- ivars:成员变量的数组
下面来个例子:
- 新建一个类Parent,继承于NSObject, 里面有成员方法-(void)selectorP,类方法+(void)ClassSelectorP。
- 新建一个类Child,继承于Parent,里面有成员方法-(void)selectorC, 类方法+(void)ClassSelectorC.
现在我们新建一个实例Child* child = [Chlid new];
- 当我们调用[child class] 的时候,child就会通过isa指针去找到Child。
- 当我们调用[child superclass]的时候,child 通过isa找到Child,再通过Child的isa,找到Parent。
对象方法 - 接着,调用[child SelectorC],child通过isa找到Child,在Child的方法列表里面找到SelectorC;
- 再试着调用[child SelectorP],child通过isa找到Child,发现Child里面并没有这个方法,再通过Child的isa,找到Parent,在Parent里面的方法列表找到了SelectorP;
类方法 - 再是类方法[Child ClassSelectorC],Child(请注意是类调用)通过isa找到Child的metaclass,在metaclass的方法列表里面找到了ClassSelectorC;
- 再试着调用[Child ClassSelectorP],Child通过isa找到Child的metaclass,发现metaclass里面并没有这个方法,通过metaclass里面的isa找到Parent的metaclass,在里面的方法列表找到了ClassSelectorP;
-
所有的元类最终继承一个根元类,根元类isa指针指向本身,形成一个封闭的内循环
图解如下:
isa基本就讲完了,下面来个小李子再次加深理解:
NSArray *array = [[NSArray alloc] init];流程剖析
- [NSArray alloc]先被执行。先去NSArray中查找+alloc方法(类方法),因为NSArray没有+alloc方法,于是去父类NSObject去查找。
- 检测NSObject是否响应+alloc方法,发现响应,于是检测NSArray类,并根据其所需的内存空间大小开始分配内存空间,然后把isa指针指向NSArray类。同时,+alloc也被加进cache列表里面。
- 接着,执行-init方法,如果NSArray响应该方法,则直接将其加入cache;如果不响应,则去父类查找。
- 在后期的操作中,如果再以[[NSArray alloc] init]这种方式来创建数组,则会直接从cache中取出相应的方法,直接调用。
总结:
- isa是一个共用体;isa标识对象的类;它指向一个从类定义编译而来的结构。通过isa,一个对象可以在运行时找到它需要的任何信息,比如它在继承层次结构中的位置、它的实例变量的大小和结构,以及它在响应消息时可以执行的imp的位置;
- 也可以说isa是一个Class 类型的指针,每个实例对象有个isa的指针,他指向对象的类结构。
- Objective-C 2.0中的描述是:新的对象被创建,其内存同时被分配,实例变量也同时被初始化;对象的第一个实例变量是一个指向 该对象的类结构的指针,叫做 isa。通过该指针,对象可以访问它对应的类以及相应的父类。
文章参考:
https://www.jianshu.com/p/cf93dc9d2262
https://www.jianshu.com/p/83b9f172c43c