您的当前位置:首页iOS运行时runtime及相关Objective-C底层讲解

iOS运行时runtime及相关Objective-C底层讲解

2024-12-13 来源:哗拓教育

2.Runtime储备知识

2.1.前言

       在体验中,我写过这样一句话“并且我认为这个技术算是高阶开发里面一个投机的技巧,绝大多数的UI开发都不会使用runtime,容易出现很严重的问题,并且官方也不是特别推荐使用”。

        但是经过了第一张体验,我会告诉你。虽然说第一在UI开发中你可能不会经常使用,第二使用不当时,会出现很严重的问题。第三官方也不是推荐使用的。但是!!即使你并不使用,学习Objective-C的运行时Runtime系统也是非常有必要的,并且当你学会了Runtime系统,你会发现你可以在任何地方使用到它。毕竟Runtime系统被称之为iOS开发的黑魔法。

       Objective-C提供了编译运行时,只要有可能,它都可以动态地运作。这意味着不仅需要编译器,还需要运行时系统执行编译的代码。运行时系统充当Objective-C语言的操作系统,有了它才能运作。

       运行时系统所提供功能是非常强大的,在一些第三方库开发中是经常使用到的。比如,苹果不允许我们给Category追加扩展属性,是因为它不会自动生成成员变量,那么我们通过运行时就可以很好的解决这个问题。另外,常见的模型转字典或者字典转模型,对象归档等。后续我们再来学习如何应用,本节只是讲讲理论。

       在使用runtime中,我们会通过如下三种方法来使用runtime。并且使用的难度级风险也越来越大。

通过Objective-C源代码

通过Foundation库中定义的NSObject提供的方法

通过直接调用runtime方法

安全系数及难度:零

       在我们日常开发中,所有编写的Objective-C代码系统都会自动帮助我们编译成runtime代码。我们使用它只是写源代码并编译源代码。当编译包含Objective-C类和方法的代码时,编译器会创建实现了语言动态特性的数据结构和函数调用。

安全系数及难度:低级

        在Cocoa编程中,大部分的类都继承于NSObject,也就是说NSObject通常是根类,大部分的类都继承于NSObject。有些特殊的情况下,NSObject只是提供了它应该要做什么的模板,却没有提供所有必须的代码。

       有些NSObject提供的方法仅仅是为了查询运动时系统的相关信息,这此方法都可以反查自己。比如-isKindOfClass:和-isMemberOfClass:都是用于查询在继承体系中的位置。-respondsToSelector:指明是否接受特定的消息。+conformsToProtocol:指明是否要求实现在指定的协议中声明的方法。-methodForSelector:提供方法实现的地址。

安全系数及难度:高级

runtime库函数在usr/include/objc目录下,我们主要关注是这两个头文件:

#import <objc/runtime.h>

#import <objc/objc.h>

2.3.消息(Message)

       为什么叫消息呢?因为面向对象编程中,对象调用方法叫做发送消息。在编译时,应用的源代码就会被编将对象发送消息转换成runtime的objc_msgSend函数调用。

       在Objective-C,消息在运行时并不要求实现。编译器会转换消息表达式:

[receiver message];

       在编译时会转换成类似这样的函数调用:

id objc_msgSend(id self,SEL op,...)

        具体会转换成哪个,我们来看看官方的文章注释:

/**

* Sends a message with a simple return value to an instance of a class.

* @param self A pointer to the instance of the class that is to receive the message.

* @param op The selector of the method that handles the message.

* @param ...

*   A variable argument list containing the arguments to the method.

* @return The return value of the method.

* @note When it encounters a method call, the compiler generates a call to one of the

*  functions \c objc_msgSend, \c objc_msgSend_stret, \c objc_msgSendSuper, or \c objc_msgSendSuper_stret.

*  Messages sent to an object’s superclass (using the \c super keyword) are sent using \c objc_msgSendSuper;

*  other messages are sent using \c objc_msgSend. Methods that have data structures as return values

*  are sent using \c objc_msgSendSuper_stret and \c objc_msgSend_stret.

*/

        也就是说,我们是通过编译器来自动转换成运行时代码时,它会根据类型自动转换成下面的其它一个函数:

objc_msgSend:其它普通的消息都会通过该函数来发送

objc_msgSend_stret:消息中需要有数据结构作为返回值时,会通过该函数来发送消息并接收返回值

objc_msgSendSuper:与objc_msgSend函数类似,只是它把消息发送给父类实例

