Swift 5 以后 "Method Swizzling"?

转自公众号《让技术一瓜公食》,文章底部扫码关注。git

引子

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

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

class Test {
    dynamic func foo() {
        print("bar")
    }
}
    
extension Test {
    @_dynamicReplacement(for: foo())
    func foo_new() {
        print("bar new")
    }
}
    
Test().foo() // bar new
复制代码

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

开始的时候只是惊喜,可是在平时的我的开发中,其实不多会用到 hook 逻辑(固然这里说的不是公司项目)。直到有一天,朋友遇到了一个问题,因而又对这个东西作了一次较为深刻的研究 ....bash

Method Swizzling in Objective-C

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

//
// PersonObj.m
// MethodSwizzlingDemo
//
// Created by Harry Duan on 2019/7/26.
// Copyright © 2019 Harry Duan. All rights reserved.
//
    
#import "PersonObj.h"
#import <objc/runtime.h>
    
@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。frontend

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

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);
}
复制代码

因为 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,做者上来就贴了两段颇有意思的代码:

/// 片断一
// 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()
复制代码

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

经过关键字搜索,我在 MetadataLookup.cpp 这个文件的 L1523-L1536 中找到了 enabledisable 两个方法的实现源码:

// 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<DynamicReplacementChainEntry *>(
      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<DynamicReplacementChainEntry *>(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<DynamicReplacementChainEntry *>(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<DynamicReplacementChainEntry *>(prev);
  previous->next = thisEntry->next;
  previous->implementationFunction = thisEntry->implementationFunction;
}
复制代码

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

经过 Unit Test 解决问题

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

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

// 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 在处理 dynamic 是将全部实现的 imp 保存,而且也有办法根据记录的链表来触发实现

一些不怀好意的揣测

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

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

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

相关文章
相关标签/搜索