一、简介

一、所谓的aop编程(面向切面编程),其原理也就是在不更改正常的业务流程的前提下,通过生成一个动态代理类,从而实现对目标对象嵌入附加的操作。在ios中,想要实现相似的效果也很简单,利用OC的动态性,通过Method Swizzling 改变目标函数的selector所指向的实现,然后在新的实现中实现附加的操作,完成之后再货到原来的处理逻辑。

二、在一个类没有实现源码的情况下,如果你要改变一个类的实现方法,你可以选择重继承该类,然后重写方法,或者使用Category类别名暴力抢先的方式。但是这两种方式,都需要我们在使用的时候改变我们的编程方式,或者继承该类,或者需要引入Category。下面推出的一种方式,不需要我们修改我们编写逻辑的代码,就能实现函数的Hook功能,那就是RunTime中的Method Swizzling—交换方法的实现。

二、实现原理

在Object-C中每一个Method都是由一个SEL(方法名的散列值)和一个方法实现的指针(IMP)组成,他们在类实例化过程中SEL和IMP一一对应组成我们需要的完整的Method。

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

如果我们不做任何处理,SEL和IMP都是一一对应的。如果我们使用Method Swizzling交换Method1和Method2的实现的时候,我们只需要在运行时把IMP2和IMP3的指向地址做个交换就可以了。其实我们调用的就是RunTime中的

/** 
 * Exchanges the implementations of two methods.
 * 
 * @param m1 Method to exchange with second method.
 * @param m2 Method to exchange with first method.
 * 
 * @note This is an atomic version of the following:
 *  \code 
 *  IMP imp1 = method_getImplementation(m1);
 *  IMP imp2 = method_getImplementation(m2);
 *  method_setImplementation(m1, imp2);
 *  method_setImplementation(m2, imp1);
 *  \endcode
 */
OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

第一个参数为:原始方法名。

第二个参数为:交换方法名。

四、页面埋点的实现

我们新建一个UIViewController的分类

#import <UIKit/UIKit.h>
#import <objc/runtime.h>

NS_ASSUME_NONNULL_BEGIN

@interface UIViewController (AOP)

@end

@interface UIControl (AOP)

@end

typedef void(^StopScrollBlock)(UIScrollView *scrollView);

@interface UIScrollView (AOP)

@property(nonatomic, copy) StopScrollBlock stopScrollBlock;

@end

NS_ASSUME_NONNULL_END
#import "UIViewController+AOP.h"

@implementation UIViewController (AOP)

+ (void)load
{
    Method originalMtd = class_getInstanceMethod([self class], @selector(viewDidAppear:));
    Method swizzleMtd = class_getInstanceMethod([self class], @selector(aop_viewDidAppear:));
    method_exchangeImplementations(originalMtd, swizzleMtd);
    
    Method originalMtd1 = class_getInstanceMethod([self class], @selector(viewWillDisappear:));
    Method swizzleMtd1 = class_getInstanceMethod([self class], @selector(aop_viewWillDisAppear:));
    method_exchangeImplementations(originalMtd1, swizzleMtd1);
}

- (void)aop_viewDidAppear:(BOOL)animated
{
    /**
     MTA业务操作 记录日志,上传服务器,页面显示
     */
    [self aop_viewDidAppear:animated];
}

- (void)aop_viewWillDisAppear:(BOOL)animated
{
    /**
     MTA业务操作 记录日志,上传服务器,页面消失
     */
#ifdef DEBUG
    if ([self isContainScrollView]) {
        /**
         关闭滑动场景的卡顿监控,在相关页面进行如下代码的打点
         */
    }
#endif
    [self aop_viewWillDisAppear:animated];
}

-(BOOL)isContainScrollView{
    for (id view in self.view.subviews) {
        if ([view isMemberOfClass:[UIScrollView class]]) {//判断当前VC是否有UIScrollView及其子类
            return YES;
        }
    }
    return NO;
}

@end

@implementation UIControl (AOP)

