AOP实现 iOS无侵入列表曝光埋点

最近的需求自己造个轮子
Lits+Exposure.h

#import 

NS_ASSUME_NONNULL_BEGIN

typedef enum : NSUInteger {
    YBMExposureScrollDirectionVertical = 0,
    YBMExposureScrollDirectionHorizontal = 1,
} YBMExposureScrollDirection;

@protocol YBMExposureDelegate 

@optional
/// 提供给TableView / CollectionView回调的曝光方法
/// @param indexPath 对应的列表位置
- (void)exposureBuriedPointWithIndexPath:(NSIndexPath *)indexPath;

/// 提供给ScrollView 回调曝光的方法 使用时要注意scrollView上自带的滚动条
/// @param view 对应的子视图
- (void)exposureBuiedPointWithView:(UIView *)view;

/// 完全停止滚动的回调
/// @param scrollView 当前的滚动视图
- (void)scrollViewDidEndScroll:(UIScrollView *)scrollView;

@end

@interface UIScrollView (Exposure)

@property (nonatomic, weak) id exposureDelegate;

/// 设置滚动方向 默认YBMExposureScrollDirectionVertical
@property (nonatomic, assign) YBMExposureScrollDirection direction;

@end

@interface UITableView (Exposure)

@property (nonatomic, weak) id exposureDelegate;

@property (nonatomic, readonly, getter=getLastVisibleIndexPaths , copy) NSArray * lastVisibleIndexPaths;

@property (nonatomic, readonly, getter=getCalculateExposureIndexPaths , copy) NSArray * calculateExposureIndexPaths;

@end


@interface UICollectionView (Exposure)

@property (nonatomic, weak) id exposureDelegate;

@property (nonatomic, readonly, getter=getLastVisibleIndexPaths , copy) NSArray * lastVisibleIndexPaths;

@property (nonatomic, readonly, getter=getCalculateExposureIndexPaths , copy) NSArray * calculateExposureIndexPaths;

/// 设置滚动方向 默认YBMExposureScrollDirectionVertical
@property (nonatomic, assign) YBMExposureScrollDirection direction;

@end
NS_ASSUME_NONNULL_END

Lits+Exposure.m

#import "List+Exposure.h"
#import 

static const int TableCacheIndexsKey;

static const int CollectionCacheIndexsKey;

static const int ScrollExposureDelegateKey;
static const int ScrollExposureDirection;

// 露出曝光 百分比
CGFloat const ExposurePercentage = 0.8;

void SwizzlingMethod(Class cls, SEL originSEL, SEL swizzledSEL) {
    Method originMethod = class_getInstanceMethod(cls, originSEL);
    Method swizzledMethod = nil;
    if (!originMethod) {
        originMethod  =  class_getClassMethod(cls, originSEL);
        if (!originMethod) {
            return;
        }
        swizzledMethod = class_getClassMethod(cls, swizzledSEL);
        if (!swizzledMethod) {
            return;
        }
    } else {
        swizzledMethod = class_getInstanceMethod(cls, swizzledSEL);
        if (!swizzledMethod) {
            return;
        }
    }
    if (class_addMethod(cls, originSEL, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
    } else {
        method_exchangeImplementations(originMethod, swizzledMethod);
    }
}

void SwizzlingDelegateMethod(Class delegateClass, Class listClass, SEL originSEL, SEL swizzledSEL) {
    Method originMethod = class_getInstanceMethod(delegateClass, originSEL);
    
    Method swizzledMethod = class_getInstanceMethod(listClass, swizzledSEL);
    
    class_addMethod(delegateClass, swizzledSEL, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
    
    BOOL c = class_addMethod(delegateClass, originSEL, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    
    if (!c) {
        method_exchangeImplementations(originMethod, swizzledMethod);
    }
}

@interface UIView (Window)

@end

@implementation UIView (Window)

- (UIWindow *)lastWindow{
    NSArray *windows = [UIApplication sharedApplication].windows;
    for (UIWindow *window in [windows reverseObjectEnumerator]) {
        if ([window isKindOfClass:[UIWindow class]] && CGRectEqualToRect(window.bounds, [UIScreen mainScreen].bounds)) {
            return window;
        }
    }
    return windows.lastObject;
}

@end

#pragma mark - 用来弱引用动态绑定的代理类
@interface YBMExposureDelegateModel: NSObject

@property (nonatomic, weak) id delegate;

@end

@implementation YBMExposureDelegateModel

@end

#pragma mark - 用来缓存已经上报的indexpaths
@interface YBMExposureCacheIndexPaths: NSObject

@property (nonatomic, strong) NSMutableArray * curExposureIndexPaths;

@property (nonatomic, strong) NSMutableArray * historyIndexPaths;

@end

@implementation YBMExposureCacheIndexPaths

- (NSMutableArray *)curExposureIndexPaths {
    if (!_curExposureIndexPaths) {
        _curExposureIndexPaths = [NSMutableArray array];
    }
    return _curExposureIndexPaths;
}

- (NSMutableArray *)historyIndexPaths {
    if (!_historyIndexPaths) {
        _historyIndexPaths = [NSMutableArray array];
    }
    return _historyIndexPaths;
}

@end

@implementation UIScrollView (Exposure)

+ (void)load {
    /// hook 设置代理的方法
    SEL originSelector = @selector(setDelegate:);
    SEL swizzledSelector = @selector(swizzled_setDelegate:);
    SwizzlingMethod(self, originSelector, swizzledSelector);
}

- (void)swizzled_setDelegate:(id)delegate {
    [self swizzled_setDelegate: delegate];
    /// 如果当前类 是tableview 或其子类 并且准守 曝光代理 则进行hook方法 检测滚动停止及刷新方法
    if ([self isKindOfClass:[UIScrollView class]] && [delegate conformsToProtocol:@protocol(YBMExposureDelegate)]) {
        [self hookScrollEndMethod];
    }
}

#pragma mark - hook滚动停止方法
- (void)hookScrollEndMethod {
    ///  hook 自然滚动停止
    [self hookEndDecelerating];
    
    ///  hook 手势介入停止
    [self hookEndDragging];
    
    ///  hook 滚动到顶部
    [self hookScrollTop];
}

- (void)hookEndDecelerating {
    SEL originSelector = @selector(scrollViewDidEndDecelerating:);
    SEL swizzledSelector = @selector(swizzled_scrollViewDidEndDecelerating:);
    SwizzlingDelegateMethod([self.delegate class], [self class], originSelector, swizzledSelector);
}

- (void)hookEndDragging {
    SEL originSelector = @selector(scrollViewDidEndDragging:willDecelerate:);
    SEL swizzledSelector = @selector(swizzled_scrollViewDidEndDragging:willDecelerate:);
    SwizzlingDelegateMethod([self.delegate class], [self class], originSelector, swizzledSelector);
}

- (void)hookScrollTop {
    SEL originSelector = @selector(scrollViewDidScrollToTop:);
    SEL swizzledSelector = @selector(swizzled_scrollViewDidScrollToTop:);
    SwizzlingDelegateMethod([self.delegate class], [self class], originSelector, swizzledSelector);
}

#pragma mark - 监听滚动停止
- (void)swizzled_scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    if ([self respondsToSelector:@selector(swizzled_scrollViewDidEndDecelerating:)]) {
        [self swizzled_scrollViewDidEndDecelerating:scrollView];
    }
    BOOL scrollToScrollStop = !scrollView.tracking && !scrollView.dragging && !scrollView.decelerating;
    if (scrollToScrollStop) {
        [scrollView scrollViewDidEndScroll];
    }
}

- (void)swizzled_scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    if ([self respondsToSelector:@selector(swizzled_scrollViewDidEndDragging:willDecelerate:)]) {
        [self swizzled_scrollViewDidEndDragging:scrollView willDecelerate:decelerate];
    }
    if (!decelerate) {
        BOOL dragToDragStop = scrollView.tracking && !scrollView.dragging && !scrollView.decelerating;
        if (dragToDragStop) {
            [scrollView scrollViewDidEndScroll];
        }
    }
}

- (void)swizzled_scrollViewDidScrollToTop:(UIScrollView *)scrollView {
    if ([self respondsToSelector:@selector(swizzled_scrollViewDidScrollToTop:)]) {
        [self swizzled_scrollViewDidScrollToTop: scrollView];
    }
    [scrollView scrollViewDidEndScroll];
}

- (void)scrollViewDidEndScroll {
    if (self.exposureDelegate && [self.exposureDelegate respondsToSelector:@selector(scrollViewDidEndScroll:)]) {
        [self.exposureDelegate scrollViewDidEndScroll:self];
    }
    for (UIView * view in self.subviews.objectEnumerator) {
        if (view.isHidden == NO && view.alpha > 0) {
            CGRect previousCellRect = view.frame;
            UIWindow * window = [self lastWindow];
            CGRect convertRect = [self convertRect:previousCellRect toView:window];
            CGRect scrollRect = CGRectIntersection([self.superview convertRect:self.frame toView:window], window.bounds);
            if (CGRectContainsRect(scrollRect, convertRect)) {
                if (self.exposureDelegate && [self.exposureDelegate respondsToSelector:@selector(exposureBuiedPointWithView:)]) {
                    [self.exposureDelegate exposureBuiedPointWithView:view];
                }
            }
        }
        
    }
}

#pragma mark - 动态绑定代理
- (id)exposureDelegate {
    YBMExposureDelegateModel * model = objc_getAssociatedObject(self, &ScrollExposureDelegateKey);
    return model.delegate;
}

- (void)setExposureDelegate:(id)exposureDelegate {
    YBMExposureDelegateModel * model = [[YBMExposureDelegateModel alloc] init];
    model.delegate = exposureDelegate;
    objc_setAssociatedObject(self, &ScrollExposureDelegateKey, model, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

#pragma mark - 绑定属性
- (YBMExposureScrollDirection)direction {
    NSNumber * number = objc_getAssociatedObject(self, &ScrollExposureDirection);
    YBMExposureScrollDirection direction = [number unsignedIntegerValue];
    return direction;
}

- (void)setDirection:(YBMExposureScrollDirection)direction {
    NSNumber * number = [NSNumber numberWithUnsignedInteger:direction];
    objc_setAssociatedObject(self, &ScrollExposureDirection, number, OBJC_ASSOCIATION_ASSIGN);
}
@end

#pragma mark - UITableView
@interface UITableView()

@property (nonatomic, strong) YBMExposureCacheIndexPaths * cacheIndexs;

@end


@implementation UITableView (Exposure)

#pragma mark - hook滚动停止方法
- (void)hookScrollEndMethod {
    [super hookScrollEndMethod];
    /// 设置缓存属性
    self.cacheIndexs = [[YBMExposureCacheIndexPaths alloc] init];
    
    ///  hook Reload 方法
    [self hookReload];
}

-  (void)hookReload {
    SEL originSelector = @selector(reloadData);
    SEL swizzledSelector = @selector(swizzled_Reload);
    SwizzlingMethod([self class], originSelector, swizzledSelector);
}

#pragma mark - 获取上一次上报的位置信息,供外部使用
- (NSArray *)getLastVisibleIndexPaths {
    return self.cacheIndexs.curExposureIndexPaths;
}

#pragma mark - 重新计算当前区域曝光的IndexPath
- (NSArray *)getCalculateExposureIndexPaths {
    __block NSMutableArray * array = [NSMutableArray array];
    NSArray * indexPathsForVisibleRows = self.indexPathsForVisibleRows;
    
    [indexPathsForVisibleRows enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([self calculateExposureForIndexPath:obj]) {
            [array addObject:obj];
        }
    }];
    return array;
}

#pragma mark - 监听滚动停止

- (void)scrollViewDidEndScroll {
    if (self.exposureDelegate && [self.exposureDelegate respondsToSelector:@selector(scrollViewDidEndScroll:)]) {
        [self.exposureDelegate scrollViewDidEndScroll:self];
    }
    NSLog(@"分割线 -----------**************----------- 分割线");
    
    self.cacheIndexs.historyIndexPaths = [self.cacheIndexs.curExposureIndexPaths copy];
    [self.cacheIndexs.curExposureIndexPaths removeAllObjects];
    
    NSArray * indexPathsForVisibleRows = self.indexPathsForVisibleRows;
    
    [indexPathsForVisibleRows enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [self exposureBuriedPoint: obj];
    }];
}

#pragma mark - 曝光位置判断
- (void)exposureBuriedPoint:(NSIndexPath *)indexPath {
    BOOL isExposure = [self calculateExposureForIndexPath: indexPath];
    if (isExposure) {
        [self callDelegateExposure:indexPath];
    }
}

- (BOOL)calculateExposureForIndexPath:(NSIndexPath *)indexPath {
    CGRect previousCellRect = [self rectForRowAtIndexPath:indexPath];
    
    UIWindow * window = [self lastWindow];
    
    CGRect convertRect = [self convertRect:previousCellRect toView:window];
    
    CGRect tabRect = CGRectIntersection([self.superview convertRect:self.frame toView:window], window.bounds);
    
    CGFloat currentTop = CGRectGetMinY(convertRect) - CGRectGetMinY(tabRect);
    if (currentTop < 0) {
        CGFloat percentage = (convertRect.size.height + currentTop) / convertRect.size.height;
        if (percentage >= ExposurePercentage) {
            return YES;
        }
    } else {
        CGFloat currentBottom = CGRectGetMaxY(tabRect) - CGRectGetMaxY(convertRect);
        if (currentBottom < 0) {
            CGFloat percentage = (convertRect.size.height + currentBottom) / convertRect.size.height;
            if (percentage >= ExposurePercentage) {
                return YES;
            }
        } else {
            return YES;
        }
    }
    return NO;
}

- (void)callDelegateExposure:(NSIndexPath *)indexPath {
    /// 重复上报控制
    if (![self.cacheIndexs.historyIndexPaths containsObject:indexPath]) {
        if (self.exposureDelegate && [self.exposureDelegate respondsToSelector:@selector(exposureBuriedPointWithIndexPath:)]) {
            [self.exposureDelegate exposureBuriedPointWithIndexPath:indexPath];
        }
    }
    [self.cacheIndexs.curExposureIndexPaths addObject:indexPath];
}

/// 计算tableview的总行数
- (NSInteger)calculationTotalNumber {
    NSInteger totalNumber = 0;
    NSInteger sections = self.numberOfSections;
    for (NSInteger section = 0; section < sections; section ++) {
        totalNumber += [self numberOfRowsInSection:section];
    }
    return totalNumber;
}

#pragma mark - 刷新方法
- (void)swizzled_Reload {
    /// 记录页面Item个数
    /// 刷新后item个数增加 则判定为加载更多,此时不做重置处理。 如果刷新后item个数减少或不变则判定为刷新,重置上次曝光的位置信息
    
    /// 记录item数量
    NSInteger curNumber = [self calculationTotalNumber];
    
    /// 调用原方法
    [self swizzled_Reload];
    
    dispatch_async(dispatch_get_main_queue(), ^{
        NSInteger endNumber = [self calculationTotalNumber];
        if (endNumber <= curNumber) {
            [self.cacheIndexs.curExposureIndexPaths removeAllObjects];
        }
        /// 刷新结束后 触发一次上报
        [self scrollViewDidEndScroll];
    });
}

#pragma mark - 绑定属性
- (YBMExposureCacheIndexPaths *)cacheIndexs {
    YBMExposureCacheIndexPaths * cache = objc_getAssociatedObject(self, &TableCacheIndexsKey);
    return cache;
}

- (void)setCacheIndexs:(YBMExposureCacheIndexPaths *)cacheIndexs {
    objc_setAssociatedObject(self, &TableCacheIndexsKey, cacheIndexs, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end


#pragma mark - UICollectionView
@interface UICollectionView()

@property (nonatomic, strong) YBMExposureCacheIndexPaths * cacheIndexs;

@end

@implementation UICollectionView (Exposure)

#pragma mark - hook滚动停止方法
- (void)hookScrollEndMethod {
    [super hookScrollEndMethod];
    
    /// 设置缓存属性
    self.cacheIndexs = [[YBMExposureCacheIndexPaths alloc] init];
    
    ///  hook Reload 方法
    [self hookReload];
}

-  (void)hookReload {
    SEL originSelector = @selector(reloadData);
    SEL swizzledSelector = @selector(swizzled_reload);
    SwizzlingMethod([self class], originSelector, swizzledSelector);
}

#pragma mark - 获取当前可见的IndexPath 供外部使用
- (NSArray *)getLastVisibleIndexPaths {
    return self.cacheIndexs.curExposureIndexPaths;
}

#pragma mark - 重新计算当前区域曝光的IndexPath
- (NSArray *)getCalculateExposureIndexPaths {
    __block NSMutableArray * array = [NSMutableArray array];
    NSArray * indexPathsForVisibleRows = self.indexPathsForVisibleItems;
    
    [indexPathsForVisibleRows enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([self calculateExposureForIndexPath:obj]) {
            [array addObject:obj];
        }
    }];
    return array;
}

#pragma mark - 监听滚动停止
- (void)scrollViewDidEndScroll {
    if (self.exposureDelegate && [self.exposureDelegate respondsToSelector:@selector(scrollViewDidEndScroll:)]) {
        [self.exposureDelegate scrollViewDidEndScroll:self];
    }
    NSLog(@"分割线 -----------**************----------- 分割线");
    self.cacheIndexs.historyIndexPaths = [self.cacheIndexs.curExposureIndexPaths copy];
    [self.cacheIndexs.curExposureIndexPaths removeAllObjects];
    
    NSArray * indexPathsForVisibleRows = self.indexPathsForVisibleItems;
    
    [indexPathsForVisibleRows enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [self exposureBuriedPoint: obj];
    }];
}

#pragma mark - 曝光位置判断
- (void)exposureBuriedPoint:(NSIndexPath *)indexPath {
    BOOL isExposure = [self calculateExposureForIndexPath: indexPath];
    if (isExposure) {
        [self callDelegateExposure:indexPath];
    }
}

- (BOOL)calculateExposureForIndexPath:(NSIndexPath *)indexPath {
    CGRect previousCellRect = [self.collectionViewLayout layoutAttributesForItemAtIndexPath:indexPath].frame;
    
    UIWindow * window = [self lastWindow];
    
    CGRect convertRect = [self convertRect:previousCellRect toView:window];
    
    CGRect tabRect = CGRectIntersection([self.superview convertRect:self.frame toView:window], window.bounds);
    
    if (self.direction == YBMExposureScrollDirectionVertical) {
        CGFloat currentTop = CGRectGetMinY(convertRect) - CGRectGetMinY(tabRect);
        if (currentTop < 0) {
            CGFloat percentage = (convertRect.size.height + currentTop) / convertRect.size.height;
            if (percentage >= ExposurePercentage) {
                return YES;
            }
        } else {
            CGFloat currentBottom = CGRectGetMaxY(tabRect) - CGRectGetMaxY(convertRect);
            if (currentBottom < 0) {
                CGFloat percentage = (convertRect.size.height + currentBottom) / convertRect.size.height;
                if (percentage >= ExposurePercentage) {
                    return YES;
                }
            } else {
                return YES;
            }
        }
    } else {
        CGFloat currentLeft = CGRectGetMinX(convertRect) - CGRectGetMinX(tabRect);
        if (currentLeft < 0) {
            CGFloat percentage = (convertRect.size.width + currentLeft) / convertRect.size.width;
            if (percentage >= ExposurePercentage) {
                return YES;
            }
        } else {
            CGFloat currentRight = CGRectGetMaxX(tabRect) - CGRectGetMaxX(convertRect);
            if (currentRight < 0) {
                CGFloat percentage = (convertRect.size.width + currentRight) / convertRect.size.width;
                if (percentage >= ExposurePercentage) {
                    return YES;
                }
            } else {
                return YES;
            }
        }
    }
    return NO;
}

- (void)callDelegateExposure:(NSIndexPath *)indexPath {
    /// 重复上报控制
    if (![self.cacheIndexs.historyIndexPaths containsObject:indexPath]) {
        if (self.exposureDelegate && [self.exposureDelegate respondsToSelector:@selector(exposureBuriedPointWithIndexPath:)]) {
            [self.exposureDelegate exposureBuriedPointWithIndexPath:indexPath];
        }
    }
    [self.cacheIndexs.curExposureIndexPaths addObject:indexPath];
}

/// 计算CollectionView的总行数
- (NSInteger)calculationTotalNumber {
    NSInteger totalNumber = 0;
    if (self.dataSource != nil) {
        NSInteger sections = [self.dataSource numberOfSectionsInCollectionView:self];
        for (NSInteger section = 0; section < sections; section ++) {
            totalNumber += [self.dataSource collectionView:self numberOfItemsInSection:section];
        }
    }
    return totalNumber;
}

#pragma mark - 刷新方法
- (void)swizzled_reload {
    /// 记录页面Item个数
    /// 刷新后item个数增加 则判定为加载更多,此时不做重置处理。 如果刷新后item个数减少或不变则判定为刷新,重置上次曝光的位置信息
    
    /// 记录item数量
    NSInteger curNumber = [self calculationTotalNumber];
    
    /// 调用原方法
    [self swizzled_reload];
    
    dispatch_async(dispatch_get_main_queue(), ^{
        NSInteger endNumber = [self calculationTotalNumber];
        if (endNumber <= curNumber) {
            [self.cacheIndexs.curExposureIndexPaths removeAllObjects];
        }
        /// 刷新结束后 触发一次上报
        [self scrollViewDidEndScroll];
    });
}

#pragma mark - 绑定属性
- (YBMExposureCacheIndexPaths *)cacheIndexs {
    YBMExposureCacheIndexPaths * cache = objc_getAssociatedObject(self, &CollectionCacheIndexsKey);
    return cache;
}

- (void)setCacheIndexs:(YBMExposureCacheIndexPaths *)cacheIndexs {
    objc_setAssociatedObject(self, &CollectionCacheIndexsKey, cacheIndexs, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

你可能感兴趣的:(AOP实现 iOS无侵入列表曝光埋点)