转载自这里

最近看了一本书——iOS6 programming Pushing the Limits(亚马逊有中文版),最后一章是关于Deep ObjC的,主要内容是ObjC的runtime。虽然之前看过runtime的programming guide,但读之乏味也不知道能用在何处。现在有点小小的理解,觉得别有乾坤,索性把runtime的相关东西给整理一下。 下面就从官方文档开始,看看runtime有哪些特性,以及各自的应用场合。

基本概念

对于现在绝大多数的64位操作系统而言,我们接触到的都是ObjC2.0的modern runtime。ObjC程序从3个层次来使用到runtime:

1.ObjC源码

这说明了runtime是ObjC的基石,你定义的类/方法/协议等等,最后都需要使用到runtime。其中,最重要的部分就是方法的messaging。

2.ObjC方法(Method)

绝大多数ObjC都继承自NSObject,他们都可以在运行的时候检查属于/继承哪个类,某个对象是否有某个方法,是否实现了某个协议等等。这一部分是编程时,经常会使用到的。

3.ObjC函数(Function)

Runtime相关的头文件在: /usr/include/objc中,我们可以使用其中定义的对象和函数。通常情况下,我们很少会使用到。但个别情况我们可能需要使用,比如swizzling。此外,这些纯C的实现说明了我们可以用C来实现ObjC的方法。

Messaging

之前说过,所有的ObjC方法最后都通过runtime实现,这都是通过调用函数objc_msgSend. 也就是说诸如: [receiver doSomething] 的调用最终都是展开调用objc_msgSend完成的。 在此之前,先看下ObjC的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;
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;

其中:

typedef struct objc_class *Class;

因为现在的objc是2.0,所以上述的Class可以简化为:

struct objc_class {
Class isa;
}

Class只是一个包含了指向自身结构体的isa指针的结构体,虽然这个结构体具体的内容没有找到定义,但是根据头文件里的写法我们可以猜测,它必定还包含父类,变量,方法,协议等信息(最新的runtime信息可以在opensource中查看)。 而objc_msgSend定义在Message.h文件里:

id objc_msgSend(id theReceiver, SEL theSelector, ...)
  • theReceiver: 处理该消息的对象
  • theSelector: 处理该消息的方法
  • ...: 消息需要的参数
  • id: 消息完成后的返回值。

文档中提到:

When it encounters a method call, the compiler generates a call to one of the functions objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, or objc_msgSendSuper_stret. Messages sent to an object’s superclass (using the super keyword) are sent using objc_msgSendSuper; other messages are sent using objc_msgSend. Methods that have data structures as return values are sent using objc_msgSendSuper_stret and objc_msgSend_stret.

从函数类型和说明可以知道,最关键的就是要获得selector。Selector本质上是一个函数指针,有了这个指针就能执行相应的程序。当某一个对象实例化后,首先通过isa指针来访问自身Class的信息,寻找相应的selector的地址。如果找不到,那就可以通过指向父类的指针遍历父类的selector的地址,如此这般,直到根类,如下图:

Messaging Framework

大致原理就是如此,当然为了提高速度,objc_msgSend是做了很多优化的。知道了这些,我们就可以自己实现一个objc_msgSend,所需要的关键无非是:调用对象,执行函数(获得函数指针的地址即可),以及相应的参数。iOS6PTL最后部分有相应的说明,这里就不多说,把代码发出来:

//MyMsgSend.c
#include <stdio.h>
#include <objc/runtime.h>
#include "MyMsgSend.h" static const void *myMsgSend(id receiver, const char *name) {
SEL selector = sel_registerName(name);
IMP methodIMP =
class_getMethodImplementation(object_getClass(receiver),
selector);
return methodIMP(receiver, selector);
} void RunMyMsgSend() {
// NSObject *object = [[NSObject alloc] init];
Class class = (Class)objc_getClass("NSObject");
id object = class_createInstance(class, 0);
myMsgSend(object, "init"); // id description = [object description];
id description = (id)myMsgSend(object, "description"); // const char *cstr = [description UTF8String];
const char *cstr = myMsgSend(description, "UTF8String"); printf("%s\n", cstr);
}

方法的动态实现(Dynamic Method Resolution)

有了上面的基础,我们就很容易给类在runtime添加方法。比如,objc中有dynamic的属性关键字(使用过coredata的都知道),这个就提示该属性的方法在运行时提供。在运行时添加方法,只要实现:

+ (BOOL)resolveInstanceMethod:(SEL)sel
//相应的也存在+ (BOOL)resolveClassMethod:(SEL)sel
{
DLog(@"");
if (sel == @selector(xxx))
{
class_addMethod(.....);
return YES;
} return [super resolveInstanceMethod:sel];
}

在调用的时候使用 performSelector:方法,或者直接调用某个定义过但是没有实现的方法,resolveInstanceMethod都会被出发进行方法查找,下图是运行时的调用栈信息:

可以看到runtime依次调用了两个函数来查找selector,当它在类以及父类中没有找到时,就会调用resolveInstanceMethod。

动态加载(Dynamic Loading)

(这部分主要侧重于Mac OS 系统) 我们知道category是在第一次使用到的时候添加到class的,因此objc也提供了动态添加class的机制。比如OS的系统偏好里的一些设置就是通过动态添加实现的,当然还有插件系统。 runtime提供了相应的函数(objc/objc-load.h),但对于cocoa系统,我们可以使用NSBundle来更好的操作。下面简单的说一下步骤:

  1. 新建一个cocoa的工程,选择bundle模板;
  2. 新建一个class,然后添加一个方法并实现之;
  3. 修改plist文件,在principle class一行将新建的class名填进去;
  4. build工程,然后在Finder里找到bundle;
  5. 新建一个测试bundle的工程,模板任选(可以选择application)
  6. 把之前的bundle文件添加的测试工程,然后添加相应的代码:
    - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
    {
    // Insert code here to initialize your application NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"DynamicClassBundle" ofType:@"bundle"];
    NSBundle *bundle = [NSBundle bundleWithPath:bundlePath];
    if (bundle)
    {
    Class principleClass = [bundle principalClass];
    if (principleClass)
    {
    id bundleInstance = [[principleClass alloc] init];
    [bundleInstance performSelector:@selector(print) withObject:nil withObject:nil];
    }
    }
    }

消息路由(Message Forwarding)

向一个对象发送未定义的消息时,程序往往会奔溃。其实,在崩溃前,runtime还做了一些工作:

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

使用forwardInvocation的话,上述两个方法都要实现。runtime先寻找是否存在方法签名(NSMethodSignature),如果找到了再去执行forwardInvocation。注意在这里,消息的参数(假设存在的话)没有出现,这就说明被runtime通过某种方式保存起来了。当然我们可以通过获得的NSInvocation来修改。 这是常规的消息路由方式,runtime也提供了“捷径”:

- (id)forwardingTargetForSelector:(SEL)aSelector
{
}

这种方式可以直接把消息传递给需要(能够)处理的对象,而且这种方式比上述forwardInvocation要快,引用文档的话说:

This method gives an object a chance to redirect an unknown message sent to it before the much more expensive forwardInvocation: machinery takes over. This is useful when you simply want to redirect messages to another object and can be an order of magnitude faster than regular forwarding. It is not useful where the goal of the forwarding is to capture the NSInvocation, or manipulate the arguments or return value during the forwarding.

可见单纯转发可以用这种方式,但是如果要纪录NSInvocation或者改变参数之类的,就要用forwardInvocation。 消息转发模拟了多继承(ObjC本身是不支持多继承),可以在子类调用父类的父类的实现;当然也提供了调用任意类的方法的途径。Cocoa中有Distributed Object就利用了这种特性,它可以在一个application中使用另一个application(甚至是运行在同一网络中不同电脑上的application)中定义的对象。这部分暂时放一放,有兴趣的可以深入。

类型编码(Type Encodings)

看一下动态添加方法到类的函数:

class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)

注意最后一个参数,为了支持runtime,编译器需要知道每一个参数的类型,因此预先定义了相应的字符。这个types所代表的意思的含义依次是:

返回值,receiver类型,SEL,参数1,。。。参数n

具体的类型定义参见官方文档,由此我们可以得知该参数的第二和第三为参数必定是"@:"。

属性声明(Declare Properties)

如果可以在runtime的时候获得类的属性,这将会很有用处,比如对json数据序列化。runtime提供了相应的函数来实现:

unsigned int propertyCount = 0;
objc_property_t *propertyArray = class_copyPropertyList([MyClass class], &propertyCount);
NSLog(@"property of MyClass:");
for (int i = 0; i < propertyCount; i++)
{
objc_property_t property = propertyArray[i];
fprintf(stdout, "%s : %s\n",property_getName(property),property_getAttributes(property));
} propertyCount = 0;
propertyArray = class_copyPropertyList([MyChildClass class], &propertyCount);
NSLog(@"property of MyChildClass:");
for (int i = 0; i < propertyCount; i++)
{
objc_property_t property = propertyArray[i];
fprintf(stdout, "%s : %s\n",property_getName(property),property_getAttributes(property));
}

runtime只会获取当前类的属性——父类的以及扩展里实现的属性都不能通过这样的方式获取。property_getAttributes获得的属性的“属性”会以如下的形式:

T<类型>,Attribute1,...AttributeN,V_propertyName

其中的Attibute是属性的类型编码,具体的在官方文档。 这些就是runtime的基本内容,好像有点枯燥,平时也不怎么用的上。最初我也觉得是,不过隐约的感觉runtime大有用武之地。让我们接下去一起慢慢发掘吧。

