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<