(长文预警)面向切面 Aspects 源码阅读

前言

AOP(Aspect-oriented programming) 也称之为 “面向切面编程”, 是一种通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。简单来说可以做到 业务隔离解耦 等等效果。AOP 技术在JAVASpring 框架中已经提供了非常全面成熟的解决方案。然而 iOS 等移动端在这方面的运用并不是很多,但是不妨碍它涌现出非常出色的三方库,比如,我们接下来要说的三方库 Aspects .

那么我们什么时候使用 AOP 比较适合呢

  • 当我们执行某种方法时候,对方法进行安全检查 (例如,NSArray的数组越界问题)
  • 对某些操作进行日志记录
  • 对购物车进行交互时候,根据用户的操作,触发建议或者提示
  • 激进一些,也可以用来去基类继承,例如笔者的架构实例:NonBaseClass-MVVM-ReactiveObjc (干货带代码)

大家会说,传统的 OOP(Object Oriented Programming) 即面向对象编程,也完全能够实现这些功能。
是的,没错,但是一个好的 OOP 架构应该是单一职责的,添加额外的切面需求意味着破坏了单一职责。例如,一个 Module 仅仅负责订单业务,但是你其添加了安全检查,日志记录,建议提示等等功能,这个 Module 会变得难以理解和维护,而且整个应用都将充斥着 日志,安全检查 等等逻辑,想想都头皮发麻。

头皮发麻

AOP 正是解决这一系列问题的良药。

切面
Aspects基础用法

Aspects使用起来也是非常简单,只需要使用两个简单的接口,同时支持 类Hook实例Hook,提供了更细致的操作

/// Adds a block of code before/instead/after the current `selector` for a specific class.
+ (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;

例如,我们需要统计用户进入某个 ViewController 的次数

[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
    NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
} error:NULL];

从此,我们不必在基类中添加丑陋的代码了

Aspects架构剖析
架构
swizzledClassesDict 全局字典

访问接口

static NSMutableDictionary *aspect_getSwizzledClassesDict() {
    static NSMutableDictionary *swizzledClassesDict;
    static dispatch_once_t pred;
    dispatch_once(&pred, ^{
        swizzledClassesDict = [NSMutableDictionary new];
    });
    return swizzledClassesDict;
}

存储对象类型 <Class : AspectTracker *>
此全局字典记录了对Hook的类及其父类的追踪信息对象 AspectTracker

swizzledClasses 全局集合

访问接口

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);
    }
}

swizzledClasses 是一个全局集合,存储对象类型 <Class>
被Hook的对象类名都会被存储在此容器中

AspectBlockRef

Aspects 中的 AspectBlockRef 也就是我们使用接口中的参数 usingBlock 中的Block,在上述的例子中如下形式

^(id<AspectInfo> aspectInfo, BOOL animated) {
    NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
}

AspectBlockRef 的源代码如下

typedef NS_OPTIONS(int, AspectBlockFlags) {
    AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25),
    AspectBlockFlagsHasSignature          = (1 << 30)
};
/////////
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就是这样的结构体,在这里 AspectBlockRef 其实就是 _GloablBlock类型,AspectBlockRef 只是名字改了一下而已,本质上就是 Block
很幸运,苹果开源了Block的相关代码 :Block实现源码传送门 我们得以一窥究竟

/////////////////////// Block_private.h

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
    BLOCK_IS_GC =             (1 << 27), // runtime
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
};

#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    void (*copy)(void *dst, const void *src);
    void (*dispose)(const void *);
};

#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
    // requires BLOCK_HAS_SIGNATURE
    const char *signature;
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

其中 flags 代表着Block的操作数

//  AspectBlockRef
AspectBlockFlags flags;   
//  Block_private.h
volatile int32_t flags; // contains ref count

Aspects 中只有两种flag

AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25),
AspectBlockFlagsHasSignature          = (1 << 30)

对应 Block 定义枚举中的

BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
  • 当Block被Copy时 flag & AspectBlockFlagsHasCopyDisposeHelpers 为真时,Block布局中将会添加 Block_descriptor_2
  • 当Block带有方法签名时 flag & AspectBlockFlagsHasSignature 为真时,Block布局中存在Block_descriptor_3

invoke 函数指针也只是第一个参数由泛型变成了_AspectBlock而已

//  AspectBlockRef
void (__unused *invoke)(struct _AspectBlock *block, ...);
//  Block_private.h
void (*invoke)(void *, ...);

扯远了,打住。

AspectToken
AspectToken

Aspects 内定义的协议,用来撤销Hook

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

Hook方法会返回一个遵循 AspectToken 协议的方法,要取消对应的Hook,只需要调用代理对象的协议方法 remove就可以了

AspectInfo
AspectInfo

AspectInfo 对象遵循了 AspectInfo协议(同名协议),代表着切面信息,也是上文 AspectBlockRef 中的首个参数

  • instance :当前被Hook的实例
  • originalInvocation :当前被Hook 原始的 NSInvocation 对象
  • arguments:所有方法的参数,一个计算属性,被调用的时候才会有值

这里特别提一下,参数是从 NSInvocation 中拿到的

NSMutableArray *argumentsArray = [NSMutableArray array];
for (NSUInteger idx = 2; idx < self.methodSignature.numberOfArguments; idx++) {
    [argumentsArray addObject:[self aspect_argumentAtIndex:idx] ?: NSNull.null];
}

这里参数下标是从2开始的,因为下标 0,1,已经分别对应了 消息接受对象selector, 对于参数装箱细节,可以细看 Aspects 的内部 NSInvocation 分类

AspectIdentifier
AspectIdentifier

实际上 AspectIdentifier 就是 aspect_hookSelector 函数的返回值

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

AspectIdentifier 提供了remove 方法的实现,然而我并没有在源码中见到 AspectIdentifier 有声明遵循 <AspectToken>协议

以下方法对需要hook的类进行信息封装操作,方法内部对usingBlock参数中的Block进行适配检查

+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error 
{
    // 获取Block的方法签名
    NSMethodSignature *blockSignature =   aspect_blockMethodSignature(block, error); // TODO: check   signature compatibility, etc.
    // 兼容性检测
    if (!aspect_isCompatibleBlockSignature(blockSignature, object, selector, error)) {
        return nil;
    }
    // hook 信息封装
    AspectIdentifier *identifier = nil;
    if (blockSignature) {
        identifier = [AspectIdentifier new];
        identifier.selector = selector;
        identifier.block = block;
        identifier.blockSignature = blockSignature;
        identifier.options = options;
        identifier.object = object; // weak
    }
    return identifier;
}
AspectsContainer
AspectsContainer

AspectsContainer 顾名思义,也就是切面的容器类,内部根据不同的option选项将 AspectIdentifier 放入不同的容器内

  • NSArray *beforeAspects 对应 AspectPositionBefore 选项
  • NSArray *insteadAspects 对应 AspectPositionInstead 选项
  • NSArray *afterAspects 对应 AspectPositionAfter 选项

要注意的是 Asepcts通过 aspect_getContainerForObject 方法从关联的对象获取

static AspectsContainer *aspect_getContainerForObject(NSObject *self, SEL selector) {
     // 获取方法别名(原方法添加 aspects_ 前缀)
    SEL aliasSelector = aspect_aliasForSelector(selector);
    AspectsContainer *aspectContainer = objc_getAssociatedObject(self, aliasSelector);
    if (!aspectContainer) {
        aspectContainer = [AspectsContainer new];
       // 以方法别名来关联 AspectsContainer 容器对象
        objc_setAssociatedObject(self, aliasSelector, aspectContainer, OBJC_ASSOCIATION_RETAIN);
    }
    return aspectContainer;
}
AspectTracker
AspectTracker

AspectTracker 代表着对切面的追踪,存储在全局字典 swizzledClassesDict 中,从子类向上追踪记录信息

  • selectorNames 记录当前被追踪的类需要hook的方法名
  • selectorNamesToSubclassTrackers 通过addSubclassTracker: hookingSelectorName记录子类的 AspectTracker 对象
// Add the selector as being modified.
AspectTracker *subclassTracker = nil;
do {
    tracker = swizzledClassesDict[currentClass];
    if (!tracker) {
        tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass];
        swizzledClassesDict[(id<NSCopying>)currentClass] = tracker;
    }
    if (subclassTracker) {
        [tracker addSubclassTracker:subclassTracker hookingSelectorName:selectorName];
    } else {
        [tracker.selectorNames addObject:selectorName];
    }

    // All superclasses get marked as having a subclass that is modified.
    subclassTracker = tracker;
}while ((currentClass = class_getSuperclass(currentClass)));
Aspects核心解析
核心
方法入口
方法入口
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error {
    return aspect_add((id)self, selector, options, block, error);
}

/// @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 {
    return aspect_add(self, selector, options, block, error);
}

Aspects 既支持对类的 Hook,也支持对实例的Hook, 其核心在于 aspect_add 方法

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

    __block AspectIdentifier *identifier = nil;
    aspect_performLocked(^{
        // 判断是否允许Hook类
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
            // 获取关联容器
            AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
            // hook信息封装成AspectIdentifier对象
            identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
            if (identifier) {
                [aspectContainer addAspect:identifier withOptions:options];

                // hook 核心操作
                aspect_prepareClassAndHookSelector(self, selector, error);
            }
        }
    });
    return identifier;
}

基本上 aspect_add 核心操作有三步

  • aspect_isSelectorAllowedAndTrack方法 判断是否允许 Hook
  • hook信息封装成AspectIdentifier对象
  • aspect_prepareClassAndHookSelector 方法执行 Hook 操作(核心操作)

接下来一步步进行解析

aspect_isSelectorAllowedAndTrack 判断是否允许Hook

1、黑名单过滤

static NSSet *disallowedSelectorList;
static dispatch_once_t pred;
dispatch_once(&pred, ^{
    disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
});

NSString *selectorName = NSStringFromSelector(selector);
if ([disallowedSelectorList containsObject:selectorName]) {
    ......
    return NO;
}

特殊方法不允许hook

2、dealloc 方法hook只允许使用 AspectPositionBefore 选项

AspectOptions position = options&AspectPositionFilter;
if ([selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore) {
    ......
    return NO;
}

3、过滤无法响应的方法

if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) {
    return NO;
}

4、实例和类过滤

在此之前插播下元类的概念

元类的概念

元类的定义:元类是类对象的类

还有需要注意的一点,class_isMetaClass(object_getClass(self)) 使用的是 object_getClass 方法 而不是类似 [obj class]的方法
两者有何区别呢?

object_getClass [obj class]
实例对象 isa指针指向 isa指针指向
类对象 isa指针指向 对象本身
元类对象 isa指针指向 对象本身

好了,接下来我们看下过滤的代码

if (class_isMetaClass(object_getClass(self))) {
    ......
}else{
    return YES;
}

在这里,笔者认为 class_isMetaClass(object_getClass(self)) 作用是判断 hook 的对象是 实例 还是

NSObject *obj = [[NSObject alloc] init];
[obj aspect_hookSelector:@selector(copy) withOptions:AspectPositionAfter usingBlock:^(id <AspectInfo> info){
} error:nil];

Class getClass = object_getClass(self);
BOOL isMeta = class_isMetaClass(getClass); // 打印 NO

如果是实例对象的话 getClass 也就是isa指针,指向了对应的类,然后 class_isMetaClass判断自然是为NO

[NSObject aspect_hookSelector:@selector(copy) withOptions:AspectPositionAfter usingBlock:^(id <AspectInfo> info){

} error:nil];

Class getClass = object_getClass(self);
BOOL isMeta = class_isMetaClass(getClass); // 打印 YES

如果是类对象的话 getClass 也就是isa指针,指向了对应的元类,然后 class_isMetaClass判断自然是为YES

如果class_isMetaClass(object_getClass(self))返回YES,也就是说我们 Hook 的对象是类对象的话

Class klass = [self class];
NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict();
Class currentClass = [self class];

//  已经hook过的方法不再进行重复hook
AspectTracker *tracker = swizzledClassesDict[currentClass];
if ([tracker subclassHasHookedSelectorName:selectorName]) {
    return NO;
}

// 向上查找父类
do {
    tracker = swizzledClassesDict[currentClass];
    if ([tracker.selectorNames containsObject:selectorName]) {
        if (klass == currentClass) {
            // 如果是已经遍历到了父类顶层
            return YES;
        }
        //  已经hook过的方法不再进行重复hook
        return NO;
    }
} while ((currentClass = class_getSuperclass(currentClass)));

// 向上查找父类,生成 AspectTracker 信息
currentClass = klass; 
AspectTracker *subclassTracker = nil;
do {
    tracker = swizzledClassesDict[currentClass];
    if (!tracker) {
        tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass];
        swizzledClassesDict[(id<NSCopying>)currentClass] = tracker;
    }
    if (subclassTracker) {
        [tracker addSubclassTracker:subclassTracker hookingSelectorName:selectorName];
    } else {
        [tracker.selectorNames addObject:selectorName];
    }

    // All superclasses get marked as having a subclass that is modified.
    subclassTracker = tracker;
}while ((currentClass = class_getSuperclass(currentClass)));

所以,hook 实例方法不需要向上遍历父类方法,这也符合直觉和逻辑

Aspects hook 之前做了非常健全的前置检查,非常值得学习!


OK,下一个tips

Hook 信息封装成AspectIdentifier对象

+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error {

    NSMethodSignature *blockSignature = aspect_blockMethodSignature(block, error); // TODO: check signature compatibility, etc.
    if (!aspect_isCompatibleBlockSignature(blockSignature, object, selector, error)) {
        return nil;
    }

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

方法首先通过aspect_blockMethodSignature提取 Block 的方法签名

static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) {
    AspectBlockRef layout = (__bridge void *)block;
    // 通过判断block的flags 来确定Block是否存在方法签名
    if (!(layout->flags & AspectBlockFlagsHasSignature)) {
        return nil;
    }
    // 获取 descriptor 
    void *desc = layout->descriptor;
    /* 计算内存地址偏移量来确定 signature 的位置 */
    // 加上 reserved 和 size的偏移量 (类型皆为unsigned long int 所以 乘以 2)
    desc += 2 * sizeof(unsigned long int);
    // 如果有 copy 和 dispose 的 flag 需要 加上对应偏移量
    if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) {
        desc += 2 * sizeof(void *);
    }
    if (!desc) {
        return nil;
    }
    // 获取到了真正的方法签名
    const char *signature = (*(const char **)desc);
    return [NSMethodSignature signatureWithObjCTypes:signature];
}

涨姿势了,通过如上代码我们知道了如何获得Block的方法签名,
我们可以先理解消化本文解释 AspectBlockRef 的内存布局,然后再来理解这段代码

至于代码中的二级指针(*(const char **)desc)

// descriptor 是一个结构体指针,保存的是结构体的起始地址
struct {
    const char *signature;
} *descriptor;
// 结构体指针 赋值给 泛型指针desc
void *desc = layout->descriptor;
//  当我们获取到真正的方法签名,此时 desc 已经指向了 signature 所在的地址
//  signature 的类型是 const char *,那么 signature 的地址自然就是const char **咯
//  最后我们要取 desc 指针中的值,也就是 *obj 咯
const char ** obj = (const char **)desc;
const char *signature = *obj;

恩,没毛病,下一个Tips, 也是我们的重头戏

aspect_prepareClassAndHookSelector 方法执行 Hook 操作

我们来看下aspect_prepareClassAndHookSelector的源码

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {

    Class klass = aspect_hookClass(self, error);
    Method targetMethod = class_getInstanceMethod(klass, selector);
    IMP targetMethodIMP = method_getImplementation(targetMethod);
    if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
        // Make a method alias for the existing method implementation, it not already copied.
        const char *typeEncoding = method_getTypeEncoding(targetMethod);
        SEL aliasSelector = aspect_aliasForSelector(selector);
        if (![klass instancesRespondToSelector:aliasSelector]) {
            __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
        }

        // We use forwardInvocation to hook in.
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
    }
}

这里源码中比较重要和核心的在于 aspect_hookClass 方法

static Class aspect_hookClass(NSObject *self, NSError **error) {
    NSCParameterAssert(self);
        // class
    Class statedClass = self.class;
       // isa指针
    Class baseClass = object_getClass(self);
    NSString *className = NSStringFromClass(baseClass);

    // 已经子类化过了,也就是已经添加过子类后缀 “_Aspects_”
    if ([className hasSuffix:AspectsSubclassSuffix]) {
        return baseClass;
    }
    // 如果 Hook 的是 类对象,那么混写 类对象的ForwardInvocation方法 (类对象的指针指向本身)
    else if (class_isMetaClass(baseClass)) {
        return aspect_swizzleClassInPlace((Class)self);
    }
    // 如果 Hook 的是 一个 已经被kvo子类化的实例对象,我们需要混写它的 metaClass 的ForwardInvocation方法
    else if (statedClass != baseClass) {
        return aspect_swizzleClassInPlace(baseClass);
    }

    // 混写实例对象

   //  添加 默认后缀 _Aspects_ 
    const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
   //  获取 添加默认后缀后的类
    Class subclass = objc_getClass(subclassName);

    if (subclass == nil) {
    // 如果还没有动态创建过子类
        subclass = objc_allocateClassPair(baseClass, subclassName, 0);
        if (subclass == nil) {
            return nil;
        }
        // 混写 forwardInvocation:方法
        aspect_swizzleForwardInvocation(subclass);
        // 替换 class 的 IMP 指针
        // subClass.class = statedClass
        aspect_hookedGetClass(subclass, statedClass);
        // subClass.isa.class = statedClass 
        aspect_hookedGetClass(object_getClass(subclass), statedClass);
        // 注册新类
        objc_registerClassPair(subclass);
    }
    // 复写 isa 指针,指向新的类
    object_setClass(self, subclass);
    return subclass;
}

值得注意的是

