用户行为统计整体设计探讨 — 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 页面的业务埋点思路同上,但是需要再做额外的处理;

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

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