objective-c动态运行时语言,Objective-C 运行时以及 Swift 的动态性

前言

1、Objective-C 是一门基于运行时的编程语言,

这意味着所有方法、变量、类之间的link,都会推迟到应用实际运行的最后一刻才会建立。

这将给开发人员极高的灵活性,因为我们可以修改这些link。

2、而不同的是,Swift 绝大多数时候是一门面向编译时的语言。

因此在 Swift 当中,灵活性受到了限制,不过您会因此得到更多的安全性。

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

而 KVO是Foundation框架基于运行时实现的一个特性。因此本文先从Objective-C 的运行时 开始描述。

Objective-C 的运行时

本质上是一个库。它负责了 “Objective” 这个部分,

#import

它主要由 C 和汇编编写而成,其实现了诸如类、对象、方法调度、协议等等这些东西。它是完全开源的,并且开源了很长一段时间了。

对象在 runtime.h 当中是这样定义的:

typedef struct objc_class *Class;

struct objc_object {

Class isa;

};

对象只与一个类建立引用关联,也就是这个 isa 的意思所在。

类的定义

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 这个值。除了 NSObject 这个类之外,super_class 的值永远不会为 nil,因为 Objective-C 当中的其余类都是以某种方式继承自 NSObject 的.

更多的应该是关注变量列表 (ivars)、方法列表 (methodLists) 和这个协议列表 (protocols)。

这些就是我们能在运行时修改和读取的。可以看到,对象其实本质上是一个非常简单的结构体,类同样也是。我们可以借助运行时函数,从而在运行时动态创建类。

Creates a new class and metaclass.

BJC_EXPORT Class _Nullable

objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name,

size_t extraBytes)

OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

Class myClass =

objc_allocateClassPair([NSObject

class], "MyClass", 0);

// 在这里添加变量、方法和协议

objc_registerClassPair(myClass);

// 当类注册之后,变量列表将会被锁定

[[myClass alloc] init];

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

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

「内省 (introspection)」机制:判别这个类能执行何种操作

[myObject isMemberOfClass:NSObject.class];

[myObject respondsToSelector:@selector(doStuff:)];

// isa == class

class_respondsToSelector(myObject.class, @selector(doStuff:));

在运行时层面,isMemberOfClass 对比两者的 isa 是否相同。

respondsToSelector" 则封装了一个 Objective-C 运行时函数:

respondsToSelector,其接受 Selector 和类为参数。

BJC_EXPORT BOOL

class_respondsToSelector(Class _Nullable cls, SEL _Nonnull sel)

OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

XCTest 的最简单版本:借助 Objective-C 的运行时机制实现

在编写 XCTestCase 的时候,需要完成 setUp 和 tearDown 的设定,随后才能编写相关的 test 函数。

当测试运行的时候,系统会自行遍历所有的测试函数,并自动运行。

unsigned int count;

Method *methods = class_copyMethodList(myObject.class,

&count);

//Ivar *list = class_copyIvarList(myObject.class,&count);

for(unsigned i = 0; i < count; i++) {

SEL selector = method_getName(methods[i]);

NSString *selectorString =

NSStringFromSelector(selector);

if ([selectorString containsString:@"test"]) {

[myObject performSelector:selector];

}

}

free(methods);

变量和方法是由什么组成

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) 则是内存管理方面的内容。

方法还用编码字符串来表示其类型。之后便是方法的实现,它使用了一种特定的表示方式,

在运行时向对象当中添加方法

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);

这个例子具体的方法实现部分我们取了个巧,因为我们使用了既有的 doStuff 方法,

因此能够很简单地获取其方法实现和方法类型。

不过我们还可以用其他方法来完成:交换方法的实现。可以使用运行时当中最著名的动态特性:方法混淆 (swizzling)。

objc_msgSend

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

[self doStuff];

[self performSelector:@selector(doStuff)];

objc_msgSend(self, @selector(message));

与类别Category相比运行时的setAssociatedObject、getAssociatedObject好处:向既有的类当中添加存储属性

扩展一个不是自己创建的类,想要向其中添加函数。Swift 的扩展与之非常相似。

类别的一个问题便在于,它无法添加存储属性。您可以添加一个计算属性,但是存储属性是无法添加的。

运行时的另一个特性便是:

我们可以借助 setAssociatedObject 和 getAssociatedObject 这两个函数,向既有的类当中添加存储属性。

@implementation NSObject (AssociatedObject)

@dynamic associatedObject;

- (void)setAssociatedObject:(id)object {

objc_setAssociatedObject(self,

@selector(associatedObject), object,

OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

- (id)associatedObject {

return objc_getAssociatedObject(self,

@selector(associatedObject));

}

方法转发:崩溃之前会预留几个步骤

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

// 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];

}

ps:试图桥接两个不同的框架的时候,这个功能便非常有用

第一步

当调用了某个不存在的方法时,运行时首先会调用一个名为 resolveInstanceMethod 的类方法,

如果所调用的方法是类方法的话,则为调用 resolveClassMethod。

这时候我们便有机会来添加方法了,即上面提到的利用运行时动态添加方法

第2步

如果不想创建新方法的话,第一步返回了NO,

还有 forwardingTargetForSelector。可以直接返回需要调用方法的目标对象即可,之后这个对象就会调用 Selector。

第三步骤:forwardInvocation

略为复杂的 forwardInvocation。

如果您需要这么做,那么还需要实现 methodSignatureForSelector。

所有的调用过程都被封装到 NSInvocation 对象当中,之后你便可以使用特定的对象进行调用了。

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);

}

});

}

2种实现方式:

1、class_addMethod originalSelector 的时候使用swizzledMethod的IMP、TYP

之后进行class_replaceMethod,来达到交换的目的。

即先换掉originalSelector的IMP,再换掉swizzledMethod的IMP,达到exchange 的目的

2、直接使用exchange 方法

小结

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

之所以想要混淆,是因为它可以用于日志记录和 Mock 测试。例如上报用户打开的界面所在VC的名称,就可以使用swizzling 统一处理

运行时层面的上一层,Foundation 框架

Foundation 框架实现了基于运行时的一个特性:

键值编码 (key-value-coding, KVC) 以及键值观察 (key-value observing, KVO)。

KVC 和 KVO 允许我们将 UI 和数据进行绑定。这也是 Rx 以及其他响应式框架实现的基础。

ps:而且MVVM的实现又可以借助“V-VM”第三方绑定框架进行实现

KVC 的工作方式

@property (nonatomic, strong) NSNumber *number;

[myClass valueForKey:@"number"];

[myClass setValue:@(4) forKey:@"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 缺失的部分。

第二大内容:Swift

Swift 是一种强类型语言。类型静态,也就是说 Swift 的默认类型是非常安全的。

如果需要的话,不安全类型也是存在的,Swift 中的动态性可以通过 Objective-C 运行时来获得。

但是 Swift 开源并迁移到 Linux 之后,由于 Linux 上的 Swift 并不提供 Objective-C 运行时,事情就。。

@objc 和 @dynamic

Swift 当中存在有这两个修饰符 @objc 和 @dynamic,此外我们同样还可以访问 NSObject。@objc 将您的 Swift API 暴露给 Objective-C 运行时,但是它仍然不能保证编译器会尝试对其进行优化。

如果您真的想使用动态功能的话,就需要使用 @dynamic。(一旦您使用了 @dynamic 修饰符之后,就不需要添加 @objc 了,因为它已经隐含在其中。)

Swift 当中的动态特性

方法转发

// 1

override class func resolveInstanceMethod(_ sel: Selector!)

-> Bool {

// 添加实例方法并返回 true 的一次机会,它随后会再次尝试发送消息

}

// 2

override func forwardingTarget(for aSelector: Selector!) ->

Any? {

// 返回可以处理 Selector 的对象

}

// 3 - Swift 不支持 NSInvocation

方法混淆

load 在 Swift 不再会被调用,因此我们需要在 initialize 中进行混淆。

在 Objective-C 当中,我们使用 dispatch_once,但是自 Swift 3 之后,dispatch_once 便不复存在于 Swift 当中了。事情变得略为复杂。

内省

if self is MyClass {

// YAY

}

let myString = "myString";

let mirror = Mirror(reflecting: myString)

print(mirror.subjectType) // “String"

let string = String(reflecting: type(of:

myString)) // Swift.String

// No native method introspection

is 替代了 isMemberOfClass

小结

无法自动遍历所有的函数

如果打算为 Linux 编写单元测试的时候,就无法自动遍历所有的函数。您必须实现 static var allTests,然后手动列出所有的测试函数。这很。。。。

KVO 和 KVC 在 Swift 被极大地削弱了。

KVO 的魅力在于,您可以在不是自己所创建的类当中使用它,也可以只对您想要监听变化的类使用。所观察的对象必须要继承自 NSObject,并且使用一个 Objective-C 类型。所观察的变量必须要生命为 dynamic。导致你必须要对想要观察的事务了如指掌。

只能使用基于协议来观察对象,语言自身是没有原生的解决方案的。或者使用一些符合 Swift 风格的方法来暴露一些运行时函数的 ObjectiveKit 的开源库

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值