Objective-C Runtime - 基础篇

前言

2016年6月9号开始Runtime

NSObject方法

  • Cocoa中大多数对象是继承自NSObjectl类的子类(唯一例外的是NSProxy类)
    NSObject类定义了一个description实例方法,返回一个描述类内容的字符串。这主要是用于调试,从这个方法通过GDB命令打印对象并返回打印字符串。

  • 还有一些类方法,它要求一个对象来确定其类;isKindOfClass:方法和isMemberOfClass根据对象来确定其类;respondsToSelector:表明一个对象是否可以接受一个特定的消息;conformsToProtocol:表明一个对象是否要求实现一个由协议定义的特定的方法;methodForSelector:提供一个方法实现地址,这个方法给一个对象自我反省能力。

消息传递

本章介绍了消息传递是如何转换为objc_msgsend函数调用,它说明了如何可以利用objc_msgsend,如果你需要使用它,如何可以规避动态绑定。

objc_msgSend函数

在Objective - C中,消息没有绑定到方法实现,直到Runtime时, 编译器才将消息传递绑定到方法的实现。

通过调用objc_msgSend插入一个消息功能。该函数所提到的参数主要有两个selector和method(receiver指向Method)

objc_msgSend(receiver, selector)

在所有Runtime 以char *定义的API都被视为UTF-8编码

在消息传递的任何参数都交给objc_msgSend来处理
objc_msgSend(receiver, selector, arg1, arg2, ...)

消息传递函数动态绑定所需的一切事情

  • 它首先通过IMP指针查找的程序的实现(方法实现)紧接着查找选择器(selector),由于同样的方法可以用不同的类来实现,它能够精确定位取决于接收器的类。
  • 然后调用该程序,通过选择器来接收对象(一个指向接收数据的指针),紧接着接收传入的参数以及特定的方法,
  • 最后,它将程序返回的值作为自身的返回值

编译器生成消息传递函数的调用。你不应该直接在你写的代码中直接调用它。

消息传递的关键在于编译器为每个类和对象生成结构。每一类结构都包含了这2个基本要素:

  • 一个指向父类。。
  • 一个指向类调度表。这个表将方法的选择器列表与其对应类的方法地址关联,通过setOrigin方法实现selector选择器与method实现的地址相关联起来,显示的选择器与显示的方法关联 ,等等。当一个新的对象创建时,并且实例变量被初始化时,将分配给他们内存,对象的变量是一个指向class结构的指针,这个指针被称为isa,能通过这个指针访问他自身的类以及访问它所有父类

isa指针等效”objc_object"结构体,继承自NSObject的或NSProxy时,所有对象都自动有一个isa变量。

类和对象结构消息传递框架如图1-1所示

Objective-C Runtime - 基础篇_第1张图片
类和对象结构消息传递框架
  • 当一个消息被发送到一个对象时,消息传递函数遵循该对象的isa指针,该指针指向调度表中查找方法选择器的类结构。如果它找不到选择器,消息传递通过objc_msgsend指向父类的调度表并试图查找选择器。连续的失败造成objc_msgsend攀登类层次结构直至NSObject类。一旦它(函数调用表中输入的方法)找到了选择器,将其传递给接收对象的数据结构。

  • 这是在Runtime被选择实现的方法,或者用面向对象编程的行话说,即这个方法是被动态的绑定到消息。

  • 为了加快消息传递处理,通过Runtime系统的缓存,使用它们的选择器和方法地址,每一个类都有单独的缓存,它可以包含继承的方法选择器以及类中定义的方法。在搜索调度表之前,消息传递程序首先检查接收对象类的缓存(在理论上,一个被使用的方法可能会被再次使用)。如果该方法选择器在缓存中,则消息仅略慢于函数调用。当一个程序运行足够长的时间直至可以“活跃”它的缓存时,几乎所有的消息都会发送到缓存的方法。在高速缓存的动态变化下,以适应新消息的程序运行。

>有一种方法是指接收的对象是self,并有选择作为_cmd,_cmd指给selector陌生的方法和该接收陌生的消息的对象。

获得方法地址

规避动态绑定的唯一方法是获取一个方法的地址,将其称为一个函数。在特殊的适当情况下,特定的方法将被执行多次,每次执行该方法时,都要避免架空的消息
在一个NSObject类中定义的方法,methodForSelector:,你可以要求一个指针来实现该方法,然后用指针来调用程序。methodForSelector:返回必须小心地转换为适当的函数类型。返回的参数类型都应经过一定的计算
下面的例子展示了如何实现setFilled程序

void (*setter)(id, SEL, BOOL);
int i;
 
