ObjC之RunTime(上)
转载自这里。
最近看了一本书——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来更好的操作。下面简单的说一下步骤:
- 新建一个cocoa的工程,选择bundle模板;
- 新建一个class,然后添加一个方法并实现之;
- 修改plist文件,在principle class一行将新建的class名填进去;
- build工程,然后在Finder里找到bundle;
- 新建一个测试bundle的工程,模板任选(可以选择application)
- 把之前的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(上)的更多相关文章
- ObjC之RunTime(下)
之前通过学习官方文档对runtime有了初步的认识,接下来就要研究学习runtime到底能用在哪些地方,能如何改进我们的程序. 本文也可以从icocoa浏览. Swizzling Swizzling可 ...
- 利用objc的runtime来定位次线程中unrecognized selector sent to instance的问题
昨天遇到一个仅仅有一行错误信息的问题: -[NSNull objectForKey:]: unrecognized selector sent to instance 0x537e068 因为这个问题 ...
- 由objC运行时所想到的。。。
objC语言不仅仅有着面向对象的特点(封装,继承和多态),也拥有类似脚本语言的灵活(运行时),这让objC有着很多奇特的功能-可在运行时添加给类或对象添加方法,甚至可以添加类方法,甚至可以动态创建类. ...
- runtime 运行机制2
Mike_zh QQ:82643885 end: blogTitle 博客的标题和副标题 博客园 首页 新随笔 联系 订阅 <a id="MyLinks1_XMLLink" ...
- iOS学习之Runtime(一)
一.Runtime简介 因为Objective-C是一门动态语言,所以它总是想办法把一些决定性工作从编译链接推迟到运行时,也就是说只有编译器是不够的,还需要一个运行时系统(runtime system ...
- KVO的使用三:基于runtime实现KVO
苹果的KVO原理通过isa-swizzling技术实现,本质实现逻辑是在runtime时添加一个子类,重写set方法进行操作,现在我们也基于runtime来实现一个KVO. 首先新建一个Person类 ...
- iOS上Delegate的悬垂指针问题
文章有点长,写的过程很有收获,但读的过程不一定有收获,慎入 [摘要] 悬垂指针(dangling pointer)引起的crash问题,是我们在iOS开发过程当中经常会遇到的.其中由delegat ...
- Runtime - ② - NSObject类
首先,我们都知道NSObject是大多数类的根类,但是,这个类的是怎么实现的呢?我们可以去下载开源的Runtime源码,探究下NSObject类的实现. 1. NSObject.h文件 我们可以直接使 ...
- Runtime介绍
本文目录 1.Runtime简介 2.Runtime相关的头文件 3.技术点和应用场景 3_1.获取属性\成员变量列表 3_2.交换方法实现 3_3.类\对象的关联对象,假属性 3_4.动态添加方法, ...
随机推荐
- MySQL行(记录)详细操作
一 介绍 MySQL数据操作: DML ======================================================== 在MySQL管理软件中,可以通过SQL语句中的 ...
- mui.ajax()和asp.net sql服务器数据交互【1】
简单的ajax和asp.net的交互,例如遍历数据,前端显示复杂内容没有添加代码,可自行研究!非常适合懂那么一点点的我们! 实现步骤: 1.APP前端HTML: <div class=" ...
- es6的一些基本语法
首先说一下什么是es6: ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准; let 和 const 命令 let的基本用法: 上面代码在代码块之中,分别用l ...
- vue.js与angular.js的区别(个人)
刚进入实训 讲师就要发一些什么比较高大上的东西,本人才疏学浅 浅浅的分享一下angularjs 和vue.js的区别.只是简单的理解一下 大神勿喷. 生实训之前学习的angular.js 只是理解了 ...
- Django—XSS及CSRF
一.XSS 跨站脚本攻击(Cross Site Scripting),为不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS.恶意攻击者往W ...
- Open images from USB camera on linux using V4L2 with OpenCV
I have always been using OpenCV's VideoCapture API to capture images from webcam or USB cameras. Ope ...
- Codeforces Round #416 (Div. 2) A+B
A. Vladik and Courtesy 2 seconds 256 megabytes At regular competition Vladik and Valera won a and ...
- nginx 两台机器 出现退款失败问题
今天早上来公司后,测试人员告诉我 退款失败了.上周五还好好的,怎么这周三就出问题了,赶快让测试发来订单号,查询数据库,查询日志,发现还是以前的问题: search hit TOP, continuin ...
- writing a javascript module ready for ES6 import
javascript模块化是一个比较大也是比较容易混淆的topic.通常几乎所有的第三方Library都支持CMD,AMD,ES6,Global object方式来引用lib所暴露出来的服务. 那么如 ...
- U盘中毒了?教你如何删除System Volume Information这个顽固文件夹
不得不说cmd命令很好用呢.最近我的U盘中毒了,格式化都删除不了System Volume Information这个顽固的文件夹,真心伤不起哇!还好现在解决了问题.看来以后得好好对待U盘,不能乱用了 ...