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

github repo