目前市场埋点方案分以下三种,稍后一一说明拙见。
(1),代码埋点;
(2),视觉化埋点,本文略。
(3),全埋点;以前有个噱头的名字是无埋点技术,应该叫代码无侵入技术埋点。目前,在效率,快捷性,通用性上,更多的是使用这个方案。
OC类
一、代码埋点
优点:灵活,毫无约束,脑力成本低;
缺点:笨重,无组织,修改、回撤需逐点勘查,重复工作量大。
实施方式:
*ViewController以及子类控制器
通常在这里采集一个页面出现到消失的时间,当然需要配合用户与当前页面进行交互的控件才能更能体现数据的收集反馈效果。
VC的埋点代码如下:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
//coding here. 上传当前页面的信息例如“页面title”“进入时间”
CFAbsoluteTime currentTime = CFAbsoluteTimeGetCurrent()
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
}
对于一个父控制器会包含多个子控制器的可能,同理而已。
*UIView控件埋点
1,UIGestureRecognizer手势类控件(即继承于UIView当在UIControl之上),更多的是对于UITapGestureRecognizer和UILongPressGestureRecognizer的行为监测。在手势绑定的方法中,进行数据的收集上传。
1.1对于UITableView或UICollectionView类,可能需要统计某个cell的具体信息展示的时间,有两种方式可以实现:
(a),使用以下对称代码,经过项目中【我的】模块实测,发现没有显示的cell同样也调用了willDisplayCell和didEndDisplayingCell,因为是成对出现的,所以我们可以计算每一个cell出现的时间,如果过小直接忽略不计,就可以统计每个cell信息的展示时长。
UITableView解决方式:
// Display customization
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section API_AVAILABLE(ios(6.0));
- (void)tableView:(UITableView *)tableView willDisplayFooterView:(UIView *)view forSection:(NSInteger)section API_AVAILABLE(ios(6.0));
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath API_AVAILABLE(ios(6.0));
- (void)tableView:(UITableView *)tableView didEndDisplayingHeaderView:(UIView *)view forSection:(NSInteger)section API_AVAILABLE(ios(6.0));
- (void)tableView:(UITableView *)tableView didEndDisplayingFooterView:(UIView *)view forSection:(NSInteger)section API_AVAILABLE(ios(6.0));
UICollectionView解决方式:
- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath API_AVAILABLE(ios(8.0));
- (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementaryView:(UICollectionReusableView *)view forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath API_AVAILABLE(ios(8.0));
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath;
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupplementaryView:(UICollectionReusableView *)view forElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath;
(b)也有通过重写以下父类方法,测试之后发现,并不能成对出现。所以不能放弃。如有实现,还望不吝赐教。
- (void)willMoveToSuperview:(nullable UIView *)newSuperview;
- (void)didMoveToSuperview;
2,UIControl以及子类控件,直接在target绑定的方法中,进行数据的收集上传。
二、视觉化埋点
在此不赘述,直接进入全埋点实例。
三、全埋点
*Swizzling 技术:方法交换
1,对于ViewController,经常使用在category分类中,自定义一个custom_viewWillAppear方法与系统的viewWillAppear方法进行IMP指针交换。
具体代码实现:
+(void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL customSel = @selector(custom_viewWillAppear:);
SEL sel = @selector(viewWillAppear:);
Method customeMed = class_getInstanceMethod([self class], customSel);
Method med = class_getInstanceMethod([self class], sel);
BOOL isDidAdd = class_addMethod([self class], sel, method_getImplementation(customeMed), method_getTypeEncoding(customeMed));
if (isDidAdd) {
class_replaceMethod([self class], customSel, method_getImplementation(med), method_getTypeEncoding(med));
} else {
method_exchangeImplementations(med, customeMed);
}
});
}
- (void)custom_viewWillAppear:(BOOL)animated {
[self custom_viewWillAppear:animated];
//coding here
}
2, 对于UIControl以及子类控件实现的方法交换
@implementation UIControl (Swizzling)
+(void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL customSel = @selector(custom_sendAction:to:forEvent:);
SEL sel = @selector(sendAction:to:forEvent:);
Method cutomMed = class_getInstanceMethod([self class], customSel);
Method med = class_getInstanceMethod([self class], sel);
BOOL isDidAdd = class_addMethod([self class], sel, method_getImplementation(cutomMed), method_getTypeEncoding(cutomMed));
if (isDidAdd) {
class_replaceMethod([self class], customSel, method_getImplementation(med), method_getTypeEncoding(med));
} else {
method_exchangeImplementations(cutomMed, med);
}
});
}
- (void)custom_sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event{
[self custom_sendAction:action to:target forEvent:event];
//coding here...
}
@end
3,UIGestureRecognizer手势交互类控件。对于此类控件要想拦截手势的动作,分类遵守<UIGestureRecognizerDelegate>,.m方式实现如下:
+(void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL swizzlingSel = @selector(override_initWithTarget:andAction:);
SEL sysSel = @selector(initWithTarget:action:);
Method swizzlingMed = class_getInstanceMethod([self class], swizzlingSel);
Method sysMed = class_getInstanceMethod([self class], sysSel);
BOOL isAdd = class_addMethod([self class], sysSel, method_getImplementation(swizzlingMed), method_getTypeEncoding(swizzlingMed));
if (isAdd) {
class_replaceMethod([self class], swizzlingSel, method_getImplementation(sysMed), method_getTypeEncoding(sysMed));
} else {
method_exchangeImplementations(sysMed, swizzlingMed);
}
});
}
- (instancetype)override_initWithTarget:(id)obj andAction:(SEL)action{
self.delegate = self;
return [self override_initWithTarget:obj andAction:action];
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
UIView *sender = gestureRecognizer.view;
if (<#tag#> == sender.tag) {
printf("----------"); //测试ok。外部view设置好tag
}
// <#如果需要过滤的控件操作很多,此处可以单独写一个管理类,可以根据设置tag过滤判断也可以根据设置好值的accessibilityLabel#>
return true;
}
3.1 UITableView和UICollectionView的埋点监听和cell-item数据显示时长统计;
+(void)load {
SEL cus_sel = @selector(custom_setDelegate:);
SEL sel = @selector(setDelegate:);
[self exchangeSystemSel:sel withCustomSel:cus_sel from:[self class]];
}
- (void)custom_setDelegate:(id<UITableViewDelegate>)obj {
[self custom_setDelegate:obj];
//<#点击cell的方法#>
SEL cus_sel = @selector(custom_tableView:didSelectRowIndexPath:);
SEL sel = @selector(tableView:didSelectRowAtIndexPath:);
Method med = class_getInstanceMethod([obj class], sel);
if (med) {
[UITableView exchangeSystemSel:sel withCustomSel:cus_sel from:[obj class]];
} else {
/**
如果 UITableView 的 delegate 未实现 tableView:didSelectRowAtIndexPath
那么 UITableViewCell 发生点击之后,不会再 respondsToSelector tableView:didSelectRowAtIndexPath
直接给未实现 method 的 SEL 添加实现的方式不再适用,单独处理:
这里直接选取了 @selector(_userSelectRowAtPendingSelectionIndexPath:) 进行交换
*/
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
SEL privateSel = @selector(_userSelectRowAtPendingSelectionIndexPath:);
#pragma clang diagnostic pop
[UITableView exchangeSystemSel:privateSel withCustomSel:cus_sel from:[self class]];
}
//<#cell将要显示时#>
SEL willCell_cusSel = @selector(custom_tableView:willDisplayCell:forRowAtIndexPath:);
SEL willCell_sysSel = @selector(tableView:willDisplayCell:forRowAtIndexPath:);
[UITableView exchangeSystemSel:willCell_sysSel withCustomSel:willCell_cusSel from:[obj class]];
// <#cell将要消失#>
SEL endCell_cusSel = @selector(custom_tableView:didEndDisplayingCell:forRowAtIndexPath:);
SEL endCell_sysSel = @selector(tableView:willDisplayCell:forRowAtIndexPath:);
[UITableView exchangeSystemSel:endCell_sysSel withCustomSel:endCell_cusSel from:[obj class]];
}
+ (void)exchangeSystemSel:(SEL)sel withCustomSel:(SEL)cus_sel from:(id)obj {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method cus_Med = class_getInstanceMethod([obj class], cus_sel);
Method med = class_getInstanceMethod([obj class], sel);
BOOL isAdd = class_addMethod([obj class], sel, method_getImplementation(cus_Med), method_getTypeEncoding(cus_Med));
if (isAdd) {
class_replaceMethod([obj class], cus_sel, method_getImplementation(med), method_getTypeEncoding(med));
} else {
method_exchangeImplementations(med, cus_Med);
}
});
}
- (void)custom_tableView:(UITableView *)tableView didSelectRowIndexPath:(NSIndexPath *)indexPath {
//coding here
[self custom_tableView:tableView didSelectRowIndexPath:indexPath];
}
// 如果代理未实现
- (void)noDelegateObj_userSelectedRowAtPendingSelectionIndexPath:(NSIndexPath *)indexpath {
//coding here
[self noDelegateObj_userSelectedRowAtPendingSelectionIndexPath:indexpath];
}
//用于统计cell对应的item信息展示的时长统计
//cell 将要展示
- (void)custom_tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
//coding here
[self custom_tableView:tableView willDisplayCell:cell forRowAtIndexPath:indexPath];
}
//cell将要消失
- (void)custom_tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath {
//coding here
[self custom_tableView:tableView didEndDisplayingCell:cell forRowAtIndexPath:indexPath];
}
Swift(小测试,有误,请斧正)
在埋点时,我们会需要对一个控件进行标识,例如elementid以便数据的精准分析。为了避免逐个给控件的繁重,有一种例如Mirror反射的方式,可以将每一个控件的控件变量名设置elementid。但是有一个缺点,就是当前页面所有的控件要提前定义为全局变量。否则,无法捕获。实例如下:
protocol ElementIdDelegate {
var elementid: String? { get}
}
extension UIView: ElementIdDelegate {
var elementid: String? {
get {
self.accessibilityValue
}
}
}
class Sky: UIView {
let s0: UILabel = UILabel()
let s1: UIView = UIView()
let s2: UIButton = UIButton(type: .custom)
var s4: UIButton?
override init(frame: CGRect) {
s4 = UIButton(type: .custom)
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class Ocean {
let c0: UILabel = UILabel()
let c1: UIView = UIView()
let c2: UIButton = UIButton(type: .custom)
var c4: UIButton?
}
func printMirror(with obj: Any) {
let childs = Mirror(reflecting: obj)
for ch in childs.children {
print("\(ch.label)----\( ch.value)")
if let view = ch.value as? UIView {
view.accessibilityValue = ch.label
}
}
}
let sky = Sky()
printMirror(with: sky)
print(sky.s0.accessibilityValue)
print("\n")
let oc = Ocean()
printMirror(with: oc)
print(oc.c0.accessibilityValue)