ObjC之RunTime(上)的更多相关文章

  1. ObjC之RunTime(下)

    之前通过学习官方文档对runtime有了初步的认识,接下来就要研究学习runtime到底能用在哪些地方,能如何改进我们的程序. 本文也可以从icocoa浏览. Swizzling Swizzling可 ...

  2. 利用objc的runtime来定位次线程中unrecognized selector sent to instance的问题

    昨天遇到一个仅仅有一行错误信息的问题: -[NSNull objectForKey:]: unrecognized selector sent to instance 0x537e068 因为这个问题 ...

  3. 由objC运行时所想到的。。。

    objC语言不仅仅有着面向对象的特点(封装,继承和多态),也拥有类似脚本语言的灵活(运行时),这让objC有着很多奇特的功能-可在运行时添加给类或对象添加方法,甚至可以添加类方法,甚至可以动态创建类. ...

  4. runtime 运行机制2

    Mike_zh QQ:82643885 end: blogTitle 博客的标题和副标题 博客园 首页 新随笔 联系 订阅 <a id="MyLinks1_XMLLink" ...

  5. iOS学习之Runtime(一)

    一.Runtime简介 因为Objective-C是一门动态语言,所以它总是想办法把一些决定性工作从编译链接推迟到运行时,也就是说只有编译器是不够的,还需要一个运行时系统(runtime system ...

  6. KVO的使用三:基于runtime实现KVO

    苹果的KVO原理通过isa-swizzling技术实现,本质实现逻辑是在runtime时添加一个子类,重写set方法进行操作,现在我们也基于runtime来实现一个KVO. 首先新建一个Person类 ...

  7. iOS上Delegate的悬垂指针问题

    文章有点长,写的过程很有收获,但读的过程不一定有收获,慎入 [摘要]   悬垂指针(dangling pointer)引起的crash问题,是我们在iOS开发过程当中经常会遇到的.其中由delegat ...

  8. Runtime - ② - NSObject类

    首先,我们都知道NSObject是大多数类的根类,但是,这个类的是怎么实现的呢?我们可以去下载开源的Runtime源码,探究下NSObject类的实现. 1. NSObject.h文件 我们可以直接使 ...

  9. Runtime介绍

    本文目录 1.Runtime简介 2.Runtime相关的头文件 3.技术点和应用场景 3_1.获取属性\成员变量列表 3_2.交换方法实现 3_3.类\对象的关联对象,假属性 3_4.动态添加方法, ...

随机推荐

  1. 排序算法Nb三人组-归并排序

    归并排序只能对两个已经有序的列表进行合并排序,所以要我们自己创建出两个有序列表.最后在进行合并. def merge2list(li1, li2): li = [] i = 0 j = 0 while ...

  2. javascript刷新页面的集中办法

    1. history.go(0) 2. location.reload() 3. location=location 4. location.assign(location) 5. document. ...

  3. 【Python】Python3基本语法入门学习

    0.Python概述 1.First Word Game 2.变量与字符串 3.improved game 4.Python数据类型 5.常用操作符 6.分支与循环 7.列表 8.元组 9.字符串内置 ...

  4. EF多实体对应单表

    1.EF多实体对应单表 适用场景:单数据库表,表数据有较长用字段,有不常用或者大数据字段. 2.建表语句 CREATE TABLE [Chapter2].[Photograph]( ,) primar ...

  5. day 15

    1.input标签默认内容 Title <input value="默认内容"/> <hr /> <textarea>默认内容</text ...

  6. C++ *this与this的区别(系个人转载,个人再添加相关内容)

    转载地址:http://blog.csdn.net/stpeace/article/details/22220777 return *this返回的是当前对象的克隆或者本身(若返回类型为A, 则是克隆 ...

  7. 【NLP_Stanford课堂】文本分类2

    一.实验评估参数 实验数据本身可以分为是否属于某一个类(即correct和not correct),表示本身是否属于某一类别上,这是客观事实:又可以按照我们系统的输出是否属于某一个类(即selecte ...

  8. 【Leetcode】【Easy】Reverse Linked List

    题目: Reverse a singly linked list. 解题: 反转单链表,不再多介绍了. 如果会“先条件->定参数->确定不变式->验证后条件”的思维方法,一定会bug ...

  9. Ubuntu下python的第三方module无法在pycharm中导入

    换了台笔记本,新安装的requests module无法在pycharm导入: Traceback (most recent call last): File "/home/winsterc ...

  10. np.arrange用法

    np.arange()经常用,其用法总结如下: np.arange(0,60,2) 生成从0到60的步距为2的数组,其中0为初始值,60为终值,2步距, np.arange(60) 生成从0到59的默 ...