objc_msgSendSuper_stret:与objc_msgSend_stret函数类似,只是它把消息发送给父类实例并接收数组结构作为返回值

       另外,如果函数返回值是浮点类型,官方说明如下:

       注意:有另外一个需要注意的地方是当函数返回的值是浮点类型是,官方注释有这样的解释:

/* Floating-point-returning Messaging Primitives

*

* Use these functions to call methods that return floating-point values

* on the stack.

* Consult your local function call ABI documentation for details.

*

* arm:    objc_msgSend_fpret not used

* i386:   objc_msgSend_fpret used for `float`, `double`, `long double`.

* x86-64: objc_msgSend_fpret used for `long double`.

*

* arm:    objc_msgSend_fp2ret not used

* i386:   objc_msgSend_fp2ret not used

* x86-64: objc_msgSend_fp2ret used for `_Complex long double`.

*

* These functions must be cast to an appropriate function pointer type

* before being called.

*/

       其实总来来说不用担心这个问题,只需要调用objc_msgSend_fpret函数就好了。

        注意事项:一定要调用所调用的API支持哪些平台,乱调在导致部分平台上不支持而崩溃的。

       现在我们来看看当消息被发送到实例对象时,它是如何处理的:

消息传递

        这个算是iOS开发消息传递的基础知识了,就不在更多解释。一直找到NSObject如果都还没有找到,就会崩溃报错Unreconized selector。

2.4.Message Forwarding

       当发送消息给一个不处理该消息的对象是错误的。然后在宣布错误之前,运行时系统给了接收消息的对象处理消息的第二个机会。

       当某对象不处理某消息时,可以通过重写-forwardInvocation:方法来提供一个默认的消息响应或者避免出错。当对象中找不到方法实现时,会按照类继承关系一层层往上找。我们看看类继承关系图:

类继承关系图

       所有元类中的isa指针都指向根元类,而根元类的isa指针则指向自身。根元类是继承于根类的,与根类的结构体成员一致,都是objc_class结构体,不同的是根元类的isa指针指向自身,而根类的isa指针为nil

我们再看看消息处理流程:

消息处理

        当对象查询不到相关的方法,消息得不到该对象处理,会启动“消息转发”机制。消息转发还分为几个阶段:先询问receiver或者说是它所属的类是否能动态添加方法,以处理当前这个消息,这叫做“动态方法解析”,runtime会通过+resolveInstanceMethod:判断能否处理。如果runtime完成动态添加方法的询问之后,receiver仍然无法正常响应则Runtime会继续向receiver询问是否有其它对象即其它receiver能处理这条消息,若返回能够处理的对象,Runtime会把消息转给返回的对象,消息转发流程也就结束。若无对象返回,Runtime会把消息有关的全部细节都封装到NSInvocation对象中,再给receiver最后一次机会,令其设法解决当前还未处理的这条消息。

        我们可以这样理解:

        向一个对象发送它不处理的消息是一个会报错,但在报错之前 Runtime系统会给接收对象来处理这些错误的机会。这个需要用到以下方法:

-(void) forwardInvocation: (NSInvocation*)invocation

        如果对象没有实现这个方法,就调用NSObject 的forwardInvocation:方法。那句不能识别消息的错误,实际就是NSObject 的forwardInvocation 抛出来的异常。

        也就是说,Message Forwarding的作用就是你可以覆盖forwardInvocation方法,来改变NSObject 的抛异常的处理方式。所以,你可以把A不能处理的消息转发给B去处理。

提示:消息处理越往后,开销也就会越大,因此最好直接在第一步就可以得到消息处理。

        我们来看看NSInvocation官方头文件:

@interfaceNSInvocation:

NSObject{

@private

__strongvoid*_frame;

__strongvoid*_retdata;

id_signature;

id_container;

uint8_t_retainedArgs;

uint8_t_reserved[15];

}

+(NSInvocation*)invocationWithMethodSignature:(NSMethodSignature*)sig;

@property(readonly,retain)NSMethodSignature*methodSignature;

-(void)retainArguments;

@property(readonly)BOOLargumentsRetained;

@property(nullable,assign)idtarget;

@propertySELselector;

-(void)getReturnValue:(void*)retLoc;

-(void)setReturnValue:(void*)retLoc;

-(void)getArgument:(void*)argumentLocationatIndex:(NSInteger)idx;

-(void)setArgument:(void*)argumentLocationatIndex:(NSInteger)idx;

-(void)invoke;

-(void)invokeWithTarget:(id)target;

@end

        实际上NSInvocation是一个包含了target、selector ,Arguments,也就是它包含了向一个对象发送消息的所有元素:对象、方法名、参数序列。可以调用NSInvocation 的invoke 方法将这个消息激活。

        后面Message Forwarding会单独继续精讲。这里做一个理解。

        Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。

        查看objc/objc.h中Class的定义如下:

/// An opaque type that represents an Objective-C class.

typedefstructobjc_class*Class;

         查看objc/runtime.h中objc_class结构体的定义如下:

structobjc_class{

ClassisaOBJC_ISA_AVAILABILITY;

#if !__OBJC2__

Classsuper_classOBJC2_UNAVAILABLE;//

父类

constchar*nameOBJC2_UNAVAILABLE;//

类名

longversionOBJC2_UNAVAILABLE;//

类的版本信息,默认为0

longinfoOBJC2_UNAVAILABLE;//

类信息,供运行期使用的一些位标识

longinstance_sizeOBJC2_UNAVAILABLE;//

该类的实例变量大小

structobjc_ivar_list*ivarsOBJC2_UNAVAILABLE;//

该类的成员变量链表

structobjc_method_list**methodListsOBJC2_UNAVAILABLE;//

方法定义的链表

structobjc_cache*cacheOBJC2_UNAVAILABLE;//

方法缓存

structobjc_protocol_list*protocolsOBJC2_UNAVAILABLE;//

协议链表

#endif

}OBJC2_UNAVAILABLE;

         我们可以看到每个类结构体都会有一个isa指针,它是指向元类的。它还有一个父类指针super_class,指针父类。还包含了类的名称name、类的版本信息version、类的一些标识信息info、实例大小instance_size、成员变量地址列表ivars、方法地址列表methodLists、缓存最近使用的方法地址cache、协议列表protocols。其中有一下几个字段是我们需要特别关注的:

isa:需要注意的是在Objective-C中,所有的类自身也是一个对象,这个对象的Class里面也有一个isa指针,它指向metaClass(元类),我们会在后面介绍它。

super_class:指向该类的父类,如果该类已经是最顶层的根类(如NSObject或NSProxy),则super_class为NULL。

cache:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。

version:我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。

         针对cache,我们用下面例子来说明其执行过程:

NSArray*array=[[NSArrayalloc]init];

其流程是:

        [NSArray alloc]先被执行。因为NSArray没有+alloc方法,于是去父类NSObject去查找。

        检测NSObject是否响应+alloc方法,发现响应,于是检测NSArray类,并根据其所需的内存空间大小开始分配内存空间,然后把isa指针指向NSArray类。同时,+alloc也被加进cache列表里面。

        接着,执行-init方法,如果NSArray响应该方法,则直接将其加入cache;如果不响应,则去父类查找。

         在后期的操作中,如果再以[[NSArray alloc] init]这种方式来创建数组,则会直接从cache中取出相应的方法,直接调用。

2.5.1.1.objc_object与id

         objc_object是表示一个类的实例的结构体,它的定义如下(objc/objc.h):

#if !OBJC_TYPES_DEFINED

/// An opaque type that represents an Objective-C class.

typedefstructobjc_class*Class;

/// Represents an instance of a class.

structobjc_object{

ClassisaOBJC_ISA_AVAILABILITY;

};

/// A pointer to an instance of a class.

typedefstructobjc_object*id;

#endif

       从官方的头文件可以看到objc_object是一个结构体并且只有一个成员,即指向其类的isa指针。这样,当我们向一个Objective-C对象发送消息时,runtime系统会根据实例对象的isa指针找到这个实例对象所属的类。Runtime系统会在类的方法列表及父类的方法列表中去寻找与消息对应的selector指向的方法。找到后即运行这个方法。

       当创建一个特定类的实例对象时,分配的内存包含一个objc_object数据结构,然后是类的实例变量的数据。NSObject类的alloc和allocWithZone:方法使用函数class_createInstance来创建objc_object数据结构。

       另外还有我们常见的id,它是一个objc_object结构类型的指针。它的存在可以让我们实现类似于C++中泛型的一些操作。该类型的对象可以转换为任何一种对象,有点类似于C语言中void *指针类型的作用。

2.5.1.2.objc_cache

        上面提到了objc_class结构体中的cache字段,它用于缓存调用过的方法。这个字段是一个指向objc_cache结构体的指针,其定义如下:

typedefstructobjc_cache*CacheOBJC2_UNAVAILABLE;

#define CACHE_BUCKET_NAME(B)  ((B)->method_name)

#define CACHE_BUCKET_IMP(B)   ((B)->method_imp)