setter = (void (*)(id, SEL, BOOL))[target
    methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
    setter(targetList[i], @selector(setFilled:), YES);

参数传递给程序接收的对象(self)和方法选择器(_cmd)。这几个参数都隐藏在方法语法中,但是当该方法被称为函数时,必须进行显式的调用。
使用methodForSelector:规避动态绑定可以节省通信所需要的大部分时间。然而,存储过程将是显着的,只有特定的信息重复多次,如在上面的循环显示,这样存储才有更意义

注意,methodForSelector:由Cocoa Runtime系统提供的;它不是一个功能而是Objective-C语言本身。

动态方法解析

在有些情况下,你可能要动态地提供方法的实现。例如,Objective - C声明属性特征

@dynamic propertyName;
  • 它告诉编译器,将动态提供与该属性相关联的方法。
  • 你可以通过实现方法resolveInstanceMethod:和resolveClassMethod:
    动态提供了一个实例和类方法分别指定selector实现。
  • 一个Objective - C方法仅仅是一个C函数,至少需要两个参数self和_cmd。
void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}

您可以添加一个函数到一个类把它作为一个方法来使用函数class_addMethod。

@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
@end

转发方法(如消息转发描述)和动态方法的解析,在很大程度上。一个类有机会解决动态的转发机制开始之前的方法,如果respondsToSelector:或instancesRespondToSelector:被调用时,动态方法是有机会的首先为selector提供一个IMP。如果要实现resolveInstanceMethod:但要实际上selector选择器可以通过转发机制转发,还没有选择器时返回NO。

@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
@end

转发方法(如消息转发描述)和动态方法的解析,在很大程度上。一个类有机会解决动态的转发机制开始之前的方法,如果respondsToSelector:或instancesRespondToSelector:被调用时,动态方法是有机会的首先为selector提供一个IMP。如果要实现resolveInstanceMethod:但要实际上selector选择器可以通过转发机制转发,还没有选择器时返回NO。

消息转发

当我们发送一条消息给一个对象时,这个过程消息不经过处理的,而且不知道其错误。在宣布错误之前,Runtime系统给接收对象提供了一个机会来处理这个消息。

转发

如果你发送一条消息给一个对象,而消息是没有经过处理的,在宣布一个错误之前,运行时系统会对对象发送向forwardInvocation: message
方法发送消息并且使用NSInvocation对象作为其唯一参数(NSInvocation对象装入传递的原始消息和参数)

为了查看转发的范围和意图,有以下几种情况:假设你正在设计一个可以响应消息的对象,并且你希望它的响应包括另一个对象的响应。你可以轻而易举地将一个negotiate信息传递给对方

进一步,假设你想让你的对象相应一个negotiate消息到达准确在另一个类的实现中相应。完成这一个方法可以使你的类继承到另一个类的方法。然后,它可能不会按照这种方式来达成此过程。可能有很好的方式,你的类和其他类,实现negotiate的继承层次结构的不同分支。

即使你的类不能继承negotiate的方法,你仍然可以“借用”它通过实施一个版本的方法,简单地传递给一个实例的其他类

- (id)negotiate
{
    if ( [someOtherObject respondsTo:@selector(negotiate)] )
        return [someOtherObject negotiate];
    return self;
}

这样做的事情可能会有点麻烦,特别是如果有一些消息,你希望你的对象传递到另一个对象。你必须实现一个方法来覆盖每个你想借用其他类的方法。此外,在你不知道的情况下,在你写代码的时候,你可能想要向前的全部信息是不可能的。这个集合可能依赖于运行时的事件,它可能在未来实现会改变新的方法和类。

第二次改变机会是由forwardInvocation: message 方法提供:这个方法提供了一个特设的解决方案,动态的而不是静态的解决方案。它是这样工作的:当一个对象不能响应消息造成它没有一个方法在消息选择器匹配时,Runtime系统就会通知对象发送一个消息forwardInvocation。每一个对象从NSObject类的方法都继承了一个forwardinvocation方法:。然而,NSObject的版本的方法只是简单调用doesNotRecognizeSelector:。通过重写NSObject的方法来实现,你可以利用在这个forwardInvocation: message 方法机会,提供转发消息到其他对象

