返回介绍

9.7 竞品技术五瞥:数据采集工具

发布于 2024-08-17 23:46:11 字数 10784 浏览 0 评论 0 收藏 0

9.7.1 页面跳转器

页面跳转器是页面打点的前提。

对于Android而言,有Intent来帮助我们进行页面跳转和传值。但是你会发现,想从A页面跳转到B页面,在A页面要声明B页面的实例,这是一个强引用,如下所示:

Intent intent = new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent);

对于iOS而言,就连Intent这样的机制都没有了。我们不但要在A页面声明B页面实例,还要通过为B设置属性的方式,进行页面间传值。如下所示:

- (void) jumpTo {
  APageViewController* b = [[APageViewController alloc] init];
  b.version = "5.7.1";
  [self.navigationController pushViewController: b animated: YES];
  [b release];
}

我们一直在强调解耦,但是在iOS和Android的页面传值上却不遵守这个原则。于是很多公司开始致力于解决这个问题。写一个Navigator类,通过使用反射技术可以接触页面间的耦合性,这样我们就可以把所有的页面都定义在一个XML配置文件中,每个节点包括该页面的key、对应的类名称、打开方式。

我们先解决iOS的页面传参。使用一个字典作为页面间参数传递的载体,为此,在ViewController的基类中定义一个字典参数,这样在Navigator反射的时候,将传递进来的参数设置给页面实例即可,下面,分别是Navigator的h和m文件:

#import <Foundation/Foundation.h>
@interface Navigator : NSObject {
}
+ (Navigator *)sharedInstance;
+ (void)navigateTo:(NSString *)viewController;
+ (void)navigateTo:(NSString *)viewController
  withData:(NSDictionary *)param;
@end
#import "Navigator.h"
#import "BaseViewController.h"
#import "SynthesizeSingleton.h"
@implementation Navigator
SYNTHESIZE_SINGLETON_FOR_CLASS(Navigator);
+ (void)navigateTo:(NSString *)viewController {
  [self navigateTo:viewController withData:nil];
}
+ (void)navigateTo:(NSString *)viewController
    withData:(NSDictionary *)param {
  BaseViewController * classObject = (BaseViewController *)
    [[NSClassFromString(viewController) alloc] init];
  classObject.param = param;
  [classObject.navigationController
    pushViewController:classObject animated:YES];
  [classObject release];
}

为了解决页面间传参的问题,我们需要在BaseViewController中增加一个params属性,这是一个字典,在跳转前把要传递的属性塞进去,在跳转后把字典中的值再取出来:

@interface BaseViewController : UIViewController {
  NSDictionary* _param;
}
@property (nonatomic, retain) NSDictionary* param;

那么在使用时就非常简单了,如下所示:

- (void) jumpTo {
  NSMutableDictionary* dict = [NSMutableDictionary dictionary];
  [dict setObject: @"5.7.1" forKey:@"version"];
  [Navigator navigateTo: @"BViewController" withData: dict];
}

而在目标页BViewController要接收这个参数:

if(self.param!=nil){
  version = [self.param objectForKey: @"version"];
}

接下来要解决的是Android的页面耦合。不必新建一个Navigator类,我们完全可以利用Activity基类,增加一个navigatorTo方法,利用反射把要跳转的页面实例化出来,如下所示:

public abstract class AppBaseActivity extends BaseActivity {
  public void navigatorTo(final String activityName, final Intent intent) {
    Class<?> clazz = null;
    try {
      clazz = Class.forName(activityName);
      if (clazz != null) {
        intent.setClass(this, clazz);
        this.startActivity(intent);
      }
    } catch (ClassNotFoundException ignore) {
      return;
    }
  }
}

相应的,我们要创建ActivityNameConstants这个类,用来存放每个Activity的用于反射的全名称,如下所示:

public class ActivityNameConstants {
  public final static String SecondActivity 
    = "com.example.navigator.SecondActivity";
}

在Activity使用navigatorTo方法的时候就非常简单了,如下所示:

Intent intent = new Intent();
intent.putExtra("name", "Jianqiang");
navigateTo(ActivityNameConstants.SecondActivity, intent);

相应的,还应该有一个startActivityForResult方法,实现原理差不多,我这里就不赘述了。

9.7.2 打点统计

1.打点统计的两大痛点

如何寻找一种好的打点统计方法,是整个App业界都在做的一件事情。我这里只是抛砖引玉,把我这三年来的实战经验和切身感受分享给大家。

确保App打点数据的准确和无遗漏,是实现“数据驱动产品”的第一步,非常重要。纵观各大公司的打点办法,都非常原始,往往是哪个页面或哪个事件需要打点,就在相应的方法体中写一行打点的语句。

这种原始的打点方式直接导致以下问题:

·不全,经常漏打。

·不准,经常打错。

一旦发生了上述问题,要等下次发版后,数据才会恢复正常。基于此,我们需要解决2个痛点:

1)如何在发版前就能检查出漏打的和打错的点。

2)如果在发版后发现漏打的和打错的点,快速修复快速上线,而不必等新版本发布。

打点分为两种,页面打点,事件打点。接下来我们逐个讨论。

2.页面打点

相比较而言,页面打点比较容易实现。我们可以统一在页面跳转时,进行页面打点统计。还记得前面章节介绍的跳转器吗?我们只要在这个地方加上页面打点语句即可。

iOS的实现是在Navigator的navigateTo方法中,我们在9.7.1节介绍过这个类,如下所示:

+ (void)navigateTo:(NSString *)viewController
    withData:(NSDictionary *)param {
  // 在这里执行页面打点的操作



  BaseViewController * classObject = (BaseViewController *)
        [[NSClassFromString(viewController) alloc] init];
  classObject.param = param;
  [classObject.navigationController
        pushViewController:classObject animated:YES];
  [classObject release];
}

Android的实现则是在BaseActivity基类的navigateTo方法中,我们在9.7.1节中介绍过这个方法,如下所示:

public void navigateTo(final String activityName, 
    final Intent intent) {
  // 在这个位置执行


PV打点的操作



  Class<?> clazz = null;
  try {
    clazz = Class.forName(activityName);
    if (clazz != null) {
      intent.setClass(this, clazz);
      this.startActivity(intent);
    }
  } catch (final ClassNotFoundException e) {
    return;
  }
}

只要把页面打点语句写在上面代码片段的注释位置就好了。在这个位置,我们可以搜集到页面名称(viewController或activityName参数),也可以解析param字典或Intent参数,从中找出一些重要的参数记录下来,比如说movieId。

采取上述机制,能有效防止页面打点遗漏的问题。

此外,为了防止打点错误,应该动态传递当天ViewController或Activity的名称,而不是手动去拼写这个字符串,这就增加了出错的可能性。

相比较而言,页面打点的解决方案比较简单,我们甚至可以使用这种机制,计算出页面停留时间。接下来要介绍的事件打点的优化方案,可就不那么简单了。

3.事件打点

事件打点是比较棘手的。一般而言,我们为事件打点都是在事件方法中,增加一行事件打点的代码。这样的代码多了,就很难维护,经常发生打错点或者有遗漏的情况,有时则是这个迭代有某个事件的打点数据,但是下个迭代却不小心删除了。

我们系望App开发人员在写代码的时候,不需要考虑打点的事情,不需要额外准备打点所需要的信息,比如说哪个页面哪个控件以及相关的数据。为此,我们写一个基类,把打点逻辑封装在这个基类中。任何继承自这个基类的控件,就能自动打点,而不用把打点逻辑写在业务代码中。

这里我们先看按钮,因为绝大多数打点,都是基于按钮的点击。

对于iOS,为一个按钮添加点击事件是通过addTarget方法,如下所示:

UIButton* getInfoButton;
[getInfoButton addTarget: self
          action: @selector(getInfo)
    forControlEvents:UIControlEventTouchUpInside];

那么我们要写一个继承自UIButton的新控件,比如就叫UVButton。我发现,所有的UI控件都继承自UIControl这个基类,它有一个sendAction方法,这个方法会在点击事件发生后第一个执行,之后才执行addTarget上绑定的方法。于是就可以在UVButton中重写这个sendAction方法,如下所示:

@interface UVButton : UIButton
@end
#import "UVButton.h"
@implementation UVButton
- (void)sendAction:(SEL)action to:(id)target
    forEvent:(UIEvent *)event
{
  // 在这里写一个方法,执行打点操作



  [super sendAction:action to:target forEvent:event];
}

打点操作的方法我这里就不提供了,反正就是搜集一些信息,存在某个地方,等待发送出去。

那么在程序中,所有的按钮我们都将使用UVButton,你会发现,之前的逻辑是什么,完全不需要修改。只要把按钮声明为UVButton即可。

对于Android,其实也可以这么做,创建一个新的按钮,重写它的click方法。但是我们发现Android为控件绑定响应方法的语法是通过setOnClickListener,如下所示:

btnLogin.setOnClickListener(
  new View.OnClickListener() {
    @Override
    public void onClick(View v) {
      gotoLoginActivity();
    }
  });

我们已经习惯在程序中使用OnClickListener,复写它的onClick方法,来实现按钮点击后的业务逻辑。我们为什么不能从OnClickListener中派生出一个子类呢?比如叫OnUVClickListener,复写它的onClick方法,实现事件打点的逻辑。

那么接下来在程序中就使用OnUVClickListener来代替OnClickListener了,除此之外,代码逻辑和之前一样。

上面的讨论虽然只是按钮,但也可以适用于Image控件。而对于列表控件、Tab之类的复合控件,则需要特殊情况特殊处理。

4.事件打点的验证

如果可能,我们希望采集每个页面和每个事件的点。但并不总是这样,所以我们需要有一个配置文件,每次页面跳转或点击控件的时候,都检查这个动作是否需要采集打点。

按照上述这种解决方案,我们需要写一个Python小程序,每次发版前验证一下这个配置文件,确保打点数据是全的,而且没有错误。

做得再极致一些,这个配置文件可以设计成从服务器动态下载。这样发现错了或者漏了,就可以在服务器提供一份新的配置文件供用户下载。

对于大多数App而言,是没有这个配置文件的。代码已经写成这样的,再改一遍不划算,那么要使用Python做静态代码检查就不能依赖于配置文件这个统一的出口了。那么我们有必要统计代码中所需要打点的地方,所在的类和方法,具体的代码行位置,然后每次执行Python就检查这些地方。

静态代码检查只能确保打点的代码都存在,但并不能确保在运行期间相应的打点代码被执行到了。

为此,需要引入App自动化测试。

首先在App端编写一组能够完整覆盖打点的自动化测试用例,在即将发版前,执行一遍这组测试用例。

然后,在服务器端,也需要编写一个自动化脚本,每当App端打点的自动化测试用例执行完,我们就执行服务器端的这个自动化脚本,检查是否所有点都打上了,以及打点是否正确。

5.如何在发版后即时修复线上打点的错误

目前我所想到的解决方案有:

1)iOS使用Lua,临时把漏打或者打错的点修好。

2)Android使用插件化编程,更新有问题的插件。

3)还记得我们上面说到的那个记录打点的配置文件吗?把这个配置文件做成服务器下载的,如果漏打或者打错,那么就更新这个配置文件。

6.处理App中的HTML5页面打点

App中有很多HTML5页面,它们也需要打点统计数据。

一种做法则是回调App的打点机制,让App把打点数据传到服务器。这时候经常发生的情况是,App有bug,某个版本上线后突然就不能回传数据了。所以HTML5和Native之间的协议是非常重要的,每次发版前都要逐一测试。

另一种做法是HTML5页面自己编写打点语句,然后上传到服务器,这样即使出错了,也能够立刻修复立刻上线。

9.7.3 ABTest

很多产品经理做一个功能是根据主观臆断出来的,拿不出切实的数据来证明方案的可行性,只能根据上线后的订单转化率,来猜测该方案是否有效。

这样做就像是赌博,这个问题也是最近一两年才暴露出来,于是很多App开始在所有页面打点,采集用户行为,把这些数据放到Hadoop中做大数据分析,最后基于数据来决定哪种方案是可行的。于是我们采用ABTest这种强大工具,用于判断:

·做一个新功能,做之前和做之后哪个更有效果。

·做一个新功能,方案A和方案B哪个更有效果。

1.什么是ABTest

我们可以将ABTest的定义归纳为以下几点:

·场景:对于某一个页面,UI样式的修改。

·结果:得到旧版和新版(或者A方案和B方案)的订单转化率,比较后决定使用哪种UI。

·策略:产品经理和运营人员在新版本上线后比较一周,最终确定使用哪一种。这个决定必须在一个迭代内迅速作出,否则App接下来的版本就要维护两套页面的代码逻辑。

·规则:ABTest不一定是A和B各占50%,也有可能是A占20%而B占80%,也有可能是ABC三种策略各占一定的比例。

ABTest的设计难点在于如何确保数据准确。

对于同一个设备,在第一次获取到A策略后,今后每次重启App访问那个页面都将一直是A策略了,除非我们关闭了该页面的ABTest并决定从此以后使用B策略的页面,该设备才有机会看到另一种页面。这样就避免了ABTest期间,同一个用户每次看这个页面都随即有不同的UI样式,这样我们就不能判断这位用户下单是受A策略还是B策略的影响。

2.为App量身打造ABTest

根据上述策略,我们对App和MobileAPI改造如下:

·App对于要做ABTest的页面,如果是新页面,那么要做两套UI,A方案和B方案;如果是改造原有页面,那么不是在原有页面上进行修改,而是copy一份这个页面的副本,然后在副本上进行修改。总之,无论是哪种情况,都要确保有两套UI。

·App每次启动时就调用MobileAPI的一个接口A,获取哪些页面要进行ABTest,以及要采用哪种策略,把这些数据保存到本地文件中,注意,这里是覆写,也就是说之前保存的数据都不要了。

那么每次跳转到一个页面,都要判断一下,该页面是否要做ABTest以及相应的策略,就从本地文件中读取到这些数据。还记得我在9.7.1节介绍的页面跳转器吗?我们可以把判断逻辑写在这个统一的页面跳转器中。

·每次启动App时才会调用MobileAPI的接口A获取ABTest策略,但是大多数用户是不会关闭App的,只是简单地将其切换到后台,为了确保ABTest的策略及时更新,在App每次从后台切换到前台时,都要调用一次MobileAPI的接口A。

3.如何确保ABTest公平

接下来介绍MobileAPI中ABTest的策略分配算法。MobileAPI应该有一个自增的整数count,每次请求都会加1。如果是AB各占50%的话,那么策略就是count除以2,根据余数来分配A和B两种策略。如果ABC各三分之一,则对count除以3取余数来分配ABC三种策略。这里的count取余数的算法直接决定了ABTest策略的公平性。

策略分配后,每次有新的设备号来请求ABTest策略,就要把设备和分配到的策略保存下来,此外还要保存要做ABTest的App版本号、页面名称、Android还是iPhone。把这些数据保存在数据库中是不划算的,频繁的IO操作会导致性能问题,我们可以将每笔数据写成日志保存在服务器,然后每隔几个小时就发送到Hadoop上,进行大数据分析。

4.如何衡量ABTest的结果

对ABTest的结果进行衡量,自然还是要使用大数据分析。

比如,对某个页面做ABTest,AB两种策略各占50%。我们观察了一周后得到的数据是这样的:

1)订单的总转化率是40%,分子40,分母100。100是点击搜索的次数,而40是下单的次数。

2)分子40由10个A和30个B组成,分母100由30个A和70个B组成,那么A的转化率就是10/30=33%,B的转化率就是30/70=42%,于是我们愉快的决定采用策略B。

也许会有人问,为什么A的分母是30,而B的分母是70,取样儿怎么差距这么大。这是因为我们在App启动时就为用户分配了ABTest策略,但是用户不一定会进入搜索页和要做ABTest的那个页面,这样无形中就白白分配了很多策略。

比较精准的办法是,在需要做ABTest的页面,才会分配策略,但是这样做就要求每进入一个页面都要请求MobileAPI获取策略,这无疑会对App性能产生影响。

另一种折中的解决方案是,把这些只进入过搜索页但是没进入到ABTest页面的数据,从分母中剔除。这就需要9.7.2节中采集的PV打点数据来协助了。

5.为产品经理和运营人员提供ABTest的配置后台和报表

我们要为运营人员或产品经理设计一个配置ABTest策略的工具,可以灵活配置在哪个页面、哪个版本做ABTest,包括有几种UI样式(枚举),每个样式的百分比是多少,等等。

对于每个品类,一次只做一个页面的ABTest。比如说火车票,如果有两个页面同时做ABTest,将难以判断转化率的提升,是受哪一个页面修改后的影响。

我们可以每次测一个页面,得到结论后,再去测另一个页面。如果每次发版的间隔是2周的话,那就每个策略测试一周。

此外,还需要有报表,能在采集到数据后,看到ABTest的结果,以便于运营人员或产品经理迅速做决策。

对于Android和iOS,应该可以分开看报表,也可以合在一起看数据。

6.如何快速采用ABTest得到的策略

一旦通过ABTest收集的数据分析出最终使用B策略了,那么如何能快速的通知App该页面将不再进行ABTest并永远进入B页面呢?在下个版本删除A页面然后永远进入B页面,这件事情是肯定要做的。但这样就太晚了,我们要等待很久才能看到新版本的上线,所以我们要在当前线上的版本就立刻把页面切到B。因此,我们要在刚才提到的那个MobileAPI接口A中,永远返回策略B。这样就能解决及时更新策略的问题了。

7.实施ABTest中遇到的一些问题和解决方案

我在设计ABTest的实现方案时,被质疑最多的是,每做一次ABTest,都要设计两套UI,App开发人员的工时倍增。其实呢,这是一个磨刀不误砍柴工的概念。如果我们猜着在本轮迭代中开发A方案,两周后发现效果不好,然后在下个迭代再开发B方案——开发的人力没有省,但是开发的周期拉长了,除非你中途离职,不然活儿永远也躲不掉。

另一种做ABTest的方法是使用Lua脚本。MobileAPI返回不同的Lua脚本,动态绘制不同的UI样式。这样就不用在App中准备两套UI了。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文