【问题标题】:Call instance method with objc_msgSend使用 objc_msgSend 调用实例方法
【发布时间】:2012-09-19 03:31:31
【问题描述】:

我正在尝试使用objc_msgSend 方法动态调用某些方法。 假设我想从 A 类调用 B 类中的某个方法,而 B 类中有两种方法,例如:

- (void) instanceTestWithStr1:(NSString *)str1 str2:(NSString *)str1;
+ (void) methodTestWithStr1:(NSString *)str1 str2:(NSString *)str1;

并且我可以在A类中成功调用这样的类方法:

objc_msgSend(objc_getClass("ClassB"), sel_registerName("methodTestWithStr1:str2:"), @"111", @"222");

我也可以在A类中成功调用这样的实例方法:

objc_msgSend([[objc_getClass("ClassB") alloc] init], sel_registerName("instanceTestWithStr1:str2:"), @"111", @"222");

但问题是要获得 B 类的实例,我必须调用“initWithXXXXXX:XXXXXX:XXXXXX”而不是“init”所以 将一些必要的参数传递给 B 类来执行初始化操作。所以我将 ClassB 的实例存储在 A 类中作为变量: self.classBInstance = [[ClassB alloc] initWithXXXXXX:XXXXXX:XXXXXX];

然后我这样调用方法(成功):

问题是,我想通过简单地应用类名和方法 sel 来调用方法,例如“ClassName”和“SEL”,然后动态调用它:

  1. 如果是类方法。然后像这样称呼它: objc_msgSend(objc_getClass("ClassName"), sel_registerName("SEL"));

  2. 如果是实例方法,在调用类中找到已有的类实例变量然后: objc_msgSend([self.classInstance, sel_registerName("SEL"));

所以我想知道有没有办法:

  1. 检查一个类是否有给定的方法(我发现“responseToSelector”就是那个)

  2. 检查给定方法是类方法还是实例方法(可能也可以使用responseToSelector

  3. 检查一个类是否有给定类的实例变量所以我可以调用一个实例方法,如: objc_msgSend(objc_getClassInstance(self, "ClassB"), sel_registerName("SEL"));

【问题讨论】:

  • 重新标记为objective-c运行时,因为我想要我的金牌:D
  • 如果您使用的是属性,而不是 iVar,那么这是可能的。仅使用 iVar,这很困难,但可能并非不可能。
  • 在多个结果的场景下怎么办?假设我发送选择器intValue,并且我有两个 NSNumber iVar。我应该对所有这些进行评估吗?还是只是第一个?
  • 知道您要做什么会很有帮助。这闻起来像一座语言桥。

标签: objective-c cocoa objective-c-runtime


【解决方案1】:

您可能想要read this。您要问的实际上是“我想制作一个新的调度程序”,要回答 那个 问题,您应该对现有调度程序的工作原理有一个透彻的了解。

请告诉meet这是你在做什么?在语言之间架起一座桥梁?因为如果不是这样,那么您将陷入一个非常有趣的兔子洞,但可能不是一个非常有效或优雅的解决方案。

现在:

问题是,我想通过简单地应用 类名和方法 sel 像 "ClassName" 和 "SEL" 然后调用 它是动态的:

  1. 如果它是一个类方法。然后像这样称呼它: objc_msgSend(objc_getClass("ClassName"), sel_registerName("SEL"));
Class klass = objc_getClass("ClassName"); // NSClassFromString(@"ClassName")
SEL sel = sel_getUID("selector"); // NSSelectorFromString(@"selector");
if ( [klass respondsToSelector:sel] )
    objc_msgSend(klass, sel);

如果您有要传递的参数,请参见下文。理查德的回答中的NSInvocation 是一种高级方法,但间接使用了objc_msgSend()(并且 NSInvocation 有限制)。

“2”。如果是实例方法,则在调用类中找到现有的类实例变量,然后: objc_msgSend([self.classInstance, sel_registerName("SEL"));

这没有意义。类没有实例变量。一个类的实例有一个实例变量,但是你可能需要一个特定的实例,而不是你在这个地方创建的一些随机实例。实例携带状态并随着时间的推移累积该状态。

在任何情况下,您都可以使用上述机制轻松地在类上调用 classInstance 方法(这完全没有意义——只需编写 [self classInstance] 并完成它),然后从那里:

id classInstance = [self classInstance];
SEL sel = ... get yer SEL here ...;
if ([classInstance respondsToSelector:sel])
   objc_msgSend(classInstance, sel);

显然,如果您需要参数,请参见下文。

所以我想知道有没有办法:

  1. 检查一个类是否有给定的方法(我发现“responseToSelector”就是那个)

见上文。类响应respondsToSeletor:。如果你想检查类的实例是否响应选择器,你可以调用instancesRespondToSelector:

Class klass = ... get yer class on...;
SEL someSelector = ... get that SEL ...;
if ([klass instancesRespondToSelector:someSelector])
    objc_msgSend(instanceOfKlassObtainedFromSomewhere, someSelector);

再次,争论?见下文。

“2”。检查类方法或实例方法中的给定方法(可能也可以使用responseToSelector

见上文。给定一个类,您检查该类或实例是否响应任何给定的选择器。请注意,对于 NSObject 协议中的许多选择器,类将响应许多 NSObject 实例方法,因为 元类 - 类是其实例的类 - 实现了相当多的上述方法.

“3”。检查一个类是否具有给定类的实例变量所以我可以调用一个实例方法,如: objc_msgSend(objc_getClassInstance(self, "ClassB"), sel_registerName("SEL"));

setter/getter 方法与实例变量之间的关系完全是巧合。不需要 ivar,也不需要任何给定的 ivar 的 setter 和/或 getter。因此,这个问题没有意义,因为随意调用基于 ivar 名称的方法通常会失败。

正如 Richard 建议的那样,您可以使用键值编码,但这意味着手动装箱传递给 setter 的值以及手动取消装箱从 getter 检索到的非对象类型的值。

在幕后,KVC 实现了一种启发式方法,以在类中搜索名称与请求名称大部分匹配的方法或 ivar。主要是因为它会做一些事情,比如搜索 _ 前缀等。NSKeyValueCoding.h 标头是一个有趣的阅读。

在任何情况下,都不需要选择器。给定一个名字,做:

id foo = [myInstance valueForKey:@"iVarName"];

还有:

[myInstance setValue:[NSNumber numberWithInt:42] forKey:@"ivarName"];

显然,打字是一个主要问题。如果您有非对象类型,那么您将不得不处理让它们进出 NSValue 容器的问题,并且并非所有东西都适合,这使您不得不对 KVC 方法/ivar 搜索算法进行逆向工程(不太hard -- 只是一堆字符串操作和查找),然后传递任意参数,如下所示。


请注意,您对objc_msgSend() 的两次调用在技术上都是错误的,因为两者都没有将objc_msgSend() 类型转换为具有显式参数类型的非可变参数形式。你需要类似的东西:

// - (void) instanceTestWithStr1:(NSString *)str1 str2:(NSString *)str1;
void (*msgSendVoidStrStr(id, SEL, NSString*, NSString*) = (void*)objc_msgSend;
msgSendVoidStrStr(...obj..., @selector(instanceTestWithStr1:str2:), str1, str2);

这是因为可变参数 ABI 和显式参数类型 ABI 不一定在所有架构上都兼容。 ARC,IIRC,明确地强制执行。


还要注意,任意调用类或实例方法(其中调用实例方法会即时实例化类的实例)的概念实际上没有多大意义。但是,嘿……你的代码。


请注意,您也永远不想以这种方式致电sel_registerName();如果你要调用一个选择器,它最好已经存在。该函数显式存在用于在运行时定义类。最好使用NSSelectorFromString()sel_getUid()(不幸的是,由于多年来没有纪律的程序员,实际上最终调用sel_registerName())。至少你的意图是正确的。


现在,如您所愿使用objc_msgSend(),您需要回答一个问题,而得到的答案将完全不同。一个答案是“哦,做X”的简单路线,另一个是“哦,天哪,你正走在一条痛苦的道路上”。

问题:您是否有一组固定的方法签名,还是必须传递多种类型的任意参数集?

最终,有多少和多少不同类型的参数将决定代码的复杂程度。如果您只有 0,1 或 2 个参数并且它们始终是对象,请坚持使用 @ 987654342@、invokeSelector:withObject:invokeSelector:withObject:withObject:

如果答案是“固定的方法签名集”,那么答案就在上面;只需声明一个函数指针,其中包含您想要使用的所有不同可能的方法签名,并在运行时选择正确的方法,然后按照上述方法将其作为函数调用调用。

现在,如果答案是“具有许多不同参数组合的任意选择器集”,则答案要困难得多。您需要使用libffi(或类似的东西)以编程方式执行编译器在编译msgSendVoidStrStr(...obj..., @selector(instanceTestWithStr1:str2:), str1, str2); 时所做的事情。 libffi 提供了使用几乎任意参数和返回类型对调用进行编码所需的一切。

它并不容易使用。事实上,使用 libffi 构建自己的堆栈框架已经够难了,编写一个脚本来转储所有可能的调用组合并为每个组合创建一个覆盖函数可能会更容易,可能会将参数作为NSArray* 容器和在内部解码它们。类似(自动生成):

void msgSendVoidStrStr(id obj, SEL _cmd, NSArray*args) {
    objc_msgSend(obj, _cmd, [args objectAtIndex:0], [args objectAtIndex:1]);
}

事实证明,这比编写一堆技巧性运行时代码更容易调试。

【讨论】:

  • 您真正缺少的是对子类化的支持。 [class instancesRespondToSelector:] 很棒,但是如果我将 addObject: 方法发送到 NSArray 并且我有 NSMutableArray 成员怎么办?在实践中,您的最终“解决方案”并不是真正的解决方案,因此这确实不能回答问题。
  • 另外,如果没有大量的copy-pasta代码,就无法解决可变参数问题,因为C标准要求最小个参数可以是传递给函数的数量高达 1024。使用 [args objectAtIndex:0][args objectAtIndex:1] 很棒,但如果我的数组不包含所有这些参数怎么办。只是思考的食物。
  • 最后,我想详细说明一下为什么我选择NSInvocation 而不是试图弄乱objc_msgSend 真正的原因,就像你提到的那样是类型转换。在幕后,NSInvocation 为您完成了这项工作(具有相当多的 C++ 魔法),因此我可以确信这些参数是正确对齐的。通过在释放时自动释放initnew 返回的任何对象,它还有助于解决仅随机调度objc_msgSend 调用(在某种程度上)存在的内存管理问题。 (不幸的是,其他方法无法通过该过滤器,属性也无法通过)。
  • 您的回复中有几个错误;首先,+instanceRespondsToSelector: 和 -respondsToSelector: 之间的唯一区别是后者首先执行 [self class]。您的第二条评论没有任何意义;我从不建议发送未定义/未使用的参数。最后,在 NSInvocation/NSMethodSignature 机制中没有一行 C++。它们的工作方式与 libffi 类似。
  • 话虽如此,NSInvocation 更容易使用,如果你能适应类的限制。然而,使用 libffi 的自动生成解决方案和抽象解决方案都非常可行,并且根据需要,加载速度更快并且可能更易于维护(例如,由于 NSInvocations 限制,PyObjC 使用 libffi)。
【解决方案2】:

好的,这是我的基本实现。它假设相当多:

  • 所有对象都必须是NSObject 的子类。时期。如果您不这样做,那么您将遇到-methodSignatureForSelector 的问题。
  • 所有方法都必须有一个有效的签名。这意味着像我这样的运行时黑客在动态添加方法时有点搞砸了,必须先进行研究。
  • 假定向原语发送消息是可以的(使用 KVC 提供的自动装箱,例如 double 被提升为 NSNumber
  • 它不支持传递给函数的原始参数(因此这里只支持对象,或者如果您想对桥接感到疯狂,则可以使用指针)
  • 它也不支持变长函数(NSInvocation 的限制)。如果您想这样做,请尝试查找采用 va_list 的函数版本并改用它们。
  • 它只检查 iVar,而不是属性,但是 @synthesize'd 属性应该已经在列表中。

以下是一些编译说明:

  • 必须启用 ARC。我不再为非 ARC 编写代码,所以如果有人想尝试反向移植,他们可以成为我的客人。
  • 还需要 C99 VLA。任何编译 ARC Objective-C 的编译器都应该已经有了这个(事实上,我认为 clang 是唯一支持 ARC 的编译器,它确实支持 C99 VLA),但如果没有,那么你可以尝试与@987654328 混淆@ 和朋友。
  • 这在为 iOS 架构编译时未经测试。我只使用 Mac OS 进行了测试,但我在此处使用的方法应该在 iOS 上可用,如果不可用,请告诉我,我会修复它。

废话不多说,代码如下(我在其中添加了几个NSNumberNSString类别用于测试,但它们与此代码的目的无关):

#import <objc/runtime.h>

@interface NSObject(dynamicSELlookup)

-(void) performSelectorOnClassOrIvar:(Class) cls selector:(SEL) selector arguments:(NSArray *) args;

@end

@implementation NSObject (dynamicSELlookup)

-(void) performSelectorOnClassOrIvar:(Class) cls selector:(SEL) selector arguments:(NSArray *) args
{
    // we must copy to a C-array so we can take adresses. here we use C99's VLAs, so we don't have to free anything
    __unsafe_unretained id argsArray[args.count];
    [args getObjects:argsArray];

    // if its a static method, then our job is simple. create a NSInvocation from our arguments, and send it on it's way
    if ([cls respondsToSelector:selector])
    {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[cls methodSignatureForSelector:selector]];

        for (int i = 0; i < args.count; i++)
        {
            // notice the '+ 2' here. this is because there are two 'hidden' arguments to an objective-c message call - '_cmd' & 'self'.
            [invocation setArgument:&argsArray[i] atIndex:i + 2];
        }

        // set the selector of the invocation, and fire it off!
        [invocation setSelector:selector];
        [invocation invokeWithTarget:cls];
        return;
    }

    // otherwise loop through all the iVars.
    unsigned iVarCount = 0;
    Ivar *iVars = class_copyIvarList([self class], &iVarCount);

    for (int i = 0; i < iVarCount; i++)
    {
        // We are going to use KVC here, so we can auto-box our return values (thus it works for primitives too)
        id value = [self valueForKey:@(ivar_getName(iVars[i]))];

        // make sure the target class is OK, and that we respond to the selector
        if ([value isKindOfClass:cls] && [value respondsToSelector:selector])
        {
            // just like before, we create our invocation
            NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[value methodSignatureForSelector:selector]];

            for (int i = 0; i < args.count; i++)
            {
                // notice the '+ 2' here. this is because there are two 'hidden' arguments to an objective-c message call - '_cmd' & 'self'. 
                [invocation setArgument:&argsArray[i] atIndex:i + 2];
            }

            // set the selector of the invocation, and fire it off!
            [invocation setSelector:selector];
            [invocation invokeWithTarget:value];
            // uncomment the below line if you only want to execute on the first target found
            // break;
        }
    }

    free(iVars);
}

@end

@interface MyObject : NSObject
{
    @public
    int someIntegerVar;
    double someDoubleVar;

    NSObject *someObjectVar;
}

@end

@implementation MyObject
@end

@implementation NSNumber(print)

+(void) classMethod
{
    NSLog(@"Hey, I'm a class method!");
}

// simple category for showing ivars off
-(void) printValue
{
    NSLog(@"%@", self);
}

-(void) printValueWithArg:(id) argument
{
    NSLog(@"%@ - %@", self, argument);
}

@end

@implementation NSString (print)

-(void) print
{
    NSLog(@"%@", self);
}

-(void) printFormat:(id) arg
{
    NSLog(self, arg);
}

@end

// Sample Usage
int main()
{
    @autoreleasepool
    {
        MyObject *obj = [MyObject new];
        obj->someDoubleVar = M_PI;
        obj->someIntegerVar = 5;
        obj->someObjectVar = @"hello there, %@";

        [obj performSelectorOnClassOrIvar:[NSNumber class] selector:@selector(printValue) arguments:nil];
        [obj performSelectorOnClassOrIvar:[NSNumber class] selector:@selector(classMethod) arguments:nil];
        [obj performSelectorOnClassOrIvar:[NSNumber class] selector:@selector(printValueWithArg:) arguments:@[ @"Hello" ]];
        [obj performSelectorOnClassOrIvar:[NSString class] selector:@selector(print) arguments:nil];
        [obj performSelectorOnClassOrIvar:[NSString class] selector:@selector(printFormat:) arguments:@[ @"Richard J Ross III"]];
    }
}

输出:

2012-09-19 00:05:07.150 测试项目 [8592:303] 5 2012-09-19 00:05:07.152 TestProj[8592:303] 3.141592653589793 2012-09-19 00:05:07.152 TestProj[8592:303] 嘿,我是类方法! 2012-09-19 00:05:07.153 TestProj[8592:303] 5 - 你好 2012-09-19 00:05:07.153 TestProj[8592:303] 3.141592653589793 - 你好 2012-09-19 00:05:07.154 TestProj[8592:303] 你好,%@ 2012-09-19 00:05:07.154 TestProj[8592:303] 你好,理查德 J 罗斯三世

【讨论】:

    猜你喜欢
    • 2016-08-30
    • 1970-01-01
    • 1970-01-01
    • 2014-10-12
    • 2016-07-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多