为了转发消息, forwardInvocation: method 需要做的是:
确定信息应该在哪里去了
把它发送给它的原始参数。
消息可以发送到invokeWithTarget: method:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
            [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

forwardInvocation: method获取处理消息,如果他们不是在接收器中调用一个现有的方法。例如,如果你希望你的对象将消息转发到另一个对象,它就不能有一个negotiate的方法。如果是这样,该消息将不会达到forwardinvocation:。
在转发和调用的更多信息,见在基础框架参考NSInvocation类规范。

转发和多重继承

在这个例子中,Warrior类的一个实例将一个negotiate信息转发给一个Diplomat类的实例。Warrior发出negotiate给Diplomat问它想要做什么。它对negotiate的消息作出反应,并为所有的实际目的,它确实作出回应(尽管这是一个真正的在做Diplomat工作)。
转发消息的对象,因此从继承层次结构的2个分支中国“继承”方法,它自己的分支以及响应的对象的分支。在上面的例子中,仿佛Warrior类继承的Diplomat以及它自己的父类。
转发提供了通常想要从多个继承的功能。然而,两者之间有一个重要的区别:多继承将不同的功能组合在一个单独的对象中。它趋向于大、多方面的对象。转发,另一方面,分配单独的责任,不同的对象。它将问题分解为较小的对象,但将这些对象关联在一个对消息发送者透明的方式中

转发和继承

虽然转发模仿继承NSObject类,但是不要把这两个方法混淆。方法:和respondstoselector isKindOfClass:方法看起来只能继承层次,从不在处理转发链。例如,如果一个Warrior对象被问到它是否会对negotiate信息作出反应,

if ( [aWarrior respondsToSelector:@selector(negotiate)] )
...

答案是NO的,即使它可以接收negotiate的消息,没有错误,并响应他们,在一种意义上,通过转发给一名Diplomat。(参见图5-1。)
在许多情况下,没有正确的答案。但它可能不是。如果您使用转发来设置代理对象或扩展一个类的功能,显而易见转发机制很可能是继承。如果你希望你的对象作为如果他们真正继承的对象来转发消息,你就需要重新实现respondstoselector:和isKindOfClass: methods方法包括你的转发算法

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if ( [super respondsToSelector:aSelector] )
        return YES;
    else {
        /* Here, test whether the aSelector message can     *
         * be forwarded to another object and whether that  *
         * object can respond to it. Return YES if it can.  */
    }
    return NO;

}

instancesRespondToSelector:该方法用的也是转发算法
conformsToProtocol:如果使用该方法也应该被添加到列表,将任何远程信息的作为接收对象
methodSignatureForSelector:返回最终被转发的消息做出准确的描述,通常用在一个对象能够将消息转发给它的代理

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
       signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}

Runtime源码分析

Working with Classes
Adding Classes
Instantiating Classes
Working with Instances
Obtaining Class Definitions
Working with Instance Variables
Associative References
Sending Messages
Working with Methods
Working with Libraries
Working with Selectors
Working with Protocols
Working with Properties
Using Objective-C Language Features

类与对象基础数据结构

本文介绍了Objective-C 2.0 Runtime库支持的功能和数据结构。该功能在可以在/usr/lib/libobjc.A.dylib共享库的找到具体实现。这个共享库提供了Objective-C语言的动态特性的支持,应用于Objective-C

该文档引用主要用于在实际开发中Objective-C和其他语言之间进行桥接,或低级别的调试。你通常不需要使用Objective-C Runtime 库通过编程直接作用在OC

OS X实现Objective-C Runtime库对于MAC来说是独一无二的。在其他平台,GNU编译器提供不同执行情况但是相似的API集合。本文档只涉及OS X 的实现。

低级别的Objective-C运行时API是在OS X 10.5版本大幅更新。许多功能和所有现有的数据结构都用新的函数来代替。旧的结构和功能中不推荐使用32位、64位模式。API的限制值为32位整型数即使在64位模式的类数,协议数,每个类的方法,每个类的Ivars列表,该类方法的参数,sizeof(所有参数的个数)每个方法和类的版本号。此外,新的Objective-C ABI(这里没有描述)进一步对32位sizeof(...)进行约束

Data Types

Class定义的数据结构

Class:一个不透明的类型,表示一个Objective-C类。

定义:

typedef struct objc_class *Class;

数据结构

struct objc_class {

    Class isa  OBJC_ISA_AVAILABILITY;



#if !__OBJC2__

    Class super_class                       OBJC2_UNAVAILABLE;  // 父类

    const char *name                        OBJC2_UNAVAILABLE;  // 类名

    long version                            OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0

    long info                               OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识

    long instance_size                      OBJC2_UNAVAILABLE;  // 该类的实例变量大小

    struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 该类的成员(实例)变量链表

    struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定义的链表

    struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法缓存

    struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 协议链表

#endif



} OBJC2_UNAVAILABLE;

Discussion:

