Android全埋点(基于神策数据埋点方案)

1,945 阅读7分钟

问题背景

最近在研发中项目,剥离出了很多组件,考虑到这些组件不单单用于当前项目,以后可能会应用到其他项目,组件内部是没有对点击事件做埋点的,而且有的组件还是发布到maven远程仓库的,那这里就会有一个问题,本地依赖组件和远程依赖的组件上要怎么埋点呢?并且埋点的上报数据的时候这些数据有一部分是需要壳子端提供的,比如用户id,账号等?针对这种情形我想到一种方案,就是将埋点独立出来,封装成一个库,由这个库去做埋点的工作,但是这个库的实现方案有很多,但最终都是在点击事件里面添加埋点上报的代码;在点击事件里面添加的埋点代码的方式有:

1.在项目编译的时候修改项目源文件编译成的.class文件,在class文件里面的点击事件里面注入埋点代码

2.通过代理点击事件的方式,根据自己的规则重写点击事件的监听,注入埋点代码

这两种方式我们可以把它分为静态和动态代理,总的来说都是,在编译或者运行的时候,注入埋点代码,达到埋点上报效果

静动态的区别在于注入时机不同:静态是在编译期就把埋点代码注入,动态是在运行时把埋点代码注入,从性能的角度来说静态要好些

注入埋点代码的方式有很多:Asm、事件代理、 javassist、AST、AspectJ等注入代码的方式;

全埋点:

也叫无埋点、无码埋点、无痕埋点、自动埋点。全埋点是指无须Android应用程序开发工程师写代码或者只写少量的代码,就能预先自动收集用户的所有行为数据,然后就可以根据实际的业务分析需求从中筛选出所需行为数据并进行分析

正文内容

  • Android的打包流程:简单说明下一个Apk安装包的构建过程(静态代理的时候会用到,需要重新修改其class文件)

  • 埋点方案:静态代理和动态代理对比

  • 其他技术:JavaPot、Gradle TransForms、JavaAssist、ASM等相关技术点-----这里只会简单提及,相关技术细节自行查阅

打包流程:

blog.csdn.net/c10WTiybQ1Y…

埋点方案对比:

image.png

埋点事件分类:

  • 应用启动事件($AppStart):

是指应用程序启动,同时包括冷启动和热启动场景。热启动也就是指应用程序从后台恢复的情况,实现方案:在应用启动,初始化的入口添加即可

  • 应用结束事件($AppEnd):

是指应用程序退出,包括应用程序的正常退出、按Home键进入后台、应用程序被强杀、应用程序崩溃等场景;实现方案:全局错误处理,前后台监听等

  • 页面预览事件($AppViewScreen):

是指应用程序页面浏览,对于Android应用程序来说,就是指切换Activity(Window)或Fragment

  • 控件的点击事件:

是指应用程序控件点击,也即View被点击,比如点击Button、列表中的Item、TextView等,方案较多,而且也是埋点的重点对象

应用启动和结束事件埋点

监控当前App处于前后台的时候进行上报:应用程序前后台

原理说明:

总体来说,我们首先注册一个Application.ActivityLifecycleCallbacks回调,用来监听应用程序内所有Activity的生命周期。然后我们再分两种情况分别进行处理。

在页面退出的时候(即onPause生命周期函数),我们会启动一个30s的倒计时,如果30s之内没有新的页面进来(或显示),则触发AppEnd事件;如果有新的页面进来(或显示),则存储一个标记位来标记已有新的页面进来。这里需要注意的是,由于Activity之间可能是跨进程的(即给Activity设置了android:process属性),所以标记位需要实现进程间的共享,即通过ContentProvider+SharedPreferences来进行存储。然后通过ContentObserver监听到新页面进来的标记位改变,从而可以取消上个页面退出时启动的倒计时。如果30s之内没有新的页面进来(比如用户按Home键/返回键退出应用程序、应用程序发生崩溃、应用程序被强杀),则会触发AppEnd事件,或者在下次启动的时候补发一个AppEnd事件。之所以要补发AppEnd事件,是因为对于一些特殊的情况(应用程序发生崩溃、应用程序被强杀),应用程序可能停止运行了,导致我们无法及时触发$AppEnd事件,只能在用户下次启动应用程序的时候进行补发。当然,如果用户再也不去启动应用程序或者将应用程序卸载,就会导致“丢失”AppEnd事件。

在页面启动的时候(即onStart生命周期函数),我们需要判断一下与上个页面的退出时间间隔是否超过了30s,如果没有超过30s,则直接触发AppViewScreen事件。如果已超过了30s,我们则需要判断之前是否已经触发了AppEnd事件(因为如果App崩溃了或者被强杀了,可能没有触发$AppEnd事件),如果没有,则先触发AppEnd事件,然后再触发AppStart和AppViewScreen事件。

核心代码示例:



 /**

 * 注册 Application.ActivityLifecycleCallbacks * * @param application Application

 */

@TargetApi(14)

public static void registerActivityLifecycleCallbacks(Application application) {

    mDatabaseHelper = new DatabaseHelper(application.getApplicationContext(), application.getPackageName());

    //4.当有新的页面打开的时候会取消此倒计时

    countDownTimer = new CountDownTimer(SESSION_INTERVAL_TIME, 10 * 1000) {

        @Override

        public void onTick(long l) {

        }



        @Override

        public void onFinish() {

            trackAppEnd(mCurrentActivity.get());

        }

    };

    application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycle - Callbacks() {

        @Override public void onActivityCreated (Activity activity, Bundle bundle){}

        @Override public void onActivityStarted (Activity activity){

            mDatabaseHelper.commitAppStart(true);

            double timeDiff = System.currentTimeMillis() - mDatabaseHelper.getAppPausedTime();

            //2.补发上一次没有提交的end事件

            if (timeDiff > 30 * 1000) {

                if (!mDatabaseHelper.getAppEndEventState()) {

                    trackAppEnd(activity);

                }

            }

            //1.正常进入,提交start事件

            if (mDatabaseHelper.getAppEndEventState()) {

                mDatabaseHelper.commitAppEndEventState(false);

                trackAppStart(activity);

            }

        }

        @Override public void onActivityResumed (Activity activity){

            trackAppViewScreen(activity);

        }

        @Override public void onActivityPaused (Activity activity){

        //3.页面结束,启动倒计时,当没有新页面进入的时候触发end事件上报

            mCurrentActivity = new WeakReference<>(activity);

            countDownTimer.start();

            mDatabaseHelper.commitAppPausedTime(System.currentTimeMillis());

        }

        @Override public void onActivityStopped (Activity activity){}

        @Override public void onActivitySaveInstanceState (Activity activity, Bundle bundle){}

        @Override public void onActivityDestroyed (Activity activity){}

    });

}

页面浏览埋点:

原理说明:

调用Application的registerActivityLifecycleCallback(ActivityLifecycleCallbacks callback)方法注册Application.ActivityLifecycleCallbacks回调。对当前应用程序中所有的Activity的生命周期事件进行集中处理(监控)了。在注册的Application.ActivityLifecycleCallbacks的onActivityResumed(Activity activity)回调方法中,我们可以拿到当前正在显示的Activity对象,然后调用SDK的相关接口触发页面浏览事件(AppViewScreen)即可。

核心代码示例:



@TargetApi(14)

public static void registerActivityLifecycleCallbacks(Application application) {

    application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {

        @Override

        public void onActivityCreated(Activity activity, Bundle bundle) {

        }



        @Override

        public void onActivityStarted(Activity activity) {

        }



        @Override

        public void onActivityResumed(Activity activity) {

            //埋点上报

            trackAppViewScreen(activity);

        }



        @Override

        public void onActivityPaused(Activity activity) {

        }



        @Override

        public void onActivityStopped(Activity activity) {

        }



        @Override

        public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {

        }



        @Override

        public void onActivityDestroyed(Activity activity) {

        }

    });

}

控件的点击事件 埋点

控件上报涉及的方式较多,总体分为静态代理和动态代理两类,这里分别举例两个典型的案例进行说明

1.View绑定listener的方式(动态代理):

原理说明:

android.R.id.content对应的视图是一个FrameLayout布局,它目前只有一个子元素,就是我们平时开发的时候,在onCreate方法中通过setContentView设置的View。换句说法就是,当我们在layout文件中设置一个布局文件时,实际上该布局会被一个FrameLayout容器所包含,这个FrameLayout容器的android:id属性值就是android.R.id.content

调用registerActivityLifecycleCallback方法来注册Application.ActivityLifecycleCallbacks回调。对当前应用程序中所有Activity的生命周期事件进行集中处理(监控)了。在onActivityResumed(Activity activity)回调方法中,我们可以拿到当前正在显示的Activity实例,通过activity.findViewById(android.R.id.content)方法就可以拿到整个内容区域对应的RootView(是一个FrameLayout)。然后再逐层遍历这个RootView,并判断当前View是否设置了mOnClickListener对象,如果已设置mOnClickListener对象并且mOnClickListener又不是我们自定义的WrapperOnClickListener类型,则通过WrapperOnClickListener代理当前View设置的mOnClickListener。WrapperOnClickListener是我们自定义的一个类,它实现了View.OnClickListener接口,在WrapperOnClickListener的onClick方法里会先调用View的原有mOnClickListener处理逻辑,然后再调用埋点代码,即可实现“插入”埋点代码,从而达到自动埋点的效果。

核心代码示例:


 /** * 注册 Application.ActivityLifecycleCallbacks * * @param application Application  */

@TargetApi(14)

fun registerActivityLifecycleCallbacks(application: Application) {

    application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks() {

        fun onActivityCreated(activity: Activity?, bundle: Bundle?) {}

        fun onActivityStarted(activity: Activity?) {}

        fun onActivityResumed(activity: Activity) {

            val rootView = activity.findViewById<ViewGroup>(R.id.content)

            //代理点击事件

            delegateViewsOnClickListener(activity, rootView)

        }



        fun onActivityPaused(activity: Activity?) {}

        fun onActivityStopped(activity: Activity?) {}

        fun onActivitySaveInstanceState(activity: Activity?, bundle: Bundle?) {}

        fun onActivityDestroyed(activity: Activity?) {}

    })

}







@TargetApi(15)

private open fun delegateViewsOnClickListener(context: Context?, view: View?) {

    if (context == null || view == null) {

        return

    } //获取当前 view 设置的mOnClickListener   

    val listener: View.OnClickListener = getOnClickListener(view)

    //判断已设置的mOnClickListener 类型,如果是自定义的 WrapperOnClickListener,说明已                  经被代理过,不要再去代理,防止重复代理   

    if (listener != null && listener !is WrapperOnClickListener) {        //替换成自定义的 WrapperOnClickListener       

        view.setOnClickListener(WrapperOnClickListener(listener))

    } //如果 view 是 ViewGroup,需要递归遍历子 View 并代理    

    if (view is ViewGroup) {

        val viewGroup = view

        val childCount = viewGroup!!.childCount

        if (childCount > 0) {

            for (i in 0 until childCount) {

                val childView: View = viewGroup!!.getChildAt(i) 

                //递归            

                delegateViewsOnClickListener(context, childView)

            }

        }

    }

}





 /** * 获取 View 当前设置的 OnClickListener对象 * * @param view View * @return View.OnClickListener  */

@TargetApi(15)

private fun getOnClickListener(view: View): View.OnClickListener? {

    val hasOnClick: Boolean = view.hasOnClickListeners()

    if (hasOnClick) {

        try {

            val viewClazz = Class.forName("android.view.View")

            val listenerInfoMethod: Method = viewClazz.getDeclaredMethod("getListenerInfo")

            if (!listenerInfoMethod.isAccessible()) {

                listenerInfoMethod.setAccessible(true)

            }

            val listenerInfoObj: Any = listenerInfoMethod.invoke(view)

            val listenerInfoClazz = Class.forName("android.view.View$ListenerInfo")

            val onClickListenerField: Field = listenerInfoClazz.getDeclaredField("mOnClickListener")

            if (!onClickListenerField.isAccessible()) {

                onClickListenerField.setAccessible(true)

            }

            return onClickListenerField.get(listenerInfoObj) as View.OnClickListener

        } catch (e: ClassNotFoundException) {

            e.printStackTrace()

        } catch (e: NoSuchMethodException) {

            e.printStackTrace()

        } catch (e: InvocationTargetException) {

            e.printStackTrace()

        } catch (e: IllegalAccessException) {

            e.printStackTrace()

        } catch (e: NoSuchFieldException) {

            e.printStackTrace()

        }

    }

    return null

}
问题一:无法收集DataBinding绑定的点击事件

原因:

这是由于DataBinding框架给Button设置mOnClickListener对象的动作稍微晚于onActivityResumed生命周期函数。即我们去代理Button已设置的mOnClickListener对象时,DataBinding框架还没有完成给Button设置mOnClickListener对象的操作,所以我们去遍历RootView时,当前View不满足hasOnClickListener的判断条件,因此没有去代理其mOnClickListener对象,从而导致无法采集其点击事件。

方案:

等主线程处理完成在handler中延时处理代理事件:

//增加一个延时处理的方式,设置代理事件

@Overridepublic

fun onActivityResumed(activity: Activity) {

    Handler().postDelayed(Runnable {

  delegateViewsOnClickListener(

            activity, activity.findViewById<View>(

                R.id.content

 )

        )

    } , 300)

}
问题二:无法采集MenuItem控件的点击事件

原因:

这是因为我们通过android.R.id.content获取到的RootView是不包含Activity标题栏的,也就是不包括MenuItem的父容器,一开始介绍android.R.id.content时也提到过。所以,当我们去遍历RootView时是无法遍历到MenuItem控件的,因此也无法去代理其mOnClickListener对象,从而导致无法采集MenuItem的点击事件

方案:

将采集对象content换成更高层级的decorView:



@Overridepublic

fun onActivityResumed(activity: Activity) {

    Handler().postDelayed(Runnable {

  delegateViewsOnClickListener(

            activity,

            activity.window.decorView

 )

    } , 300)

}
问题三:Resume之后动态创建的View的点击事件不能收集

原因:

这是因为我们是在Activity的onResume生命周期之前去遍历整个RootView并代理其mOnClickListener对象的。如果是在onResume生命周期之后动态创建的View,当时肯定是无法被遍历到的,后来我们又没有再次去遍历,所以它的mOnClickListener对象就没有被我们代理过。因此,点击控件时,是无法采集到其点击事件的。

方案:

当一个视图树的布局发生改变时,如果我们给当前的View设置了ViewTreeObserver.OnGlobalLayoutListener监听器,就可以被ViewTreeObserver.OnGlobalLayoutListener监听器监听到(实际上是触发了它的onGlobalLayout回调方法)。所以,基于这个原理,我们可以给当前Activity的RootView也添加一个ViewTreeObserver.OnGlobalLayoutListener监听器,当收到onGlobalLayout方法回调时(即视图树的布局发生变化,比如新的View被创建),我们重新去遍历一次RootView,然后找到那些没有被代理过mOnClickListener对象的View并进行代理,即可解决上面提到的问题



@Overridepublic

fun onActivityResumed(activity: Activity?) {

    val rootView: ViewGroup = getRootViewFromActivity(activity, true)

    rootView.viewTreeObserver.addOnGlobalLayoutListener {

  delegateViewsOnClickListener(

            activity,

            rootView

        )

    }

}

使用这个方案之后,问题一和问题二存在的问题也可以解决,使用的时候需要注意在生命周期结束的时候要移除该监听

缺点:
  1. 由于该方案遍历的是Activity的RootView,所以游离于Activity之上的View的点击是无法采集的,比如Dialog、PopupWindow等。
  2. 由于使用反射,效率比较低,对App的整体性能有一定的影响,也可能会引入兼容性方面的风险
  3. 要求Api14以上
拓展:

