寫在前面
傳送門:
前面的系列章節(jié)可以查看上面連接卷中,本章節(jié)主要是介紹 iOS全埋點(diǎn)序列文章(4)UITaleView和UICollectionView的點(diǎn)擊事件
前言
在$AppClick
事件采集中,還有兩個(gè)比較特殊 的控件议忽。
- UITableView
-
UICollectionView
這兩個(gè)控件的點(diǎn)擊事件栈幸,一般指的是點(diǎn)擊UITableViewCell
和UICollectionViewCell
帮辟。而UITableViewCell
和UICollectionViewCell
都是直接繼承自UIView類由驹,而不是UIControl
類,因此并炮,我們之前實(shí)現(xiàn)$AppClick
事件全埋點(diǎn)的兩個(gè)方案均不適用于UITableView和UICollectionView控件甥郑。
關(guān)于實(shí)現(xiàn)UITableView和UICollectionView控 件$AppClick事件的全埋點(diǎn)澜搅,常見的方案有三種。
- 方法交換
- 動(dòng)態(tài)子類
- 消息轉(zhuǎn)發(fā)
這三種方案各有優(yōu)缺點(diǎn)养篓。下面赂蕴,我們以 UITableView
控件為例概说,分別介紹如何使用這三種 方案實(shí)現(xiàn)$AppClick
事件的全埋點(diǎn)。
支持UITableView控件
方案一:方法交換
眾所周知萍丐,如果需要處理UITableView
的點(diǎn)擊操作放典,需要先設(shè)置 UITableView
的delegate
屬性,并實(shí)現(xiàn)UITableViewDelegate
協(xié)議的- tableView:didSelectRowAtIndexPath:
方法壳影。因此,我們也很容易想到使用 Method Swizzling
交換-tableView:didSelectRowAtIndexPath:
方法來實(shí)現(xiàn) UITableView
控件$AppClick
事件的全埋點(diǎn)
初始思路
首先根灯,我們使用Method Swizzling
交換UITableView
的- setDelegate:
方法烙肺;然后氧卧,獲取實(shí)現(xiàn)UITableViewDelegate
協(xié)議的delegate
對象,在得到delegate
對象之后怎栽,交換delegate
對象的- tableView:didSelectRowAtIndexPath:
方法;最后脚祟,在交換后的方法中觸發(fā) $AppClick
事件,從而實(shí)現(xiàn)UITableView
控件$AppClick
事件全埋點(diǎn)为黎。
新建一個(gè)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對象是在程序運(yùn)行時(shí)設(shè)置的铭乾,其有可能是UItableView對象本身娃循,也有可能是UIviewController或者其他對象。因此需要給delegate對象動(dòng)態(tài)地添加需要交換的方法笛质,然后與原來的tableView:didSelectRowAtIndexPath:方法進(jìn)行交換妇押。
*/
- (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:");
//發(fā)送消息姓迅,調(diào)用原始的tableView:didSelectRowAtIndexPath:方法實(shí)現(xiàn)
((void (*)(id,SEL,id,id))objc_msgSend)(object,destinationSelector,tableView,indexPath);
[[SensorsAnalyticsSDK sharedInstance]AppClickWithTableView:tableView didSelectRowAtIndexPath:indexPath properties:nil];
}
#pragma mark- 私有方法,負(fù)責(zé)給delegate對象添加一個(gè)方法并進(jìn)行交換
-(void)CountData_swizzleDidSelectRowAtIndexPathMethodWithDelegate:(id)delegate {
//獲取delegate對象的類
Class delegateClass = [delegate class];
NSLog(@"獲取當(dāng)前對象的類型名字為---%@",NSStringFromClass([delegate class]));
//方法名
SEL sourceSelector = @selector(tableView:didSelectRowAtIndexPath:);
//當(dāng)delegate對象中沒有實(shí)現(xiàn)方法tableView:didSelectRowAtIndexPath:,直接返回
if (![delegate respondsToSelector:sourceSelector]) {
NSLog(@"沒有實(shí)現(xiàn)tableView:didSelectRowAtIndexPath方法");
return;
}
SEL destinationSelector = NSSelectorFromString(@"CountData_tableView:didSelectRowAtIndexPath:");
//當(dāng)delegate對象已經(jīng)存在了CountData_tableView:didSelectRowAtIndexPath:潭袱,說明已經(jīng)交換锋恬,可以直接返回
if ([delegate respondsToSelector:destinationSelector]) {
return;
}
Method sourceMethod = class_getInstanceMethod(delegateClass, sourceSelector);
const char *encoding = method_getTypeEncoding(sourceMethod);
//當(dāng)類中已經(jīng)存在相同的方法時(shí),則會(huì)添加方法失敗彤悔。當(dāng)時(shí)前面已經(jīng)判斷過方法是否存在晕窑。因此卵佛,此處一定會(huì)添加成功
if (!class_addMethod([delegate class], destinationSelector,(IMP)CountData_tableViewDidSelectRow, encoding)) {
return;
}
//方法添加之后,進(jìn)行方法交換
[delegateClass sensorsdata_swizzleMethod:sourceSelector withMethod:destinationSelector];
}
@end
方案二:動(dòng)態(tài)子類
初始思路
在運(yùn)行時(shí)截汪,給實(shí)現(xiàn)了UITableViewDelegate
協(xié)議的- tableView:didSelectRow-AtIndexPath:
方法的類創(chuàng)建一個(gè)子類,讓該子類的對象變成我們自己創(chuàng)建的子類的對象阳柔。同時(shí)舌剂,在創(chuàng)建的子類中動(dòng)態(tài)添加- tableView:didSelectRowAtIndexPath:
方法暑椰。那么,當(dāng)用戶點(diǎn)擊UITableViewCell
控件時(shí)避消,就會(huì)先運(yùn)行自己創(chuàng)建的子類中的- tableView:didSelectRow-AtIndexPath:
方法角虫。我們在實(shí)現(xiàn)該方法的時(shí)候戳鹅,先調(diào)用delegate
原來的方法實(shí)現(xiàn),再觸發(fā)$AppClick
事件妇穴,即可實(shí)現(xiàn) UITableView
控件$AppClick
事件全埋點(diǎn)。
創(chuàng)建一個(gè)動(dòng)態(tài)添加子類的工具類: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:");
//當(dāng)Delegate中沒有實(shí)現(xiàn)tbaleView:didSelectRowAtIndexPath:方法時(shí)跑筝,直接返回
if (![delegate respondsToSelector:originalSelector]) {
NSLog(@"沒有實(shí)現(xiàn)tbaleView:didSelectRowAtIndexPath:方法");
return;
}
//動(dòng)態(tài)創(chuàng)建一個(gè)新類
Class originalClass = object_getClass(delegate);
NSString *originalClassName = NSStringFromClass(originalClass);
//判斷這個(gè)delegate對象是否已經(jīng)動(dòng)態(tài)創(chuàng)建的類時(shí)曲梗,無須重復(fù)設(shè)置妓忍,直接返回
if([originalClassName hasPrefix:kSensorsDelegatePrefix]) {
return;
}
NSString *subClassName = [kSensorsDelegatePrefix stringByAppendingString:originalClassName];
Class subclass = NSClassFromString(subClassName);
if (!subclass) {
//注冊一個(gè)新的子類,其父類為originalclass
subclass = objc_allocateClassPair(originalClass, subClassName.UTF8String, 0);
//獲取TableViewDynamicDelegate中的tableView:didSelectRowAtIndexPath指針
Method method = class_getInstanceMethod(self, originalSelector);
//獲取方法實(shí)現(xiàn)
IMP methodIMP = method_getImplementation(method);
//獲取方法類型的編碼
const char *types = method_getTypeEncoding(method);
//在subClass中添加 tableView:didSelectRowAtIndexPath: 方法
if(!class_addMethod(subclass, originalSelector,methodIMP , types)) {
NSLog(@"方法已經(jīng)存在");
}
/*刪除動(dòng)態(tài)生成的前綴定罢,動(dòng)態(tài)添加方法(sensorsdata_class)*/
// 獲取 TableViewDynamicDelegate 中的 sensorsdata_class 方法指針
Method classMethod = class_getInstanceMethod(self, @selector(sensorsdata_class));
// 獲取方法實(shí)現(xiàn)
IMP classIMP = method_getImplementation(classMethod);
//獲取方法的類型編碼
const char *classTypes = method_getTypeEncoding(classMethod);
//在subclass中添加class方法
if (!class_addMethod(subclass, @selector(class), classIMP, classTypes)) {
NSLog(@"添加方法失敗");
}
//子類和原始類的大小必須一致祖凫,不能有更多的ivars或者屬性
//如果不同會(huì)導(dǎo)致設(shè)置新的子類時(shí)惠况,會(huì)重新設(shè)置內(nèi)存粱年,導(dǎo)致重寫了對象的isa指針
if (class_getInstanceSize(originalClass) != class_getInstanceSize(subclass)) {
return;
}
//將delegate對象設(shè)置為新的子類對象
objc_registerClassPair(subclass);
}
if (object_setClass(delegate, subclass)) {
NSLog(@"創(chuàng)建成功");
}
}
//刪除自動(dòng)創(chuàng)建類名的私有方法
- (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]);
//第二步:調(diào)用開發(fā)者自己實(shí)現(xiàn)的方法
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);
}
//第三步:埋點(diǎn)
[[SensorsAnalyticsSDK sharedInstance]AppClickWithTableView:tableView didSelectRowAtIndexPath:indexPath properties:@{@"$app_click":@"動(dòng)態(tài)創(chuàng)建類事件"}];
}
最后調(diào)用:修改UITableView+CountData.m
文件中的-CountData_setDelegate:
方法拉队,添加調(diào)用TableViewDynamicDelegate
類的+proxyWithTableViewDelegate
方法`
+ (void)load {
[UITableView sensorsdata_swizzleMethod:@selector(setDelegate:) withMethod:@selector(CountData_setDelegate:)];
}
- (void)CountData_setDelegate:(id<UITableViewDelegate>)delegate {
// 方案2 動(dòng)態(tài)子類
[self CountData_setDelegate:delegate];
//設(shè)置delegate的動(dòng)態(tài)子類
[TableViewDynamicDelegate proxyWithTableViewDelegate:delegate];
}
方案三:消息轉(zhuǎn)發(fā)
在iOS應(yīng)用開發(fā)中粱快,自定義類一般需要繼承自NSObject
類或者NSObject
子類叔扼。但是,NSProxy類不是繼承自NSObject
類或者NSObject
子類鳍咱,而是一 個(gè)實(shí)現(xiàn)了NSObject
協(xié)議的抽象基類谤辜。
當(dāng)然,在大部分情況下丑念,使用NSObject
類也可以實(shí)現(xiàn)消息轉(zhuǎn)發(fā),實(shí)現(xiàn) 方式與NSProxy
類相同渔彰。但是推正,大部分情況下使用NSProxy
類更為合適。
理由如下
- NSProxy類實(shí)現(xiàn)了包括NSObject協(xié)議在內(nèi)基類所需的基礎(chǔ)方法乳丰。
- 通過NSObject類實(shí)現(xiàn)的代理類不會(huì)自動(dòng)轉(zhuǎn)發(fā)NSObject協(xié)議中的方 法内贮。
- 通過NSObject類實(shí)現(xiàn)的代理類不會(huì)自動(dòng)轉(zhuǎn)發(fā)NSObject類別中的方 法,例如上面調(diào)用實(shí)例中的-valueForKey:方法什燕,如果是使用NSObject類實(shí) 現(xiàn)的代理類竞端,會(huì)拋出異常。
步驟如下:
步驟一:創(chuàng)建CountDataDelegateProxy類 (繼承自NSProxy類)技俐,實(shí)現(xiàn)UITableViewDelegate協(xié)議雕擂。然后添加一個(gè)類 方法+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
步驟二:為了可以同時(shí)支持UICollectionView控件贵扰,我們直接在UIScrollView中擴(kuò)展countData_delegareProxy屬性。
創(chuàng)建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:
方法恰画,添加調(diào)用TableViewDynamicDelegate
類的+proxyWithTableViewDelegate
方法`
- (void)CountData_setDelegate:(id<UITableViewDelegate>)delegate {
/*方案3 NSProxy 消息轉(zhuǎn)發(fā)*/
self.countData_delegareProxy = nil;
if (delegate) {
CountDataDelegateProxy *proxy = [CountDataDelegateProxy proxywithTableViewDelegate:delegate];
self.countData_delegareProxy = proxy;
[self CountData_setDelegate:proxy];
}else {
[self CountData_setDelegate:nil];
}
}
總結(jié)
對于UITableView
控件$AppClick
事件全埋點(diǎn)的三種方案,它們各有優(yōu)缺點(diǎn)跨晴,讀者可以根據(jù)實(shí) 際情況選擇相應(yīng)的方案片林。
方案一:方法交換
優(yōu)點(diǎn):簡單、易理解焕妙;Method Swizzling屬于 成熟技術(shù)弓摘,性能相對來說較高。
缺點(diǎn):對原始類有入侵末患,容易造成沖突锤窑。
方案二:動(dòng)態(tài)子類
優(yōu)點(diǎn):沒有對原始類入侵,不會(huì)修改原始類 的方法渊啰,不會(huì)和第三方庫沖突,是一種比較穩(wěn)定的方案隧膏。
缺點(diǎn):動(dòng)態(tài)創(chuàng)建子類對性能和內(nèi)存有比較大 的消耗嚷那。
方案三:消息轉(zhuǎn)發(fā)
優(yōu)點(diǎn):充分利用消息轉(zhuǎn)發(fā)機(jī)制车酣,對消息進(jìn)行 攔截索绪,性能較好。
缺點(diǎn):容易與一些同樣使用消息轉(zhuǎn)發(fā)進(jìn)行攔 截的第三方庫沖突
擴(kuò)展
獲取控件內(nèi)容
為了能獲取更復(fù)雜的UIView的顯示內(nèi)容娘摔,該方法需要修改成支持通過 遞歸遍歷獲取子控件的顯示內(nèi)容唤反。
定義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 {
// 如果是隱藏控件逆趋,不獲取控件內(nèi)容
if (self.isHidden || self.alpha == 0) { return nil; }
// 初始化數(shù)組闻书,用于保存子控件的內(nèi)容
NSMutableArray *contents = [NSMutableArray array];
for (UIView *view in self.subviews) {
// 獲取子控件的內(nèi)容
// 如果子類有內(nèi)容脑慧,例如UILabel的text,獲取到的就是text屬性
// 如果子類沒有內(nèi)容坑律,就遞歸調(diào)用該方法囊骤,獲取其子控件的內(nèi)容
NSString *content = view.elementContent;
if (content.length > 0) {
// 當(dāng)該子控件有內(nèi)容時(shí),保存在數(shù)組中
[contents addObject:content];
}
}
// 當(dāng)未獲取到子控件內(nèi)容時(shí)藕各,返回nil焦除。如果獲取到多個(gè)子控件內(nèi)容時(shí),使用"-"拼接
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];
// 設(shè)置事件名稱
event[@"event"] = @"TableView的點(diǎn)擊事件";
// 設(shè)置事件發(fā)生的時(shí)間戳浙踢,單位為毫秒
event[@"time"] = [NSNumber numberWithLong:NSDate.date.timeIntervalSince1970 * 1000];
NSMutableDictionary *eventProperties = [NSMutableDictionary dictionary];
// 添加預(yù)置屬性
[eventProperties addEntriesFromDictionary:self.automaticProperties];
// 添加自定義屬性
[eventProperties addEntriesFromDictionary:properties];
//判斷是否位被動(dòng)啟動(dòng)狀態(tài)
if(self.isLaunchedPassively) {
//添加應(yīng)用程序狀態(tài)屬性
eventProperties[@"$app_state"] = @"background";
}
if (tableView) {
// TODO:獲取用戶點(diǎn)擊的UITableViewCell控件對象
UITableViewCell *cell = [tableView cellForRowAtIndexPath:index];
// TODO:設(shè)置被用戶點(diǎn)擊的UITableViewCell控件上的內(nèi)容($element_content)
eventProperties[@"$element_content"] = cell.elementContent;
}
// 設(shè)置事件屬性
event[@"properties"] = eventProperties;
[self printEvent:event];
}
最后支持UICollectionView控件和UITableView的實(shí)現(xiàn)原理相似洛波,同樣可以使用以上三種方案去實(shí)現(xiàn)。