埋点技术方案-实施简要

目前市场埋点方案分以下三种,稍后一一说明拙见。
(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)

埋点上传方案:后续
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容