Aspects源码解析

A delightful, simple library for aspect oriented programming

  • 关键字:面向切片编程OC动态性消息转发类型编码Swizzle...

  • 使用场景:

    • 1.统一处理逻辑
    • 2.在不改变源码的情况下,插入代码(如无侵染更改第三方库代码,干一些坏坏的事情)

Aspects只有一个类文件,非常轻量级,在实现的思路上和JSPatch差不多。都主要用到OC的消息转发,最终都交给ForwardInvocation实现。二者很多地方有异曲同工之妙。

基本原理

我们知道 OC 是动态语言,我们执行一个函数的时候,其实是在发一条消息:[receiver message],这个过程就是根据 message 生成 selector,然后根据 selector 寻找指向函数具体实现的指针IMP,然后找到真正的函数执行逻辑。这种处理流程给我们提供了动态性的可能,试想一下,如果在运行时,动态的改变了 selector 和 IMP 的对应关系,那么就能使得原来的[receiver message]进入到新的函数实现了。

还是先来普及一下:

OC上,每个类都是这样一个结构体:

struct objc_class {
  struct objc_class * isa;
  const char *name;
  ….
  struct objc_method_list **methodLists; /*方法链表*/
};

其中 methodList 方法链表里存储的是 Method类型:

typedef struct objc_method *Method;
typedef struct objc_ method {
  SEL method_name;
  char *method_types;
  IMP method_imp;
};

Method 保存了一个方法的全部信息,包括 SEL 方法名,type各参数和返回值类型,IMP该方法具体实现的函数指针。

通过 Selector 调用方法时,会从methodList 链表里找到对应Method进行调用,这个 methodList上的的元素是可以动态替换的,可以把某个Selector对应的函数指针IMP替换成新的,也可以拿到已有的某个 Selector 对应的函数指针IMP,让另一个Selector 跟它对应,Runtime提供了一些接口做这些事。

比如:

static void viewDidLoadIMP (id slf, SEL sel) {
 // Custom Code
}

Class cls = NSClassFromString(@"UIViewController");
SEL selector = @selector(viewDidLoad);
Method method = class_getInstanceMethod(cls, selector);

//获得viewDidLoad方法的函数指针
IMP imp = method_getImplementation(method)

//获得viewDidLoad方法的参数类型
char *typeDescription = (char *)method_getTypeEncoding(method);

//新增一个ORIGViewDidLoad方法,指向原来的viewDidLoad实现
class_addMethod(cls, @selector(ORIGViewDidLoad), imp, typeDescription);

//把viewDidLoad IMP指向自定义新的实现
class_replaceMethod(cls, selector, viewDidLoadIMP, typeDescription);

这样就把 UIViewController-viewDidLoad方法给替换成我们自定义的方法,APP里调用 UIViewControllerviewDidLoad 方法都会去到上述 viewDidLoadIMP 函数里,在这个新的IMP函数里调用新增的方法,就实现了替换viewDidLoad 方法,同时为 UIViewController新增了个方法 -ORIGViewDidLoad指向原来viewDidLoadIMP, 可以通过这个方法调用到原来的实现。

.Aspect要的是实现一个通用的IMP,任意方法任意参数都可以通过这个IMP中转。上面讲的都是针对某一个方法的替换,但如果这个方法有参数,怎样把参数值传给我们新的 IMP 函数呢?例如 UIViewController-viewDidAppear:方法,调用者会传一个 Bool值,我们需要在自己实现的IMP(上述的viewDidLoadIMP)上拿到这个值,怎样能拿到?如果只是针对一个方法写IMP,是可以直接拿到这个参数值的。如何达到通用的效果呢?

如何实现方法替换

  • va_list实现(一次取出方法的参数)

这段代码摘至JSPatch:

static void commonIMP(id slf, ...)
  va_list args;
  va_start(args, slf);
  NSMutableArray *list = [[NSMutableArray alloc] init];
  NSMethodSignature *methodSignature = [cls  instanceMethodSignatureForSelector:selector];
  NSUInteger numberOfArguments = methodSignature.numberOfArguments;
  id obj;
  for (NSUInteger i = 2; i < numberOfArguments; i++) {
      const char *argumentType = [methodSignature getArgumentTypeAtIndex:i];
      switch(argumentType[0]) {
          case 'i':
              obj = @(va_arg(args, int));
              break;
          case 'B':
              obj = @(va_arg(args, BOOL));
              break;
          case 'f':
          case 'd':
              obj = @(va_arg(args, double));
              break;
          …… //其他数值类型
          default: {
              obj = va_arg(args, id);
              break;
          }
      }
      [list addObject:obj];
  }
  va_end(args);
  [function callWithArguments:list];
}

这样无论方法参数是什么,有多少个,都可以通过va_list的一组方法一个个取出来,组成 NSArray 。很完美地解决了参数的问题,一直运行正常,但是在arm64va_list 的结构改变了,导致无法上述这样取参数。

所以需要找到另一种方法。

  • ForwardInvocation实现

    • 看图说话

从上面我们可以发现,在发消息的时候,如果 selector 有对应的 IMP ,则直接执行,如果没有,oc给我们提供了几个可供补救的机会,依次有 resolveInstanceMethodforwardingTargetForSelectorforwardInvocation