// 如果 Hook 的是 类对象,那么混写 类对象 (类对象的指针指向本身)
Class baseClass = object_getClass(self);
else if (class_isMetaClass(baseClass)) {
    return aspect_swizzleClassInPlace((Class)self);
}
// 如果 Hook 的是 一个 已经被kvo子类化的实例对象,我们需要混写它的 metaClass
else if (statedClass != baseClass) {
    return aspect_swizzleClassInPlace(baseClass);
}

class_isMetaClass(baseClass)前面已经说过了,是用来判断是否 hook 的是类对象,那么 statedClass != baseClass 是什么含义呢

首先我们得要先知道 KVO 实现的原理: 当观察某对象 A 时,KVO 机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性 keyPath 的 setter 方法

从前文我们知道,实例对象 object_getClass[obj class] 的指向是一致的,当hook被KVO子类化的实例时候,实例对象的isa指针的指向 和 class 的指向才会不一致

我们还学会了动态创建一个类的流程

  • objc_allocateClassPair
  • class_addMethod
  • class_addIvar
  • objc_registerClassPair

小结:对类对象的hook是通过混写类对象的 forwardInvocation 方法来实现,对实例对象的 hook 是通过子类化,然后混写子类的 forwardInvocation 来实现的

emmmmm, 接下来我们聊一聊混写forwardInvocation 的一些细节

aspect_swizzleForwardInvocation 混写 forwardInvocation

方法转发流程

方法转发流程,这里就不赘述,Aspects 核心思想就是通过混写 forwardInvocation 来实现切面执行自定义代码

static void aspect_swizzleForwardInvocation(Class klass) {
    NSCParameterAssert(klass);
    // If there is no method, replace will act like class_addMethod.
    IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
    if (originalImplementation) {
        class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
    }
}

上述代码将类的 forwardInvocation 方法 IMP 替换成 __ ASPECTS_ARE_BEING_CALLED __ ,原来的 IMP 与 AspectsForwardInvocationSelectorName 相关联了

接下来我们看看 __ ASPECTS_ARE_BEING_CALLED __ 方法

static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {

    SEL originalSelector = invocation.selector;
    SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
    invocation.selector = aliasSelector;
    // 获取实例的容器
    AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
    // 获取类的容器
    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) {
        // 执行 insteadAspects 中的操作
        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 (!respondsToAlias) {
        invocation.selector = originalSelector;
        SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);

        // 如何hook对象不响应 aliasSelector 那么执行原有的 forwardInvocation
        if ([self respondsToSelector:originalForwardInvocationSEL]) {
            ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
        }else {
            // 如果类没有实现 forwardInvocation 方法的话,将会抛出 doesNotRecognizeSelector 错误
            [self doesNotRecognizeSelector:invocation.selector];
        }
    }

    // 移除 aspectsToRemove 中的 AspectIdentifier,并执行  AspectIdentifier 的 remove 方法
    [aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
}

上述代码执行了对应的 beforeAspects,insteadAspects,afterAspects
如果有insteadAspects 操作则执行insteadAspects 操作,否则执行 aliasSelector, 若不响应 aliasSelector, 那么将执行 hook 之前的 forwardInvocation的方法,没有实现 forwardInvocation那么将抛出 doesNotRecognizeSelector 异常

我们再来看看 aspect_invoke 执行切面的细节

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

宏定义执行了AspectIdentifierinvokeWithInfo:方法

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

    // 检查参数是否适配,已经是第二次检查了
    if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) {
        return NO;
    }

    // 设置参数
    if (numberOfArguments > 1) {
        [blockInvocation setArgument:&info atIndex:1];
    }
    
    // 遍历 originalInvocation 参数,将参数值复制到  blockInvocation
    void *argBuf = NULL;
    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))) {
            return NO;
        }
        
        [originalInvocation getArgument:argBuf atIndex:idx];
        [blockInvocation setArgument:argBuf atIndex:idx];
    }
    // 执行 blockInvocation
    [blockInvocation invokeWithTarget:self.block];
    
    if (argBuf != NULL) {
        free(argBuf);
    }
    return YES;
}

这里比较有意思的是 [blockInvocation setArgument:&info atIndex:1]; 按照正常来说 参数下标0 和 1 分别是 target, selector

打印 blockInvocation 的方法签名,发现参数下标为 1 类型 并不是selector ,而是 id <AspectInfo>
有知道的大佬希望能够指出提点下

完结,撒花

还有什么?

  • aspect_remove 对Hook的清理操作,这里就不赘述了

  • 有对实践感兴趣的话,可以看笔者粗浅的架构实例:NonBaseClass-MVVM-ReactiveObjc (干货带代码)

  • 最后祝愿大家新年快乐,过个好年,且行且珍惜,么么哒😘


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