isa:需要注意的是在Objective-C中,所有的类自身也是一个对象,这个对象的Class里面也有一个isa指针,它指向metaClass(元类),我们会在后面介绍它。
super_class:指向该类的父类,如果该类已经是最顶层的根类(如NSObject或NSProxy),则super_class为NULL。
cache:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。
version:我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。

注意:OBJC2_UNAVAILABLE是一个Apple对Objc系统运行版本进行约束的宏定义,主要为了兼容非Objective-C 2.0的遗留版本,但我们仍能从中获取一些有用信息

objc_method_description数据结构

objc_method_description:定义一个Objetive-C方法

定义

struct objc_method_description { SEL name; char *types; };

字段:
name
在运行时的方法名称

types
该方法参数的类型

objc_method_list数据结构

objc_method_list:包含一切方法的定义

定义

struct objc_method_list { struct objc_method_list *obsolete; int method_count; struct objc_method method_list[1]; }

字段
obsolete
保留字段,供将来使用

method_count
在方法列表数组中指定方法的数量

method_list
一个指向objc_method数据结构数组

objc_cache数据结构

objc_cache:作用于方法调用的性能优化(存储近期使用的方法缓存结构)。其中包含最近使用的方法指针

定义

struct objc_cache { unsigned int mask; unsigned int occupied; Method buckets[1]; };

字段
mask
一个整数,指定分配缓存buckets(块),这里的buckets块总数(减一)。在方法查找,Objective-C Runtime使用该字段通过索引来确定开始线性搜索的buckets数组。使用逻辑和操作(索引=(掩码和选择器))对该方法的selector(选择器)指针进行屏蔽。这是一个简单的哈希算法。

occupied
一个整数,指定占用的缓存buckets(块)总数。

buckets
一个指向Method data structures(方法数据结构)数组。这个数组可以包含不超过mask + 1 项。注意,指针可以为空,表示缓存buckets(块)是空着的,并且被占据的buckets(块)可能不是连续的。这个数组可能会随着时间的推移而增长。

Discussion

为了限制经常访问的方法而把使用过的方法存入Cache
,当需要执行某个方法的时候,通过线性搜索(这个线性搜索算法应该使用的是哈希算法,在buckets字段描述有提到,所以效率才有所提高)Cache,这一操作可以大大减缓方法的查找---Objective-C Runtime 函数存储指针(* Cache)指向objc_cache数据结构

objc_protocol_list数据结构

objc_protocol_list:表示协议列表

定义

struct objc_protocol_list { struct objc_protocol_list *next; int count; Protocol *list[1]; };

字段
next
一个指向另一个objc_protocol_list数据结构指针
count
在此协议列表中的协议的数目

list
表示一个指向Class数据结构的协议数组

objc_method_list数据结构

Discussion

一个正式的协议是一个类定义,它声明了一组方法,该方法是一个类必须实现的。这样的类定义不包含实例变量。一个类定义可以保证实现任意数量的形式化协议。

objc_property_attribute_t数据结构

typedef struct { const char *name; const char *value; } objc_property_attribute_t;

字段
name
属性的名称

value
属性的值,通常为NULL

Instance Data Types

这些数据结构类型表示Object、Classes、superClasses

  • id 指向一个class的实例指针

  • objc_object 表示class的实例

  • objc_super 指定实例的superclass(基类、父类)

objc_object

objc_object:标识类的实例

objc_property_attribute_t数据结构

struct objc_object { Class isa; };

字段
isa

指向该对象的类而定义的一个实例指针

Discussion:
当你创建一个特定类的实例,其分配的内存其中就包含一个objc_object数据结构,它是直接跟随类实例变量的数据。
alloc和allocWithZone:基础框架类NSObject通过使用class_CreateInstance方法来创建objc_object数据结构。

objc_super

objc_super:指定实例的父类(superclass)

objc_super数据结构

struct objc_super { id receiver; Class class; };

字段
指向id标识的指针。特指一个类的实例

Discussion
编译器产生一个object_super数据结构,当它遇到super关键字实例时,作为接收器的信息,它特指父类,应该把消息通知发送给类定义。

class
一个指向Class数据结构的指针。特指实例的父类信息

