您的位置:首页 > 移动开发

App竞品技术分析 (5)数据采集工具

2015-10-22 21:45 447 查看
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.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];
}

@end


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

@interface BaseViewController : UIViewController {

NSDictionary* _param;

}

@property (nonatomic, retain) NSDictionary* param;


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

- (void) jumpTo {
    NSMutableDictionary* dict = [NSMutableDictionary dictionary];
    [dict setObject: @"5.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方法,实现原理差不多,我这里就不敷述了。

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

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

  这种原始的打点方式,直接导致:

  1)不全,经常漏打。

  2)不准,经常打错。

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

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

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

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

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

  iOS的实现是在Navigator的navigateTo方法中,我们在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方法中,我们在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的名称,而不是手动去拼写这个字符串,这就增加了出错的可能性。

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

2.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];
}

@end


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

  那么在程序中,所有的按钮我们都将使用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之类的复合控件,则需要特殊情况特殊处理。

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

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

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

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

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

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

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

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

2.5 如何在发版后即时修复线上打点的错误
  目前我所想到的解决方案有:

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

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

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

2.6 处理App中的Html5页面打点
  App中有很多Html5页面,它们也需要打点统计数据。

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

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

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

  这样做就像是***,是对公司的不负责,让公司为产品经理无根据的尝试去买单。这个问题也是最近一两年才暴露出来,于是很多App开始在所有页面打点,采集用户行为,把这些数据放到Hadoop中做大数据分析,最后基于数据说话来决定哪种方案是可行的。于是我们发明了ABTest这种强大的工具,用于判断:

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

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

  ABTest是“数据驱动产品”的战略中最犀利的武器。

3.1 什么是ABTest
  我们可以将ABTest的定义归纳为四点:

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

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

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

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

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

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

3.2 为App量身打造ABTest
  根据ABTest的定义,我们对App和MobileAPI改造如下:

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

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

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

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

3.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上,进行大数据分析。

3.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页面的数据,从分母中剔除。这就需要2中采集的PV打点数据来协助了。

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

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

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

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

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

3.6 如何快速采用ABTest得到的策略
  最后介绍善后工作。

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

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

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

  本文介绍了多套UI的ABTest方案。但其实还有一种仅限于数据层面的ABTest方案,比如说,点击搜索按钮,50%的用户看到A方案的数据列表,而50%的用户看到B方案的数据列表,然后分别统计这两种方案下的订单转化率,最终采用转化率高的那种方案。由于不需要App的介入,所以稍微容易一些。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: