写在前面
传送门:
前面的系列章节可以查看上面连接,本章节主要是介绍 iOS全埋点序列文章(4)UITaleView和UICollectionView的点击事件
前言
在$AppClick
事件采集中,还有两个比较特殊 的控件。
- UITableView
-
UICollectionView
这两个控件的点击事件,一般指的是点击UITableViewCell
和UICollectionViewCell
。而UITableViewCell
和UICollectionViewCell
都是直接继承自UIView类,而不是UIControl
类,因此,我们之前实现$AppClick
事件全埋点的两个方案均不适用于UITableView和UICollectionView控件。
关于实现UITableView和UICollectionView控 件$AppClick事件的全埋点,常见的方案有三种。
- 方法交换
- 动态子类
- 消息转发
这三种方案各有优缺点。下面,我们以 UITableView
控件为例,分别介绍如何使用这三种 方案实现$AppClick
事件的全埋点。
支持UITableView控件
方案一:方法交换
众所周知,如果需要处理UITableView
的点击操作,需要先设置 UITableView
的delegate
属性,并实现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
类更为合适。
理由如下
- NSProxy类实现了包括NSObject协议在内基类所需的基础方法。
- 通过NSObject类实现的代理类不会自动转发NSObject协议中的方 法。
- 通过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的实现原理相似,同样可以使用以上三种方案去实现。
网友评论