互联网科技

【js3311com金沙网站】《Effective Objective-C 2.0》读书笔记(二)

作者:金沙国际官网    发布时间:2020-05-04 23:27     浏览次数 :178

[返回]

由上面一章中,我们了解了什么是RunTime的数据结构,下面了解一下Runtime的消息发送。

简要简介

Objective-C是基于C语言加入了面向对象特性和消息转发机制的动态语言,这意味着它不仅需要一个编译器,还需要Runtime系统来动态创建类和对象,进行消息发送和转发。

当执行[object doSomething]会被编译器转化为:objc_msgSend(receiver, selector) 如果消息含有参数,则为,objc_msgSend(receiver, selector, arg1, arg2, ...)

如果消息的接受者能够找到对应的selector,那么就直接执行这个方法;否则,消息要么被转发,或者临时向接受者动态添加这个selector对应的实现内容;如果最后没有找到任何解决办法,就干脆崩溃掉。

最后,可以看出[object doSomething]真不是一个简简单单的方法调用。因为这只是在编译阶段确定了要向接受者发送doSomething这条信息,而object如何响应这条信息,那就要看运行时发生的情况来决定了。

iOS RunTime之二:数据结构
objc_class、元类(Meta Class)、SEL、IMP、Method、Ivar

SEL:Objective-C在编译时,会依据每个方法的名字、参数序列,生成一个唯一的整型标识(int类型的地址)这个标识就是SEL。在本质上,SEL只是一个指向方法的指针(被hash化得KEY值),能提高方法的查询速度。

IMP:就是Implementation的缩写,本质就是一个函数指针,这个被指向的函数包含一个接收消息的对象id,调用方法的SEL,以及一些方法参数,并返回一个id。因此我们可以通过SEL获得它所对应的IMP,在取得了函数指针之后,也就意味着我们取得了需要执行方法的代码入口,这样我们就可以像普通的C语言函数调用一样使用这个函数指针。

Method

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
} 

方法名 类型为SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
方法类型method_types是个char指针,其实存储着方法的参数类型和返回值类型。
method_imp指向了方法的实现,本质上是一个函数指针。

Ivar:是一种代表类中实例变量的类型。

struct objc_ivar {
    char *ivar_name                                          OBJC2_UNAVAILABLE;
    char *ivar_type                                          OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}

Cache:是一个存储Method的链表,主要是为了优化方法调用的性能。

iOS RunTime之三:消息发送

objc_msgSend它具体是如何发送消息:

  • 检查selector是否需要忽略。
  • 检查target是否为nil。如果为nil,直接cleanup,然后return。这就是我们可以向nil发送消息的原因。
  • 然后再target的Class中根据Selector去找IMP。
  • 寻找IMP的过程:
    1、先从当前class的cache方法列表(cache methodLists)中去找;
    2、找到了,就调到对应函数实现;
    3、没找到,没就从class的方法列表(methodLists)中找;
    4、还没找到,就到super class中重复上述步骤找,直到找到基类(NSObject)为止
    5、最后再找不到,就会进入动态方法解析和消息转发的机制。

理解:
Objective-C的动态绑定:消息的发送其实就是先确定object接受者对象,然后根据isa指针查找其方法然后跳转过去并执行。但是编译期间,是无法确定object接受者对象。只有在程序运行期间,object接受者对象才能得到确定。<u>这种在运行期间才确定object接受者对象,Objective-C称为动态绑定</u>。

C++或者Java调用对象的函数,函数与对象之间的关系,在编译期间就必须严格确定。

消息发送的机制使得在不重新编译的情况下,在运行期间,干预或者说hook原来的target(方法、变量等)变得更易于实现,更有实际应用价值,这个是需要依赖于消息发送和动态绑定的实现机制——Runtime。

iOS RunTime之四:消息转发

消息发送和消息转发流程可以概括为:消息发送是 Runtime 通过 selector 快速查找 IMP 的过程,有了函数指针就可以执行对应的方法实现;<u>消息转发是在查找 IMP 失败后执行一系列转发流程的慢速通道</u>,如果不作转发处理,则会打日志和抛出异常。

消息转发过程:

1、动态方法解析

+ (BOOL)resolveInstanceMethod:(SEL)sel;
+ (BOOL)resolveClassMethod:(SEL)sel;

