特准四不像肖必中一肖中特图:Android全埋點講解 [復制鏈接]

2019-5-23 10:29
kengsirLi 閱讀:395 評論:0 贊:0
Tag:  

什么是全埋點?

致富之地一肖中特 www.qcfmo.icu 也叫做無埋點,預先收集用戶的所有行為數據,然后根據實際需求,從中提取行為數據。

采集數據的點:

  • $AppStart 冷啟動?熱啟動

  • $AppEnd 正常退出?進入后臺?崩潰?強殺等

  • $AppViewScreen 切換Activity

  • $AppClick (重點?難點)控件的點擊事件

本質原理

  • 自動攔截 =>Android對View的點擊處理

  • 自動插入 =>在編譯階段插入相應Java代碼

自動插入的流程如下

JavaCode --> .java --> .class --> .dex

具體方案

  • 動態代理

  • 代理View.OnClickListener

  • 代理Window.Callback

  • 代理View.AccessibilityDelegate

  • 靜態代理

  • AspectJ 切面編程(AOP)

  • ASM

  • Javassist

  • APT 注解處理器

Q:何為動態代理?

A:在代碼運行的時候去進行代理。比如我們常見的代理View.OnClickListener、Window.Callback、View.AccessibilityDelegate等

Q:何為靜態代理?

A:通過Gradle Plugin在編譯期間插入后者修改代碼(.class文件)。比如AspectJ,ASM,Javassist,APT等。這幾種方案的處理時機參考下圖。

圖片描述

1、$AppViewScreen全埋點

ActivityLifecycleCallbacks是Appliaction的一個內部接口,從 API 14 開始提供。在Appliaction中實現這個接口,便可以對所有Activity的生命周期進行監控。

在onCreate中調用如下代碼。

 registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {

}

@Override
public void onActivityStarted(Activity activity) {

}

@Override
public void onActivityResumed(Activity activity) {
   Log.e("Mr.S","resumed          "+activity.getLocalClassName());

}

@Override
public void onActivityPaused(Activity activity) {
    Log.e("Mr.S","paused          "+activity.getLocalClassName());
}

@Override
public void onActivityStopped(Activity activity) {

}

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

}

@Override
public void onActivityDestroyed(Activity activity) {

}
        });

運行結果如下:

2018-12-20 12:52:37.377 12534-12534/? E/Mr.S: resumed          MenuActivity
2018-12-20 12:52:40.385 12534-12534/com.ssy.qbd E/Mr.S: paused          MenuActivity
2018-12-20 12:52:40.496 12534-12534/com.ssy.qbd E/Mr.S: resumed          HellowActivity
2018-12-20 12:52:50.736 12534-12534/com.ssy.qbd E/Mr.S: paused          HellowActivity
2018-12-20 12:52:50.744 12534-12534/com.ssy.qbd E/Mr.S: resumed          MenuActivity

2、 $AppStart/End全埋點

因為系統沒有直接的方法判斷APP處于前臺還是后臺,所以我們需要一些假定邏輯來實現這個功能。

圖片描述

但是這些技術都無法解決以下兩個問題

  • App多進程如何判斷?

  • App奔潰被強殺怎么判斷?

解決方案也很簡單,采用ContentProvider機制來解決多進程的問題。并通過數據庫或者SharedPreferences來存儲這些狀態。

對于奔潰強殺問題,我們引入Session這個概念。

  • 當一個頁面退出了,如果 30 s 內沒有新的頁面打開那么我們認為應用進入后臺了。

  • 當一個頁面顯示了,如果和上一個頁面退出的時間超過了 30 s 我們認為 App 重新處于前臺了。

具體方案:

1、注冊ActivityLifecycleCallbacks,監聽Activity的生命周期。并采用ContentProvider+SharedPreferences的方式進行進程間數據共享,注冊ContentObserver來監聽跨進程間的數據通信。

2、頁面退出的時候(onPause)啟動一個倒計時 30 s ,如果 30 s 內沒有新的界面顯示觸發 AppEnd 。如果有些頁面那么,我們存儲一個新的標記為來標記這個新頁面(cp+sp)進行存儲。然后通過ContentObserver 監聽新頁面標記位的改變,取消定時器。如果 30 s 內沒有新的頁面(按 home建 、退出、奔潰、強退等)我們會在下一次啟動的時候補發這個AppEnd 事件。

3、在下一次啟動的時候,(onStart()),首先判斷是否與上一個頁面退出的時間間隔超過了 30 s ,如果沒有超過 30 s 那么,那么無需補發 AppEnd,直接出發 AppScreen 事件。然后判斷是否觸發了 AppEnd,如果標志位是true,那么出發 AppStart。反之不觸發。如果超過了 30 s 那么就去看看是否已經觸發了 AppEnd,如果沒有則先補發 AppEnd,然后在 AppStart,最后AppScreen。如果已經出發那么直接出發 AppStart,最后AppScreeen。

3、AppClick全埋點

這一小結是本文的重點,也是難點,也正是他復雜的情況和對性能的影響,產生了各種各樣的方案。

具體方案

  • 動態代理

  • 代理View.OnClickListener

  • 代理Window.Callback

  • 代理View.AccessibilityDelegate

  • 靜態代理

  • AspectJ 切面編程(AOP)

  • ASM

  • Javassist

  • APT 注解處理器

那么我們就詳細的介紹一下這些方案的使用以及優劣點。

3.1 代理View.OnClickListener

代理的OnClickListenerer。

public class MyWrapperOnClickListenerer implements View.OnClickListener {

    private View.OnClickListener onClickListener;

    public MyWrapperOnClickListenerer(View.OnClickListener onClickListener) {
        this.onClickListener = onClickListener;
    }

    @Override
    public void onClick(View v) {

        preClick();
        onClickListener.onClick(v);
        afterClick();

    }

    private void preClick() {
        Log.e("Mr.S", "preClick ");
    }

    private void afterClick() {
        Log.e("Mr.S", "afterClick ");
    }
}

獲取rootView,并開始代理。

   @Override
public void onActivityResumed(Activity activity) {
    // Log.e("Mr.S", "resumed          " + activity.getLocalClassName());

    ViewGroup rootView = activity.findViewById(android.R.id.content);
        
 //ViewGroup rootView = (ViewGroup) activity.getWindow().getDecorView();
    try {
        setViewProxy(rootView);
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
}

循環遍歷ViewGrop

 private void setViewProxy(ViewGroup viewGroup) throws IllegalAccessException, InvocationTargetException {
        int count = viewGroup.getChildCount();
        for (int i = 0; i < count; i++) {
if (viewGroup.getChildAt(i) instanceof ViewGroup) {
    setViewProxy((ViewGroup) viewGroup.getChildAt(i));
} else {
    hook(viewGroup.getChildAt(i));
}
        }
    }

通過反射 用MyWrapperOnClickListenerer 替換原來的OnClickListener。

    private void hook(View view) throws IllegalAccessException, InvocationTargetException {

        try {
Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
getListenerInfo.setAccessible(true);
Object listenereInfo = getListenerInfo.invoke(view);
try {
    Class<?> listenerInfoClazz = Class.forName("android.view.View$ListenerInfo");
    try {
        Field mOnClickListener = listenerInfoClazz.getDeclaredField("mOnClickListener");
        mOnClickListener.setAccessible(true);
        View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenereInfo);
        if (originOnClickListener==null||originOnClickListener instanceof MyWrapperOnClickListenerer) {
return;
        } else {
MyWrapperOnClickListenerer proxyOnClick = new MyWrapperOnClickListenerer(originOnClickListener);
mOnClickListener.set(listenereInfo, proxyOnClick);
        }

    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    }

} catch (ClassNotFoundException e) {
    e.printStackTrace();
}


        } catch (NoSuchMethodException e) {
e.printStackTrace();
        }

    }

我們的rootView可以:
1、android.R.id.content
2、DecorView

但是onResume() 之后動態添加的View,就無法監聽到了。所以我們又引入了

3、ViewTreeObserver.OnGlobalLayoutListeener

給rootViewe 添加ViewTreeObserver.OnGlobalLayoutListeener監聽,收到回調(視圖樹發生變化的時候)我們會重新遍歷一次rootview。當然在stop()的時候記得調用removeOnGlobalLayoutListener方法。免得不必要的內存問題。

@Override
public void onActivityResumed(Activity activity) {

    // ViewGroup rootView = activity.findViewById(android.R.id.content);
    rootView = (ViewGroup) activity.getWindow().getDecorView();
    onGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
try {
    setViewProxy(rootView);
} catch (IllegalAccessException e) {
    e.printStackTrace();
} catch (InvocationTargetException e) {
    e.printStackTrace();
}
        }
    };

    rootView.getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener);
    try {
        setViewProxy(rootView);
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
}

至此動態代理也就結束。我們的全埋點也基本實現。但是有沒有發現一些問題呢?

1、使用反射,效率比較低,對于性能會有影響,可能也會有兼容性問題
2、Application.ActivityLifecycleCallbacks 需要 API 14+
3、View.hasOnClickListeneers 需要 API 15+
4、removeOnGlobalLayoutListener 需要 API 16+
5、游離于Activity 之上的View的點擊比如Dialog,PopupWindow無法被監視

當然我們可以代理Window.Callback 和上面的原理相同。不過問題依然存在。
代理View.AccessibilityDelegate效果也是差不多的,問題依然存在。

面對這些問題,靜態代理也是呼之欲出了。

靜態代理

AspectJ 切面編程(AOP)
不了解的可以先看一下這個(

代碼如下:

@Aspect
public class TestAspect {

 @Pointcut("execution(* *(..))")
    public void pointcut() {

    }

  @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String name = signature.getName();
        if (name.equals("onClick")) {
Log.e("Mr.S", "preClick ");

joinPoint.proceed();
Log.e("Mr.S", "afterClick ");
        }else {
return  joinPoint.proceed();
        }


        return null;
    }
}

結果:

2018-12-21 15:43:59.245 30961-30961/com.ssy.qbd E/Mr.S: preClick 
2018-12-21 15:43:59.259 30961-30961/com.ssy.qbd E/Mr.S: afterClick 

一切感覺都很完美,但是也是缺點的:

不過目前來看,這個方案很是很不錯的。值得我們去實施。因為這是靜態編譯中學習成本相對最低的一個方案。


我來說兩句
您需要登錄后才可以評論 登錄 | 立即注冊
facelist
所有評論(0)
領先的中文移動開發者社區
18620764416
7*24全天服務
意見反?。[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 致富之地一肖中特 )