Swift 5 之后 "Method Swizzling"?

引子

随着六月份的 WWDC 上对 SwiftUI 的发布,感觉 Swift 又变成了炽手可热的话题。在大会结束后,发现了有这么几条 Twitter 在讨论一个叫做 @_dynamicReplacement(for:) 的新特性。

Swift 5 之后

这是一个什么东西呢,于是我在 Swift 社区中也检索了对应的关键字,看到一个 Dynamic Method Replacement[1] 的帖子。在爬了多层楼之后,大概看到了使用的方式(环境是 macOS 10.14.5,Swift 版本是 5.0,注意以下 Demo 只能在工程中跑,Playground 会报 error: Couldn't lookup symbols: 错误)。

class Test {	
    dynamic func foo() {	
        print("bar")	
    }	
}	

	
extension Test {	
    @_dynamicReplacement(for: foo())	
    func foo_new() {	
        print("bar new")	
    }	
}	

	
Test().foo() // bar new

看到这里是不是眼前一亮?我们期待已久的 Method Swizzling 仿佛又回来了?

开始的时候只是惊喜,但是在平时的个人开发中,其实很少会用到 hook 逻辑(当然这里说的不是公司项目)。直到有一天,朋友遇到了一个问题,于是又对这个东西做了一次较为深入的研究 ....

Method Swizzling in Objective-C

首先我们先写一段 ObjC 中 Method Swizzling 的场景:

//	
//  PersonObj.m	
//  MethodSwizzlingDemo	
//	
//  Created by Harry Duan on 2019/7/26.	
//  Copyright © 2019 Harry Duan. All rights reserved.	
//	

	
#import "PersonObj.h"	
#import 	

	
@implementation PersonObj	

	
+ (void)load {	
    static dispatch_once_t onceToken;	
    dispatch_once(&onceToken, ^{	
        Class class = [self class];	
        SEL oriSelector = @selector(sayWords);	
        SEL swiSelector = @selector(sayWordsB);	
        Method oriMethod = class_getInstanceMethod(class, oriSelector);	
        Method swiMethod = class_getInstanceMethod(class, swiSelector);	
        method_exchangeImplementations(oriMethod, swiMethod);	

	
        SEL swi2Selector = @selector(sayWorkdsC);	
        Method swi2Method = class_getInstanceMethod(class, swi2Selector);	
        method_exchangeImplementations(oriMethod, swi2Method);	
    });	
}	

	
- (void)sayWords {	
    NSLog(@"A");	
}	

	
- (void)sayWordsB {	
    NSLog(@"B");	
    [self sayWordsB];	
}	

	
- (void)sayWorkdsC {	
    NSLog(@"C");	
    [self sayWorkdsC];	
}	

	
@end

上述代码我们声明了 - (void)sayWords 方法,然后再 + (void)load 过程中,使用 Method Swizzling 进行了两次 Hook。

在执行处,我们来调用一下 - sayWords 方法:

PersonObj *p = [PersonObj new];	
[p sayWords];	

	
// log	
2019-07-26 16:04:49.231045+0800 MethodSwizzlingDemo[9859:689451] C	
2019-07-26 16:04:49.231150+0800 MethodSwizzlingDemo[9859:689451] B	
2019-07-26 16:04:49.231250+0800 MethodSwizzlingDemo[9859:689451] A

正如我们所料,结果会输出 CBA,因为 - sayWords 方法首先被替换成了 - sayWordsB ,其替换后的结果又被替换成了 - sayWordsC 。进而由于 Swizze 的方法都调用了原方法,所以会输出 CBA。

来复习一下 Method Swizzling 在 Runtime 中的原理,我们可以概括成一句话来描述它:方法指针的交换。以下是 ObjC 的 Runtime 750 版本的源码:

void method_exchangeImplementations(Method m1, Method m2)	
{	
    if (!m1  ||  !m2) return;	

	
    mutex_locker_t lock(runtimeLock);	

	
        // 重点在这里,将两个方法的实例对象 m1 和 m2 传入后做了一次啊 imp 指针的交换	
    IMP m1_imp = m1->imp;	
    m1->imp = m2->imp;	
    m2->imp = m1_imp;	

	

	
    // RR/AWZ updates are slow because class is unknown	
    // Cache updates are slow because class is unknown	
    // fixme build list of classes whose Methods are known externally?	

	
    flushCaches(nil);	

	
        // 来更新每个方法中 RR/AWZ 的 flags 信息	
        // RR/AWZ = Retain Release/Allow With Zone(神奇的缩写)	
    updateCustomRR_AWZ(nil, m1);	
    updateCustomRR_AWZ(nil, m2);	
}

Swift 5 之后

由于 ObjC 对于实例方法的存储方式是以方法实例表,那么我们只要能够访问到其指定的方法实例,修改 imp 指针对应的指向,再对引用计数和内存开辟等于 Class 相关的信息做一次更新就实现了 Method Swizzling。

一个连环 Hook 的场景

上面的输出 ABC 场景,是我朋友遇到的。在制作一个根据动态库来动态加载插件的研发工具链的时候,在主工程会开放一些接口,模拟 Ruby 的 alias_method 写法,这样就可以将自定义的实现注入到下层方法中,从而扩展实现。当然这种能力暴露的方案不是很好,只是一种最粗暴的插件方案实现方法。

当然我们今天要说的不是 ObjC,因为 ObjC 在 Runtime 机制上都是可以预期的。如果我们使用 Swift 5.0 中 Dynamic Method Replacement 方案在 Swift 工程中实现这种场景。

import UIKit	

	
class Person {	
    dynamic func sayWords() {	
        print("A")	
    }	
}	

	
extension Person {	
    @_dynamicReplacement(for: sayWords())	
    func sayWordsB() {	
        print("B")	
        sayWords()	
    }	
}	

	
extension Person {	
    @_dynamicReplacement(for: sayWords())	
    func sayWordsC() {	
        print("C")	
        sayWords()	
    }	
}	

	
class ViewController: UIViewController {	

	
    override func viewDidLoad() {	
        super.viewDidLoad()	
        Person().sayWords()	
    }	
}

从视觉角度上来看,通过对 Swift Functions 的显式声明(有种 Swift 真香的感觉),我们完成了对于 Method Swizzling 的实现。跑一下代码,发现运行结果却不如我们的预期:

C	
A

为什么结果只显示两个?我交换了一下两个 extension 的顺序继续尝试,其打印结果又变成了 BA 。于是差不多可以总结一下规律,在执行顺序上,后声明的将会生效。那么应该如何实现这种连环 Hook 的场景呢?从代码层面我没有想到任何办法。

从 Swift 源码来猜测 @_dynamicReplacement 实现

按照正常程序员的逻辑,如果我们在重构一个模块的代码,新的模块代码无论从功能还是效率上,都应该优于之前的方式、覆盖之前所有的逻辑场景。如果 Swift 支持这种连环修改的场景,那这个新的 Feature 放出其实是功能不完备的!于是我们开始翻看 Swift 这个 Feature 的 PR 代码,来一探 Dynamic Method Replacement 的原理。

首先来看这个 Dynamic Method Replacement 特性的 Issue-20333[2],作者上来就贴了两段很有意思的代码:

/// 片段一	
// Module A	
struct Foo {	
 dynamic func bar() {}	
}	
// Module B	
extension Foo {	
  @_dynamicReplacement(for: bar())	
  func barReplacement() {	
    ...	
    // Calls previously active implementation of bar()	
    bar()	
  }	
}	

	
/// 片段二	
dynamic_replacement_scope AGroupOfReplacements {	
   extension Foo {	
     func replacedFunc() {}	
   }	
   extension AnotherType {	
     func replacedFunc() {}	
   }	
}	

	
AGroupOfReplacements.enable()	
...	
AGroupOfReplacements.disable()

大概意思就是,他希望这种动态替换的特性,通过一些关键字标记和内部标记,兼容动态替换和启用开关。既然他们有规划 enable 和 disable 这两个方法来控制启用,那么就来检索它们的实现。

通过关键字搜索,我在 MetadataLookup.cpp 这个文件的 L1523-L1536[3] 中找到了 enable 和 disable 两个方法的实现源码:

// Metadata.h#L4390-L4394:	
// https://github.com/apple/swift/blob/659c49766be5e5cfa850713f43acc4a86f347fd8/include/swift/ABI/Metadata.h#L4390-L4394	

	
/// dynamic replacement functions 的链表实现	
/// 只有一个 next 指针和 imp 指针	
struct DynamicReplacementChainEntry {	
  void *implementationFunction;	
  DynamicReplacementChainEntry *next;	
};	

	
// MetadataLookup.cpp#L1523-L1563	
// https://github.com/aschwaighofer/swift/blob/fff13330d545b914d069aad0ef9fab2b4456cbdd/stdlib/public/runtime/MetadataLookup.cpp#L1523-L1563	
void DynamicReplacementDescriptor::enableReplacement() const {	
    // 拿到根节点	
  auto *chainRoot = const_cast(	
      replacedFunctionKey->root.get());	

	
  // 通过遍历链表来保证这个方法是 enabled 的	
  for (auto *curr = chainRoot; curr != nullptr; curr = curr->next) {	
    if (curr == chainEntry.get()) {	
            // 如果在 Replacement 链中发现了这个方法,说明已经 enable,中断操作	
      swift::swift_abortDynamicReplacementEnabling();	
    }	
  }	

	
  // 将 Root 节点的 imp 保存到 current,并将 current 头插	
  auto *currentEntry =	
      const_cast(chainEntry.get());	
  currentEntry->implementationFunction = chainRoot->implementationFunction;	
  currentEntry->next = chainRoot->next;	

	
  // Root 继续进行头插操作	
  chainRoot->next = chainEntry.get();	
    // Root 的 imp 换成了 replacement 实现	
  chainRoot->implementationFunction = replacementFunction.get();	
}	

	
// 同理 disable 做逆操作	
void DynamicReplacementDescriptor::disableReplacement() const {	
  const auto *chainRoot = replacedFunctionKey->root.get();	
  auto *thisEntry =	
      const_cast(chainEntry.get());	

	
  // Find the entry previous to this one.	
  auto *prev = chainRoot;	
  while (prev && prev->next != thisEntry)	
    prev = prev->next;	
  if (!prev) {	
    swift::swift_abortDynamicReplacementDisabling();	
    return;	
  }	

	
  // Unlink this entry.	
  auto *previous = const_cast(prev);	
  previous->next = thisEntry->next;	
  previous->implementationFunction = thisEntry->implementationFunction;	
}

我们发现 Swift 中处理每一个 dynamic 方法,会为其建立一个 dynamicReplacement 链表来记录实现记录。那么也就是说不管我们对原来的 dynamic 做了多少次 @_dynamicReplacement ,其实现原则上都会被记录下来。但是调用方法后的执行代码我始终没有找到对应的逻辑,所以无法判断 Swift 在调用时机做了哪些事情。

通过 Unit Test 解决问题

以下思路是朋友 @Whirlwind[4] 提供的。既然我们无法找到调用的实现,那么另辟蹊径:既然 Swift 已经通过链式记录了所有的实现,那么在单元测试的时候应该会有这种逻辑测试。

在根据关键字和文件后缀搜索了大量的单元测试文件后,我们发现了这个文件dynamic_replacement_chaining.swift[5] 。我们注意到 L13[6] 的执行命令:

// RUN: %target-build-swift-dylib(%t/%target-library-name(B)) -I%t -L%t -lA %target-rpath(%t) -module-name B -emit-module -emit-module-path %t -swift-version 5 %S/Inputs/dynamic_replacement_chaining_B.swift -Xfrontend -enable-dynamic-replacement-chaining

在命令参数中增加了 -Xfrontend -enable-dynamic-replacement-chaining ,第一反应:这个东西像 Build Settings 中的 Flags。翻看了 Build Settings 中所有的 Compile Flags,将其尝试写入 Other Swift Flags 中:

Swift 5 之后

重新编译运行,发现了一个神奇的结果:

Swift 5 之后

出乎意料的达到了我们想要的结果。说明我们的实验和猜想是正确的,Swift 在处理 dynamic 是将所有实现的 imp 保存,并且也有办法根据记录的链表来触发实现

一些不怀好意的揣测

@_dynamicReplacement 虽然在 Swift 5 中就已经带入了 Swift 中,但是在官方论坛和官方仓库中并未找到 Release 日志痕迹,而我是从 Twitter 友的口头才了解到的。并且这个 PR 虽然已经开了一年之久,苹果在今年的 WWDC 之前就偷偷的 Merge 到了 master 分支上。

不得不猜,Apple 是为了实现 SwiftUI,而专门定制的 Feature。另外,在其他的一些特性中也能看到这种现象,例如 Function Builder(为了实现 SwiftUI 的 DSL),Combine(为了实现 SwiftUI 中的 Dataflow & Binding)也是如此。这种反社区未经同意的情况下,为了自己的技术产品而定制语言是一件好事吗?

这里脑补一个黑人问号,也许明年的我会说出“真香”!

References

[1] Dynamic Method Replacement: https://forums.swift.org/t/dynamic-method-replacement/16619[2] Issue-20333: https://github.com/apple/swift/pull/20333[3] 这个文件的 L1523-L1536: https://github.com/aschwaighofer/swift/blob/fff13330d545b914d069aad0ef9fab2b4456cbdd/stdlib/public/runtime/MetadataLookup.cpp#L1523-L1563[4] @Whirlwind: https://github.com/Whirlwind[5] dynamic_replacement_chaining.swift: https://github.com/apple/swift/blob/master/test/Interpreter/dynamic_replacement_chaining.swift[6] L13: https://github.com/apple/swift/blob/master/test/Interpreter/dynamic_replacement_chaining.swift#L13

特别的:我将定期推送一些流弊的技术文章,本篇推送来自 瓜神 ,作者不定期分享好玩的算法及客户端技术,欢迎关注。

你可能感兴趣的:(Swift 5 之后 "Method Swizzling"?)