咱们都知道, UIScrollView
将 pan gesture 信号转换成 scrollViewDidXXX:
消息而后发送给它的 delegate
,多数时候你只须要理解这二者的关系而后在 delegate
监听这些消息就能够了。可是若是你要干预 pan gesture recognizer 的工做怎么办?我是说,干预 pan gesture 的识别。git
在这里,若是咱们不选择修改 UIScrollView
的内部机制,那咱们将不得不选择建立一个子类。github
因为 UIScrollView
的 pan gesture recognizer 将他的 delegate
固化成了拥有这个 gesture recognizer 的 UIScrollView
,若是你将它的 delegate
设置为其余的「中间人」,你将会获得一个运行时异常。在这里,多数人都会想到建立一个子类。可是若是你指望此次修改也能影响其余 UIScrollView
的子类时怎么办?objective-c
在物件导向(陆译面向对象,但我更喜欢「物件导向」这个译法,感受更精准)编程范式中咱们并不鼓励修改一个已存在的类的内部机制。由于物件导向编程是创建在不断做出是什么的断言上——一个类的所做所为造就了这个类是这个类自己的缘由,故而物件导向编程的核心概念之一即是「扩展而不是修改」。修改已存在的类的内部机制打破了这个范式。若是你选择修改,那么这个「是什么」断言便不成立了,软件架构的基础也就随之开始动摇。编程
因此咱们永远不该该是某一种编程教派的狂热信徒。这一次你须要剖面导向编程。有了它,你就能够不用建立一个新类而达到干预 pan gesture recognizer 的工做这个目的了。而这也能够影响到继承自 UIScrollView
的子类。swift
剖面导向编程多是编程世界中最被解释得过于复杂的术语了。bash
与剖面导向编程相比,最类似的概念我认为应该是植物嫁接。markdown
植物嫁接的意思是:将一颗植物的枝或芽固定在另外一颗活体植物的主干或者茎的深切面上,这样枝或者芽就能够从这颗活体植物接受营养并继续生长。闭包
剖面导向编程着实相似植物嫁接。架构
如上图所示,剖面导向编程关心以下三件事:app
咱们能够将剖面导向编程中新加入的代码比做植物嫁接中植物的枝或芽,将剖面比做深切面,将被操做的对象比做活体植物。因而剖面导向编程就是将这三者固定在一块儿的过程。
在 Objective-C 中,关于剖面导向编程有一个误解:苹果官方并不支持剖面导向编程。
不是的。
Objective-C 中的 Key-Value Observation 就是一个特设的剖面导向编程框架,而且这是由苹果带来的官方特性。咱们能够将 Key-Value Observation 代入以前的植物嫁接模型中:
因而咱们能够知道,Key-Value Observation 就是剖面导向编程,可是这个「剖面」是「特设」的。苹果没有官方支持的是支持「通用」剖面的剖面导向编程。
剖面导向编程的状况在 Swift 中比较复杂。经过借助 Objective-C,Swift 默认支持 Key-Value Observation。可是由于函数调用的派发能够在编译时被决议而且被写入编译产物,而 Key-Value Observation 又是在运行时生成代码,这些编译产物有可能永远都不知道如何调用这些运行时生成的代码。因此你须要将要被观察的 properties 标记上 @objc
的属性。这样就会强制编译器生成运行时决议该函数派发的代码。
就像 Objective-C,在 Swift 中并无对支持「通用」剖面的剖面导向编程提供支持。
好了。苹果造了个好框架而后咱们很是开心,然而你仍是不能达成干预 UIScrollView
的 pan gesture recognizer 的目的——这就是故事的结尾了吗?
非也。
在 Objective-C 中,最简单的不经过 subclassing 来修改一个类的实例其行为的方法就是 method swizzling 了。网上有许多资料讨论如何在 Objective-C 和 Swift 中进行 method swizzling 的,因此我并不想在这里再重复一遍。我想说说这个途径的缺点。
首先,method swizzling 是在类上干活的。若是咱们 swizzle 了 UIScrollView
,那么全部 UIScrollView 及其子类的实例都会得到一样的行为。
而后,虽然咱们在进行剖面导向编程,这并不意味着咱们就放弃了做「是什么」断言的行为。而「做『是什么』断言」这种行为是划分组件责任边界的关键步骤,也是不论什么编程范式中的一块基石。Method swizzling 是一种匿名的修改途径,这种修改途径绕过了「做『是什么』断言」,很容易动摇软件架构的基础,同时也是难以察觉和追踪的。
再者,由于 Swift 不支持重载 Objective-C 桥接类的 class func load()
方法,许多文章都建议你将 swizzle 代码放入 class func initialize()
中去。由于对于每个模块的每个类,app 在启动时只会调用一个 class func initialize()
的重载,因而你必须将同一个类的全部 swizzle 代码都放入一个文件——不然你将搞不清楚启动时到底将调用哪个 class func initialize()
重载。这最终将致使 method swizzling 在代码管理方面潜在的混乱。
一瞥官方支持的剖面导向编程框架 Key-Value Observation,咱们能够察觉到其根本没有咱们说的上述缺点。苹果是如何作到的?
实际上,苹果是经过一种叫 is-a swizzling 的技术实现这个剖面导向编程框架的。
Is-a swizzling 十分简单,甚至反映到代码上都是——设置一个对象的 is-a 指针为另外一个类的。
Foo * foo = [[Foo alloc] init]; object_setClass(foo, [Bar class]); 复制代码
而 Key-Value Observation 就是建立一个被观察对象的类的子类,而后设置这个对象的 is-a 指针为这个新建类的 is-a 指针。整个过程以下列代码所示:
@interface Foo: NSObject // ... @end @interface NSKVONotifying_Foo: Foo // ... @end NSKVONotifying_Foo * foo = [[NSKVONotifying_Foo alloc] init]; object_setClass(foo, [NSKVONotifying_Foo class]); 复制代码
由于 Apple 已经给出了一个关于「特设」剖面导向编程的成熟的解决方案,那么建立一个对象的类的子类,而后再将其 is-a 指针设置为该对象的这条途径应该是行得通的。可是当咱们在作系统设计的时候,最重要的问题是:为何应该是行得通的?
打开 Swift Playground 而后键入下列代码:
import Cocoa class Foo: NSObject { @objc var intValue: Int = 0 } class Observer: NSObject { } let foo = Foo() let observer = Observer() We need to use `object_getClass` to check the real is-a pointer. print(NSStringFromClass(object_getClass(foo)!)) print(NSStringFromClass(object_getClass(observer)!)) foo.addObserver(observer, forKeyPath: "intValue", options: .new, context: nil) print(NSStringFromClass(object_getClass(foo)!)) print(NSStringFromClass(object_getClass(observer)!)) 复制代码
而后你会看到下列输出:
__lldb_expr_2.Foo
__lldb_expr_2.Observer
NSKVONotifying___lldb_expr_2.Foo
__lldb_expr_2.Observer
复制代码
__lldb_expr_2
是由 Swift Playground 生成而且由 Swift 编译器在桥接 Swift 类至 Objective-C 时加入的模块名。 NSKVONotifying_
是由 KVO 生成的保护性前缀。 Foo
和 Observer
是咱们在代码中使用的类名。
经过对 KVO 内部的一瞥,咱们能够知道,KVO 为被观察的对象建立了一个新类。可是这就足够了吗?我是说,针对一个被观察对象的类建立一个子类就够了吗?
因为 KVO 是一个成熟的框架,咱们固然能够经过直觉回答「是」。可是若是咱们这么作了,那么咱们将丧失一次学习个中起因的机会。
实际上,由于在 KVO 观察一个对象的 properties 中,全部可变的因素都在观察者的事件处理函数:[NSObject -observeValueForKeyPath:ofObject:change:context:]
中,另外一方面,又因为被观察的对象仅仅只须要机械地发送事件,被观察对象一方实际上是很是固定的。这意味着针对一个被观察对象的类建立一个子类是彻底足够的——由于这些同一个类的被观察对象工做起来彻底同样。
将 Swift Playground 中的代码替换成以下代码:
import Cocoa class Foo: NSObject { @objc var intValue: Int = 0 } class Observer: NSObject { } let foo = Foo() let observer = Observer() func dumpObjCClassMethods(class: AnyClass) { let className = NSStringFromClass(`class`) var methodCount: UInt32 = 0; let methods = class_copyMethodList(`class`, &methodCount); print("Found \(methodCount) methods on \(className)"); for i in 0..<methodCount { let method = methods![numericCast(i)] let methodName = NSStringFromSelector(method_getName(method)) let encoding = String(cString: method_getTypeEncoding(method)!) print("\t\(className) has method named '\(methodName)' of encoding '\(encoding)'") } free(methods) } foo.addObserver(observer, forKeyPath: "intValue", options: .new, context: nil) dumpObjCClassMethods(class: object_getClass(foo)!) 复制代码
因而你将获得:
Found 4 methods on NSKVONotifying___lldb_expr_1.Foo NSKVONotifying___lldb_expr_1.Foo has method named 'setIntValue:' of encoding 'v24@0:8q16' NSKVONotifying___lldb_expr_1.Foo has method named 'class' of encoding '#16@0:8' NSKVONotifying___lldb_expr_1.Foo has method named 'dealloc' of encoding 'v16@0:8' NSKVONotifying___lldb_expr_1.Foo has method named '_isKVOA' of encoding 'c16@0:8' 复制代码
经过 dump 出 KVO 建立的类的方法,咱们能够注意到它重载了一些方法。重载 setIntValue:
的目的是直截了当的——咱们已经告诉了框架要观察 intValue
这个 property,因此框架重载了这个方法以加入通知代码;class
的重载则必定是要返回一个指向该对象原类的伪 is-a 指针;dealloc
重载的意图则应该是释放垃圾用的。经过 Cocoa 的命名法则,咱们能够猜想新方法 _isKVOA
应该是一个返回布尔值的方法。咱们能够在 Swift Playground 中加入如下代码:
let isKVOA = foo.perform(NSSelectorFromString("_isKVOA"))!.toOpaque() print("isKVOA: \(isKVOA)") 复制代码
而后咱们将获得:
isKVOA: 0x0000000000000001
复制代码
由于在 Objective-C 的实践中,布尔真在内存中就被储存为 1
,因此咱们能够确认 _isKVOA
就是一个返回布尔值的方法。显然,咱们能够推测 _isKVOA
是用来指示该类是不是一个 KVO 生成的类的(尽管咱们并不知道结尾的那个 A
是什么意思)。
咱们的系统和 KVO 大相径庭。
首先,咱们的目标是设计一个提供「通用」剖面支持的剖面导向编程系统。这意味着你能够对任何对象的任何方法注入自定义实现。这也致使针对一个被注入对象的类建立一个子类以统筹全部变动的方法再也不适用了。
其次,咱们想要一个「具名」的途径而不是一个「不具名」,或者说「匿名」的途径来实施代码注入。「名以命之」使咱们划分出事物责任的边界,而这些边界就是干净的软件架构的基础。
第三,咱们但愿这个系统不会引入任何会致使「惊吓」到开发者的机制。
经过参考 KVO 的设计,咱们能够给出以下设计
你可能已经注意到了这个由咱们的系统建立的类的名字包含了字符串 “->”。这在源代码中是非法字符。可是在 Objective-C 运行时环境中,这些字符是被容许的在类名称中出现的。这些字符在系统建立的类和用户建立的类之间创建起了一个有保证的围栏。
实现的过程至关简单,直到你接触到解析 protocol 的继承层级为止:我应该注入哪些方法?
考虑下列代码:
@protocol Foo<NSObject> - (void)bar; @end 复制代码
因为 Foo
继承自 NSObject
protocol,那么方法 -isKindOfClass:
的声明也必然包含在 Foo
的继承层级之中。当咱们将这个 protocol 看成一个剖面时,咱们应该将方法 -isKindOfClass:
一同注入到对象中去吗?
显然不行。
由于剖面是方法注入的 proposal,而类提供要注入的实现,我在这里设置了一点限制:系统将仅仅注入在提供自定义实现的类的子叶层级有具体实现的方法。这意味着若是你不在提供自定义实现的类的子叶层级提供具体实现,诸如 -isKindOfClass:
的方法是不会被注入的;而你又能够经过在提供自定义实现的类的子叶层级提供具体实现来注入此类方法。
最终,这里是代码仓库。而后 API 看起来是这样:
最后是干预 UIScrollView 的 pan gesture recognizer 的范例代码:
MyUIScrollViewAspect.h
@protocol MyUIScrollViewAspect<NSObject> - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer; @end 复制代码
MyUIScrollView.h
#import <UIKitUIKit.h> @interface MyUIScrollView: UIScrollView<MyUIScrollViewAspect> @end 复制代码
MyUIScrollView.m
#import "MyUIScrollView.h" @implementation MyUIScrollView - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer; { // Do what you wanna do. return [super gestureRecognizerShouldBegin: gestureRecognizer]; } @end 复制代码
MyViewController.m
// ... UIScrollView * scrollView = [UIScrollView alloc] init]; object_graftImplemenationOfProtocolFromClass(scrollView, @protocol(MyUIScrollViewAspect), [MyUIScrollView class]); // ... 复制代码
我于 2017 年设计了这个框架。当时我并无设计一个真正有助于减轻软件开发痛苦的框架的经验,而我最惦记的一件事情就是划清楚责任的边界以让咱们能够构建更加清澈的软件架构。可是软件的开发是一个过程。这种设计也许给了清澈的软件架构一个可能性,可是强迫开发者在一开始就给一个剖面命名的作法下降了开发速度。
名可名,很是名。
——「老子」
咱们给一件东西取名字总有一个目的。若是目的改变了,名字就会跟着改变。举例来讲,猪的组成成分的划分在一个屠夫眼中和一个生物学家眼中是不一样的。在软件开发的过程当中,这个目的来自于咱们如何定义问题和解释问题。而这又会随着软件开发过程的发展而改变。因此一个真正有助于减轻软件构建痛苦的好的框架应该拥有一部分的使用匿名函数的 API,或者你也能够叫 Swift 中的闭包,Objective-C 中的 blocks。这样就能够防止咱们在对一件事物有充分认知以前就去给它取一个名字。可是因为这个框架在 2017 年设计完成,我当时并无意识到上面我所说起的,因此这个框架并不支持匿名函数。
要让这个框架支持匿名函数的话我还须要更多研究。至少目前从我初步观察,Swift 的函数引用大小竟然长达两个 words,而 C 语言的是一个;另外 Swift 的编译时决议也是一个很是麻烦的问题。显然这须要不少工做,而我目前并无时间。可是在未来的某一刻,这将成为现实。
本文中提到的代码仓库
原文刊发于本人博客(英文)
本文使用 OpenCC 进行繁简转换