NSInvocation的巧妙用法
iOS消息转发机制中介绍,Objective-C 是动态语言,所有的消息都是在 Runtime 进行派发的。
1
OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ ) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
objc_msgSend
是 C 函数,苹果不提倡我们直接使用该函数来向对象消息。
performSelector
1
2
3
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
在 ARC 场景下 performSelector 可能会造成内存泄漏
1 2 3 4 5 6 7
//#pragma clang diagnostic push //#pragma clang diagnostic ignored "-Warc-performSelector-leaks" if (data.cellCustomSEL) { // warning: PerformSelector may cause a leak because its selector is unknown [self performSelector:data.cellCustomSEL withObject:cell withObject:data]; } //#pragma clang diagnostic pop
performSelector
至多接收 2 个参数,如果参数多余 2 个,我们就无法使用performSelector
来向对象发送消息了。performSelector 限制参数类型为 id,以标量数据(int double NSInteger 等)为参数的方法使用 performSelector 调用会出现各种各样诡异的问题
NSInvocation
场景一 注册某一类方法或是模块为多方提供统一调用
场景介绍
多个客户端需要调用同一个服务的多个能力,每个客户端都要跟服务器制定一套自己的沟通协议,但是调用是服务的能力其实都是同一个,如下图
导致代码复用性极差,改版后如下:
其中的关键技术用的就是NSInvocation来实现的。
首先我们要整合多个客户端,定义用同一套协议,不能各自为营。
其次就是对于协议能够调用到正确的Native模块中。
实战
定义层
假设我们定义协议如下:
1
2
3
4
5
6
{
"handlerName": "bridge_share",
"data": {
"type": "wechat"
}
}
桥接类我们这样设计:
公共父类
BridgeModule
,所有的需要与客户机沟通的能力或是模块都需要继承至BridgeModule
来实现。定义标准规范方法
1
- (void)bridge_share:(id)data nativeBidgeResponseCallback:(Callback)responseCallback
使用层
先总结
- 中继器注册所有桥接类和类中的方法
- 客户机开始调用,我们能够拿到
handlerName=bridge_share
- 通过
handlerName
我们就能根据第一步在中继器中找到我们需要执行NSInvocation
的类和方法了
中继器注册所有桥接类和类中的方法
使用runtime方法获取所有继承至
BridgeModule
的类和方法1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
gMessageModuleClassMap = [NSMutableDictionary new]; int count = objc_getClassList(NULL,0); NSMutableArray * moduleClasses = [NSMutableArray new]; Class *classes = (Class *)malloc(sizeof(Class) * count); objc_getClassList(classes, count); for (int i = 0; i < count; i++) { if ([BridageModule class] == class_getSuperclass(classes[i])) { [moduleClasses addObject:classes[i]]; } } free(classes); for (Class moduleClass in moduleClasses) { unsigned int count; Method *methodList = class_copyMethodList(moduleClass, &count); for (unsigned int i = 0; i < count; i++) { Method method = methodList[i]; NSString *methodName = NSStringFromSelector(method_getName(method)); if ([methodName hasSuffix:@":nativeBidgeResponseCallback:"]) { NSArray *components = [methodName componentsSeparatedByString:@":"]; NSString *messageName = components.firstObject; if (messageName.length > 0) { [gMessageModuleClassMap setObject:moduleClass forKey:messageName]; } } } free(methodList); }
这样之后
gMessageModuleClassMap
中就保存着我们定义的协议handlerName
和要执行他的类客户机开始调用,我们能够拿到
handlerName=bridge_share
1 2 3 4
Class moduleClass = gMessageModuleClassMap[handlerName]; BridgeModule *nativeModule = [[moduleClass alloc] init]; NSString *methodString = [NSString stringWithFormat:@"%@%@", handlerName, @":nativeBidgeResponseCallback:"]; SEL selector = NSSelectorFromString(methodString);
通过
handlerName
我们就能根据第一步在中继器中找到我们需要执行NSInvocation
的类和方法了1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
NSMethodSignature *signature = [nativeModule methodSignatureForSelector:selector]; if (!signature) { return NO; } id requestData = message.data; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; invocation.target = nativeModule; [invocation setSelector:selector]; [invocation setArgument:&requestData atIndex:2]; if (message.callback) { Block_copy((__bridge void *)message.callback); [invocation setArgument:&message.callback atIndex:3]; } [invocation invoke];
场景二 block
有关block使用NSInvocation,问题是如何获得block的MethodSignature,block并没有selector,我们可以通过获取block的编码的Signature来使用-[NSMethodSignature signatureWithObjCTypes:]
其中Clang官方文档和stackoverflow都有获取block的函数编码的讲解。
iOS的lua热修复详解的热修复中,block的方法替换采用的是CTObjectiveCRuntimeAdditions的解法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
struct CTBlockLiteral {
void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
int flags;
int reserved;
void (*invoke)(void *, ...);
struct block_descriptor {
unsigned long int reserved; // NULL
unsigned long int size; // sizeof(struct Block_literal_1)
// optional helper functions
void (*copy_helper)(void *dst, void *src); // IFF (1<<25)
void (*dispose_helper)(void *src); // IFF (1<<25)
// required ABI.2010.3.16
const char *signature; // IFF (1<<30)
} *descriptor;
// imported variables
};
enum {
CTBlockDescriptionFlagsHasCopyDispose = (1 << 25),
CTBlockDescriptionFlagsHasCtor = (1 << 26), // helpers have C++ code
CTBlockDescriptionFlagsIsGlobal = (1 << 28),
CTBlockDescriptionFlagsHasStret = (1 << 29), // IFF BLOCK_HAS_SIGNATURE
CTBlockDescriptionFlagsHasSignature = (1 << 30)
};
typedef int CTBlockDescriptionFlags;
@interface SpaBlockDescription : NSObject
@property (nonatomic, readonly) CTBlockDescriptionFlags flags;
@property (nonatomic, readonly) NSMethodSignature *blockSignature;
@property (nonatomic, readonly) unsigned long int size;
@property (nonatomic, readonly) id block;
- (id)initWithBlock:(id)block;
@end
@implementation SpaBlockDescription
- (id)initWithBlock:(id)block
{
if (self = [super init]) {
_block = block;
struct CTBlockLiteral *blockRef = (__bridge struct CTBlockLiteral *)block;
_flags = blockRef->flags;
_size = blockRef->descriptor->size;
if (_flags & CTBlockDescriptionFlagsHasSignature) {
void *signatureLocation = blockRef->descriptor;
signatureLocation += sizeof(unsigned long int);
signatureLocation += sizeof(unsigned long int);
if (_flags & CTBlockDescriptionFlagsHasCopyDispose) {
signatureLocation += sizeof(void(*)(void *dst, void *src));
signatureLocation += sizeof(void (*)(void *src));
}
const char *signature = (*(const char **)signatureLocation);
_blockSignature = [NSMethodSignature signatureWithObjCTypes:signature];
}
}
return self;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"%@: %@", [super description], _blockSignature.description];
}
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
struct Block_literal_1 {
void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 {
unsigned long int reserved; // NULL
unsigned long int size; // sizeof(struct Block_literal_1)
// optional helper functions
// void (*copy_helper)(void *dst, void *src); // IFF (1<<25)
// void (*dispose_helper)(void *src); // IFF (1<<25)
// required ABI.2010.3.16
// const char *signature; // IFF (1<<30)
void* rest[1];
} *descriptor;
// imported variables
};
enum {
BLOCK_HAS_COPY_DISPOSE = (1 << 25),
BLOCK_HAS_CTOR = (1 << 26), // helpers have C++ code
BLOCK_IS_GLOBAL = (1 << 28),
BLOCK_HAS_STRET = (1 << 29), // IFF BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30),
};
static const char *__BlockSignature__(id blockObj)
{
struct Block_literal_1 *block = (__bridge void *)blockObj;
struct Block_descriptor_1 *descriptor = block->descriptor;
assert(block->flags & BLOCK_HAS_SIGNATURE);
int offset = 0;
if(block->flags & BLOCK_HAS_COPY_DISPOSE)
offset += 2;
return (char*)(descriptor->rest[offset]);
}
// NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:[NSMethodSignature signatureWithObjCTypes:__BlockSignature__(block)]];
// invocation.target = block;
场景三 热修复中的消息转发
以这个iOS的lua热修复详解为例子,先简单介绍先热修复中方法修复原理:
热修复核心逻辑,方法替换,将需要热修的方法实现指向_objc_msgForward
,原实现指向我们新定义的sel
然后hookforwardInvocation
,使他指向我们实现的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static void replaceMethod(Class klass, SEL sel)
{
if (klass == nil || sel == nil) {
return ;
}
SEL originSelector = spa_originForSelector(sel);
Method targetMethod = class_getInstanceMethod(klass, sel);
if (targetMethod) {
const char *typeEncoding = method_getTypeEncoding(targetMethod);
class_addMethod(klass, originSelector, method_getImplementation(targetMethod), typeEncoding);
spa_swizzleForwardInvocation(klass);
// We use forwardInvocation to hook in.
class_replaceMethod(klass, sel, spa_getMsgForwardIMP(klass, sel), typeEncoding);
[[SpaClass replacedClassMethods] addObject:@{@"class":NSStringFromClass(klass), @"sel":NSStringFromSelector(sel)}];
}
}
static void spa_swizzleForwardInvocation(Class klass)
{
NSCParameterAssert(klass);
// get origin forwardInvocation impl, include superClass impl,not NSObject impl, and class method to kClass
SEL originForwardSelector = NSSelectorFromString(SPA_ORIGIN_FORWARD_INVOCATION_SELECTOR_NAME);
if (![klass instancesRespondToSelector:originForwardSelector]) {
Method originalMethod = class_getInstanceMethod(klass, @selector(forwardInvocation:));
IMP originalImplementation = method_getImplementation(originalMethod);
class_addMethod(klass, NSSelectorFromString(SPA_ORIGIN_FORWARD_INVOCATION_SELECTOR_NAME), originalImplementation, "v@:@");
}
// If there is no method, replace will act like class_addMethod.
class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__SPA_ARE_BEING_CALLED__, "v@:@");
}
static void __SPA_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation)