steipete/Aspects
Design, quite enigmatic
- Generally we hook, hook methods of all instances of a class
Here the granularity of hooks is down to the object level
- And you don’t have to build a lot of IMPs
Using the inherent method of forwarding process
This article mainly refers to WOshICcm /Aspect
Analyze swift source code implementation of Aspects
General design of Aspects
1. The method is executed at another time
1.1. Process of method forwarding
Method, replace the hook selector implementation IMP with _objc_msgForward or _objc_msgForward_stret,
Now, if I call selector, I’m going to forward the message
1.2. Functions that actually do things
Replace the forwardInvocation implementation with a custom function
__ASPECTS_ARE_BEING_CALLED__
And add the __aspects_forwardInvocation implementation to the original forwardInvocation implementation.
The method selector that we hook, we need to forward the message
Our hook methods, selectors, all execute custom functions
__ASPECTS_ARE_BEING_CALLED__
1.3. Custom functions
Execute the original method and the block of the hook
In this way, the original method still ran, but at a different time
Convenient for us to deal with
2. Hook methods to be executed
Got a good time
The method of replacing the hook before and after is executed
2.1. Execute corresponding blocks
Retrieve NSMethodSignature from the description of the block of the hook in Aspects
Using NSMethodSignature, execute a block of a hook
2.2. Perform the original method
NSInvocation instance under Invoke
Concrete implementation in Swift Aspect
3.1 call
There’s an instance method
@objc dynamic func test(id: Int, name: String) {
print("come on")
}
Copy the code
Hook above the method
_ = try? hook(selector: #selector(ViewController.test(id:name:)), strategy: .before) { (_, id: Int, name: String) in
print("done")
}
Copy the code
Start the hooks
@discardableResult func hook<Arg1, Arg2>( selector: Selector, strategy: AspectStrategy = .before, block: @escaping(AspectInfo, Arg1, Arg2) -> Void) throws -> AspectToken {// Get the block that needs to be inserted. @convention(block) (AspectInfo) -> Void = { aspectInfo in guard aspectInfo.arguments.count == 2, let arg1 = aspectInfo.arguments[0] as? Arg1, let arg2 = aspectInfo.arguments[1] as? Arg2 else { return } block(aspectInfo, arg1, arg2) } let wrappedObject: AnyObject = unsafeBitCast(wrappedBlock, to: Anyobject.self) // Hook return try hook(selector: selector, strategy: strategy, block: wrappedObject)}Copy the code
Add categories,
Grammar sugar, convenience method
extension NSObject {
public func hook(selector: Selector, strategy: AspectStrategy = .before, block: AnyObject) throws -> AspectToken {
return try ahook(object: self, selector: selector, strategy: strategy, block: block)
}
}
Copy the code
3.2 Hook main process
People who actually do things
func ahook(object: AnyObject, selector: Selector, strategy: AspectStrategy, block: AnyObject) throws -> AspectToken {// Return try lock. PerformLocked {// Record all information let identifier = try AspectIdentifier.identifier(with: selector, object: object, strategy: strategy, block: Let cache = getAspectCache(for: object, selector: selector) cache.add(identifier, option: Let subclass: AnyClass = try hookClass(object: object, selector: selector) let method = class_getInstanceMethod(subclass, selector) ?? class_getClassMethod(subclass, selector) guard let impl = method.map(method_getImplementation), let typeEncoding = method.flatMap(method_getTypeEncoding) else { throw AspectError.unrecognizedSelector } assert(checkTypeEncoding(typeEncoding)) let aliasSelector = selector.alias if ! subclass.instancesRespond(to: AliasSelector) {// here we're adding the original implementation of this method to another selector. Invoke hook method let succeeds = class_addMethod(subclass, aliasSelector, IMPl, typeEncoding) Precondition (succeeds, "Not OK")} // replace the hook invocation with // forwardInvocation // with 1.1 above, Class_replaceMethod (subclass, selector, _aspect_objc_msgForward, typeEncoding) return identifier}}Copy the code
3.3 dynamically subclassing (KVO)
private func hookClass(object: AnyObject, selector: Selector) throws -> AnyClass { let perceivedClass: RealClass: AnyClass = object.objcClass let realClass: AnyClass = object_getClass(object)! let className = String(cString: Class_getName (realClass)) if className.hasprefix (Constants. SubclassPrefix) {// Before hook when hook an object selector // Subclasses are given a prefix. // The corresponding class of object is the generated subclass. RealClass} else if class_isMetaClass(realClass) { Swizzle if class_getInstanceMethod(perceivedClass, selector) = nil {swizzle if class_getInstanceMethod(perceivedClass, selector) = nil { // because the KVO object ISA pointer points to an intermediate class, swizzle the indirect class directly. // the intermediate class ISA dynamically created subclass // hook. Hook swizzleForwardInvocation(realClass) return realClass} else {swizzleForwardInvocation(perceivedClass) return perceivedClass } } let subclassName = Constants.subclassPrefix+className let subclass: AnyClass? = subclassName.withCString { cString in if let existingClass = objc_getClass(cString) as! AnyClass? {return existingClass} else {// Create subclass if let subclass: AnyClass = objc_allocateClassPair(perceivedClass, cString, 0) {// replace the forwardInvocation of the current class with // __ASPECTS_ARE_BEING_CALLED__ // aspectForwardInvocation here SwizzleForwardInvocation (subclass) // The isa invocation from the subclass to replaceGetClass(in: subclass, decoy: Objc_registerClassPair (subClass) return subClass} else {return nil}} guard let NonnullSubclass = ttf_subclass else {throw AspectError. FailedToAllocateClassPair} / / / / the current object isa pointer to point to just generate a subclass / / KVO Object_setClass (object, nonnullSubclass) return nonnullSubclass}Copy the code
3.4 Method Exchange
Interchange implementation, the method forward implementation, using custom anonymous function closure, aspectForwardInvocation
AspectForwardInvocation, equivalent to the OC version
__ASPECTS_ARE_BEING_CALLED__
private func swizzleForwardInvocation(_ realClass: AnyClass) {
guard let originalImplementation = class_replaceMethod(realClass,
ObjCSelector.forwardInvocation,
imp_implementationWithBlock(aspectForwardInvocation as Any),
ObjCMethodEncoding.forwardInvocation) else {
return
}
}
Copy the code
Execute both the original method and the inserted block
private let aspectForwardInvocation: @convention(block) (Unmanaged<NSObject>, AnyObject) -> Void = { objectRef, Invocation information in / / ready to perform the let object. = objectRef takeUnretainedValue () as AnyObject let the selector = invocation. The selector. var aliasSelector = selector.alias var aliasSelectorKey = AssociationKey<AspectsCache? >(aliasSelector) let selectorKey = AssociationKey<AspectsCache? >(selector) let associations = Associations(object.objcClass as AnyObject) let aspectCache: AspectsCache if let cache = associations. Value (forKey: selectorKey) { aspectCache = cache } else if let cache = (objectRef.takeUnretainedValue() as NSObject).associations.value(forKey: aliasSelectorKey) { aspectCache = cache } else { return } var info = AspectInfo(instance: object, invocation: Invocation) / / Before hooks. / / insert Before aspectCache. BeforeAspects. Invoke (with: & info) if aspectCache. InsteadAspects. IsEmpty {/ / perform original invocation. SetSelector (aliasSelector) invocation. The invoke ()} the else {/ home/hooks / / execution, insert replacement aspectCache. InsteadAspects. Invoke (with: After & info)} / / insert / / After hooks. AspectCache. AfterAspects. Invoke (with: & info)}Copy the code
3.5 Preparations and Information Collection
Record the selector that was hooked, and the execution block that was inserted
static func identifier(with selector: Selector, object: AnyObject, strategy: AspectStrategy, block: AnyObject) throws -> AspectIdentifier {
guard let blockSignature = AspectBlock(block).blockSignature else {
throw AspectError.missingBlockSignature
}
do {
try isCompatibleBlockSignature(blockSignature: blockSignature, object: object, selector: selector)
var aspectIdentifier = AspectIdentifier(selector: selector, object: object, strategy: strategy, block: block)
aspectIdentifier.blockSignature = blockSignature
return aspectIdentifier
} catch {
throw error
}
}
Copy the code
Judgment before hook
If the function signature of the hook does not match the function signature of the inserted block,
I can’t do it anymore
The first two characters of the signature information parameter are default,
Argument 0 is self/block,
Argument 1 is SEL or id
,
So index equals 2, which is the third bit
static func isCompatibleBlockSignature(blockSignature: AnyObject, object: AnyObject, selector: Selector) throws { let perceivedClass: AnyClass = object.objcClass let realClass: AnyClass = object_getClass(object)! let method = class_getInstanceMethod(perceivedClass, selector) ?? class_getClassMethod(realClass, selector) guard let nonnullMethod = method, let typeEncoding = method_getTypeEncoding(nonnullMethod) else { object.doesNotRecognizeSelector? (selector) throw AspectError.unrecognizedSelector } let signature = NSMethodSignature.signature(objCTypes: typeEncoding) var signaturesMatch = true if blockSignature.objcNumberOfArguments > signature.objcNumberOfArguments { // The block signature parameter must be, No greater than the method signature parameters / / check the length of the signature signaturesMatch = false} else {if blockSignature. ObjcNumberOfArguments > 1 {/ / judge the second rawEncoding = blockSignature.getArgumentType(at: UInt(1)) let encoding = ObjCTypeEncoding(rawValue: rawEncoding.pointee) ?? . Undefined // // Follows the AspectInfo protocol object if encoding! =.object {signaturesMatch = false}} if signaturesMatch {// For index in 2.. < blockSignature.objcNumberOfArguments { let methodRawEncoding = signature.getArgumentType(at: index) let blockRawEncoding = blockSignature.getArgumentType(at: index) let methodEncoding = ObjCTypeEncoding(rawValue: methodRawEncoding.pointee) ?? .undefined let blockEncoding = ObjCTypeEncoding(rawValue: blockRawEncoding.pointee) ?? .undefined if methodEncoding ! = blockEncoding { signaturesMatch = false break } } } } if ! signaturesMatch { throw AspectError.blockSignatureNotMatch } }Copy the code
Features:
The source code for Aspects has a lot of twists
Because a lot of the Runtime data structures are not public
So I need to define something like this for myself
The internal memory structure of the Runtime at compile time
With the memory structure we defined,
A couple of them deserve it, so they can force it
contrast
Swift version of the logic, the basic conversion from the OC version
-
__aspect_forwardInvocation: Useless method, also useless
-
Alias method, the original method name is prefixed
It’s like swapping variables, replacing the value of A with the value of B
Original method IMP, change method forward
Add the alias method, run the IMP of the original method
Swift version, enough, but not enough
Disadvantages:
The Swift version, compared to the OC version,
Not functional (generally not useful)
The existing features are not as powerful as the OC version
OC version of hook block, parameter processing more elegant