iOS 全埋点-UITaleView和UICollectionView的点击事件3

281 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第28天,点击查看活动详情

方案三:消息转发

在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的实现原理相似,同样可以使用以上三种方案去实现。