+ (void)load {
    Method originalMtd = class_getInstanceMethod([self class], @selector(sendAction:to:forEvent:));
    Method swizzleMtd = class_getInstanceMethod([self class], @selector(aop_sendAction:to:forEvent:));
    method_exchangeImplementations(originalMtd, swizzleMtd);
    
    Method originalMtd2 = class_getInstanceMethod([self class], @selector(sendActionsForControlEvents:));
    Method swizzleMtd2 = class_getInstanceMethod([self class], @selector(aop_sendActionsForControlEvents:));
    method_exchangeImplementations(originalMtd2, swizzleMtd2);
}

- (void)aop_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    if ([self isKindOfClass:[UIButton class]]){
        /**
         MTA次数统计,某按钮点击及频率次数日志
         */
    }
    [self aop_sendAction:action to:target forEvent:event];
}

// 时长统计
- (void)aop_sendActionsForControlEvents:(UIControlEvents)controlEvents {
    if (controlEvents == UIControlEventTouchDown){
        /**
         MTA 处理按钮点击事件
         */
        NSLog(@"aop_sendAction ============= 时长统计 =============处理按钮点击事件  ");
    }else if (controlEvents == UIControlEventTouchUpInside || controlEvents == UIControlEventTouchUpOutside){
        /**
         处理按钮松开状态
         */
        NSLog(@"aop_sendAction ============= 时长统计 =============处理按钮松开状态  ");
    }
    [self aop_sendActionsForControlEvents:controlEvents];
}

@end


@implementation UIScrollView (AOP)

#pragma mark - Setter And Getter

static const char p_stopScrollBlock = '\0';

- (StopScrollBlock)stopScrollBlock {
    return objc_getAssociatedObject(self, &p_stopScrollBlock);
}

- (void)setStopScrollBlock:(StopScrollBlock)stopScrollBlock {
    objc_setAssociatedObject(self, &p_stopScrollBlock, stopScrollBlock, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

/**load初始化方法 代理方法互换*/
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method originalMethod = class_getInstanceMethod([UIScrollView class], @selector(setDelegate:));
        Method replaceMethod = class_getInstanceMethod([UIScrollView class], @selector(hook_setDelegate:));
        method_exchangeImplementations(originalMethod, replaceMethod);
    });
}

/*
 1、快速滚动,自然停止;
 2、快速滚动,手指按压突然停止;
 3、慢速上下滑动停止
 */
#pragma mark - scrollView 停止滚动监测
- (void)stopScroll:(UIScrollView *)scrollView {
    if (self.stopScrollBlock) {
        self.stopScrollBlock(scrollView);
    }
#ifdef DEBUG
    NSLog(@"scrollView停止滚动了,存储日志上报服务器,可作为结束滑动钩子函数");
#endif
}

#pragma mark - Hook UIScrollView setDelegate
- (void)hook_setDelegate:(id<UIScrollViewDelegate>)delegate {
    [self hook_setDelegate:delegate];
    if ([self isMemberOfClass:[UIScrollView class]]) {
        //Hook (scrollViewDidEndDecelerating:) 方法
        Hook_Method([delegate class], @selector(scrollViewDidEndDecelerating:), [self class], @selector(p_scrollViewDidEndDecelerating:), @selector(add_scrollViewDidEndDecelerating:));
        
        //Hook (scrollViewDidEndDragging:willDecelerate:) 方法
        Hook_Method([delegate class], @selector(scrollViewDidEndDragging:willDecelerate:), [self class], @selector(p_scrollViewDidEndDragging:willDecelerate:), @selector(add_scrollViewDidEndDragging:willDecelerate:));
        
        //Hook (scrollViewWillBeginDragging:) 方法
        Hook_Method([delegate class], @selector(scrollViewWillBeginDragging:), [self class], @selector(p_scrollViewWillBeginDragging:), @selector(add_scrollViewWillBeginDragging:));
        
    }
    //不是UIScrollView,不需要hook方法
}