#define CACHE_BUCKET_VALID(B) (B)

#ifndef __LP64__

#define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))

#else

#define CACHE_HASH(sel, mask) (((unsigned int)((uintptr_t)(sel)>>3)) & (mask))

#endif

structobjc_cache{

unsignedintmask/* total = mask + 1 */OBJC2_UNAVAILABLE;

unsignedintoccupiedOBJC2_UNAVAILABLE;

Methodbuckets[1]OBJC2_UNAVAILABLE;

};

该结构体的字段描述如下:

mask:一个整数,指定分配的缓存bucket的总数。在方法查找过程中,Objective-C runtime使用这个字段来确定开始线性查找数组的索引位置。指向方法selector的指针与该字段做一个AND位操作(index = (mask & selector))。这可以作为一个简单的hash散列算法。

occupied:一个整数,指定实际占用的缓存bucket的总数。

buckets:指向Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。

2.5.2.元类(Meta Class)

        在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。如:

NSArray*array=[NSArrayarray];

        这个例子中,+array消息发送给了NSArray类,而这个NSArray也是一个对象。既然是对象,那么它也是一个objc_object指针,它包含一个指向其类的一个isa指针。那么这些就有一个问题了,这个isa指针指向什么呢?为了调用+array方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念

meta-class是一个类对象的类。

        当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。

         meta-class之所以重要,是因为它存储着一个类的所有类方法。每个类都会有一个单独的meta-class,因为每个类的类方法基本不可能完全相同。

        其实,meta-class也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。这样就形成了一个完美的闭环。

        通过上面的描述,再加上对objc_class结构体中super_class指针的分析,我们就可以描绘出类及相应meta-class类的一个继承体系了,如下图所示:

类及相应meta-class类的一个继承体系

        对于NSObject继承体系来说,其实例方法对体系中的所有实例、类和meta-class都是有效的;而类方法对于体系内的所有类和meta-class都是有效的。

3.操作函数

3.1.类名(name)

/**

* Returns the name of a class.

*

* @param cls A class object.

*

* @return The name of the class, or the empty string if \e cls is \c Nil.

*/

OBJC_EXPORTconstchar*class_getName(Classcls)

__OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);

        对于class_getName函数,如果传入的cls为Nil,则返回一个char字字符串。

3.2.父类(super_class)和元类(meta-class)

/**

* Returns a Boolean value that indicates whether a class object is a metaclass.

*

* @param cls A class object.

*

* @return \c YES if \e cls is a metaclass, \c NO if \e cls is a non-meta class,

*  \c NO if \e cls is \c Nil.

*/

OBJC_EXPORTBOOLclass_isMetaClass(Classcls)

__OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);

/**

* Returns the superclass of a class.

*

* @param cls A class object.

*

* @return The superclass of the class, or \c Nil if

*  \e cls is a root class, or \c Nil if \e cls is \c Nil.

*

* @note You should usually use \c NSObject's \c superclass method instead of this function.

*/

OBJC_EXPORTClassclass_getSuperclass(Classcls)

__OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);

/**

* Sets the superclass of a given class.

*

* @param cls The class whose superclass you want to set.

* @param newSuper The new superclass for cls.

*

* @return The old superclass for cls.

*

* @warning You should not use this function.

*/

OBJC_EXPORTClassclass_setSuperclass(Classcls,ClassnewSuper)

__OSX_AVAILABLE_BUT_DEPRECATED(__MAC_10_5,__MAC_10_5,__IPHONE_2_0,__IPHONE_2_0);

class_isMetaClass函数,如果是cls是元类,则返回YES;如果否或者传入的cls为Nil,则返回NO。

class_getSuperclass函数,当cls为Nil或者cls为根类时,返回Nil。不过通常我们可以使用NSObject类的superclass方法来达到同样的目的。

class_setSuperclass函数,为一个cls设置一个新的superclass,并且返回cls以前的superclass。

3.3.版本(version)

/**

* Returns the version number of a class definition.

*

* @param cls A pointer to a \c Class data structure. Pass

*  the class definition for which you wish to obtain the version.

*

* @return An integer indicating the version number of the class definition.

*

* @see class_setVersion

*/

OBJC_EXPORTintclass_getVersion(Classcls)

__OSX_AVAILABLE_STARTING(__MAC_10_0,__IPHONE_2_0);

