用户行为统计整体设计探讨 — iOS技术实现篇

在保证移动端流量不会受较大影响的前提下,PM 们总是希望埋点覆盖面越广越好与此同时,作为开发者的我们更希望以一种可复用、解耦、动态可配、易于维护的可执行方案。所以,本文旨在探讨一种可复、解耦、动态可配的、容易维护的用户行为统计 (User Behavior Statistics, UBS) 方案。我将尽量从整体设计的视野,以 iOS 技术实现为例,与大家一起探讨 UBS (俗称:埋点)。本文整体分为三部分:

常规做法的优缺点

目前常规的做法是将埋点代码封装成工具类,但凡工程中需要埋点(如点击事件、页面跳转)的地方都插入埋点代码:

  • 优点:哪里需要哪里注入代码,简单明了;不会出现莫名其妙的崩溃问题。 ps.相较于使用 Runtime 的实现而言
  • 缺点:随着项目越来越复杂,埋点的代码散落在程序的各个角落,不利于维护以及复用;统计操作完全依赖于移动端的版本升级,无法动态配置;工作量大

可复用、解耦方案初步落成

我们的项目中是怎么处理的呢?首先谈一下解耦,避免埋点代码散落在程序的各个角落与业务代码糅杂在一起。同时,不得不谈到 AOP 名词这里不做过多解释了,自己Google 吧。在 iOS 中实现 AOP 编程的技术就是基于 Objective-C Runtime 特性的 Method Swizzling。上干货:

+ (void)swizzlingInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector
{
    Class class = cls;
    
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    BOOL didAddMethod =
    class_addMethod(class,
                    originalSelector,
                    method_getImplementation(swizzledMethod),
                    method_getTypeEncoding(swizzledMethod));
    
    if (didAddMethod) {
        class_replaceMethod(class,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

接下来就是 hook 的方法:

  • 对于页面事件的收集,主要通过 hook 系统类 UIViewController 的生命周期方法来实现,比如:viewDidAppear
  • 所有的 UIControl 类型的控件、UITabBarButton 以及在导航栏上自定义添加的 UIBarButtonItem 的点击事件,都可以通过 hook 系统类UIApplication 的 sendAction:to:from:forEvent: 方法进行拦截。但是,这个方法并不能拦截到导航栏上系统自动添加的返回按钮的点击,因此又 hook 了 UINavigationController 的 navigationBar:shouldPopItem: 方法来实现对它的点击的拦截

这时候问题来了,项目中每个页面都会有自己的页面事件编号(pad),此处的埋点代码如何知道要发送什么 pad 给服务端呢?轻松祭出 KVC与Dictionary ,创建一个配置文件 GSUserStatisticsConfig.plist 代码示例:

{
    "GSLoginViewController": {
        "pad": {
            "enter":"at1"
        },
        "ped": {
            "onxxxBtnPressed":"xxx"
        }
    }
}

通过上述处理,基本上实现了埋点代码与业务代码的解耦,作为一个统计模块复用性也非常显著。接下来谈一下

探索动态可配与易维护

在探索动态可配与已维护的数据采集方面,业界有一种方案称之为无埋点也叫全埋点,即不需要用户主动埋点,可以收集用户所有的操作行为。
接下来贴一张图来的更为直观一点:
userStastics
从上图可以看出,在实现无埋点数据收集时,主要分为3步:上传统计配置文件、请求统计配置文件、业务数据的收集与上报。
配置文件的设计与自身业务息息相关不再详述,请求配置文件也不是要讨论的核心,核心在于业务数据根据配置文件的动态收集,业界开发者称之为无埋点去获取配置文件中想要的业务数据。
参考案例:

学习该方案与我们自身产品业务埋点的梳理:

优点非常突出:

  • 维护成本,主要管理配置文件即可
  • 弥补埋点时存在错埋、漏埋等情况,动态更新及添加
  • 埋点代码无需跟随APP版本一起发布,不耽误数据的收集与统计
  • 对于一些动态事件做到很好的支持,例如:同一位置显示不同的内容,同一内容显示在不同的位置
  • 可以统计同一个按钮事件,在产品上可能代表不同的状态,例如在播放状态下关闭按钮和回放状态下关闭按钮,按钮的事件是一样的,但是需求去判断是播放状态还是回放状态

缺点:

  • 我们的埋点注重的是用户的浏览行为是一条路径(亦可称之为一条线),对单点操作行为统计要求不强,有点“杀鸡焉用牛刀”的感觉;
  • 无埋点方案需要大量hook系统方法,该行为导致 crash 的几率升高;
  • 如果收集数据量比较庞大,遍历事件然后拿到配置文件匹配事件的能耗会显现出来导致 APP 卡顿;
  • 开发周期会相对长一些:需要前端要搭建配置文件的平台;需要 BI 配合下发和处理 采集的数据;需要 iOS 和 Android 两端攻克无埋点采集数据的技术难点。

补充:

weex 页面的业务埋点思路同上,但是需要再做额外的处理;

最后,探讨毕竟是探讨,欢迎留言讨论。

移动端埋点方式探讨 – iOS端

目标:灵活的埋点方式,移动端不需要大量修改代码,甚至不用重新发包即可按照PM要求发送需要的统计数据。

要求:高度解耦,复用性强,动态配置,易于维护。

知道了要努力的目标和要求,我们先来看一下现在老版教师端和重构版教师端的优缺点。

一.老版教师端采用的是一个工具类发送数据,需要的个性化数据由每个界面自行传值进入。

优点:

1.灵活性较高,能满足个性需求:按照现在的发送的数据映射表,个别界面需要发送一些特殊信息,此种实现方式可以较好的满足这种情况的出现。

举个栗子:播放教案时,需要发送教案本身的id以及教案所在讲次的id,这两个数据其余界面并不需要发送。

2.有一定的解耦:这里解耦的就是发送数据这块,做成了工具,每个界面调用工具的API然后传值进行发送,不需要每个界面都写一遍工具类中的实现代码。

缺点:

1.对界面侵入性强,后期维护困难:由于工具类只负责发送数据,每个界面都会写一遍调用API方法,这样就造成重复代码冗余,解决方案是所有的界面都继承一个公用基类,每个界面自己去实现基类的方法即可。

但是由于老版的界面没有一个基类来管理,所以导致现在如果添加基类需要修改大量代码,需要大量的测试工作以保证APP本身的质量和埋点的完善。

2.灵活性不强:这里所说的不灵活是指当统计需要的数据映射改变后,尤其是一些个性公用字段的修改(比如:界面id),就会导致APP涉及埋点界面全部修改一遍,非常不灵活。同时由于iOS本身的审核机制,导致了上架周期相对安卓变长,会造成数据的丢失和上架的不及时。

二.新版教师端的埋点,采用的是统一配置表,由基类抓取当前界面,然后根据配置表获取到界面,再根据界面来发送数据。

优点:

1.高度解耦:每个界面自身的id有一个配置表,通过运行时的一个抓取类来抓取当前界面,然后在配置表来获取界面id再发送数据。

2.侵入性低,维护简单:由于有基类的存在,每个界面都实现了基类的方法,就可以被钩子抓取到界面,统计的所有代码没有和业务代码在一起,后期维护时只需要修改配置表即可。

缺点:

1.灵活性不够,个性数据获取困难:钩子抓取到的界面,通过配置表获取需要数据发送,但是这些都是写死的,个别界面自身的特殊数据无法获取然后进行发送,可以看上面的例子,这种实现方式就无法获取,如果获取就需要侵入相应界面。

 

新老的埋点都是在原生写死的映射,无法实现灵活的修改更新。而且后期定下来的技术方案最好在新版本中进行实现,因为老版本的代码的问题可能不容易或无法满足一些条件。

 

下面是最近看的文章中关于统计方案的思路和实现方法。大部分的文章采用的都是和新版教师端一样的方法,利用OC的RunTime黑科技Method Swizzling来Hook界面注入代码实现。

先来这些文章的链接

http://blog.csdn.net/w10207010218/article/details/56274431

http://www.jianshu.com/p/0497afdad36d

http://blog.csdn.net/kaka_2928/article/details/61201320

http://www.cocoachina.com/ios/20170427/19108.html 主要查看了这篇文章,里面还有个链接可以查看基础篇

重点讲一下最后链接文章的内容,文章是网易乐得的技术人员写的,不过他们的SDK没有对外公开(好像安卓公开了),他们声称:SDK 已经具备不需要代码埋点就能 自动的、动态可配的、全面且正确 的收集用户在使用 App 时的所有事件数据。除此之外,还单独开发了与之配合的圈选SDK,能够在 App 端完成对界面元素的圈配以及 KVC 配置的上传。而界面元素圈配的工作完全可以交给用研与产品人员来做,减轻了开发人员的工作量。

完成了两大部分:基本事件数据的收集和业务层数据的收集

文章中的SDK的实现思路和大部分主流一样是利用了Runtime 特性的 Method Swizzling 黑魔法,但是SDK也可以实现对界面中的子控件的数据收集,通过viewPath 及 viewId ,内容很多建议通过链接查看

Snip20170602_5

要实现灵活的进行数据收集和发送,就需要使用KVC实现。

什么是KVC配置。其实 KVC配置 就是一些用来描述 App 应该在什么时机去收集什么数据的信息

  • 上传KVC配置
    • 利用 圈选SDK 上传 KVC配置 的操作对于用户是透明的,主要由开发人员进行上传与管理。此操作可以在任何时候进行,在想要收集某个或某些版本的 App 中的业务数据时,上传相应的KVC配置信息至后台即可,达到了根据需要动态可配的效果。
  • 请求KVC配置
    • SDK 在初始化时会触发 KVC配置 的请求操作,从后台拉取 App 当前版本对应的所有KVC配置,并将请求结果缓存起来,以提供给下一步使用。

上传的所有的 KVC配置 需要与 App 的版本相对应,因为 App 版本不同会直接导致keyPath可能不一样。所以与 KVC配置 相关的工作有如下2个:

  1. 针对当前 App 版本上传相应的 KVC配置,以获取想要的业务数据
  2. 当 App 新版本发布时,需要对之前版本上的 KVC配置 逐一验证,是否仍然适用于新版本。如果仍然适用,则直接在管理后台上把新的版本号添加到此 KVC配置;如果不再适用,则对新版本再上传一个新的KVC配置。

业务数据的收集与上报的流程

8db8721df6e82075t

重构版的埋点采用的是现在的主流方案,现在需要攻克的难点就是灵活性的问题,怎么样从服务器去获取配置表,然后本地映射获取需要的数据,然后发送给统计平台。

你该知道的OC 方法–GET/SET

你该知道的OC 方法–GET/SET

TOC

  1. 1. 写在前面的话
  2. 2. OC 为什么要有 Set 方法和 Get 方法
  3. 3. Set 方法和 Get 方法
    1. 3.1. set 方法
    2. 3.2. get 方法
    3. 3.3. 成员变量注意的几点规范
    4. 3.4. set 方法和 get 方法实战
    5. 3.5. set 方法和 get 方法的好处
  4. 4. set 方法和 get 方法的调用
  5. 5. 被雪藏的 set 和 get
  6. 6. 内存管理中的 set 方法
    1. 6.1. 基本数据类型的set方法
    2. 6.2. OC 对象类型的 set 方法
  7. 7. 写在最后的话

写在前面的话

大学时期也写博客,比如用WordPress 搭个站啦,用HEXO+GITHUB 搭个博客站啦。。。但是内容比较水也没什么浏览量,全当是做做笔记,咱们公司现以“作业”的形式要求写博客,自己有些抵触情绪,我感觉完全是个人的一种分享没有必要跟学生交作业一样(ps. 仅代表个人意见)。所以,建议把写博客培养成一种乐趣,这样看来前期的强制要求也不失为一种方式方法:)那么,回归技术本文将针对 OC 中 Set 方法 Get 方法写一写自己的几点总结(ps. 本文贴出的代码均需手动管理内存 MRC),希望那些还没搞懂 Set 方法和 Get 方法而且又对OC 有兴趣的比如“鱼哥”,看到这篇文章可以有所斩获。

OC 为什么要有 Set 方法和 Get 方法

其实不只是 OC 这门语言,所有的面向对象的语言都有和这两个方法功能相同的方法,只不过各个语言的语法不同罢了。而且,要回答这个问题我感觉要先从面向对象开发的特性之一的封装开始谈起。那什么是封装?什么是面向对象呢?我靠,这个问题的确是一环扣一环,环环相扣的啊!我就不班门弄斧了。。。不耍流氓,直接贴代码来说一下吧:

#import <Foundation/Foundation.h>

@interface Student : NSObject
{
    // public 意味着所有人都可以用,不安全
    @public
    int _age;
}
@end
@implementation Student
@end

int main()
{
    Student *stu = [[Student alloc] init];

    //暴漏的成员变量,可以随意被修改——这样不好
    stu->_age = 20;        

    [stu release];

    return 0;
}

这是一段非常简陋的代码,勿喷。上述代码安全性不好,可以直接修改我创建出来的学生对象的年龄。你可能会说那直接把 public 换成 private 得了这样外面的人就无法随意改动了,很好!自己都没法访问了!!!那么我们该如何访问成员变量呢?如何对他进行赋值?方法!!! 对象的方法可以直接操作对象的成员变量,而不至于把自己的成员变量暴漏于外部。所以我们想办法创建一个方法,让人们通过方法把值传递给成员变量就安全多了。于是,就有了 set 方法和 get 方法来管理成员的访问。

Set 方法和 Get 方法

set 方法

Set 方法给外界提供一个方法设置成员变量的值,同时可以在方法里面对外面传进来的值进行过滤,以避免传递进来一些乱七八糟的东西。

  1. 它有固定的命名方法和命名规范:
    1. 方法名以 set 开头
    2. set 后面跟上成员变量的名称,成员变量的首字母必须大写
  2. 它返回值一定为void,一定要接受一个参数,而且参数类型跟成员变量类型一致
  3. 形参名称不能与成员变量名相同
  4. set 方法可以在别人传递进来值以后做一些事情,可以监听成员变量值的改变

get 方法

  1. 它有固定的命名方法和命名规范:
    1. 肯定有返回值,返回值类型肯定与成员变量类型一致
    2. 方法名跟成员变量名一样,但是不用写下划线了
    3. 不需要接受任何参数
  2. 它返回对象内部的成员变量

成员变量注意的几点规范

  1. 成员变量都以下划线 _ 开头
  2. 可以跟get方法的名称区分开
  3. 可以跟其他局部变量区分开,一看到下划线开头的变量,肯定是成员变量

set 方法和 get 方法实战

那么我们用 set 方法和 get 方法对上述代码进行改进:

#import <Foundation/Foundation.h>

@interface Student : NSObject
{
    int _age;
}
// set 方法
- (void)setAge:(int)age;
// get 方法
- (int)age;
@end
@implementation Student
// set 方法
- (void)setAge:(int)age
{
    // 过滤
    if (age<=0)
    {
        age = 1;
    }
    _age = age;
}
// get 方法
- (int)age
{
    return _age;
}
@end

int main()
{
    Student *stu = [[Student alloc] init];

    [stu setAge:20];

    int age = [stu age];

    NSLog(@"age is %d", age);

    [stu release];

    return 0;
}

set 方法和 get 方法的好处

  • 过滤不合理的值
  • 屏蔽内部的赋值过程
  • 让外界不必关注内部的细节

set 方法和 get 方法的调用

从上面的代码中我们调用 set 方法和 get 方法的方式如下:

[stu setAge:20];
[stu age];

OC 还提供了如下等价的方式:

stu.age = 10;
int age = stu.age;

相信接触过其他编程语言的人会了解点语法,即使没有接触其他编程语言,就在 C 的结构体那一部分,我们访问结构体成员也是有点语法的。不过,这里要注意的是:OC 中点语法这样的方式给成员变量赋值和取值并非是直接访问 stu 对象的成员变量 _age ,这里是方法的调用,而且规范的成员变量名的书写也帮助我们区分这一点:成员变量名都是以下划线开头的。

被雪藏的 set 和 get

在实际的开发中,为了提高开发人员的工作效率,Xcode4.5 及以上版本下的编译器提供了更为搞笑的关键字 @property。只要在类的声明中写好 @property 就可以自动帮我们生成:成员变量的定义和声明、set 和 get 方法的声明和实现。

#import <Foundation/Foundation.h>

@interface Student : NSObject
@property int age;
@end

int main()
{
    Student *stu = [[Student alloc] init];

    [stu setAge:20];

    int age = [stu age];

    NSLog(@"age is %d", age);

    [stu release];

    return 0;
}

有没有感觉一下子代码精简了不少啊!这样我们就可以花更少的时间见在这些简单的代码上。

内存管理中的 set 方法

这里我还要讲一下 set 和 get 方法在内存管理中的活跃表现,至于内存管理这里暂时不深入讲解。来看一下内存管理中 set 方法的活跃身影吧!

基本数据类型的set方法

基本数据类型的 set 方法:

- (void)setAge:(int)age
{
    _age = age;
} 

OC 对象类型的 set 方法

下代码如下:

- (void)setCar:(Car *)car
{
    // 解决重复赋值问题,检测是不是新传进来的对象
    if (car != _car)
    {
        // 旧对象做一次release
        [_car release];

        对新对象做一次retain,同时把它赋值给_car,
        // 这样_car就是最新的车

        _car = [car retain];
    }       
}

写在最后的话

set 方法和 get 方法是 OC 中的两个基本方法,虽然在开发中我们已经基本上不用敲这些东西了,但是总得知道它到底是怎么回事吧!文章中有涉猎一丁点儿内存管理的内容只是为了想把这两个方法说清楚,如果你看不懂那就先了解一下内存管理的内容。希望我的博文可以给看到文章最后的您带来些许的帮助,谢谢!