#pragma mark - Hook_Method //OC中static修饰方法有什么用?
static void Hook_Method(Class originalClass, SEL originalSel, Class replacedClass, SEL replacedSel, SEL noneSel){
    //原实例方法
    Method originalMethod = class_getInstanceMethod(originalClass, originalSel);
    // 替换的实例方法
    Method replacedMethod = class_getInstanceMethod(replacedClass, replacedSel);
    // 如果没有实现 delegate 方法,则手动动态添加
    if (!originalMethod) {
        Method noneMethod = class_getInstanceMethod(replacedClass, noneSel);
        BOOL addNoneMethod = class_addMethod(originalClass, originalSel, method_getImplementation(noneMethod), method_getTypeEncoding(noneMethod));
        if (addNoneMethod) {
            NSLog(@"******** 没有实现 (%@) 方法,手动添加成功!!",NSStringFromSelector(originalSel));
        }
        return;
    }
    // 向实现 delegate 的类中添加新的方法
    // 这里是向 originalClass 的 replaceSel(@selector(p_scrollViewDidEndDecelerating:)) 添加 replaceMethod
    BOOL addMethod = class_addMethod(originalClass, replacedSel, method_getImplementation(replacedMethod), method_getTypeEncoding(replacedMethod));
    if (addMethod) {
        //添加成功
        NSLog(@"******** 实现了 (%@) 方法并成功 Hook 为 --> (%@)", NSStringFromSelector(originalSel), NSStringFromSelector(replacedSel));
        // 重新拿到添加被添加的 method,这里是关键(注意这里 originalClass, 不 replacedClass), 因为替换的方法已经添加到原类中了, 应该交换原类中的两个方法
        Method newMethod = class_getInstanceMethod(originalClass, replacedSel);
        // 实现交换
        method_exchangeImplementations(originalMethod, newMethod);
    }else {
        // 添加失败,则说明已经 hook 过该类的 delegate 方法,防止多次交换。
        NSLog(@"******** 已替换过,避免多次替换 --> (%@)",NSStringFromClass(originalClass));
    }
    
}

// 已经实现需要hook的代理方法时,调用此处方法进行替换
#pragma mark - Replace_Method
- (void)p_scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    [self p_scrollViewDidEndDecelerating:scrollView];
    // 停止类型1、停止类型2
    BOOL scrollToScrollStop = !scrollView.tracking && !scrollView.dragging && !scrollView.decelerating;
    if (scrollToScrollStop) {
        [scrollView stopScroll:scrollView];
    }
}

- (void)p_scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    NSLog(@"%s", __func__);
    [self p_scrollViewDidEndDragging:scrollView willDecelerate:decelerate];
    if (!decelerate) {
        // 停止类型3
        BOOL dragToDragStop = scrollView.tracking && !scrollView.dragging && !scrollView.decelerating;
        if (dragToDragStop) {
            [scrollView stopScroll:scrollView];
        }
    }
}

- (void)p_scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    NSLog(@"%s", __func__);
    [self p_scrollViewWillBeginDragging:scrollView];
#ifdef DEBUG
    NSLog(@"开始监听到scrollView开始滑动,存储日志上报服务器");
#endif
}

// 那没有实现需要hook的代理方法时,调用此处方法
#pragma mark - Add_Method
- (void)add_scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    // 停止类型1、停止类型2
    BOOL scrollToScrollStop = !scrollView.tracking && !scrollView.dragging && !scrollView.decelerating;
    if (scrollToScrollStop) {
        [scrollView stopScroll:scrollView];
    }
}

- (void)add_scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    NSLog(@"%s", __func__);
    if (!decelerate) {
        // 停止类型3
        BOOL dragToDragStop = scrollView.tracking && !scrollView.dragging && !scrollView.decelerating;
        if (dragToDragStop) {
            [scrollView stopScroll:scrollView];
        }
    }
}

- (void)add_scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    NSLog(@"%s", __func__);
#ifdef DEBUG
    NSLog(@"开始监听到scrollView开始滑动,存储日志上报服务器");
#endif
}


@end