从交换方法中调用原始函数

11

我正在尝试使用方法交换技术(method swizzling),并希望在执行method_exchangeImplementations后调用原始函数。我已经为此设置了两个项目。

第一个项目是应用程序的主要项目,包括应用所有逻辑。请注意,在视图加载时调用了originalMethodName

@implementation ViewController

- (void)originalMethodName 
{
    NSLog(@"REAL %s", __func__);
}

- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"REAL %s", __func__);

    [self originalMethodName];
}

@end

第二个项目仅包含混淆代码。我有一个名为swizzle_originalMethodName的方法,其中包含我想要注入到主应用程序中的代码,并在调用originalMethodName函数时执行。

Translated:

第二个项目只包括关于交换函数的代码。我有一个名为swizzle_originalMethodName的方法,其中包含了我希望注入到主应用程序中的代码,该代码会在调用originalMethodName函数时运行。

@implementation swizzle_ViewController

- (void)swizzle_originalMethodName
{
    NSLog(@"FAKE %s", __func__);
}

__attribute__((constructor)) static void initializer(void)
{
    NSLog(@"FAKE %s", __func__);

    Class c1 = objc_getClass("ViewController");
    Class c2 = [swizzle_ViewController class];
    Method m1 = class_getInstanceMethod(c1, @selector(originalMethodName));
    Method m2 = class_getInstanceMethod(c2, @selector(swizzle_originalMethodName));
    method_exchangeImplementations(m1, m2);
}

@end

目前看来,混淆已经正常工作(如下面的输出所示),但现在我希望能够从swizzle_originalMethodName中调用originalMethodName

2016-08-17 14:18:51.765 testMacOS[7295:1297055] FAKE initializer
2016-08-17 14:18:51.822 testMacOS[7295:1297055] REAL -[ViewController viewDidLoad]
2016-08-17 14:18:51.822 testMacOS[7295:1297055] FAKE -[swizzle_ViewController swizzle_originalMethodName]

我尝试使用NSInvocation,但是没有成功。你有什么想法我做错了什么吗?

Class c1 = objc_getClass("ViewController");
Method m1 = class_getInstanceMethod(c1, @selector(originalMethodName));
NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:method_getTypeEncoding(    m1)];
NSInvocation *originalInvocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[originalInvocation invoke];

直接调用[self swizzle_originalMethodName];怎么样?该选择器应该在交换后指向原始实现。 - Phillip Mills
1
这真是天才!不确定为什么我没有想到。虽然我仍然遇到错误,但我不确定该怎么解决它。-[ViewController swizzle_originalMethodName]: 无法识别的选择器发送至实例0x7ff72974a670 - Corey Gagnon
啊,对了...你正在不同的类之间进行交换。我需要考虑一下。 - Phillip Mills
这个问题在SO上出现了几次,但从未完全回答过。有些答案深入探讨了交换技术,但从未包含任何经过测试和证明可行的代码。这已经让我疯狂了好几个小时了。哈哈 - Corey Gagnon
2个回答

14

如果您正在类层次结构中进行方法混编,例如您有一个子类,它会将其祖先的某个方法与其自己的某个方法混编在一起,那么您只需让被混编进来的方法表面上调用自身 - 这个调用实际上会调用被混编出去的方法,因为这些方法已经被交换了。在您的情况下,您将拥有:

- (void)swizzle_originalMethodName
{
   NSLog(@"FAKE %s", __func__);
   [self swizzle_originalMethodName]; // call original
}

由于您正在进行跨类交换,因此这在您的情况下不起作用,所以 self 不引用具有已交换方法的类。而且你没有一个可以调用被交换出来的方法的交换类的实例...

这里有一个简单的修复方法,你的交换方法需要能够调用原始实现,当设置交换时,你可以得到它。

在Objective-C中,一个方法是通过一个函数来实现的,该函数的前两个参数是调用该方法的对象引用和选择器,其余参数是该方法的参数。例如 NSString 方法:

- (NSRange)rangeOfString:(NSString *)aString

由类似以下函数实现:

NSRange rangeOfStringImp(NSString *self, SEL cmd, NSString *aString)

您可以使用method_getImplementation获取此实现函数的函数指针。

对于您的代码,首先在您的swizzle_ViewController中声明一个类型,用于存储您要替换的方法的实现函数,并声明一个全局变量来存储该函数指针:

typedef void (*OriginalImpType)(id self, SEL selector);
static OriginalImpType originalImp;

现在,在您的初始化器方法中,您需要保存方法实现,可以通过添加所示行来实现:

现在,在您的initializer方法中,您需要保存方法的实现,可以通过添加如下代码来实现:

Method m1 = class_getInstanceMethod(c1, @selector(originalMethodName));
originalImp = (OriginalImpType)method_getImplementation(m1); // save the IMP of originalMethodName

最后让你替换过的方法调用保存下来的实现:

- (void)swizzle_originalMethodName
{
   NSLog(@"FAKE %s", __func__);
   originalImp(self, @selector(originalMethodName)); // call the original IMP with the correct self & selector
}

可选的:上面的代码可以正常工作,但是它做了比所需更多的事情-两个方法实现被交换并存储在全局变量中,你真正需要做的只是保存m1的原始实现,然后将其实现设置为m2的实现。你可以通过将对method_exchangeImplementations的调用替换为以下内容来解决这个问题:

method_setImplementation(m1, method_getImplementation(m2));

虽然需要打字更多,但更清晰地说明了实际需要做什么。

希望有所帮助。


你太棒了!这个可行!昨天我为了让它工作而苦思冥想了几个小时。谢谢你。 - Corey Gagnon

7

有一个较为简单的方法可以调用原始实现,而不需要直接存储方法实现。当您交换方法的实现时,原始实现将存储在Swizzler类中。您可以使用class_getMethodImplementation函数获取交换之后的实现。以下是一个示例:

import Cocoa

let fooSelector = Selector("fooWithArg:")
let swizzledFooSelector = Selector("swizzled_fooWithArg:")

class A: NSObject {
    @objc dynamic func foo(arg: String) {
        print("Foo \(arg) in A")
    }
}

class B: NSObject {
    private typealias FooFunc = @convention(c) (AnyObject, Selector, String) -> Void
    @objc func swizzled_foo(arg: String) {
        print("Swizzled_foo \(arg) in B")

        unsafeBitCast(
            class_getMethodImplementation(B.self, swizzledFooSelector),
            to: FooFunc.self
        )(self, fooSelector, arg)
    }
}

method_exchangeImplementations(
    class_getInstanceMethod(A.self, fooSelector)!,
    class_getInstanceMethod(B.self, swizzledFooSelector)!
)

A().foo(arg: "bar")

Calling the swizzled out implementation


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接