一、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
: nilrepeats
: 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
] ;
總結
以上是生活随笔 為你收集整理的iOS之深入解析少见却神奇的NSProxy类的多种使用 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。