Aspects之所以选择在 forwardInvocation 这里处理是因为,这几个阶段特性都不太一样:

  • resolvedInstanceMethod: 适合给类/对象动态添加一个相应的实现,
  • forwardingTargetForSelector:适合将消息转发给其他对象处理,
  • forwardInvocation: 是里面最灵活,最能符合需求的。

因此 Aspects的方案就是,对于待 hookselector,将其指向 objc_msgForward / _objc_msgForward_stret ,同时生成一个新的 aliasSelector 指向原来的 IMP,并且 hookforwardInvocation函数,通过forwardInvocation调用到原来的IMP。

核心原理:按照上面的思路,当被 hookselector 被执行的时候,首先根据 selector找到了 objc_msgForward / _objc_msgForward_stret ,而这个会触发消息转发,从而进入 forwardInvocation。同时由于forwardInvocation 的指向也被修改了,因此会转入新的 forwardInvocation函数,在里面执行需要嵌入的附加代码,完成之后,再转回原来的 IMP

大致流程如下:

摘至
摘至

-forwardInvocation:方法的实现给替换掉了,如果程序里真有用到这个方法对消息进行转发,原来的逻辑怎么办?首先我们在替换 -forwardInvocation:方法前会新建一个方法 -ORIGforwardInvocation:,保存原来的实现IMP,在新的 -forwardInvocation:实现里做了个判断,如果转发的方法是我们想改写的,就走我们的逻辑,若不是,就调 -ORIGforwardInvocation:走原来的流程。

将了这么多可能有些饶。Talk is sheap,show me the code

源码分析

从头文件中可以看到使用aspects有两种使用方式:

  • 1.类方法
  • 2.实例方法
/// Adds a block of code before/instead/after the current `selector` for a specific class.
///
/// @param block Aspects replicates the type signature of the method being hooked.
/// The first parameter will be `id<AspectInfo>`, followed by all parameters of the method.
/// These parameters are optional and will be filled to match the block signature.
/// You can even use an empty block, or one that simple gets `id<AspectInfo>`.
///
/// @note Hooking static methods is not supported.
/// @return A token which allows to later deregister the aspect.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

两者的主要原理基本差不多.

先来看看有哪些定义:

AspectOptions

typedef NS_OPTIONS(NSUInteger, AspectOptions) {
    AspectPositionAfter   = 0,            /// Called after the original implementation (default)
    AspectPositionInstead = 1,            /// Will replace the original implementation.
    AspectPositionBefore  = 2,            /// Called before the original implementation.
    AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution.
};

定义切片的调用时机

AspectErrorCode

typedef NS_ENUM(NSUInteger, AspectErrorCode) {
    AspectErrorSelectorBlacklisted,                   /// Selectors like release, retain, autorelease are blacklisted.
    AspectErrorDoesNotRespondToSelector,              /// Selector could not be found.
    AspectErrorSelectorDeallocPosition,               /// When hooking dealloc, only AspectPositionBefore is allowed.
    AspectErrorSelectorAlreadyHookedInClassHierarchy, /// Statically hooking the same method in subclasses is not allowed.
    AspectErrorFailedToAllocateClassPair,             /// The runtime failed creating a class pair.
    AspectErrorMissingBlockSignature,                 /// The block misses compile time signature info and can't be called.
    AspectErrorIncompatibleBlockSignature,            /// The block signature does not match the method or is too large.

    AspectErrorRemoveObjectAlreadyDeallocated = 100   /// (for removing) The object hooked is already deallocated.
};

这里定义了在执行的时候的错误码,在平时开发中我们也经常使用这种方式,尤其是在定义网络请求的时候。

AspectsContainer

// Tracks all aspects for an object/class.
@interface AspectsContainer : NSObject
- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)injectPosition;
- (BOOL)removeAspect:(id)aspect;
- (BOOL)hasAspects;
@property (atomic, copy) NSArray *beforeAspects;
@property (atomic, copy) NSArray *insteadAspects;
@property (atomic, copy) NSArray *afterAspects;
@end

一个对象或者类的所有的 Aspects 整体情况,注意这里数组是通过atomic修饰的。
关于atomic需要注意在默认情况下,由编译器所合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备nonatomic特质,则不需要同步锁。

注意一共有两中容器,一个是对象的切片,一个是类的切片。

AspectIdentifier

// Tracks a single aspect.
@interface AspectIdentifier : NSObject
+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error;
- (BOOL)invokeWithInfo:(id<AspectInfo>)info;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) id block;
@property (nonatomic, strong) NSMethodSignature *blockSignature;
@property (nonatomic, weak) id object;
@property (nonatomic, assign) AspectOptions options;
@end

一个Aspect的具体内容。主要包含了单个的 aspect 的具体信息,包括执行时机,要执行 block 所需要用到的具体信息:包括方法签名、参数等等。其实就是将我们传入的bloc,包装成AspectIdentifier,便于后续使用。通过我们替换的block实例化。也就是将我们传入的block,包装成了AspectIdentifier

AspectInfo

