美文网首页
iOS 全埋点-UITaleView和UICollectionV

iOS 全埋点-UITaleView和UICollectionV

作者: smile_frank | 来源:发表于2022-02-14 17:45 被阅读0次

    写在前面

    传送门:

    前面的系列章节可以查看上面连接,本章节主要是介绍 iOS全埋点序列文章(4)UITaleView和UICollectionView的点击事件

    前言

    $AppClick事件采集中,还有两个比较特殊 的控件。

    • UITableView
    • UICollectionView
      这两个控件的点击事件,一般指的是点击 UITableViewCellUICollectionViewCell。而 UITableViewCellUICollectionViewCell都是直接继承自UIView类,而不是UIControl类,因此,我们之前实现$AppClick事件全埋点的两个方案均不适用于UITableView和UICollectionView控件。

    关于实现UITableView和UICollectionView控 件$AppClick事件的全埋点,常见的方案有三种。

    1. 方法交换
    2. 动态子类
    3. 消息转发

    这三种方案各有优缺点。下面,我们以 UITableView控件为例,分别介绍如何使用这三种 方案实现$AppClick事件的全埋点。

    支持UITableView控件

    方案一:方法交换

    众所周知,如果需要处理UITableView的点击操作,需要先设置 UITableViewdelegate属性,并实现UITableViewDelegate协议的- tableView:didSelectRowAtIndexPath:方法。因此,我们也很容易想到使用 Method Swizzling交换-tableView:didSelectRowAtIndexPath:方法来实现 UITableView控件$AppClick事件的全埋点

    初始思路
    首先,我们使用Method Swizzling交换UITableView- setDelegate:方法;然后,获取实现UITableViewDelegate协议的delegate对象,在得到delegate对象之后,交换delegate对象的- tableView:didSelectRowAtIndexPath:方法;最后,在交换后的方法中触发 $AppClick事件,从而实现UITableView控件$AppClick事件全埋点。

    新建一个UITableView的类别CountData

    UItableView+CountData.m

    #import "UITableView+CountData.h"
    #import "NSObject+Swizzler.h"
    #import <objc/message.h>
    #import <objc/runtime.h>
    #import "SensorsAnalyticsSDK.h"
    
    @implementation UITableView (CountData)
    
    + (void)load { 
        [UITableView sensorsdata_swizzleMethod:@selector(setDelegate:) withMethod:@selector(CountData_setDelegate:)];
    }
    
    /*
     *  UITableView的delegate对象是在程序运行时设置的,其有可能是UItableView对象本身,也有可能是UIviewController或者其他对象。因此需要给delegate对象动态地添加需要交换的方法,然后与原来的tableView:didSelectRowAtIndexPath:方法进行交换。
     */
    
    - (void)CountData_setDelegate:(id<UITableViewDelegate>)delegate {
        [self CountData_setDelegate:delegate];
        [self CountData_swizzleDidSelectRowAtIndexPathMethodWithDelegate:delegate];
    }
    
    
    //添加交换方法
    static void CountData_tableViewDidSelectRow(id object,SEL selector,UITableView *tableView,NSIndexPath *indexPath) {
        SEL destinationSelector = NSSelectorFromString(@"CountData_tableView:didSelectRowAtIndexPath:");
        //发送消息,调用原始的tableView:didSelectRowAtIndexPath:方法实现   
        ((void (*)(id,SEL,id,id))objc_msgSend)(object,destinationSelector,tableView,indexPath);   
        [[SensorsAnalyticsSDK sharedInstance]AppClickWithTableView:tableView didSelectRowAtIndexPath:indexPath properties:nil];
    }
    
    #pragma mark- 私有方法,负责给delegate对象添加一个方法并进行交换
    -(void)CountData_swizzleDidSelectRowAtIndexPathMethodWithDelegate:(id)delegate {
       //获取delegate对象的类
        Class delegateClass = [delegate class];
        NSLog(@"获取当前对象的类型名字为---%@",NSStringFromClass([delegate class]));
        //方法名
        SEL sourceSelector = @selector(tableView:didSelectRowAtIndexPath:);
        //当delegate对象中没有实现方法tableView:didSelectRowAtIndexPath:,直接返回
        if (![delegate respondsToSelector:sourceSelector]) {
            NSLog(@"没有实现tableView:didSelectRowAtIndexPath方法");
            return;
        }
        
        SEL destinationSelector = NSSelectorFromString(@"CountData_tableView:didSelectRowAtIndexPath:");
        //当delegate对象已经存在了CountData_tableView:didSelectRowAtIndexPath:,说明已经交换,可以直接返回
        if ([delegate respondsToSelector:destinationSelector]) {
            return;
        }
        
        Method sourceMethod = class_getInstanceMethod(delegateClass, sourceSelector);
        const char *encoding = method_getTypeEncoding(sourceMethod);
        //当类中已经存在相同的方法时,则会添加方法失败。当时前面已经判断过方法是否存在。因此,此处一定会添加成功
        if (!class_addMethod([delegate class], destinationSelector,(IMP)CountData_tableViewDidSelectRow, encoding)) {
            
            return;
        }
        
        //方法添加之后,进行方法交换
        [delegateClass sensorsdata_swizzleMethod:sourceSelector withMethod:destinationSelector];
    }
    
    @end
    

    方案二:动态子类

    初始思路
    在运行时,给实现了UITableViewDelegate协议的- tableView:didSelectRow-AtIndexPath:方法的类创建一个子类,让该子类的对象变成我们自己创建的子类的对象。同时,在创建的子类中动态添加- tableView:didSelectRowAtIndexPath:方法。那么,当用户点击UITableViewCell控件时,就会先运行自己创建的子类中的- tableView:didSelectRow-AtIndexPath:方法。我们在实现该方法的时候,先调用delegate原来的方法实现,再触发$AppClick事件,即可实现 UITableView控件$AppClick事件全埋点。

    创建一个动态添加子类的工具类:TableViewDynamicDelegate

    TableViewDynamicDelegate.h声明如下:

    #import <Foundation/Foundation.h>
    #import <UIKit/UIKit.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface TableViewDynamicDelegate : NSObject
    
    + (void)proxyWithTableViewDelegate:(id <UITableViewDelegate>)delegate;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    TableViewDynamicDelegate.m声明如下:

    #import "TableViewDynamicDelegate.h"
    #import <UIKit/UIKit.h>
    #import <objc/runtime.h>
    #import "SensorsAnalyticsSDK.h"
    
    /// Delegate 的子类前缀
    static NSString *const kSensorsDelegatePrefix = @"cn.countData.";
    // tableView:didSelectRowAtIndexPath: 方法指针类型
    typedef void (*TableDidSelectImplementation)(id, SEL, UITableView *, NSIndexPath *);
    
    @implementation TableViewDynamicDelegate
    
    + (void)proxyWithTableViewDelegate:(id <UITableViewDelegate>)delegate  {
       
        SEL originalSelector = NSSelectorFromString(@"tableView:didSelectRowAtIndexPath:");
        //当Delegate中没有实现tbaleView:didSelectRowAtIndexPath:方法时,直接返回
        if (![delegate respondsToSelector:originalSelector]) {
            NSLog(@"没有实现tbaleView:didSelectRowAtIndexPath:方法");
            return;
        }
        //动态创建一个新类
        Class originalClass =  object_getClass(delegate);
        NSString *originalClassName = NSStringFromClass(originalClass);
        
        //判断这个delegate对象是否已经动态创建的类时,无须重复设置,直接返回
        if([originalClassName hasPrefix:kSensorsDelegatePrefix])  {
            return;
        }
        
        NSString *subClassName = [kSensorsDelegatePrefix stringByAppendingString:originalClassName];
        Class subclass = NSClassFromString(subClassName);
        if (!subclass) {
            //注册一个新的子类,其父类为originalclass
            subclass = objc_allocateClassPair(originalClass, subClassName.UTF8String, 0);
            //获取TableViewDynamicDelegate中的tableView:didSelectRowAtIndexPath指针
            Method method = class_getInstanceMethod(self, originalSelector);
            //获取方法实现
            IMP methodIMP = method_getImplementation(method);
            //获取方法类型的编码
            const char *types = method_getTypeEncoding(method);
            //在subClass中添加 tableView:didSelectRowAtIndexPath: 方法
            if(!class_addMethod(subclass, originalSelector,methodIMP , types)) {
                NSLog(@"方法已经存在");
            }
            
            /*删除动态生成的前缀,动态添加方法(sensorsdata_class)*/ 
    
            // 获取 TableViewDynamicDelegate 中的 sensorsdata_class 方法指针
            Method classMethod = class_getInstanceMethod(self, @selector(sensorsdata_class));
            // 获取方法实现
            IMP classIMP = method_getImplementation(classMethod);
            //获取方法的类型编码
            const char *classTypes = method_getTypeEncoding(classMethod);
            //在subclass中添加class方法
            if (!class_addMethod(subclass, @selector(class), classIMP, classTypes)) {
                NSLog(@"添加方法失败");
            }
            //子类和原始类的大小必须一致,不能有更多的ivars或者属性
            //如果不同会导致设置新的子类时,会重新设置内存,导致重写了对象的isa指针
            if (class_getInstanceSize(originalClass) != class_getInstanceSize(subclass)) {
                return;
            }
            //将delegate对象设置为新的子类对象
            objc_registerClassPair(subclass);
        }
        if (object_setClass(delegate, subclass)) {
            NSLog(@"创建成功");
        }
    }
    
    //删除自动创建类名的私有方法
    - (Class)sensorsdata_class {
        // 获取对象的类
        Class class = object_getClass(self);
        // 将类名前缀替换成空字符串,获取原始类名
        NSString *className = [NSStringFromClass(class) stringByReplacingOccurrencesOfString:kSensorsDelegatePrefix withString:@""];
        // 通过字符串获取类,并返回
        return objc_getClass([className UTF8String]);
    }
    
    - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
        //第一步:获取原始的类
        Class cla = object_getClass(tableView.delegate);
        NSString *className = [NSStringFromClass(cla) stringByReplacingOccurrencesOfString:kSensorsDelegatePrefix withString:@""];
        Class originalClass = objc_getClass([className UTF8String]);
        
        //第二步:调用开发者自己实现的方法
        SEL originalSelector = NSSelectorFromString(@"tableView:didSelectRowAtIndexPath:");
        Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
        IMP originalIMP = method_getImplementation(originalMethod);
        if (originalIMP) {
           ((TableDidSelectImplementation)originalIMP)(tableView.delegate,originalSelector,tableView,indexPath);
        }
        
        //第三步:埋点
        [[SensorsAnalyticsSDK sharedInstance]AppClickWithTableView:tableView didSelectRowAtIndexPath:indexPath properties:@{@"$app_click":@"动态创建类事件"}];
        
    }
    

    最后调用:修改UITableView+CountData.m文件中的-CountData_setDelegate:方法,添加调用TableViewDynamicDelegate类的+proxyWithTableViewDelegate方法`

    + (void)load {   
        [UITableView sensorsdata_swizzleMethod:@selector(setDelegate:) withMethod:@selector(CountData_setDelegate:)];
    }
    
    - (void)CountData_setDelegate:(id<UITableViewDelegate>)delegate {
    //    方案2 动态子类   
        [self CountData_setDelegate:delegate];
        //设置delegate的动态子类
        [TableViewDynamicDelegate proxyWithTableViewDelegate:delegate];    
    }
    

    方案三:消息转发

    在iOS应用开发中,自定义类一般需要继承自NSObject类或者NSObject 子类。但是,NSProxy类不是继承自NSObject类或者NSObject子类,而是一 个实现了NSObject协议的抽象基类。

    当然,在大部分情况下,使用NSObject类也可以实现消息转发,实现 方式与NSProxy类相同。但是,大部分情况下使用NSProxy类更为合适。

    理由如下

    1. NSProxy类实现了包括NSObject协议在内基类所需的基础方法。
    2. 通过NSObject类实现的代理类不会自动转发NSObject协议中的方 法。
    3. 通过NSObject类实现的代理类不会自动转发NSObject类别中的方 法,例如上面调用实例中的-valueForKey:方法,如果是使用NSObject类实 现的代理类,会抛出异常。

    步骤如下:

    步骤一:创建CountDataDelegateProxy类 (继承自NSProxy类),实现UITableViewDelegate协议。然后添加一个类 方法+proxywithTableViewDelegate:。

    CountDataDelegateProxy.h 声明如下:

    #import <Foundation/Foundation.h>
    #import <UIKit/UIKit.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface CountDataDelegateProxy : NSProxy
    
    @property(nonatomic,weak) id delegate;
    +(instancetype) proxywithTableViewDelegate:(id<UITableViewDelegate>)delegate;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    CountDataDelegateProxy.m 声明如下:

    #import "CountDataDelegateProxy.h"
    #import "SensorsAnalyticsSDK.h"
    @implementation CountDataDelegateProxy
    
    + (instancetype)proxywithTableViewDelegate:(id<UITableViewDelegate>)delegate {
        CountDataDelegateProxy *proxy = [CountDataDelegateProxy alloc];
        proxy.delegate = delegate;
        return  proxy;
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
        //返回delegate对象的方法签名
        return [(NSObject *)self.delegate methodSignatureForSelector:sel];
    }
    
    - (void)forwardInvocation:(NSInvocation *)invocation {    
        [invocation invokeWithTarget:self.delegate];
        if (invocation.selector == @selector(tableView:didSelectRowAtIndexPath:)) {
            invocation.selector = NSSelectorFromString(@"countDatatableView:didSelectRowAtIndexPath:");
            [invocation invokeWithTarget:self];
        }
    }
    
    -(void)countDatatableView:(UITableView *)tableView didSelectRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
        
        [[SensorsAnalyticsSDK sharedInstance]AppClickWithTableView:tableView didSelectRowAtIndexPath:indexPath properties:@{@"$app_click":@"NSProxy的委托代理"}];
    }
    
    @end
    

    步骤二:为了可以同时支持UICollectionView控件,我们直接在UIScrollView中扩展countData_delegareProxy属性。

    创建UIScrollView的类别CountData,并在头文件中添加属性声明。

    CountDataDelegateProxy.h 声明如下:

    #import <UIKit/UIKit.h>
    #import "CountDataDelegateProxy.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface UIScrollView (CountData)
    
    @property (nonatomic,strong) CountDataDelegateProxy *countData_delegareProxy;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    UIScrollView+CountData.m声明如下:

    #import "UIScrollView+CountData.h"
    #import <objc/runtime.h>
    
    @implementation UIScrollView (CountData)
    
    - (void)setCountData_delegareProxy:(CountDataDelegateProxy *)countData_delegareProxy {
        objc_setAssociatedObject(self, @selector(setCountData_delegareProxy:), countData_delegareProxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (CountDataDelegateProxy *)countData_delegareProxy {
        return objc_getAssociatedObject(self, @selector(countData_delegareProxy));
    }
    @end
    
    

    步骤三:修改UITableView+CountData.m文件中的-CountData_setDelegate:方法,添加调用TableViewDynamicDelegate类的+proxyWithTableViewDelegate方法`

    - (void)CountData_setDelegate:(id<UITableViewDelegate>)delegate {
    
        /*方案3 NSProxy 消息转发*/
    
        self.countData_delegareProxy = nil;
        if (delegate) {
            CountDataDelegateProxy *proxy = [CountDataDelegateProxy proxywithTableViewDelegate:delegate];
            self.countData_delegareProxy = proxy;
            [self CountData_setDelegate:proxy];
        }else {
            [self CountData_setDelegate:nil];
        }
    }
    

    总结

    对于UITableView控件$AppClick事件全埋点的三种方案,它们各有优缺点,读者可以根据实 际情况选择相应的方案。

    方案一:方法交换

    优点:简单、易理解;Method Swizzling属于 成熟技术,性能相对来说较高。

    缺点:对原始类有入侵,容易造成冲突。

    方案二:动态子类

    优点:没有对原始类入侵,不会修改原始类 的方法,不会和第三方库冲突,是一种比较稳定的方案。

    缺点:动态创建子类对性能和内存有比较大 的消耗。

    方案三:消息转发

    优点:充分利用消息转发机制,对消息进行 拦截,性能较好。

    缺点:容易与一些同样使用消息转发进行拦 截的第三方库冲突

    扩展

    获取控件内容

    为了能获取更复杂的UIView的显示内容,该方法需要修改成支持通过 递归遍历获取子控件的显示内容。

    定义UIView的分类,布局页面等用到的控件的分类

    UIView+TextContentData.h 声明如下:

    #import <UIKit/UIKit.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface UIView (TextContentData)
    
    @property (nonatomic,copy,readonly) NSString *elementContent;
    @property (nonatomic,strong,readonly) UIViewController *myViewController;
    
    @end
    
    @interface UIButton (TextContentData)
    
    @end
    
    @interface UISwitch (TextContentData)
    
    @end
    
    @interface UILabel (TextContentData)
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    UIView+TextContentData.m 声明如下:

    #import "UIView+TextContentData.h"
    
    @implementation UIView (TextContentData)
    
    - (NSString *)elementContent {
        // 如果是隐藏控件,不获取控件内容
        if (self.isHidden || self.alpha == 0) { return nil; }
        // 初始化数组,用于保存子控件的内容
        NSMutableArray *contents = [NSMutableArray array];
        for (UIView *view in self.subviews) {
            // 获取子控件的内容
            // 如果子类有内容,例如UILabel的text,获取到的就是text属性
            // 如果子类没有内容,就递归调用该方法,获取其子控件的内容
            NSString *content = view.elementContent;
            if (content.length > 0) {
                // 当该子控件有内容时,保存在数组中
                [contents addObject:content];
            }
        }
        // 当未获取到子控件内容时,返回nil。如果获取到多个子控件内容时,使用"-"拼接
        return contents.count == 0 ? nil : [contents componentsJoinedByString:@"-"];
    }
    
    - (UIViewController *)myViewController {
        UIResponder *responder = self;
        while ((responder = [responder nextResponder])) {
            if ([responder isKindOfClass:[UIViewController class]]) {
                return (UIViewController *)responder;
            }
        }
        return  nil;
    }
    @end
    
    @implementation  UIButton (TextContentData)
    
    - (NSString *)elementContent {
        return self.titleLabel.text ?: super.elementContent;
    }
    
    @end
    
    
    @implementation UISwitch (TextContentData)
    
    - (NSString *)elementContent {
        return self.on ? @"checked":@"unchecked";
    }
    
    @end
    
    @implementation UILabel (TextContentData)
    
    - (NSString *)elementContent {
        
        return self.text ?: super.elementContent;
    }
    
    @end
    

    最后页面采集信息增加字段$element_content

    代码如下所示:

    -(void)AppClickWithTableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)index properties:(NSDictionary<NSString *,id> *)properties  {
        
        NSMutableDictionary *event = [NSMutableDictionary dictionary];
        // 设置事件名称
        event[@"event"] = @"TableView的点击事件";
        // 设置事件发生的时间戳,单位为毫秒
        event[@"time"] = [NSNumber numberWithLong:NSDate.date.timeIntervalSince1970 * 1000];
        NSMutableDictionary *eventProperties = [NSMutableDictionary dictionary];
        // 添加预置属性
        [eventProperties addEntriesFromDictionary:self.automaticProperties];
        // 添加自定义属性
        [eventProperties addEntriesFromDictionary:properties];
        //判断是否位被动启动状态
        if(self.isLaunchedPassively) {
            //添加应用程序状态属性
            eventProperties[@"$app_state"] = @"background";
        }
        
        if (tableView) {
            // TODO:获取用户点击的UITableViewCell控件对象
            UITableViewCell *cell = [tableView cellForRowAtIndexPath:index];
            // TODO:设置被用户点击的UITableViewCell控件上的内容($element_content)
            eventProperties[@"$element_content"] = cell.elementContent;
        }
        // 设置事件属性
        event[@"properties"] = eventProperties;
       
        [self printEvent:event];
    }
    
    

    最后支持UICollectionView控件和UITableView的实现原理相似,同样可以使用以上三种方案去实现。

    相关文章

      网友评论

          本文标题:iOS 全埋点-UITaleView和UICollectionV

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