使用objc_msgSend调用实例方法

sup*_*oon 7 cocoa objective-c objective-c-runtime

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

- (void) instanceTestWithStr1:(NSString *)str1 str2:(NSString *)str1;
+ (void) methodTestWithStr1:(NSString *)str1 str2:(NSString *)str1;
Run Code Online (Sandbox Code Playgroud)

我可以在A类中成功调用类方法:

objc_msgSend(objc_getClass("ClassB"), sel_registerName("methodTestWithStr1:str2:"), @"111", @"222");
Run Code Online (Sandbox Code Playgroud)

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

objc_msgSend([[objc_getClass("ClassB") alloc] init], sel_registerName("instanceTestWithStr1:str2:"), @"111", @"222");
Run Code Online (Sandbox Code Playgroud)

但问题是要获得一个Class BI实例必须调用"initWithXXXXX:XXXXXX:XXXXXX"而不是"init",以便将一些必要的参数传递给B类来执行init的工作.所以我在类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"));

bbu*_*bum 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);
Run Code Online (Sandbox Code Playgroud)

如果您想要传递参数,请参阅下文. 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);
Run Code Online (Sandbox Code Playgroud)

显然,如果您需要参数,请参阅下文.

所以我想知道是否有办法:

  1. 检查一个类是否有给定的方法(我发现"responseToSelector"将是一个)

往上看.课程响应respondsToSeletor:.如果要检查类的实例是否响应选择器,则可以调用instancesRespondToSelector:.

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

争论?见下文.

"2".检查类方法或实例方法中的给定方法(也可以使用responseToSelector)

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

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

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

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

在封面下,KVC实现了一种启发式方法,可以在类中搜索方法,或者使用与所请求的名称大致匹配的名称的ivar.主要是因为它会执行搜索_前缀等操作.NSKeyValueCoding.h标头是一个有趣的读取.

无论如何,不​​需要选择器.给出一个名字,只需:

id foo = [myInstance valueForKey:@"iVarName"];
Run Code Online (Sandbox Code Playgroud)

和:

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

显然,打字是一个主要问题.如果你有非对象类型,那么你将不得不处理进入/退出NSValue容器而不是所有东西都适合的情况,这使得你可以对KVC方法/ ivar搜索算法进行逆向工程(不是很难 - 只是一堆字符串操作和查找)然后传递任意参数如下.


请注意,您的两个调用objc_msgSend()在技​​术上都是错误的,因为它们都没有objc_msgSend()使用显式参数类型进行类型转换为非varargs形式.你需要这样的东西:

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

这是因为varargs ABI和显式参数类型ABI不一定兼容所有体系结构.ARC,IIRC明确强制执行.


还要注意,任意调用类或实例方法的概念,其中调用实例方法实时实例化类的实例确实没有多大意义.但是,嘿......你的代码.


请注意,您也不想以sel_registerName()这种方式打电话; 如果您要调用选择器,它最好已经存在.该函数显式存在于运行时定义类.最好使用NSSelectorFromString()或sel_getUid()(不幸的是,sel_registerName()由于多年来没有纪律的程序员,有效地结束了呼叫).至少你的意图是对的.


现在,objc_msgSend()根据您的需要使用,需要您回答一个问题,结果答案将完全不同.一个答案是"哦,只做X"的简单路线,另一个是"哦,圣牛,你正走在痛苦的道路上".

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

最终,有多少种不同的参数将决定代码的复杂程度. 如果你只有0,1或2个参数,它们总是对象,坚持invokeSelector:,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]);
}
Run Code Online (Sandbox Code Playgroud)

事实证明,这比编写一堆tricksie运行时代码要容易得多.


Ric*_*III 3

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

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

以下是一些编译注意事项:

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

话不多说,这是代码(我在其中添加了一些NSNumber和NSString类别用于测试,但它们与此代码的目的无关):

#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"]];
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

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·罗斯三世