使用objc_msgSend调用实例方法

时间:2012-09-19 03:31:31

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

我正在尝试使用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类实例我必须调用“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”));

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

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

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

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

2 个答案:

答案 0 :(得分:8)

您可能想要read this。你问的是有效的"我想做一个新的调度员"并回答那个问题,你应该彻底了解现有调度员的工作方式。

请告诉见面你正在做什么?建立语言之间的桥梁?因为如果不是这样的话,你就会深陷一个兔子洞,而这个兔子洞很难被探索,但可能不是一个非常有效也不优雅的解决方案。

现在:

  

问题是,我想通过简单地应用来调用方法   classname和方法sel like" ClassName"和" SEL"然后打电话   它是动态的:

     
      
  1. 如果是类方法。然后把它称为:   objc_msgSend(objc_getClass(" ClassName"),sel_registerName(" SEL"));
  2.   
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&#34 ;.如果它是一个实例方法,则在调用类中找到现有的类实例变量,然后:       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"将是一个)
  2.   

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

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

再一次,争论?见下文。

  

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

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

  

" 3&#34 ;.检查一个类是否有给定类的实例变量所以我可以调用一个实例方法,如:          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"];

[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);

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


另请注意,任意调用实例方法的类或实例方法的概念在运行时实例化类的实例确实没有多大意义。但是,嘿......你的代码。


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


现在,要根据需要使用objc_msgSend(),您需要回答一个问题,由此产生的答案将完全不同。一个答案是一个简单的路径,通过"哦,只做X",另一个是"哦,圣牛,你正走在痛苦的道路上。

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

最终,有多少种不同的参数会决定代码的复杂程度。如果你只有0,1或2个参数并且它们总是对象,那就坚持{ {1}},invokeSelector:invokeSelector:withObject:

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

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

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

NSArray*

这比调整一堆tricksie运行时代码更容易调试。

答案 1 :(得分:3)

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

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

以下是一些编译说明:

  • 必须启用ARC。我不再为非ARC编码了,所以如果有人想尝试后端移植,那么他们可以成为我的客人。
  • 还需要C99 VLA。任何正在编译ARC Objective-C的编译器都应该已经拥有它(事实上,我认为clang是唯一支持ARC的编译器,它确实支持C99 VLA),但如果没有,那么你可以尝试使用{{{ 1}}&朋友。
  • 在为iOS体系结构编译时,这是未经测试的。我只使用Mac OS测试过,但我在这里使用的方法应该在iOS上可用,如果不是,请告诉我,我会解决它。

不用多说,这里是代码(我在其中添加了一些mallocNSNumber类别进行测试,但它们与此代码的目的无关):

NSString

输出:

2012-09-19 00:05:07.150 TestProj[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] Hey, I'm a class method!
2012-09-19 00:05:07.153 TestProj[8592:303] 5 - Hello
2012-09-19 00:05:07.153 TestProj[8592:303] 3.141592653589793 - Hello
2012-09-19 00:05:07.154 TestProj[8592:303] hello there, %@
2012-09-19 00:05:07.154 TestProj[8592:303] hello there, Richard J Ross III