不同的View设置的监听事件是不一样的所以在代理事件的时候需要针对不同的View做处理,以下是代理点击事件的兼容处理(主要还是反射获取其事件点击):



 /**

 * Delegate view OnClickListener * * @param context Context * @param view    View

 */

@TargetApi(15)

@SuppressWarnings("all")

protected static void delegateViewsOnClickListener(final Context context, final View view) {

    if (context == null || view == null) {

        return;

    }

    if (view instanceof AdapterView) {

        if (view instanceof Spinner) {

            AdapterView.OnItemSelectedListener onItemSelectedListener = ((Spinner) view).getOnItemSelectedListener();

            if (onItemSelectedListener != null && !(onItemSelectedListener instanceof WrapperAdapterViewOnItemSelectedListener)) {

                ((Spinner) view).setOnItemSelectedListener(new WrapperAdapterViewOnItemSelectedListener(onItemSelectedListener));

            }

        } else if (view instanceof ExpandableListView) {

            try {

                Class viewClazz = Class.forName("android.widget.ExpandableListView");                //Child 

                Field mOnChildClickListenerFieldviewClazz .

                getDeclaredField("mOn ChildClickListener");

                if (!mOnChildClickListenerField.isAccessible()) {

                    mOnChildClickListenerField.setAccessible(true);

                }

                ExpandableListView.OnChildClickListener onChildClickListener =

                        (ExpandableListView.OnChildClickListener) mOnChildClick - ListenerField.get(view);

                if (onChildClickListener != null && !(onChildClickListener instanceof WrapperOnChild - ClickListener))

                {

                    ((ExpandableListView) view).setOnChildClickListener(new WrapperOnChildClickListener(onChildClickListener));

                }                //Group         

                Field mOnGroupClickListenerFieldviewClazz .

                getDeclaredField("mOn GroupClickListener");

                if (!mOnGroupClickListenerField.isAccessible()) {

                    mOnGroupClickListenerField.setAccessible(true);

                }

                ExpandableListView.OnGroupClickListener onGroupClickListener =

                        (ExpandableListView.OnGroupClickListener) mOnGroupClickListener

                Field.get(view);

                if (onGroupClickListener != null && !(onGroupClickListener instanceof WrapperOnGroupClick Listener)) {

                    ((ExpandableListView) view).setOnGroupClickListener(new WrapperOnGroupClickListener(onGroupClickListener));

                }

            } catch (Exception e) {

                e.printStackTrace();

            }

        } else if (view instanceof ListView || view instanceof GridView) {

            AdapterView.OnItemClickListener onItemClickListener = ((AdapterView) view).getOnItemClickListener();

            if (onItemClickListener != null && !(onItemClickListener instanceof WrapperAdapterView OnItemClick)) {

                ((AdapterView) view).setOnItemClickListener(new WrapperAdapterViewOnItemClick(onItemClickListener));

            }

        }

    } else {        //获取当前 view 设置的 OnClickListener      

        final View.OnClickListener listener =

                getOnClickListener(view);        //判断已设置的 OnClickListener 类型,如果是自定义的 WrapperOnClickListener,          说明已经被代理过,防止重复代理    

        if (listener != null && !(listener instanceof WrapperOnClickListener)) {            //替换成自定义的 WrapperOnClickListener           

            view.setOnClickListener(new WrapperOnClickListener(listener));

        } else if (view instanceof CompoundButton) {

            final CompoundButton.OnCheckedChangeListener onCheckedChangeListener =

                    getOnCheckedChangeListener(view);

            if (onCheckedChangeListener != null && !(onCheckedChangeListener instanceof WrapperOnCheckedChangeListener)) {

                ((CompoundButton) view).setOnCheckedChangeListener(new WrapperOnCheckedChangeListener(onCheckedChangeListener));

            }

        } else if (view instanceof RadioGroup) {

            final RadioGroup.OnCheckedChangeListener radioOnCheckedChangeListener =

                    getRadioGroupOnCheckedChangeListener(view);

            if (radioOnCheckedChangeListener != null && !(radioOnCheckedChangeListener instanceof WrapperRadioGroupOnCheckedChangeListener)) {

                ((RadioGroup) view).setOnCheckedChangeListener(new WrapperRadioGroupOnCheckedChangeListener(radioOnCheckedChangeListener));

            }

        } else if (view instanceof RatingBar) {

            final RatingBar.OnRatingBarChangeListener onRatingBarChangeListener =

                    ((RatingBar) view).getOnRatingBarChangeListener();

            if (onRatingBarChangeListener != null && !(onRatingBarChangeListener instanceof WrapperOnRatingBarChangeListener)) {

                ((RatingBar) view).setOnRatingBarChangeListener(new WrapperOnRatingBarChangeListener(onRatingBarChangeListener));

            }

        } else if (view instanceof SeekBar) {

            final SeekBar.OnSeekBarChangeListener onSeekBarChangeListener =

                    getOnSeekBarChangeListener(view);

            if (onSeekBarChangeListener != null && !(onSeekBarChangeListener instanceof WrapperOnSeekBarChangeListener)) {

                ((SeekBar) view).setOnSeekBarChangeListener(new WrapperOnSeekBarChangeListener(onSeekBarChangeListener));

            }

        }

    }

    //如果 view 是 ViewGroup,需要递归遍历子 View 并代理   

    if (view instanceof ViewGroup) {

        final ViewGroup viewGroup =

                (ViewGroup) view;

        int childCount = viewGroup.getChildCount();

        if (childCount > 0) {

            for (int i = 0; i < childCount; i++) {

                View childView = viewGroup.getChildAt(i);                //递归  

                delegateViewsOnClickListener(context, childView);

            }

        }

    }

}

2.ASM埋点方式(静态代理):

技术要点:Gradle Transform、ASM
Gradle Transform:

Gradle Transform是Android官方提供给开发者在项目构建阶段(即由.class到.dex转换期间)用来修改.class文件的一套标准API。目前比较经典的应用是字节码插桩、代码注入等。

ASM:

ASM是一个功能比较齐全的Java字节码操作与分析框架。通过使用ASM框架,我们可以动态生成类或者增强既有类的功能。ASM可以直接生成二进制.class文件,也可以在类被加载入Java虚拟机之前动态改变现有类的行为。Java的二进制被存储在严格格式定义的.class文件里,这些字节码文件拥有足够的元数据信息用来表示类中的所有元素,包括类名称、方法、属性以及Java字节码指令。ASM从字节码文件中读入这些信息后,能够改变类行为、分析类的信息,甚至能够根据具体的要求生成新的类。

原理说明:

我们可以自定义一个Gradle Plugin,然后注册一个Transform对象。在transform方法里,可以分别遍历目录和jar包,然后我们就可以遍历当前应用程序所有的.class文件。然后再利用ASM框架的相关API,去加载相应的.class文件、解析.class文件,就可以找到满足特定条件的.class文件和相关方法,最后去修改相应的方法以动态插入埋点字节码,从而达到自动埋点的效果

核心代码示例:

通过Gradle Transform找到需要修改的class文件

在具体修改相应的方法





class SensorsAnalyticsClassVisitor extends ClassVisitor implements Opcodes {

   /**省略若干代码*/

   

   //访问某个方法

    @Override

    MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {

        MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);

        String nameDesc = name + desc;

        methodVisitor = new SensorsAnalyticsDefaultMethodVisitor(methodVisitor, access, name, desc) {

        

            //方法退出的时候插入代码

            @Override

            protected void onMethodExit(int opcode) {

                super.onMethodExit(opcode) if ((mInterfaces != null && mInterface               s.length > 0)) {

                    if ((mInterfaces.contains('android/view/View$OnClickListener') && nameDesc == 'onClick(Landroid/view/View;)V') || desc == '(Landroid/view/View;)V') {

                        methodVisitor.visitVarInsn(ALOAD, 1);

                        methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/View;)V", false);

                    }

                }

            }



            @Override

            AnnotationVisitor visitAnnotation(String s, boolean b) {

                return super.visitAnnotation(s, b);

            }

        } ;

        return methodVisitor;

    }

}