Android 无埋点统计简单实现

Android 无埋点统计简单实现

一、问题的引入

  在开发过程中,都避免不了事件的统计,比如对某个界面启动次数的统计,或者对某个按钮点击次数的统计,一般大的公司会有自己的统计 SDK ,而其他的公司则会选择友盟统计等第三方平台。但是他们都需要在代码里面的每一个事件产生的地方插入统计的代码,如某个事件触发的 onClick 事件,那么就在view.setOnClickListener() 的on Click 方法里面 写入 MobclickAgent.onEvent(MyApp.getInstance(), "login_click")。这时候会有人想有没有一种办法不用这么麻烦呢。
通过上面的分析,有过 AOP 的同学就会想到,这就是典型的 AOP 的应用场景啊。是的,使用 AOP 可以很好的解决这一问题。

二、解决方案的形成

   在友盟统计当中,每一个事件都会对应着一个 id,而这个可以由开发人员自己定义,对应的 id 会有一个米描述,不然产品会看不懂,这样就形成了一个表格,将这个表格上传到友盟的后台。当需要统计哪个事件的时候,只需将对象的 id 上报即可,而后台就会记录对应的 id 的事件统计。而通过观察大部分的 事件统计都是 onClick 事件。
接下来就是需要解决的几个问题:

  • 问题一:如何在对应的事件上动态注入代码
  • 问题二:如何动态的生成一个事件的唯一 id
  • 问题三:如何将 id 和事件的描述对应上

解决方案:

  • 答案一:在 AOP 里面有 Javassist 库,可以很便利动态修改 .class 文件。在 java 文件编译成 class 文件之后,可以找到所有实现 android.view.View.onClickListener 的类,包括匿名类,然后在它的 onClick(View v) 注入统计的代码。

  • 答案二:可能会有人认为直接就可以使用 View.getId() ,但是这个 ID是自己人为设置的,而且同一个 在不同的 layout.xml 可以设置相同的 ID,所以不能作为事件的唯一 ID。那么这个 ID 就必须我们自己生成了。思路是:在事件发生之前,将当前 Activity 的 layout 的整个 ViewTree 进行遍历,将所有 View 和 ViewGroup 的 Tag 设置为我们组合的唯一 ID, 这个 ID 是由我们 ID 发生器 和 当前 View 的 ViewParent 的 ID 组合而成,然后点 onClick 事件产生时,我们可以得到当前 View 的唯一 ID 了。

  • 答案三:获得唯一 ID 之后,通过代码很难知道这个 View 的具体描述是什么,所以必须手动的去配置,这里采用了很简单的办法,那就是在界面上一一点击我们想要统计的点击事件,然后将出对应 View 的 ID 写入到一个文件,然后在这个文件的对应 ID 上写上对应 View 的描述。其实还可以更加直观一点,那就是点击的时候直接弹出一个对话框,然后在这个对话框中输入对应的描述。

三、具体方案的实现(基于 Android 6.0 系统)
3.1 Hook LayoutInflater

Hook LayoutInflater 就是通过反射式的使得当调用 context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) 返回的是 我们自定义的 CustomLayoutInflater,这个类是继承自 LayoutInflater 的,所以我们覆写 inflate 方法就可以得到对应 ViewTree 了。这时候就可以为所欲为了。

通过阅读源码(android-25)知道,getSystemService 方法最终会到 android.app.SystemServiceRegistry 这个类的静态变量 SYSTEM_SERVICE_FETCHERS

private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
            new HashMap<String, ServiceFetcher<?>>();

    public static Object getSystemService(ContextImpl ctx, String name) {
        ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
        return fetcher != null ? fetcher.getService(ctx) : null;
    }

中获取,而这个变量是在 registerService 方法赋值的

    private static <T> void registerService(String serviceName, Class<T> serviceClass,
            ServiceFetcher<T> serviceFetcher) {
        SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
        SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
    }

赋值的地方是在该类的 static 代码里面

static {
  ......
        registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
                new CachedServiceFetcher<LayoutInflater>() {
            @Override
            public LayoutInflater createService(ContextImpl ctx) {
                return new PhoneLayoutInflater(ctx.getOuterContext());
            }});
    ......
}

知道这些我们就想着 通过反射调用 registerService,将我们自定义的 CustomLayoutInflater 注册进去,然后替换掉原本的 PhoneLayoutInflater,这个当系统获取 LayoutInflater 时候得到的是 CustomLayoutInflater。

下面开始实施我们的”犯罪行为“
由于 registerService(String serviceName, Class<T> serviceClass, ServiceFetcher<T> serviceFetcher) 需要 ServiceFetcher 的示例,而 ServiceFetcher 是一个接口,并且处于 该类的内部。所以我们只能通过反射拿到这个接口,并且创建一个类实现这个接口,然后实例化它。然而通过反射得到的 ServiceFetcher 的 Class 类型,如果调用接口的 Class.newInstance() 会直接 抛出异常,无法到达我们的目的。有个办法就是通过动态代理来生成一个 实现 ServiceFetcher 接口的类。

public class Hooker {
   
    private static final String TAG = "Hooker";
    public static void hookLayoutInflater() throws Exception {
        //获取 ServiceFetcher 实例 ServiceFetcherImpl
        Class<?> ServiceFetcher = Class.forName("android.app.SystemServiceRegistry$ServiceFetcher");
        Object ServiceFetcherImpl = Proxy.newProxyInstance(Hooker.class.getClassLoader(),
                new Class[]{ServiceFetcher}, new ServiceFetcherHandler());//Proxy.newProxyInstance 返回的对象会实现指定的接口

        //获取 SystemServiceR
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值