9.7 竞品技术五瞥:数据采集工具
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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论