热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

ObjectiveC运行时

Objective-C运行时可以干什么利用运行时,我们可以做一些OC不容易实现的功能,比如:动态交换两个方法的实现(特别是交换系统自带的方法)动态添加对象的成员变量和成员方法获得某

Objective-C运行时可以干什么

利用运行时,我们可以做一些OC不容易实现的功能,比如:

  • 动态交换两个方法的实现(特别是交换系统自带的方法)
  • 动态添加对象的成员变量和成员方法
  • 获得某个类的所有成员方法、所有成员变量

由此我们可以实现:

1.将某些OC代码转为运行时代码,探究底层,比如block的实现原理(上边已讲到);

2.拦截系统自带的方法调用(Swizzle 黑魔法),比如拦截imageNamed:、viewDidLoad、alloc;

3.实现分类也可以增加属性;

4.实现NSCoding的自动归档和自动解档;

5.实现字典和模型的自动转换。

运行时这么厉害,那么运行时是什么呢?

Objective-C运行时是什么

Objective-C运行时本质上就是一个库,它负责了Objective(面向对象)这个部分,因此您所知的、所爱的面向对象编程,都是在这里实现的。如果您想要访问里面的函数的话,只需要导入这个库即可。

#import

它主要由C和汇编编写而成,其实现了诸如类、对象、方法调度、协议等等这些东西。

运行时中对象是什么

运行时负责 Objective-C 中的面向对象编程这个部分。让我们从基本的构建模块开始。那么什么是对象呢?对象在 runtime.h 当中是这样定义的:

typedef struct objc_class *Class;
struct objc_object {
Class isa;
};

对象只与一个类建立引用关联,也就是这个 isa 的意思所在。这也就是 Objective-C 当中的所有对象都需要实现的。

运行时中类是什么

那么类又是什么呢?类的定义要稍微复杂一些。

struct objc_class {
Class isa;
Class super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols;
};

类当中同样有 isa 这个值。它与 super_class 这个值进行关联。除了 NSObject 这个类之外,super_class 的值永远不会为 nil,因为 Objective-C 当中的其余类都是以某种方式继承自 NSObject 的。之后,我们还有 nameversioninfo 之类的值,不过这些并不是我们感兴趣的内容。

对于我们而言,更多的应该是关注变量列表 (ivars)、方法列表 (methodLists) 和这个协议列表 (protocols)。这些就是我们能在运行时修改和读取的。可以看到,对象其实本质上是一个非常简单的结构体,类同样也是。我们可以借助运行时函数,从而在运行时动态创建类。

Class myClass =
objc_allocateClassPair([NSObject
class], "MyClass", 0);
// 在这里添加变量、方法和协议
objc_registerClassPair(myClass);
// 当类注册之后,变量列表将会被锁定
[[myClass alloc] init];

这就是我们要用的 Objective-C 运行时函数:allocateClassPair。我们为其提供一个 isa,在本例当中我们提供了 NSObject,然后为其命名。第三个参数则是额外字节的定义,通常我们都直接赋值 0 即可。随后我们就可以添加变量、方法以及协议了,之后就注册这个 ClassPair。注册之后,我们就无法修改变量列表了,不过其余的内容仍然可以修改。

结束~

我们所创建的这个类和其余的 Objective-C 类毫无区别。

借助运行时,我们可以使用 setAssociatedObjectgetAssociatedObject 这两个函数,向既有的类别当中添加存储属性。

对于不是自己创建的类而言,使用这个方法进行扩展无疑是非常好用的。

内省机制

接下来我们要介绍的,便是判别这个类能执行何种操作。这就是所谓的「内省 (introspection)」机制。通常,我们所使用的往往是最基础的内省功能。

[myObject isMemberOfClass:NSObject.class];
[myObject respondsToSelector:@selector(doStuff:)];
// isa == class
class_respondsToSelector(myObject.class, @selector(doStuff:));

首先是这个 isMemberOfClass,这是 Foundation 当中的一部分,这里我们查看 myObject 是否是 NSObject 的子类。接下来是这个 respondsToSelector:,当我们使用了一个带有可选方法的协议时,为了避免崩溃发生,可以借助这个函数来判断这个对象是否可以调用此可选方法。在运行时层面,isMemberOfClass 对比两者的 isa 是否相同。respondsToSelector" 则封装了一个 Objective-C 运行时函数:respondsToSelector,其接受 Selector 和类为参数。

如果您写过单元测试的话,您就会知道当我们在编写 XCTestCase 的时候,需要完成 setUptearDown 的设定,随后才能编写相关的 test 函数。当测试运行的时候,系统会自行遍历所有的测试函数,并自动运行。这个功能是借助 Objective-C 的运行时机制实现的。

unsigned int count;
Method *methods = class_copyMethodList(myObject.class,
&count);
//Ivar *list = class_copyIvarList(myObject.class,
&count);
for(unsigned i = 0; i SEL selector = method_getName(methods[i]);
NSString *selectorString =
NSStringFromSelector(selector);
if ([selectorString containsString:@"test"]) {
[myObject performSelector:selector];
}
}
free(methods);