@interface AspectInfo : NSObject <AspctInfo>
- (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation;
@property (nonatomic, unsafe_unretained, readonly) id instance;
@property (nonatomic, strong, readonly) NSArray *arguments;
@property (nonatomic, strong, readonly) NSInvocation *originalInvocation;
@end

主要是 NSInvocation 信息。将NSInvocation包装一层,比如参数信息等。便于直接使用。

AspectTracker

@interface AspectTracker : NSObject
- (id)initWithTrackedClass:(Class)trackedClass parent:(AspectTracker *)parent;
@property (nonatomic, strong) Class trackedClass;
@property (nonatomic, strong) NSMutableSet *selectorNames;
@property (nonatomic, weak) AspectTracker *parentEntry;
@end

用于跟踪所改变的类,打上标记,用于替换类方法,防止重复替换类方法

流程


读源码是一件辛苦的事情。😂😂

__Block的使用

做过iOS的都知道,__BlockARCXMRC环境修饰对象下是不同的。具体的内容可以看看我的另一篇文章。这里只给出结论:

ARC环境下,__Block会对修饰的对象强引用,在MRC环境下对修饰的对象不会强引用。而且__block修饰局部变量,表示这个对象是可以在block内部修改,如果不这样写,会报Variable is not assignable (missing__block type specifier)的错误。

来看看代码:

static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
    NSCParameterAssert(self);
    NSCParameterAssert(selector);
    NSCParameterAssert(block);

    __block AspectIdentifier *identifier = nil;
    aspect_performLocked(^{
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
            AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
            identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
            if (identifier) {
                [aspectContainer addAspect:identifier withOptions:options];

                // Modify the class to allow message interception.
                aspect_prepareClassAndHookSelector(self, selector, error);
            }
        }
    });
    return identifier;
}

注意这里的__block AspectIdentifier *identifier = nil;为什么要这样写呢。上面已经说过得很清楚。因为identifier是局部变量,如果不加__block修饰,block里面不能改变identifier

自旋锁(OSSpinLockLock)

Aspect是线程安全的,那么它是通过什么方式办到的呢。如果你对iOS中的几个锁不清楚,可以看看我的另一篇文章,里面有介绍。

自旋锁是效率比较高的一种锁,相比@synchronized来说效率高得多。但是需要注意,如果访问这个所的线程不是同一优先级的话,会有死锁的潜在风险。具体原因请看YYKit作者博客。

static void aspect_performLocked(dispatch_block_t block) {
    static OSSpinLock aspect_lock = OS_SPINLOCK_INIT;
    OSSpinLockLock(&aspect_lock);
    // 加锁执行block
    block();
    // 执行完之后释放锁
    OSSpinLockUnlock(&aspect_lock);
}

通过这样的加锁方式,所以Aspect作者说它是线程安全的。

真正烧脑环节

执行的入口

 aspect_performLocked(^{
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
            AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
            identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
            if (identifier) {
                [aspectContainer addAspect:identifier withOptions:options];

                // Modify the class to allow message interception.
                aspect_prepareClassAndHookSelector(self, selector, error);
            }
        }
    });

上面这句代码是执行Aspect的入口。看是简单其实里面有非常复杂的地方。

看一看block做了哪些事情。

  • 1.对出入进来的参数进行检验,保证参数合法
  • 2.创建aspect容器,注意容器是懒加载形式动态添加到NSObject分类中作为属性。
  • 3.根据参数,比如selector,option,创建AspectIdentifier实例,上面已经说过AspectIdentifier主要包含了单个的 Aspect的具体信息,包括执行时机,要执行block 所需要用到的具体信息。
  • 4.将单个的 Aspect 的具体信息加到属性aspectContainer
  • 5.最为重要的部分进行Hook操作,生成子类,类型编码处理,方法替换等。

下面就对上面5个部分分别仔细的分析。

参数检验

严格来说,在调用每一个方法的时候都需要对传入进来的参数做一次校验。尤其是在做SDK的时候,因为你根本不知道外面传进来的是什么数据,到底是否为空,数据类型是否正确。平时开发的过程中,由于我们都知道传入的参数大部分来说都是我们复合预期的所以就没有做什么检验工作。