搜集的知识点小结

  • NS_ASSUME_NONNULL_BEGIN & NS_ASSUME_NONNULL_END
    如果需要每个属性或每个方法都去指定nonnull和nullable,是一件非常繁琐的事。苹果为了减轻我们的工作量,专门提供了两个宏:NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END。在这两个宏之间的代码,所有简单指针对象都被假定为 nonnull,因此我们只需要去指定那些nullable的指针。
  • typedef定义的类型的nullability特性通常依赖于上下文,即使是在Audited Regions中,也不能假定它为nonnull。
    复杂的指针类型(如id )必须显示去指定是nonnull还是nullable。例如,指定一个指向nullable对象的nonnull指针,可以使用”__nullable id __nonnull”。
    我们经常使用的NSError **通常是被假定为一个指向nullable NSError对象的nullable指针。
  • __nullable 和 __nonnull 。从字面上我们可以猜到,__nullable表示对象可以是NULL或nil,而__nonnull表示对象不应该为空。当我们不遵循这一规则时,编译器就会给出警告。
    可以使用const关键字的地方都可以使用__nullable和__nonnull,不过这两个关键字仅限于使用在指针类型上
 - (nullable id)itemWithName:(NSString * nonnull)name

在属性声明中,也增加了两个相应的特性,因此上例中的items属性可以如下声明:

 @property (nonatomic, copy, nonnull) NSArray * items;

当然也可以用以下这种方式:

 @property (nonatomic, copy) NSArray * __nonnull items;

推荐使用nonnull这种方式,这样可以让属性声明看起来更清晰。

  • __kindof
    这修饰符还是很实用的,解决了一个长期以来的小痛点,拿原来的 UITableView 的这个方法来说:
 - (id)dequeueReusableCellWithIdentifier:(NSString *)identifier;

使用时前面基本会使用 UITableViewCell 子类型的指针来接收返回值,所以这个 API 为了让开发者不必每次都蛋疼的写显式强转,把返回值定义成了 id 类型,而这个 API 实际上的意思是返回一个 UITableViewCell 或 UITableViewCell 子类的实例

 - (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier;

既明确表明了返回值,又让使用者不必写强转。再举个带泛型的例子,UIView 的 subviews 属性被修改成了:

 @property (nonatomic, readonly, copy) NSArray<__kindof UIView *> *subviews;
 这样,写下面的代码时就没有任何警告了:
 
 UIButton *button = view.subviews.lastObject;
  • attribute((always_inline))
    我们知道一般的函数调用都会通过call的方式来调用,这样让攻击很容易对一个函数做手脚,如果是以inline的方式编译的会,会把该函数的code拷贝到每次调用该函数的地方。而static会让生成的二进制文件中没有清晰的符号表,让逆向的人很难弄清楚逻辑。

attribute((always_inline)) 的意思是强制内联,所有加了attribute((always_inline)) 的函数再被调用时不会被编译成函数调用而是直接扩展到调用函数体内,比如定义了函数
attribute((always_inline)) void a()

 void b(){
 a();
 }

b 调用 a 函数的汇编代码不会是跳转到a执行,而是 a 函数的代码直接在 b 内成为 b 的一部分。

  • define __inline attribute((always_inline))

__inline 代替attribute((always_inline))
内声明a的时候可以直接写成__inline void a() 这样比较方便于attribute((always_inline))

  • undef
    这是预编译指令,和#define搭配使用,意思是取消之前的宏定义。
 #define PROC_ADD
 void main(void)
 {
 #ifdef PROC_ADD
 // Do this code here then undefined it to run the code in the else
 // processing work
 #undef PROC_ADD
 #else
 // now that PROC_ADD has been undefined run this code
 // processing work
 #endif
 }
  • @private

实例变量只能被声明它的类访问.

  • @protected

实例变量能被声明它的类和子类访问,所有没有显式制定范围的实例变量都是.

  • @public

实例变量可以被在任何地方访问.

  • @package
    @private outside.使用modern运行时,一个@package实例变量在实现这个类的可执行文件镜像中实际上是@public的,但是在外面就是
  • @private

Objective-C中的@package与C语言中变量和函数的private_extern类似。任何在实现类的镜像之外的代码想使用这个实例变量都会引发link error,这个类型最常用于框架类的实例变量,使用@private太限制,使用@protected或者@public又太开放.

写在最后YYModel源码分析下篇

YYModel介绍是这样的高性能 iOS/OSX 模型转换框架

特性

高性能: 模型转换性能接近手写解析代码。
自动类型转换: 对象类型可以自动转换,详情见下方表格。

  • 类型安全: 转换过程中,所有的数据类型都会被检测一遍,以保证类型安全,避免崩溃问题。
  • 无侵入性: 模型无需继承自其他基类。
  • 轻量: 该框架只有 5 个文件 (包括.h文件)。
  • 文档和单元测试: 文档覆盖率100%, 代码覆盖率99.6%。

你可能感兴趣的:(Objective-C Runtime - 基础篇)