一、NSProxy 简介
NSProxy 是一个实现了 NSObject 协议类似于 NSObject 的抽象基类,是根类,与 NSObject 类似:
NS_ROOT_CLASS
@interface NSProxy < NSObject> {
Class isa;
}
+ ( id) alloc;
+ ( id) allocWithZone: ( nullable NSZone * ) zone NS_AUTOMATED_REFCOUNT_UNAVAILABLE;
+ ( Class) class;
- ( void ) forwardInvocation: ( NSInvocation * ) invocation;
- ( nullable NSMethodSignature * ) methodSignatureForSelector: ( SEL) sel NS_SWIFT_UNAVAILABLE ( "NSInvocation and related APIs not available" ) ;
- ( void ) dealloc;
- ( void ) finalize;
@property ( readonly, copy) NSString * description;
@property ( readonly, copy) NSString * debugDescription;
+ ( BOOL) respondsToSelector: ( SEL) aSelector;
- ( BOOL) allowsWeakReference API_UNAVAILABLE ( macos, ios, watchos, tvos) ;
- ( BOOL) retainWeakReference API_UNAVAILABLE ( macos, ios, watchos, tvos) ;
Typically, a message to a proxy is forwarded to the real object or causes the proxy to load ( or transform itself into) the real object. Subclasses of NSProxy can be used to implement transparent distributed messaging ( for example, NSDistantObject) or for lazy instantiation of objects that are expensive to create.
NSProxy implements the basic methods required of a root class, including those defined in the NSObject protocol. However, as an abstract class it doesn’t provide an initialization method, and it raises an exception upon receiving any message it doesn’t respond to. A concrete subclass must therefore provide an initialization or creation method and override the forwardInvocation: and methodSignatureForSelector: methods to handle messages that it doesn’t implement itself. A subclass’s implementation of forwardInvocation: should do whatever is needed to process the invocation, such as forwarding the invocation over the network or loading the real object and passing it the invocation. methodSignatureForSelector: is required to provide argument type information for a given message; a subclass’s implementation should be able to determine the argument types for the messages it needs to forward and should construct an NSMethodSignature object accordingly. See the NSDistantObject, NSInvocation, and NSMethodSignature class specifications for more information.
看完文档的介绍,我们应该能对 NSProxy 有个初步印象,它仅仅是个转发消息的场所,至于如何转发,取决于派生类的具体实现,比如可以在内部 hold 住(或创建)一个对象,然后把消息转发给该对象,那我们就可以在转发的过程中做些“手脚”了,甚至也可以不去创建这些对象,去做任何你想做的事情,但是必须要实现它的 forwardInvocation: 和 methodSignatureForSelector: 方法。
二、NSProxy 模拟多继承
大致过程就是让它持有要实现多继承的类的对象,然后用多个接口定义不同的行为,并让 Proxy 去实现这些接口,然后在转发的时候把消息转发到实现了该接口的对象去执行,这样就好像实现了多重继承一样。注意:这个真不是多重继承,只是包含,然后把消息路由到指定的对象而已,其实完全可以用 NSObject 类来实现; NSObject 寻找方法顺序:本类 -> 父类 -> 动态方法解析 -> 备用对象 -> 消息转发; NSproxy 寻找方法顺序:本类 -> 消息转发; 同样做“消息转发”,NSObject 会比 NSProxy 多做好多事,也就意味着耽误很多时间。 首先新建两个基类如下:
@implementation classA
- ( void ) infoA {
NSLog ( @"classA" ) ;
}
@end
@implementation classB
- ( void ) infoB {
NSLog ( @"classB" ) ;
}
@end
@interface ClassProxy : NSProxy
@property ( nonatomic, strong, readonly) NSMutableArray * targetArray;
- ( void ) target: ( id) target;
- ( void ) handleTargets: ( NSArray * ) targets;
@end
NSProxy 必须以子类的形式出现,因为考虑到很可能还有其他类需要 ClassProxy 来代理,这里做一个数组来存放需要代理的类:
@interface ClassProxy ( )
@property ( nonatomic, strong) NSMutableArray * targetArray;
@property ( nonatomic, strong) NSMutableDictionary * methodDic;
@property ( nonatomic, strong) id target;
@end
然后 target 和相对应的 method name 做了一个字典来存储,方便获取:
- ( void ) registMethodWithTarget: ( id) target {
unsigned int countOfMethods = 0 ;
Method * method_list = class_copyMethodList ( [ target class] , & countOfMethods) ;
for ( int i = 0 ; i < countOfMethods; i++ ) {
Method method = method_list[ i] ;
SEL sel = method_getName ( method) ;
const char * sel_name = sel_getName ( sel) ;
NSString * method_name = [ NSString stringWithUTF8String: sel_name] ;
self . methodDic[ method_name] = target;
}
free ( method_list) ;
}
- ( void ) forwardInvocation: ( NSInvocation * ) invocation {
SEL sel = invocation. selector;
NSString * methodName = NSStringFromSelector ( sel) ;
id target = self . methodDic[ methodName] ;
if ( target) {
[ invocation invokeWithTarget: target] ;
}
}
- ( NSMethodSignature * ) methodSignatureForSelector: ( SEL) sel {
NSMethodSignature * Method;
NSString * methodName = NSStringFromSelector ( sel) ;
id target = self . methodDic[ methodName] ;
if ( target) {
Method = [ target methodSignatureForSelector: sel] ;
} else {
Method = [ super methodSignatureForSelector: sel] ;
}
return Method;
}
methodSignatureForSelector: 得到对应的方法签名,通过 forwardInvocation: 转发,调用和打印结果如下所示:
- ( void ) viewDidLoad {
[ super viewDidLoad] ;
[ self classInheritance] ;
}
- ( void ) classInheritance {
classA * A = [ [ classA alloc] init] ;
classB * B = [ [ classB alloc] init] ;
ClassProxy * proxy = [ ClassProxy alloc] ;
[ proxy handleTargets: @ [ A, B] ] ;
[ proxy performSelector: @selector ( infoA) ] ;
[ proxy performSelector: @selector ( infoB) ] ;
}
classA
classB
三、NSProxy 避免循环引用
由于苹果在 iOS10 以上给出了 timer 的 block 方式,已经可以解决循环引用的问题。因此这里只说明利用 NSProxy 如何解决循环引用,实际情况可直接使用系统的方法。 首先因为 NSTimer 创建的时候需要传入一个 target,并且持有它,而 target 本身也会持有 timer 所以会造成循环引用,因此将 target 用 NSProxy 的子类代替,如下所示:
- ( void ) viewDidLoad {
[ super viewDidLoad] ;
self . timer = [ NSTimer timerWithTimeInterval: 1
target: [ WeakProxy proxyWithTarget: self ]
selector: @selector ( invoked: )
userInfo: nil
repeats: YES] ;
[ [ NSRunLoop mainRunLoop] addTimer: self . timer forMode: NSRunLoopCommonModes] ;
}
- ( void ) invoked: ( NSTimer * ) timer {
NSLog ( @"1" ) ;
}
在 WeakProxy 中设定 target 为弱引用:
@interface WeakProxy ( )
@property ( nonatomic, weak) id target;
@end
@implementation WeakProxy
+ ( instancetype) proxyWithTarget: ( id) target {
return [ [ self alloc] initWithTarget: target] ;
}
- ( instancetype) initWithTarget: ( id) target {
self . target = target;
return self ;
}
- ( NSMethodSignature * ) methodSignatureForSelector: ( SEL) sel {
return [ self . target methodSignatureForSelector: sel] ;
}
- ( void ) forwardInvocation: ( NSInvocation * ) invocation {
SEL sel = invocation. selector;
if ( [ self . target respondsToSelector: sel] ) {
[ invocation invokeWithTarget: self . target] ;
}
}
@end
四、NSProxy 实现 AOP
AOP(Aspect Oriented Programming),它是可以通过预编译方式和运行时动态代理实现在不修改源代码的情况下给程序动态添加功能的一种技术。iOS 中面向切片编程一般有两种方式 ,一种是直接基于 runtime 的 method-Swizzling 机制来实现方法替换从而达到 hook 的目的,另一种就是基于 NSProxy。 OC 的动态语言的核心部分应该就是 objc_msgSend 方法的调用,该函数的声明大致如下:
id objc_msgSend ( id self , SEL _cmd, . . . )
只要能够 Hook 到对某个对象的 objc_msgSend 的调用,并且可以修改其参数甚至于修改成任意其它 selector 的 IMP,就可以实现 AOP:
@interface MyProxy : NSProxy {
id _innerObject;
}
+ ( instancetype) proxyWithObj: ( id) object;
@end
@interface Dog : NSObject
- ( NSString * ) barking: ( NSInteger) months;
@end
@implementation MyProxy
+ ( instancetype) proxyWithObj: ( id) object {
MyProxy * proxy = [ MyProxy alloc] ;
proxy-> _innerObject = object;
return proxy;
}
- ( NSMethodSignature * ) methodSignatureForSelector: ( SEL) sel {
return [ _innerObject methodSignatureForSelector: sel] ;
}
- ( void ) forwardInvocation: ( NSInvocation * ) invocation {
if ( [ _innerObject respondsToSelector: invocation. selector] ) {
NSString * selectorName = NSStringFromSelector ( invocation. selector) ;
NSLog ( @"Before calling %@" , selectorName) ;
[ invocation retainArguments] ;
NSMethodSignature * sig = [ invocation methodSignature] ;
NSUInteger cnt = [ sig numberOfArguments] ;
for ( int i = 0 ; i < cnt; i++ ) {
const char * type = [ sig getArgumentTypeAtIndex: i] ;
if ( strcmp ( type, "@" ) == 0 ) {
NSObject * obj;
[ invocation getArgument: & obj atIndex: i] ;
NSLog ( @"parameter (%d)'class is %@" , i, [ obj class] ) ;
} else if ( strcmp ( type, ":" ) == 0 ) {
SEL sel;
[ invocation getArgument: & sel atIndex: i] ;
NSLog ( @"parameter (%d) is %@" , i, NSStringFromSelector ( sel) ) ;
} else if ( strcmp ( type, "q" ) == 0 ) {
int arg = 0 ;
[ invocation getArgument: & arg atIndex: i] ;
NSLog ( @"parameter (%d) is int value is %d" , i, arg) ;
}
}
[ invocation invokeWithTarget: _innerObject] ;
const char * retType = [ sig methodReturnType] ;
if ( strcmp ( retType, "@" ) == 0 ) {
NSObject * ret;
[ invocation getReturnValue: & ret] ;
NSLog ( @"return value is %@" , ret) ;
}
NSLog ( @"After calling %@" , selectorName) ;
}
}
@end
@implementation Dog
- ( NSString * ) barking: ( NSInteger) months {
return months > 3 ? @"wang!" : @"Oh!" ;
}
@end
Dog * dog = [ MyProxy proxyWithObj: [ Dog alloc] ] ;
[ dog barking: 4 ] ;
上面的代码中,可以任意更改参数、调用的方法,甚至转发给其它类型的对象,这确实达到了 Hook 对象的目的,也就是可以实现 AOP 的功能:
typedef void ( ^ proxyBlock) ( id target, SEL selector) ;
NS_ASSUME_NONNULL_BEGIN
@interface AOPProxy : NSProxy
+ ( instancetype) proxyWithTarget: ( id) target;
- ( void ) inspectSelector: ( SEL) selector preSelTask: ( proxyBlock) preTask endSelTask: ( proxyBlock) endTask;
@end
@interface AOPProxy ( )
@property ( nonatomic, strong) id target;
@property ( nonatomic, strong) NSMutableDictionary * preSelTaskDic;
@property ( nonatomic, strong) NSMutableDictionary * endSelTaskDic;
@end
- ( void ) inspect {
NSMutableArray * targtArray = [ AOPProxy proxyWithTarget: [ NSMutableArray arrayWithCapacity: 1 ] ] ;
[ ( AOPProxy * ) targtArray inspectSelector: @selector ( addObject: ) preSelTask: ^ ( id target, SEL selector) {
[ target addObject: @"Begin" ] ;
NSLog ( @"%@ 加进来之前" , target) ;
} endSelTask: ^ ( id target, SEL selector) {
[ target addObject: @"End" ] ;
NSLog ( @"%@ 加进来之后" , target) ;
} ] ;
[ targtArray addObject: @"第一个元素" ] ;
}
( "Begin" ) 加进来之前
( "Begin" ,
"U662f\U4e00\U4e2a\U5143\U7d20" ,
"End" )
加进来之后
五、NSProxy 实现延迟初始化(Lazy Initialization)
在 [SomeClass lazy] 之后调用 doSomthing,首先进入 forwardingTargetForSelector,_object 为 nil 并且不是 init 开头的方法的时候会调用 init 初始化对象,然后将消息转发给代理对象 _object; 在 [SomeClass lazy] 之后调用 initWithXXX:,首先进入 forwardingTargetForSelector 返回 nil,然后进入 methodSignatureForSelector: 和 forwardInvocation: 保存自定义初始化方法的调用,最后调用 doSomthing,进入 forwardingTargetForSelector,_object 为 nil 并且不是 init 开头的方法的时候会调用自定义初始化方法,然后将消息转发给代理对象 _object。
SomeClass * object = [ SomeClass lazy] ;
[ object doSomething] ;