7

我正在尝试使用该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");

但问题是要获得 BI 类的实例,必须调用“initWithXXXXXX:XXXXXX:XXXXXX”而不是“init”,以便将一些必要的参数传递给 B 类来执行初始化操作。所以我在 A 类中存储了一个 ClassB 的实例作为变量: self.classBInstance = [[ClassB alloc] initWithXXXXX: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"));
4

2 回答 2

8

你可能想读这个。您实际上要问的是“我想制作一个新的调度程序”,要回答这个问题,您应该对现有调度程序的工作方式有一个透彻的了解。

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

现在:

问题是,我想通过简单地应用类名和方法 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 搜索算法进行逆向工程(不太硬——只是一堆字符串操作和查找),然后传递任意参数,如下所示。


请注意,您的两次调用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()or (不幸的是,由于多年来没有纪律的程序员sel_getUid(),它实际上最终被调用)。sel_registerName()至少你的意图是正确的。


现在,要objc_msgSend()按照您的意愿使用,您需要回答一个问题,而结果的答案将截然不同。一个答案是“哦,做X”的简单路线,另一个答案是“哦,天哪,你正走在一条痛苦的道路上”。

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

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

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

现在,如果答案是“具有许多不同参数组合的任意选择器集”,那么答案就困难得多。您需要使用libffi(或类似的东西)以编程方式执行编译器在 compile 时所做的事情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]);
}

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

于 2012-09-19T05:54:32.487 回答
3

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

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

以下是一些编译说明:

  • 必须启用 ARC。我不再为非 ARC 编码,所以如果有人想尝试反向移植这个,他们可以成为我的客人。
  • 还需要 C99 VLA。任何编译 ARC Objective-C 的编译器都应该已经有这个(事实上,我认为 clang 是唯一支持 ARC 的编译器,它确实支持 C99 VLA),但如果没有,那么你可以尝试和malloc朋友们搞砸.
  • 在为 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 罗斯三世
于 2012-09-19T04:13:19.480 回答