美文网首页
AOP实现 iOS无侵入列表曝光埋点

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

作者: 啥都不会啊啊啊 | 来源:发表于2022-07-13 20:35 被阅读0次

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

    #import <UIKit/UIKit.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    typedef enum : NSUInteger {
        YBMExposureScrollDirectionVertical = 0,
        YBMExposureScrollDirectionHorizontal = 1,
    } YBMExposureScrollDirection;
    
    @protocol YBMExposureDelegate <NSObject>
    
    @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<YBMExposureDelegate> exposureDelegate;
    
    /// 设置滚动方向 默认YBMExposureScrollDirectionVertical
    @property (nonatomic, assign) YBMExposureScrollDirection direction;
    
    @end
    
    @interface UITableView (Exposure)
    
    @property (nonatomic, weak) id<YBMExposureDelegate> exposureDelegate;
    
    @property (nonatomic, readonly, getter=getLastVisibleIndexPaths , copy) NSArray<NSIndexPath *> * lastVisibleIndexPaths;
    
    @property (nonatomic, readonly, getter=getCalculateExposureIndexPaths , copy) NSArray<NSIndexPath *> * calculateExposureIndexPaths;
    
    @end
    
    
    @interface UICollectionView (Exposure)
    
    @property (nonatomic, weak) id<YBMExposureDelegate> exposureDelegate;
    
    @property (nonatomic, readonly, getter=getLastVisibleIndexPaths , copy) NSArray<NSIndexPath *> * lastVisibleIndexPaths;
    
    @property (nonatomic, readonly, getter=getCalculateExposureIndexPaths , copy) NSArray<NSIndexPath *> * calculateExposureIndexPaths;
    
    /// 设置滚动方向 默认YBMExposureScrollDirectionVertical
    @property (nonatomic, assign) YBMExposureScrollDirection direction;
    
    @end
    NS_ASSUME_NONNULL_END
    

    Lits+Exposure.m

    #import "List+Exposure.h"
    #import <objc/runtime.h>
    
    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<YBMExposureDelegate> delegate;
    
    @end
    
    @implementation YBMExposureDelegateModel
    
    @end
    
    #pragma mark - 用来缓存已经上报的indexpaths
    @interface YBMExposureCacheIndexPaths: NSObject
    
    @property (nonatomic, strong) NSMutableArray<NSIndexPath *> * curExposureIndexPaths;
    
    @property (nonatomic, strong) NSMutableArray<NSIndexPath *> * historyIndexPaths;
    
    @end
    
    @implementation YBMExposureCacheIndexPaths
    
    - (NSMutableArray<NSIndexPath *> *)curExposureIndexPaths {
        if (!_curExposureIndexPaths) {
            _curExposureIndexPaths = [NSMutableArray array];
        }
        return _curExposureIndexPaths;
    }
    
    - (NSMutableArray<NSIndexPath *> *)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<UIScrollViewDelegate>)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<YBMExposureDelegate>)exposureDelegate {
        YBMExposureDelegateModel * model = objc_getAssociatedObject(self, &ScrollExposureDelegateKey);
        return model.delegate;
    }
    
    - (void)setExposureDelegate:(id<YBMExposureDelegate>)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<NSIndexPath *> *)getLastVisibleIndexPaths {
        return self.cacheIndexs.curExposureIndexPaths;
    }
    
    #pragma mark - 重新计算当前区域曝光的IndexPath
    - (NSArray<NSIndexPath *> *)getCalculateExposureIndexPaths {
        __block NSMutableArray * array = [NSMutableArray array];
        NSArray<NSIndexPath *> * 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<NSIndexPath *> * 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<NSIndexPath *> *)getLastVisibleIndexPaths {
        return self.cacheIndexs.curExposureIndexPaths;
    }
    
    #pragma mark - 重新计算当前区域曝光的IndexPath
    - (NSArray<NSIndexPath *> *)getCalculateExposureIndexPaths {
        __block NSMutableArray * array = [NSMutableArray array];
        NSArray<NSIndexPath *> * 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<NSIndexPath *> * 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无侵入列表曝光埋点

          本文链接:https://www.haomeiwen.com/subject/odpfirtx.html