回到Aspect中。检验的内容主要有如下几个:

  • 1.Swizzle了不能Swizzle的方法,比如@retain", @"release", @"autorelease", @"forwardInvocation:":如果替换了这样的方法,我们是不能成功进行Swizzle的。
  • 2.传入的执行时机是否正确,比如对于dealloc方法,`Swizzle之能在之前进行调用。
  • 3.对象或者类是否响应传入的selector
  • 4.如果替换的是类方法,则进行是否重复替换的检查

这里重点捋一捋,类方法的检验参数,为了检验类的方法只能修改一次。

 if (class_isMetaClass(object_getClass(self))) {
        Class klass = [self class];
        NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict();
        Class currentClass = [self class];
        do {
            AspectTracker *tracker = swizzledClassesDict[currentClass];
            if ([tracker.selectorNames containsObject:selectorName]) {

                // Find the topmost class for the log.
                if (tracker.parentEntry) {
                    AspectTracker *topmostEntry = tracker.parentEntry;
                    while (topmostEntry.parentEntry) {
                        topmostEntry = topmostEntry.parentEntry;
                    }
                    NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(topmostEntry.trackedClass)];
                    AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
                    return NO;
                }else if (klass == currentClass) {
                    // Already modified and topmost!
                    return YES;
                }
            }
        }while ((currentClass = class_getSuperclass(currentClass)));

        // Add the selector as being modified.
        currentClass = klass;
        AspectTracker *parentTracker = nil;
        do {
            AspectTracker *tracker = swizzledClassesDict[currentClass];
            if (!tracker) {
                tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass parent:parentTracker];
                swizzledClassesDict[(id<NSCopying>)currentClass] = tracker;
            }
            [tracker.selectorNames addObject:selectorName];
            // All superclasses get marked as having a subclass that is modified.
            parentTracker = tracker;
        }while ((currentClass = class_getSuperclass(currentClass)));
    }

  • 如何判断传入的是类而不是对象:class_isMetaClass(object_getClass(self)))object_getClass是获取当前对象由什么实例化。

类方法只能替换一次,是在整个类的继承树上校验,而不只是单单的一个类,从下面代码可以看出:

 do {
            AspectTracker *tracker = swizzledClassesDict[currentClass];
            if ([tracker.selectorNames containsObject:selectorName]) {

                // Find the topmost class for the log.
                if (tracker.parentEntry) {
                    AspectTracker *topmostEntry = tracker.parentEntry;
                    while (topmostEntry.parentEntry) {
                        topmostEntry = topmostEntry.parentEntry;
                    }
                    NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(topmostEntry.trackedClass)];
                    AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
                    return NO;
                }else if (klass == currentClass) {
                    // Already modified and topmost!
                    return YES;
                }
            }
        }while ((currentClass = class_getSuperclass(currentClass)));


注意这句(currentClass = class_getSuperclass(currentClass)当且只有当这个类为根类的时候才不会继续循环查找。

再来看看这段:

currentClass = klass;
        AspectTracker *parentTracker = nil;
        do {
            AspectTracker *tracker = swizzledClassesDict[currentClass];
            if (!tracker) {
                tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass parent:parentTracker];
                swizzledClassesDict[(id<NSCopying>)currentClass] = tracker;
            }
            [tracker.selectorNames addObject:selectorName];
            // All superclasses get marked as having a subclass that is modified.
            parentTracker = tracker;
        }while ((currentClass = class_getSuperclass(currentClass)));

这段的作用就是如果类被修改了,给其父类打上标记。然后结合上面的判断是否重复替换。这里为什么要用父类呢。这个runtime类与父类,根类关系有关为了有效的遍历,需要找到一个退出的条件,而退出的条件,结合到runtime就是根类没有父类。这就是退出的条件。

如果还没有弄懂,回去看看runtime的基本知识

创建Aspect容器

AspectsContainer的作用是为了保存整个Apects的情况,包括添加/删除的aspect,根据执行的时机(before,instead,after)而保存的所有的aspcet。其中用到了比较常用的动态添加属性。

static AspectsContainer *aspect_getContainerForObject(NSObject *self, SEL selector) {
    NSCParameterAssert(self);
    // 得到新的SEL(加上了前缀aspects_)并新增为属性
    SEL aliasSelector = aspect_aliasForSelector(selector);

    // 得到和aliasSelector相对应的AspectsContainer
    AspectsContainer *aspectContainer = objc_getAssociatedObject(self, aliasSelector);
    if (!aspectContainer) {
        aspectContainer = [AspectsContainer new];
        objc_setAssociatedObject(self, aliasSelector, aspectContainer, OBJC_ASSOCIATION_RETAIN);
    }
    return aspectContainer;
}

这一步还是比较简单。

单个的 Aspect 的具体信息AspectIdentifier

上面介绍的AspectsContainer里面装的具体的东东就是AspectIdentifierAspectIdentifier包含的是具体到每一个aspect的具体信息,直接看属性:

@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) id block;
@property (nonatomic, strong) NSMethodSignature *blockSignature;
@property (nonatomic, weak) id object; // 具体信息所属类,用weak
@property (nonatomic, assign) AspectOptions options;

初始化方法将需要的比如:selblockoption,传进去。`

  • (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error`

需要重点注意方法签名NSMethodSignature,因为我们在使用Aspect的时候是直接通过block来替换方法的,所以需要将我们传入的block转换为具体的方法。这也是为什么会在AspectIdentifier中多一个方法签名的属性。

在介绍方法签名之前需要对block具体的结构有一定了解。在Aspect中,我们定义了一个结构体用来代替系统的block。总体结构其实和系统的一样。

// 模仿系统的block结构
typedef struct _AspectBlock {
    __unused Class isa;
    AspectBlockFlags flags;
    __unused int reserved;
    void (__unused *invoke)(struct _AspectBlock *block, ...);
    struct {
        unsigned long int reserved;
        unsigned long int size;
        // requires AspectBlockFlagsHasCopyDisposeHelpers
        void (*copy)(void *dst, const void *src);
        void (*dispose)(const void *);
        // requires AspectBlockFlagsHasSignature
        const char *signature;
        const char *layout;
    } *descriptor;
    // imported variables
} *AspectBlockRef;

看一看将block转为方法签名的代码:

static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) {
    // 将block转换为自定义的形式
    AspectBlockRef layout = (__bridge void *)block;
    // 过滤
    if (!(layout->flags & AspectBlockFlagsHasSignature)) {// flags不是AspectBlockFlagsHasSignature类型
        NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block];
        AspectError(AspectErrorMissingBlockSignature, description);
        return nil;
    }

    void *desc = layout->descriptor;
    desc += 2 * sizeof(unsigned long int);
    if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) {
        desc += 2 * sizeof(void *);
    }

    if (!desc) {
        NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block];
        AspectError(AspectErrorMissingBlockSignature, description);
        return nil;
    }
    const char *signature = (*(const char **)desc);
    // Returns an NSMethodSignature object for the given Objective-C method type string.
    // 根据类型编码返回真正方法签名
    return [NSMethodSignature signatureWithObjCTypes:signature];
}

block是作为id类型进行传递的,而且是全局类型的block。如果转换成功,layout里面都会有相应的值。

这句话const char *signature = (*(const char **)desc);得到我们传入block的类型编码。通过类型编码得到block所对应的方法签名。

这里大致给出返回值为空,无参数的block转换之后的方法签名的结构:

<NSMethodSignature: 0x7f9219d064e0>
    number of arguments = 1
    frame size = 224
    is special struct return? NO
    return value: -------- -------- -------- --------
        type encoding (v) 'v'
        flags {}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
        memory {offset = 0, size = 0}
    argument 0: -------- -------- -------- --------
        type encoding (@) '@?'
        flags {isObject, isBlock}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}

得到了方法签名,需要对这个方法签名和替换实际方法进行比较。从这里可以感受到做一个第三方对参数的检测是非常非常重要的。这也是我们平时在应用开发中所欠缺的。

那怎么和原来的方法比较呢。比较直接的方法就是,拿到替换方法的方法签名和我们将block转换之后的方法签名对比。

直接看到代码:

// 原方法签名
    NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector];

    // 参数不匹配
    if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) {
        signaturesMatch = NO;
    }else {
        if (blockSignature.numberOfArguments > 1) {
            // blockSignature参数没有_cmd,
            const char *blockType = [blockSignature getArgumentTypeAtIndex:1];
            // 类型编码
            if (blockType[0] != '@') {
                signaturesMatch = NO;
            }
        }

        // Argument 0 is self/block, argument 1 is SEL or id<AspectInfo>. We start comparing at argument 2.
        // 对于block来说
        // The block can have less arguments than the method, that's ok.
        if (signaturesMatch) {
            for (NSUInteger idx = 2; idx < blockSignature.numberOfArguments; idx++) {
                const char *methodType = [methodSignature getArgumentTypeAtIndex:idx];
                const char *blockType = [blockSignature getArgumentTypeAtIndex:idx];

                // Only compare parameter, not the optional type data.
                // 参数匹配
                if (!methodType || !blockType || methodType[0] != blockType[0]) {
                    signaturesMatch = NO;
                    break;
                }
            }
        }
    }

    if (!signaturesMatch) {
        NSString *description = [NSString stringWithFormat:@"Block signature %@ doesn't match %@.", blockSignature, methodSignature];
        AspectError(AspectErrorIncompatibleBlockSignature, description);
        return NO;
    }

说一说为什么要从第二个参数开始比较呢。首先我们常见的方法都有连个隐藏的参数,一个是__cmd,一个是self。而方法签名的参数也有两个(针对一个返回值为空,参数为空的方法)。第一个是self,第二个是SEL。源代码有相关说明。

我直接打印一下:

  • methodSignature:

  • <NSMethodSignature: 0x7f9219d0cab0>
    number of arguments = 2
    frame size = 224
    is special struct return? NO
    return value: -------- -------- -------- --------
    type encoding (B) 'B'
    flags {}
    modifiers {}
    frame {offset = 0, offset adjust = 0, size = 8, size adjust = -7}
    memory {offset = 0, size = 1}
    argument 0: -------- -------- -------- --------
    type encoding (@) '@'
    flags {isObject}
    modifiers {}
    frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
    memory {offset = 0, size = 8}
    argument 1: -------- -------- -------- --------
    type encoding (:) ':'
    flags {}
    modifiers {}
    frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
    memory {offset = 0, size = 8}

  • 
    
  • blockSignature

<NSMethodSignature: 0x7f9219d064e0>
number of arguments = 1
frame size = 224
is special struct return? NO
return value: -------- -------- -------- --------
type encoding (v) 'v'
flags {}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
memory {offset = 0, size = 0}
argument 0: -------- -------- -------- --------
type encoding (@) '@?'
flags {isObject, isBlock}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
argument 1: -------- -------- -------- --------
type encoding (:) ':'
flags {}
modifiers {}
frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}


将`block`转换为方法签名之后就可以给`AspectIdentifier`具体属性赋值了。

AspectIdentifier *identifier = nil;
if (blockSignature) {
identifier = [AspectIdentifier new];
identifier.selector = selector;
identifier.block = block;
identifier.blockSignature = blockSignature;
identifier.options = options;
identifier.object = object; // weak
}


### 将具体的`AspectIdentifier `添加到容器中

这一步比较简单`[aspectContainer addAspect:identifier withOptions:options];`

### 方法替换

方法替换是这个库最为核心的部分,也是最难的部分。里面的思路以及方法很多都值得学习。这部分也是自己花了很多时间去了解。

大致的步骤:

- 1.创建子类,`hook`子类
- 2.处理实现过`MsgForwardIMP`的类或者对象
- 3.将需要替换的`selector`,指向`_objc_msgForward`

来具体看一看

#### 创建子类

创建子类将`runtime`用到了极致。其实我们所做的替换完全是在运行时动态创建子类的时候实现的。这样对原来替换的类或者对象没有任何影响而且可以在子类基础上新增或者删除`aspect`。

注意`Class statedClass = self.class;`和`Class baseClass = object_getClass(self);`的区别,前者获取类对象,后者获取本类是由什么实例化。

**所有的操作是在类对象基础上操作的,而不是一个对象**最为重要的就是`aspect_swizzleClassInPlace`方法

static Class aspect_swizzleClassInPlace(Class klass) {
NSCParameterAssert(klass);
NSString *className = NSStringFromClass(klass);

// 1.保证线程安全,2保证数组单例
_aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) {
    // 不包含这个类,如果已经包含
    if (![swizzledClasses containsObject:className]) {
        /** 深藏的核心部分 **/
        // 将原IMP指向forwardInvocation
        aspect_swizzleForwardInvocation(klass);

        // 添加进来
        [swizzledClasses addObject:className];
    }
});
return klass;

}

为了保证线程安全,所以用了`_aspect_modifySwizzledClasses`是单例方法。将传入的`block`安全执行

static void _aspect_modifySwizzledClasses(void (^block)(NSMutableSet *swizzledClasses)) {
static NSMutableSet *swizzledClasses;
static dispatch_once_t pred;
dispatch_once(&pred, ^{
swizzledClasses = [NSMutableSet new];
});
@synchronized(swizzledClasses) {
block(swizzledClasses);
}
}


来看第一个重头戏:**aspect_swizzleForwardInvocation**这个方法的目的就是将本类中的`forwardInvocation`方法替换为我们自定义的`__ASPECTS_ARE_BEING_CALLED__`方法。这样只要类没有找到消息处理着都会走到自定义的`__ASPECTS_ARE_BEING_CALLED__ `。这样就可以统一处理了。

这里有个小小的问题,如果`forwardInvocation `已经被替换了,那么就需要特殊处理。比如这里判断的方法是,如果原来实现过`forwardInvocation `。则新增一个方法`AspectsForwardInvocationSelectorName`,指向`originalImplementation `。这个时候已经`forwardInvocation`已经指向了我们自定义的`AspectsForwardInvocationSelectorName `。所以是同理的。

IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)ASPECTS_ARE_BEING_CALLED, "v@:@");

if (originalImplementation) {
    // 将__aspects_forwardInvocation:指向originalImplementation,
    // 将originalImplementation添加到Method,以便下次调用,直接就可以了
    class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
}

再来看看`__ASPECTS_ARE_BEING_CALLED__`这个方法如何将转发过来的参数成功的转换为我们需要的参数。

static void ASPECTS_ARE_BEING_CALLED(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
NSCParameterAssert(self);
NSCParameterAssert(invocation);

SEL originalSelector = invocation.selector;
// 加前缀
SEL aliasSelector = aspect_aliasForSelector(invocation.selector);

invocation.selector = aliasSelector;
// 本对象的AspectsContainer,添加到对象的aspect
AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
// 这个类的AspectsContainer,添加类上面的aspect
AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);

AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation];
NSArray *aspectsToRemove = nil;

// Before hooks.
aspect_invoke(classContainer.beforeAspects, info);
aspect_invoke(objectContainer.beforeAspects, info);

// Instead hooks.
BOOL respondsToAlias = YES;
if (objectContainer.insteadAspects.count ||
    classContainer.insteadAspects.count) {
    // 类方法和
    aspect_invoke(classContainer.insteadAspects, info);
    aspect_invoke(objectContainer.insteadAspects, info);
}else {
    Class klass = object_getClass(invocation.target);
    do {
        if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
            // 直接调用
            [invocation invoke];
            break;
        }
    }while (!respondsToAlias &&
            (klass = class_getSuperclass(klass)));
}

// After hooks.
aspect_invoke(classContainer.afterAspects, info);
aspect_invoke(objectContainer.afterAspects, info);

// If no hooks are installed, call original implementation (usually to throw an exception)
if (!respondsToAlias) {
    invocation.selector = originalSelector;
    SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
    if ([self respondsToSelector:originalForwardInvocationSEL]) {
        ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
    }else {
        [self doesNotRecognizeSelector:invocation.selector];
    }
}

// Remove any hooks that are queued for deregistration.
// 😥
[aspectsToRemove makeObjectsPerformSelector:@selector(remove)];

}


用自定的`aliasSelector`替换掉`invocation`的`selector`。然后将`invocation`转化为`AspectInfo `。`AspectInfo `主要包含了`invocation `相关的信息,比如参数数组。如何获取参数数组呢?代码是通过为`NSInvocation`写一个分类来实现。原理就是涉及到类型编码的那块。来看看:

// Thanks to the ReactiveCocoa team for providing a generic solution for this.

  • (id)aspect_argumentAtIndex:(NSUInteger)index {
    const char *argType = [self.methodSignature getArgumentTypeAtIndex:index];
    // Skip const type qualifier.
    if (argType[0] == _C_CONST) argType++;

define WRAP_AND_RETURN(type) do { type val = 0; [self getArgument:&val atIndex:(NSInteger)index]; return @(val); } while (0)

if (strcmp(argType, @encode(id)) == 0 || strcmp(argType, @encode(Class)) == 0) {
    __autoreleasing id returnObj;
    [self getArgument:&returnObj atIndex:(NSInteger)index];
    return returnObj;
} else if (strcmp(argType, @encode(SEL)) == 0) {
    SEL selector = 0;
    [self getArgument:&selector atIndex:(NSInteger)index];
    return NSStringFromSelector(selector);
} else if (strcmp(argType, @encode(Class)) == 0) {
    __autoreleasing Class theClass = Nil;
    [self getArgument:&theClass atIndex:(NSInteger)index];
    return theClass;
    // Using this list will box the number with the appropriate constructor, instead of the generic NSValue.
} else if (strcmp(argType, @encode(char)) == 0) {
    WRAP_AND_RETURN(char);
} else if (strcmp(argType, @encode(int)) == 0) {
    WRAP_AND_RETURN(int);
} else if (strcmp(argType, @encode(short)) == 0) {
    WRAP_AND_RETURN(short);
} else if (strcmp(argType, @encode(long)) == 0) {
    WRAP_AND_RETURN(long);
} else if (strcmp(argType, @encode(long long)) == 0) {
    WRAP_AND_RETURN(long long);
} else if (strcmp(argType, @encode(unsigned char)) == 0) {
    WRAP_AND_RETURN(unsigned char);
} else if (strcmp(argType, @encode(unsigned int)) == 0) {
    WRAP_AND_RETURN(unsigned int);
} else if (strcmp(argType, @encode(unsigned short)) == 0) {
    WRAP_AND_RETURN(unsigned short);
} else if (strcmp(argType, @encode(unsigned long)) == 0) {
    WRAP_AND_RETURN(unsigned long);
} else if (strcmp(argType, @encode(unsigned long long)) == 0) {
    WRAP_AND_RETURN(unsigned long long);
} else if (strcmp(argType, @encode(float)) == 0) {
    WRAP_AND_RETURN(float);
} else if (strcmp(argType, @encode(double)) == 0) {
    WRAP_AND_RETURN(double);
} else if (strcmp(argType, @encode(BOOL)) == 0) {
    WRAP_AND_RETURN(BOOL);
} else if (strcmp(argType, @encode(bool)) == 0) {
    WRAP_AND_RETURN(BOOL);
} else if (strcmp(argType, @encode(char *)) == 0) {
    WRAP_AND_RETURN(const char *);
} else if (strcmp(argType, @encode(void (^)(void))) == 0) {
    __unsafe_unretained id block = nil;
    [self getArgument:&block atIndex:(NSInteger)index];
    return [block copy];
} else {
    NSUInteger valueSize = 0;
    NSGetSizeAndAlignment(argType, &valueSize, NULL);

    unsigned char valueBytes[valueSize];
    [self getArgument:valueBytes atIndex:(NSInteger)index];

    return [NSValue valueWithBytes:valueBytes objCType:argType];
}
return nil;

undef WRAP_AND_RETURN

}

有几个基础的地方。

- @encode:获取类型编码:比如`strcmp(argType, @encode(SEL)) == 0`就是判断参数`argType`是否为SEL类型。
- WRAP_AND_RETURN:是一个宏定义,为了将一般类型,比如`Bool`,`Int`转为对象,然后能够添加到数组中。

开始调用通过宏定义`aspect_invoke`

define aspect_invoke(aspects, info) \

for (AspectIdentifier *aspect in aspects) {
[aspect invokeWithInfo:info];
if (aspect.options & AspectOptionAutomaticRemoval) {
aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect];
}
}

为什么这里用宏定义来调用呢。因为宏能够让我获得堆栈信息。

**进入到了最后调用的地方了。**

  • (BOOL)invokeWithInfo:(id<AspectInfo>)info {
    NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.blockSignature];
    NSInvocation *originalInvocation = info.originalInvocation;
    NSUInteger numberOfArguments = self.blockSignature.numberOfArguments;

    // Be extra paranoid. We already check that on hook registration.
    if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) {
    AspectLogError(@"Block has too many arguments. Not calling %@", info);
    return NO;
    }

    // The self of the block will be the AspectInfo. Optional.
    /**

    • index: Indices 0 and 1 indicate the hidden arguments self and _cmd, respectively; you should set these values directly with the target and selector properties
      */
      if (numberOfArguments > 1) {
      [blockInvocation setArgument:&info atIndex:1];
      }

    void *argBuf = NULL;
    // 根据NSInvocation参数规则,从第二个参数开始取
    // 参数处理
    for (NSUInteger idx = 2; idx < numberOfArguments; idx++) {
    // 取出类型编码
    const char *type = [originalInvocation.methodSignature getArgumentTypeAtIndex:idx];
    NSUInteger argSize;
    NSGetSizeAndAlignment(type, &argSize, NULL);

      if (!(argBuf = reallocf(argBuf, argSize))) {
          AspectLogError(@"Failed to allocate memory for block invocation.");
          return NO;
      }
    
      // 从originalInvocation取出参数
      [originalInvocation getArgument:argBuf atIndex:idx];
      // 给blockInvocation设置参数
      [blockInvocation setArgument:argBuf atIndex:idx];
    

    }

    // 调用
    [blockInvocation invokeWithTarget:self.block];

    if (argBuf != NULL) {
    free(argBuf);
    }
    return YES;
    }


用之前将`block`转为的`blockSignature`初始化`blockSignature`得到`invocation`。然后处理参数,如果参数`block`中的参数大于1个,则把包装成`AspectInfo `。

然后从`originalInvocation `中取出参数给`blockInvocation `赋值。最后调用。` [blockInvocation invokeWithTarget:self.block];`这里`Target`设置为`self.block`。注意`You must set the receiver’s selector and argument values before calling this method`

#### 在子类中将替换的`selector`指向`_objc_msgForward`

Method targetMethod = class_getInstanceMethod(kclass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);

由于`kclass`是替换类的子类,所以正常情况下`targetMethod `不会为空。相当雨得到原有的`IMP`,如果原有的`IMP`就是指向的`_objc_msgForward`。则不用处理了。我们已经在前面处理过了。

直接来看正常流程

if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
// Make a method alias for the existing method implementation, it not already copied.
const char *typeEncoding = method_getTypeEncoding(targetMethod);

    // 3- 得到替换的SEL
    SEL aliasSelector = aspect_aliasForSelector(selector);
    // 子类没有这个方法,添加方法到子类
    if (![klass instancesRespondToSelector:aliasSelector]) {
        __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
        NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass);
    }

    // 使用forwardInvocation来统一处理,将原SEL指向原SEL找不到方法的forwardInvocation的IMP
    // We use forwardInvocation to hook in.

    // 4- 将需要替换的selector,指向_objc_msgForward,进行统一处理
    class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
    AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
先获取`targetMethodIMP `的类型编码。然后将我们自定义的`aliasSelector `添加的子类上。最后进行替换。`class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);`最后来看看这个函数:

static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) {
// self 成为子类
IMP msgForwardIMP = _objc_msgForward;

// 需要兼容__arm64__,用不同的方式

if !defined(arm64)

// As an ugly internal runtime implementation detail in the 32bit runtime, we need to determine of the method we hook returns a struct or anything larger than id.
// https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/LowLevelABI/000-Introduction/introduction.html
// https://github.com/ReactiveCocoa/ReactiveCocoa/issues/783
// http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042e/IHI0042E_aapcs.pdf (Section 5.4)
Method method = class_getInstanceMethod(self.class, selector);
// 类型编码字符
const char *encoding = method_getTypeEncoding(method);
BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B;
if (methodReturnsStructValue) {
    // 通过Try Cathch 捕获异常
    @try {
        NSUInteger valueSize = 0;
        NSGetSizeAndAlignment(encoding, &valueSize, NULL);

        if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8) {
            methodReturnsStructValue = NO;
        }
    } @catch (NSException *e) {}
}
if (methodReturnsStructValue) {
    msgForwardIMP = (IMP)_objc_msgForward_stret;
}

endif

return msgForwardIMP;

}


这个函数主要是处理`__arm64__ `中`var_list`结构变了。[可参考](https://blog.nelhage.com/2010/10/amd64-and-va_arg)

` BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B;`这句话是判断类型编码第一个字符是否为`_C_STRUCT_B `

define _C_ID '@'

define _C_CLASS '#'

define _C_SEL ':'

define _C_CHR 'c'

define _C_UCHR 'C'

define _C_SHT 's'

define _C_USHT 'S'

define _C_INT 'i'

define _C_UINT 'I'

define _C_LNG 'l'

define _C_ULNG 'L'

define _C_LNG_LNG 'q'

define _C_ULNG_LNG 'Q'

define _C_FLT 'f'

define _C_DBL 'd'

define _C_BFLD 'b'

define _C_BOOL 'B'

define _C_VOID 'v'

define _C_UNDEF '?'

define _C_PTR '^'

define _C_CHARPTR '*'

define _C_ATOM '%'

define _C_ARY_B '['

define _C_ARY_E ']'

define _C_UNION_B '('

define _C_UNION_E ')'

define _C_STRUCT_B '{'

define _C_STRUCT_E '}'

define _C_VECTOR '!'

define _C_CONST 'r'

这个是对类型编码的宏定义。太深了搞起来太费脑子了。大概了解到这个程度吧。

**替换之后,调用原有的Method的时候,就会消息转发**

## 写在最后

> 平时接触事物决定了我们的高度。如果整天围着`UIKit`转,技术也就那样。居安思危!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容