问题
I created an objective-c method which will invoke a method via NSInvocation:
typedef void (^ScriptingEmptyBlock)();
typedef void (^ScriptingErrorBlock)(NSError *error);
- (void)scripting_execute:(NSString *)operation withParams:(nullable NSArray *)args {
SEL selector = [self scripting_selectorForOperation:operation];
Class class = [self class];
NSMethodSignature *signature = [class instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setSelector:selector];
[invocation setTarget:self];
for (int idx = 0; idx < args.count; idx ++) {
id arg = args[idx];
[invocation setArgument:&arg atIndex:idx + 2];
}
ScriptingEmptyBlock success = args[1];
// Breakpoint added on next line to test for nil
success(); // this is nil and would crash!
// (lldb) po args.count
// 3
// (lldb) po success
// Printing description of success:
// (ScriptingEmptyBlock) success = 0x0000000000000000
// (lldb) po args[1]
// (Function)
//[invocation getArgument:&success atIndex:2]; // also tried this and got nil as well
[invocation invoke];
}
The method takes an "operation" which is translated into a selector by overriding scripting_selectorForOperation:
in subclasses and then performs the invocation.
All of that works, except when the invoked method has block arguments they are nil, I added the test for nil I describe with comments, when attempting to read the closure from the array it will be nil.
Called like:
let successClosure: ScriptingEmptyBlock = {
print("Renamed product")
}
let errorClosure: ScriptingErrorBlock = { error in
print("Failed to rename product: \(error)")
}
let params:[Any] = [ "testName", successClosure, errorClosure]
object.scripting_execute (ScriptOperation.updateProductName.rawValue, withParams: params)
Why is closure becoming nil?
回答1:
success
is not nil
(in fact, NSArray
cannot contain nil
s). If you print it like NSLog(@"%@", success);
, it will say (Function)
, not (null)
. And if you print its class like NSLog(@"%@", [success class]);
, it will say _SwiftValue
. Basically, it is a Swift value that is bridged into Objective-C.
The problem is that the object success
points to is not an Objective-C block. It is a Swift closure, and Swift closures are not the same as Objective-C blocks. Trying to use invoke it as if it were an Objective-C block causes undefined behavior. po
in the debugger prints it wrong probably because it is printing it assuming it were type ScriptingEmptyBlock
(a block type). If you do po (id) success
, it will print (Function)
.
As to how you can explicitly put an Objective-C block into the array from Swift, the only way I figured out to do it something like:
let params:[Any] = [ "testName",
successClosure as (@convention(block) () -> Void)!,
errorClosure as (@convention(block) (NSError) -> Void)!]
object.scripting_execute (ScriptOperation.updateProductName.rawValue,
withParams: params)
I am not sure why it's necessary to put the function type inside a !
, but it doesn't seem to work otherwise. Maybe someone else can find a better way.
回答2:
I must admit that I don't fully understand why this is happening, but as far as I can tell this has nothing to do with using NSInvocation
and would happen even if we just passed a Swift closure to an Objective-C function via a parameter of type id
. Passing an Objective-C block via id
works just fine, not sure why: Swift closures are supposed to be compatible with Objective-C blocks. As you know, elements of NSArray
are of type id
, so any Objective-C object can be an array element.
To work around this problem of accessing a Swift closure passed via id
in Objective-C one can introduce a wrapper class.
// In a header:
@interface EmptyBlockWrapper : NSObject
@property EmptyBlock _blk;
@end
// In an implementation file (just an empty implementation):
@implementation EmptyBlockWrapper
@end
Then we can use a wrapper instance instead of a block as an array element in Swift:
let myBlock : EmptyBlock = {
print("In Swift EmptyBlock...")
}
let myBlockWrapper = EmptyBlockWrapper()
myBlockWrapper._blk = myBlock
In an Objective-C method we can call it as follows, assuming args
is NSArray *
:
EmptyBlockWrapper * emptyBlockWrapper = args[1];
emptyBlockWrapper._blk();
Hopefully this is helpful. Of course, this is just a simplified example to give you an idea; this could be made much fancier.
来源:https://stackoverflow.com/questions/44684601/swift-closure-in-array-becomes-nil-in-objective-c