我们可以复制方法列表,如果需要的话,还可以复制变量列表。可以获取方法名,然后将其转换为字符串,检查其是否包含有 “test”,如果有便可以运行。现在我们便搭建好了 XCTest 的最简单版本!

运行时的变量和方法

那么变量和方法是由什么组成的呢?

struct objc_ivar {
char *ivar_name;
char *ivar_type;
int ivar_offset;
}
struct objc_method {
SEL method_name;
char *method_types;
IMP method_imp;
}

变量的组成与我们实际在代码当中所定义差别不大。其中包含了变量类型和变量名称。偏移量 (offset) 则是内存管理方面的内容。

Objective-C 方法的名称则是通过 Selector 来表示的,这也就是我们在 performSelector 当中所匹配的内容。同样,方法还用编码字符串来表示其类型。之后便是方法的实现,它使用了一种特定的表示方式,对此我们不必去深究。

因此,方法是非常简单的,我们同样可以在运行时向对象当中添加方法。

Method doStuff = class_getInstanceMethod(self.class, @selector(doStuff));
IMP doStuffImplementation = method_getImplementation(doStuff);
const char *types = method_getTypeEncoding(doStuff); //“v@:@"
class_addMethod(myClass.class, @selector(doStuff:), doStuffImplementation, types);

实现这个功能,我们需要用到 class_addMethod 这个函数。它所需的参数全都是我们之前所说的,方法结构体当中的那三个值:Selector、方法实现和方法类型。具体的方法实现部分我们取了个巧,因为我们使用了既有的 doStuff 方法,因此能够很简单地获取其方法实现和方法类型,不过我们还可以用其他方法来完成。

当然,我们添加了方法目的就是要使用它们。我们可以使用 [self doStuff] 或者 [self performSelector:@selector(doStuff)] 来进行调用,实际上在运行时级别,它们都是借助 objc_msgSend 向对象发送了一个消息。

[self doStuff];
[self performSelector:@selector(doStuff)];
objc_msgSend(self, @selector(message));

但是如果调用方法所在的对象为 nil 的时候,我们就会得到一个异常,应用便会崩溃。但事实证明,在崩溃之前会预留几个步骤,从而允许我们对某个不存在的函数进行一些操作。

方法转发:

我们可以将方法转发 (forward) 给其余目标。当我们试图桥接两个不同的框架的时候,这个功能便非常有用。当我们调用某个未实现的方法时,这便是会发生的操作。

// 1
+(BOOL)resolveInstanceMethod:(SEL)sel{
// 添加实例方法并返回 YES 的一次机会,它随后会再次尝试发送消息
}
// 2
- (id)forwardingTargetForSelector:(SEL)aSelector{
// 返回可以处理 Selector 的对象
}
// 3
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
// 您需要实现它来创建 NSInvocation
}
- (void)forwardInvocation:(NSInvocation *)invocation {
// 在您所选择的目标上调用 Selector
[invocation invokeWithTarget:target];
}

当您调用了某个不存在的方法时,运行时首先会调用一个名为 resolveInstanceMethod 的类方法,如果所调用的方法是类方法的话,则为调用 resolveClassMethod。这时候我们便有机会来添加方法了,步骤的话我们之前就已经展示过了。如果我们返回了 YES,就意味着原始方法将会再次被调用。

如果您不想创建新方法的话,我们还有 forwardingTargetForSelector。您可以直接返回需要调用方法的目标对象即可,之后这个对象就会调用 Selector。

此外还有一个略为复杂的 forwardInvocation。所有的调用过程都被封装到 NSInvocation 对象当中,之后您便可以使用特定的对象进行调用了。如果您需要这么做,那么还需要实现 methodSignatureForSelector