举例:
void dynamicMethodIMP(id self, SEL _cmd){
    //实现...
}
+ (BOOL)resolveInstanceMethod:(SEL)aSEL{
    if(aSEL == @selector(resolveThisMethodDynamically)){
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}

2、重定向

- (id)forwardingTargetForSelector:(SEL)aSelector;

在消息转发机制执行前,Runtime 系统会再给我们一次偷梁换柱的机会,即通过重载- (id)forwardingTargetForSelector:(SEL)aSelector方法替换消息的接受者为其他对象:

- (id) forwardingTargetForSelector:(SEL)aSelector{
    if(aSelector == @selector(mysteriousMethod:)){
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

3、转发

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;

实现转发简单代码
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSString *sel = NSStringFromSelector(aSelector);
    if ([sel isEqualToString:@"fly"]) {
        //signatureWithObjcTypes 手动生成签名
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector: aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    Car *car = [[Car alloc] init];
    if([car respondsToSelector:[anInvocation selector]]){
        [anInvocation  invokeWithTarget:car];
    }
}

1、methodSignatureForSelector用来生成方法签名,这个签名就是给forwardInvocation中的参数NSInvocation调用的。

2、unrecognized selector sent to instance,原来就是因为methodSignatureForSelector这个方法中,由于没有找到fly对应的实现方法,所以返回了一个空的方法签名,最终导致程序报错崩溃。

js3311com金沙网站 1

消息转发流程简图

iOS RunTime之五:Category不能动态添加成员变量

  • 为什么Category中不能动态添加成员变量?

在runtime函数中,确实有一个class_addIvar()函数用于给类添加成员变量,但是阅读过苹果的官方文档的人应该会看到:

This function may only be called after objc_allocateClassPair and before objc_registerClassPair. Adding an instance variable to an existing class is not supported.

大概的意思说,这个函数只能在“构建一个类的过程中”调用。一旦完成类定义,就不能再添加成员变量了。经过编译的类在程序启动后就被runtime加载,没有机会调用addIvar。程序在运行时动态构建的类需要在调用objc_registerClassPair之后才可以被使用,同样没有机会再添加成员变量。

  • 为什么不能为一个类动态的添加成员变量,可以给类动态增加方法和属性?

因为方法和属性并不“属于”类实例,而成员变量“属于”类实例。我们所说的“类实例”概念,指的是一块内存区域,包含了isa指针和所有的成员变量。所以假如允许动态修改类成员变量布局,已经创建出的类实例就不符合类定义了,变成了无效对象。但方法定义是在objc_class中管理的,不管如何增删类方法,都不影响类实例的内存布局,已经创建出的类实例仍然可正常使用。

iOS RunTime之六:Category

代码介绍:

在runtime.h中查看定义:

typedef struct objc_category *Category;

是一个objc_category结构体,定义如下:

struct objc_category {
    char *category_name                                      OBJC2_UNAVAILABLE;
    char *class_name                                         OBJC2_UNAVAILABLE;
    struct objc_method_list *instance_methods                OBJC2_UNAVAILABLE;
    struct objc_method_list *class_methods                   OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
}        

objc源代码,在objc-runtime-new.h中我们可以发现:

struct category_t { 
    const char *name; //是指 class_name 而不是 category_name
    classref_t cls;  //要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对应到对应的类对象
    struct method_list_t *instanceMethods; //category中所有给类添加的实例方法的列表
    struct method_list_t *classMethods;//category中所有给类添加的实例方法的列表
    struct protocol_list_t *protocols;//category实现的所有协议的列表
    struct property_list_t *instanceProperties;
    //表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject
    //和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。
};

从上面的category_t的结构体中可以看出,分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性,不可以添加成员变量。

Category和Extension的区别
1、Extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。Extension一般用来隐藏类的私有信息,你必须有一个类才能为这个类添加Extension,所以你无法为系统的类比如NSString添加Extension。
2、Category则完全不一样,它是在运行期决议的。
3、Extension可以添加属性、成员变量,而Category一般不可以。
总之,就Category和Extension的区别来看,Extension可以添加实例变量,而Category是无法添加实例变量的。因为Category在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的。

面试题
一般面试官有时候会问到这样的问题:
在类和Category中都可以有study方法,那么有两个问题:

  • 在类的study方法调用的时候,我们可以调用Category中声明的study方法么?
  • 如果一个类有多个分类的时候study方法,调用顺序是咋样的呢?

答:study方法的执行顺序是先类,后Category,而Category的study方法执行顺序是根据编译顺序决定的。

Category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果Category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA

Category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的Category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休,殊不知后面可能还有一样名字的方法。

js3311com金沙网站 2

我们知道[object doSomething]被编译器转化为:

xiu!

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

关键词:消息传递、消息转发机制、方法调配、类对象

Objective-C中,消息直到运行时才会绑定到方法的实现上。编译器会把代码中[object doSomething]转换成objc_msgSend消息函数,这个函数完成了动态绑定的所有事情,它的运行流程如下:


  • 检查selector是否需要忽略。
  • 检查target是否为nil。如果为nil,直接cleanup,然后return。(这就是我们可以向nil发送消息的原因。)
  • 然后在targetClass中根据Selector去找IMP

对象等同性

上一篇:没有了
下一篇:没有了