/**

* Sets the version number of a class definition.

*

* @param cls A pointer to an Class data structure.

*  Pass the class definition for which you wish to set the version.

* @param version An integer. Pass the new version number of the class definition.

*

* @note You can use the version number of the class definition to provide versioning of the

*  interface that your class represents to other classes. This is especially useful for object

*  serialization (that is, archiving of the object in a flattened form), where it is important to

*  recognize changes to the layout of the instance variables in different class-definition versions.

* @note Classes derived from the Foundation framework \c NSObject class can set the class-definition

*  version number using the \c setVersion: class method, which is implemented using the \c class_setVersion function.

*/

OBJC_EXPORTvoidclass_setVersion(Classcls,intversion)

__OSX_AVAILABLE_STARTING(__MAC_10_0,__IPHONE_2_0);

class_getVersion函数可以获取类定义的版本号,返回一个int类型。

class_setVersion函数对cls设置一个int类型的版本号,不过通常我们可以使用NSObject类的setVersion方法来达到同样的目的。

3.4.实例变量大小(instance_size)

/**

* Returns the size of instances of a class.

*

* @param cls A class object.

*

* @return The size in bytes of instances of the class \e cls, or \c 0 if \e cls is \c Nil.

*/

OBJC_EXPORTsize_tclass_getInstanceSize(Classcls)

__OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);

3.5.成员变量(ivars)及属性

/**

* Returns the \c Ivar for a specified instance variable of a given class.

*

* @param cls The class whose instance variable you wish to obtain.

* @param name The name of the instance variable definition to obtain.

*

* @return A pointer to an \c Ivar data structure containing information about

*  the instance variable specified by \e name.

*/

OBJC_EXPORTIvarclass_getInstanceVariable(Classcls,constchar*name)

__OSX_AVAILABLE_STARTING(__MAC_10_0,__IPHONE_2_0);

/**

* Returns the Ivar for a specified class variable of a given class.

*

* @param cls The class definition whose class variable you wish to obtain.

* @param name The name of the class variable definition to obtain.

*

* @return A pointer to an \c Ivar data structure containing information about the class variable specified by \e name.

*/

OBJC_EXPORTIvarclass_getClassVariable(Classcls,constchar*name)

__OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);

/**

* Describes the instance variables declared by a class.

*

* @param cls The class to inspect.

* @param outCount On return, contains the length of the returned array.

*  If outCount is NULL, the length is not returned.

*

* @return An array of pointers of type Ivar describing the instance variables declared by the class.

*  Any instance variables declared by superclasses are not included. The array contains *outCount

*  pointers followed by a NULL terminator. You must free the array with free().

*

*  If the class declares no instance variables, or cls is Nil, NULL is returned and *outCount is 0.

*/

OBJC_EXPORTIvar*class_copyIvarList(Classcls,unsignedint*outCount)

__OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);

/**

* Adds a new instance variable to a class.

*

* @return YES if the instance variable was added successfully, otherwise NO

*         (for example, the class already contains an instance variable with that name).

*

* @note This function may only be called after objc_allocateClassPair and before objc_registerClassPair.

*       Adding an instance variable to an existing class is not supported.

* @note The class must not be a metaclass. Adding an instance variable to a metaclass is not supported.

* @note The instance variable's minimum alignment in bytes is 1<

*       variable depends on the ivar's type and the machine architecture.

*       For variables of any pointer type, pass log2(sizeof(pointer_type)).

*/

OBJC_EXPORTBOOLclass_addIvar(Classcls,constchar*name,size_tsize,

uint8_talignment,constchar*types)

__OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);

class_getInstanceVariable函数,它返回一个指向包含name指定的成员变量信息的objc_ivar结构体的指针(Ivar)。

class_getClassVariable函数,目前没有找到关于Objective-C中类变量的信息,一般认为Objective-C不支持类变量。注意,返回的列表不包含父类的成员变量和属性。

class_copyIvarList函数,它返回一个指向成员变量信息的数组,数组中每个元素是指向该成员变量信息的objc_ivar结构体的指针。这个数组不包含在父类中声明的变量。outCount指针返回数组的大小。需要注意的是,我们必须使用free()来释放这个数组。

class_addIvar函数,我们知道Objective-C不支持往已存在的类中添加实例变量,因此不管是系统库提供的提供的类,还是我们自定义的类,都无法动态添加成员变量。但如果我们通过运行时来创建一个类的话,又应该如何给它添加成员变量呢?这时我们就可以使用class_addIvar函数了。不过需要注意的是,这个方法只能在objc_allocateClassPair函数与objc_registerClassPair之间调用。另外,这个类也不能是元类。成员变量的按字节最小对齐量是1<

显示全文