因此,我们便可以将方法转发给其他对象,但是您也可以替换或者交换方法的实现。您可以使用运行时当中最著名的动态特性:方法混淆 (swizzling)。混淆的基本方法如下所示:

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(doSomething);
SEL swizzledSelector = @selector(mo_doSomething);
Method originalMethod = class_getInstanceMethod(class,
originalSelector);
Method swizzledMethod = class_getInstanceMethod(class,
swizzledSelector);
BOOL didAddMethod = class_addMethod(class, originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

当类加载之后,会调用一个名为 load 的类函数。由于我们只打算混淆一次,因此我们需要使用 dispatch_once。接着我们便可以得到该方法,然后使用 class_replaceMethod 或者 method_exchangeImplementations 来替换方法。之所以想要混淆,是因为它可以用于日志记录和 Mock 测试。

KVC KVO

从运行时的层面,我们往上一层,便来到了 Foundation 框架。Foundation 框架实现了基于运行时的一个特性:键值编码(key-value-coding, KVC) 以及键值观察 (key-value observing, KVO)。KVC 和 KVO 允许我们将 UI 和数据进行绑定。这也是 Rx 以及其他响应式框架实现的基础,这个基本的功能是内含在 Foundation 当中的。KVC 的工作方式如下所示:

@property (nonatomic, strong) NSNumber *number;
[myClass valueForKey:@"number"];
[myClass setValue:@(4) forKey:@"number"];

例如,假设我们有这个 number 属性,您可以将属性名称作为键,来获取属性值或者设置属性值。这个功能可以用在此前我们所看到的获取变量列表、协议列表,以及危险的混淆功能当中。

接下来是 KVO,您可以对状态的变化进行注册。

[myClass addObserver:self
forKeyPath:@"number"
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context{
// Respond to observation.
}

在观察的值发生变更之后,KVO 会调用此方法立即通知观察者。通过这个方法,我们便可以按需更新 UI。

我们通常所说的 Objective-C 「动态性」,往往都是指 KVO。虽然还有其余的函数,但是这些是最常见、最常用的。这也就是人们所说的,Swift 缺失的部分。

参考资料:

https://academy.realm.io/cn/posts/mobilization-roy-marmelstein-objective-c-runtime-swift-dynamic/

http://www.jianshu.com/p/ab966e8a82e2


推荐阅读
  • 本文介绍了在处理不规则数据时如何使用Python自动提取文本中的时间日期,包括使用dateutil.parser模块统一日期字符串格式和使用datefinder模块提取日期。同时,还介绍了一段使用正则表达式的代码,可以支持中文日期和一些特殊的时间识别,例如'2012年12月12日'、'3小时前'、'在2012/12/13哈哈'等。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 十大经典排序算法动图演示+Python实现
    本文介绍了十大经典排序算法的原理、演示和Python实现。排序算法分为内部排序和外部排序,常见的内部排序算法有插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。文章还解释了时间复杂度和稳定性的概念,并提供了相关的名词解释。 ... [详细]
  • 本文介绍了在Python3中如何使用选择文件对话框的格式打开和保存图片的方法。通过使用tkinter库中的filedialog模块的asksaveasfilename和askopenfilename函数,可以方便地选择要打开或保存的图片文件,并进行相关操作。具体的代码示例和操作步骤也被提供。 ... [详细]
  • Python实现变声器功能(萝莉音御姐音)的方法及步骤
    本文介绍了使用Python实现变声器功能(萝莉音御姐音)的方法及步骤。首先登录百度AL开发平台,选择语音合成,创建应用并填写应用信息,获取Appid、API Key和Secret Key。然后安装pythonsdk,可以通过pip install baidu-aip或python setup.py install进行安装。最后,书写代码实现变声器功能,使用AipSpeech库进行语音合成,可以设置音量等参数。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文介绍了计算机网络的定义和通信流程,包括客户端编译文件、二进制转换、三层路由设备等。同时,还介绍了计算机网络中常用的关键词,如MAC地址和IP地址。 ... [详细]
  • 本文介绍了在iOS开发中使用UITextField实现字符限制的方法,包括利用代理方法和使用BNTextField-Limit库的实现策略。通过这些方法,开发者可以方便地限制UITextField的字符个数和输入规则。 ... [详细]
  • python限制递归次数(python最大公约数递归)
    本文目录一览:1、python为什么要进行递归限制 ... [详细]
  • 本文介绍了在MFC下利用C++和MFC的特性动态创建窗口的方法,包括继承现有的MFC类并加以改造、插入工具栏和状态栏对象的声明等。同时还提到了窗口销毁的处理方法。本文详细介绍了实现方法并给出了相关注意事项。 ... [详细]
  • 本文介绍了RxJava在Android开发中的广泛应用以及其在事件总线(Event Bus)实现中的使用方法。RxJava是一种基于观察者模式的异步java库,可以提高开发效率、降低维护成本。通过RxJava,开发者可以实现事件的异步处理和链式操作。对于已经具备RxJava基础的开发者来说,本文将详细介绍如何利用RxJava实现事件总线,并提供了使用建议。 ... [详细]
  • 本文讨论了微软的STL容器类是否线程安全。根据MSDN的回答,STL容器类包括vector、deque、list、queue、stack、priority_queue、valarray、map、hash_map、multimap、hash_multimap、set、hash_set、multiset、hash_multiset、basic_string和bitset。对于单个对象来说,多个线程同时读取是安全的。但如果一个线程正在写入一个对象,那么所有的读写操作都需要进行同步。 ... [详细]
  • 上图是InnoDB存储引擎的结构。1、缓冲池InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。因此可以看作是基于磁盘的数据库系统。在数据库系统中,由于CPU速度 ... [详细]
  • 本文介绍了GTK+中的GObject对象系统,该系统是基于GLib和C语言完成的面向对象的框架,提供了灵活、可扩展且易于映射到其他语言的特性。其中最重要的是GType,它是GLib运行时类型认证和管理系统的基础,通过注册和管理基本数据类型、用户定义对象和界面类型来实现对象的继承。文章详细解释了GObject系统中对象的三个部分:唯一的ID标识、类结构和实例结构。 ... [详细]
author-avatar
雲悳蕲黇
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有