iOS开发笔记本技术分享iOS 知识收集

【iOS】Hook系统代理方法,添加UIScrollView滚动

2018-01-14  本文已影响384人  zhangPeng丶

简书http://www.jianshu.com/u/5690b3ad0a6f
Bloghttp://blog.zhangpeng.site
GitHubhttps://github.com/fullstack-zhangpeng

Method Swizzling

  做iOS开发的同学们一定知道Runtime,这里就不讲太多了。这个是Runtime文档,有兴趣的同学,可以自己查阅一下。网上关于Runtime的博客也有很多,官方文档看不懂,可以看看其他人的博客。(๑•̀ㅂ•́)و


需求

  我的需求是在UIScrollView停止滑动时,触发某些操作,因此,我需要监听UIScrollView的滑动状态。


UIScrollView停止滚动的监听

UIScrollView停止滚动的类型

通过调查发现UIScrollView停止滚动的类型分为三种:
1.快速滚动,自然停止
2.快速滚动,手指按压突然停止
3.慢速上下滑动停止

第1种类型,可以在UIScrollView的代理中监听到。

- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView

第2种类型和第3种类型,并没有合适的方法可以直接监听到。但是只要是滑动了,就一定会触发UIScrollView的代理。

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;

监听UIScrollView停止滚动的方式

UIScrollView有三个属性: tracking、dragging、decelerating。

// returns YES if user has touched. may not yet have started dragging
@property(nonatomic,readonly,getter=isTracking)     BOOL tracking;

// returns YES if user has started scrolling. this may require some time and or distance to move to initiate dragging 
@property(nonatomic,readonly,getter=isDragging)     BOOL dragging;

// returns YES if user isn't dragging (touch up) but scroll view is still moving
@property(nonatomic,readonly,getter=isDecelerating) BOOL decelerating;    

  在滚动和滚动结束时,这三个属性的值都不相同。我们正是利用这三个属性,完成对UIScrollView停止滚动的监听。

停止类型1:

scrollViewDidEndDecelerating:
tracking:0,dragging:0,decelerating:0

停止类型2:

scrollViewDidEndDragging:willDecelerate:
tracking:1,dragging:0,decelerating:1

scrollViewDidEndDecelerating:
tracking:0,dragging:0,decelerating:0

停止类型3:

scrollViewDidEndDragging:willDecelerate:
tracking:1,dragging:0,decelerating:0

  通过上面的解释,可以发现,我们只需要对UIScrollView的这三个属性进行相应的组合,就可以监听到UIScrollView停止滚动的事件了。

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    // 停止类型1、停止类型2
    BOOL scrollToScrollStop = !scrollView.tracking && !scrollView.dragging && !scrollView.decelerating;
    if (scrollToScrollStop) {
        [self scrollViewDidEndScroll];
    }
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    if (!decelerate) {
        // 停止类型3
        BOOL dragToDragStop = scrollView.tracking && !scrollView.dragging && !scrollView.decelerating;
        if (dragToDragStop) {
            [self scrollViewDidEndScroll];
        }
    }
}

#pragma mark - scrollView 滚动停止
- (void)scrollViewDidEndScroll {

    NSLog(@"停止滚动了!!!");
}

上面的代码具体请看监听UIScrollView停止滚动的Demo中的Demo6-UIScrollView停止滚动


进阶

  在日常开发中,我们对于UIScrollView的需求还是很大的,如果我们需要监听多个控制器中的UIScrollView滚动停止事件,难道要我们在每个控制器中都写上上面那么多代码么?不知道看文章的你们是否能接受,反正我是不能。

  于是,我便考虑给UIScrollView添加一个停止滑动的回调。用到的便是Runtime。

添加UIScrollView停止滚动回调

Hook setDelegate

  因为我们要对UIScrollView的setDelegate进行方替换,因此我们需要创建一个创建一个UIScrollView的Category,在load中进行替换。
  使用dispatch_once包住替换方法的代码,保证只进行一次替换操作,不会因多次替换同一方法,产生隐患。
  我这边只想对UIScrollView添加滚动停止的监听,所以在hook_setDelegate进行了判断,如果是[UIScrollView class]才会去Hook系统的代理方法。

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

- (void)hook_setDelegate:(id<UIScrollViewDelegate>)delegate {
    [self hook_setDelegate:delegate];
    
    if ([self isMemberOfClass:[UIScrollView class]]) {
        NSLog(@"是UIScrollView,hook方法");
        //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:));
    } else {
        NSLog(@"不是UIScrollView,不需要hook方法");
    }
}
Hook Method

如果我们想要hook某个代理方法,我们需要考虑这几种情况:

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 {
    NSLog(@"%s", __func__);
    [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];
        }
    }
}

// 那没有实现需要hook的代理方法时,调用此处方法
#pragma mark - Add_Method
- (void)add_scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    NSLog(@"%s", __func__);
    // 停止类型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];
        }
    }
}

#pragma mark - scrollView 滚动停止时触发的方法
- (void)stopScroll:(UIScrollView *)scrollView {
    NSLog(@"滚动已停止");
}
添加回调

  接下来,再通过Runtime在Category中对UIScrollView添加一个回调属性stopScrollBlock

UIScrollView+Category.h文件

@property(nonatomic, copy) StopScrollBlock stopScrollBlock;

UIScrollView+Category.m文件

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

最后在监听滚动停止的方法中调用这个回调,就大工告成了。

- (void)stopScroll:(UIScrollView *)scrollView {
    if (self.stopScrollBlock) {
        self.stopScrollBlock(scrollView);
    }
}
回调的使用
    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, kScreenW, kScreenH)];
    scrollView.contentSize = CGSizeMake(kScreenW * 8, kScreenH);
    scrollView.delegate = self;
    scrollView.stopScrollBlock = ^(UIScrollView *scrollView) {
        NSLog(@"停止滑动");
    };
    [self.view addSubview:scrollView];

附件

  1. UITableView、UICollectionView 滚动结束的监测
  2. Method Swizzling 实战:Hook 系统代理方法
上一篇下一篇

猜你喜欢

热点阅读