Android埋點系統設計

1、埋點架構設計

埋點的核心邏輯抽象:將「APP生產」「用戶數據」組織「發送給服務器」
圖片描述
1.Producer是APP,生產各類用戶數據。
2.Consumer是埋點系統的數據上傳模塊,把各類用戶數據上傳給服務器。
3.MetaData是對用戶數據的抽象。
4.Queue是存儲用戶數據的隊列。java

抽象邏輯分解爲各個子模塊並拼裝,造成最終的架構。
圖片描述
1.MetaData模塊:將用戶行爲抽象爲數據描述。
2.Producers模塊:生產用戶數據,將用戶行爲轉化爲數據集。
3.Consumers模塊:消費用戶數據,將用戶數據上傳給服務器。
4.Storage模塊:存儲用戶數據,將用戶數據暫存在文件中。
5.Broker模塊:管理用戶數據。android

2、MetaData模塊

關鍵是特徵值的提取
設備基礎屬性:
設備id:udid=12345678910
APP基礎屬性:
版本:v=8.5.0
渠道:c=xiaomi
用戶基礎屬性:
用戶id:ucid=12345678910
頁面元素描述:
屬於哪一個APP:pid=bigc_app_xinfang
屬於哪一個頁面:key=newhouse/homeindex
屬於哪一個頁面元素:爲頁面元素定義惟一的code
頁面與頁面關聯邏輯:
從哪一個頁面進入當前頁面:f=newhouse/homeindex
頁面停留時間:stt=1000
用戶行爲描述:
用戶基礎行爲描述:evt=xxx,如APP啓動/退出,頁面進入/離開/滑動,頁面元素點擊/曝光,push到達/點擊
用戶擴展行爲描述:action=json,如action={"project_name":"thyhwabktj", "xinfangapp_click":"10020"}
舉例:
{v=1.1.6, ts=1527067845806, ucid=null, ssid=b032222c-7105-4119-9bfe-a1aec5ba9285, pid=bigc_app_xinfang, key=newhouse/project, action={"sample_mark":"","project_name":"thyhwabktj"}, longitude=0.0, latitude=0.0, cid=110000, f=newhouse/homeindex?project_name=thyhwabktj&city_id=110000, stt=685, evt=2}git

LJ因爲歷史緣由,有兩套MetaData
無埋點evt定義:app啓動/退出=5,頁面進入/退出=6,頁面滑動=8,頁面元素點擊=7,push到達/點擊=9。
普通埋點evt定義:頁面進入=1,3,頁面離開=2,頁面元素點擊=10186,頁面元素曝光=11316。
Q:無埋點進入/退出都使用6,如何區分?
A:增長了一個status字段,用status=0/1表示頁面進入/退出。
建議:統一dig埋點和無埋點的evt定義sql

3、Storage模塊

1.內存緩存(List):每生產一條數據都會先進入內存緩存
2.數據庫存儲(DataBase):提供對數據庫的操做接口數據庫

4、Broker模塊

1.接收生產者的數據,並寫入數據庫(對外提供put方法)
Q:如何控制數據由內存寫入數據庫時機?
1.1.內存數據超過必定數量(如20條)時,缺點是應用在後臺被殺,最多可能丟失20條數據
1.2.生命週期onPause時,缺點是寫操做相對比較頻繁
1.3.利用定時器,缺點是後臺定時任務可能不執行,致使丟數據
LJ現行方案:1.1+1.3
建議使用:1.1+1.2
Q:多進程寫數據庫怎麼辦?
A:多進程向sqlite插入數據不會有問題,只是插入順序是亂序的;若是要保證插入順序也一致,能夠考慮啓動一個獨立進程操做sqlite,其它進程與sqlite所在進程進行通訊。json

2.讀取數據庫中的數據,並供給消費者消費(對外提供aquire/release方法)
Q:如何控制數據提供給消費者消費的時機?
A:生命週期onPause時,缺點是消費相對比較頻繁api

5、Consumers模塊

申請(aquire)數據,消費(upload)數據,釋放(release)數據
建議:LJ目前的代碼能夠參考此設計進行代碼優化緩存

6、Producers模塊

普通埋點數據生產
Producers模塊對外提供各類封裝好的add方法供開發者調用,如addClickEvent(), addPageEnterEvent(), addPageLeaveEvent()等。服務器

無埋點數據生產
Producers模塊自動生產各類埋點數據,如app啓動/退出,頁面進入/退出/滑動,頁面元素點擊,push到達/點擊等。架構

7、LJ現有埋點庫

因爲歷史緣由,LJ總共有2個埋點庫:
dig庫:LJ-APP&BK-APP均在使用,用於對無埋點沒法處理的特殊數據進行補充
無埋點庫:LJ-APP用的老版本(pid沒法自定義,主工程和插件共用主工程的pid),BK-APP用的新版本(pid能夠自定義,主工程和插件工程能夠分別定義本身的pid)
建議:兩庫合併

8、無埋點實現原理

無埋點的核心是,如何經過代碼自動蒐集想要的信息:
1.設備、APP、用戶等基礎屬性,直接經過api獲取
2.Activity進入/離開等生命週期相關屬性,直接經過LifeCycleCallback監聽獲取
3.Activity的惟一標記如pageId等屬性,直接經過註解獲取
4.UI元素點擊/滑動等行爲屬性,須要經過hook代碼才能實現

如何肯定UI元素的惟一性:
方案1:爲須要統計的元素定義惟一的code,寫入contentDescription,而後讀取這個屬性
方案2:利用ViewTree中的ViewPath惟一肯定一個UI元素

核心代碼:

public static ViewPath getPath(View view) {
    do {
      //1.構造ViewPath中於view對應的節點:ViewType[index]
      ViewType = view.getClass().getSimpleName();
      index = view在兄弟節點中的index;
      ViewPath節點 = ViewType[index];
    } while ((view = view.getParent()) instanceof View);//2.將view指向上一級的節點
  }

結果示例:

DecorView/LinearLayout[0]/FrameLayout[0]/ActionBarOverlayLayout[0]/ContentFrameLayout[0]/FrameLayout[0]/LinearLayout[0]/ViewPager[0]/ButtonFragment[0]/AppCompatButton[0]

ViewPath可讀性問題:
能夠創建一個Mapping文件,將ViewPath和描述Describe對應起來,具體實現:
寫一個工具,當咱們在手機上點擊一個按鈕的時候彈出彈窗,輸入Describe描述文字,最終生成一個ViewPath<->Describe的Mappting文件。

9、註解基礎知識

1.元數據metadata與註解annotation
Java中總共有4種類型:類Class、接口Interface、枚舉Enum、元數據@interface(就是註解)。
元數據:是添加到包、類、方法、屬性上的額外信息,對其進行描述,如@Override。
元註解:是最基本的註解:@Target、@Retention、@Documented、@Inherited
@Target取值:PACKAGE、TYPE、FIELD、METHOD
@Retention取值:SOURCE、CLASS、RUNTIME
註解的做用:編譯時可獲取到註解信息動態生成代碼,運行時科獲取到註解信息作特殊處理。

2.運行時註解
在運行時經過反射對註解進行處理,比較消耗資源,性能較差。

定義:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface MethodInfo {
 
    String author() default "xiaoming";
 
    String date();
 
    int version() default 1;
}

使用:

public class App {
 
    @MethodInfo(
        author = 「xiaoming@gmail.com」,
        date = "2018/05/10",
        version = 2)
    public String getAppName() {
        return "trinea";
    }
}

解析:

Class cls = Class.forName("com.lianjia.test.annotation.App");
for (Method method : cls.getMethods()) {
    MethodInfo methodInfo = method.getAnnotation(MethodInfo.class);
    System.out.println(「method author: 」 + methodInfo.author());
}

3.編譯時註解
在編譯時經過Java Annotation Process技術對註解進行處理,由於不使用反射,因此性能較好

模擬ButterKnife定義:

@Retention(CLASS) 
@Target(FIELD)
public @interface InjectView {
  int value();
}

模擬ButterKnife調用:

@InjectView(R.id.user) 
EditText username;

模擬ButterKnife處理:

@SupportedAnnotationTypes({"com.lianjia.InjectView "})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class MyProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (TypeElement typeElement : annotations) {    // 遍歷annotations獲取annotation類型
            for (Element element : roundEnv.getElementsAnnotatedWith(typeElement)) {    // 使用roundEnv.getElementsAnnotatedWith獲取全部被某一類型註解標註的元素,依次遍歷
                // 在元素上調用接口獲取註解值
                int annoValue = element.getAnnotation(TestAnnotation.class).value();
                String annoWhat = element.getAnnotation(TestAnnotation.class).what();

                System.out.println("value = " + annoValue);
                System.out.println("what = " + annoWhat);

                // 向當前環境輸出warning信息
                processingEnv.getMessager().printMessage(Kind.WARNING, "value = " + annoValue + ", what = " + annoWhat, element);
            }
        }
        return false;
    }
}

10、Hook代碼實現蒐集用戶點擊數據

1.Android編譯運行全流程
圖片描述

2.在onClick(View view)方法中加入埋點邏輯
2.1.編寫埋點邏輯:由埋點sdk(LianjiaAnalyticsSdk)完成
2.2.將埋點邏輯插入onClick(View view)方法中:由埋點插件(LianjiaAnalyticsPlugin)完成

3.埋點插件編寫
1.插件編寫流程?plugin編寫。
2.如何侵入編譯流程?transform庫基礎使用。
4.如何修改字節碼?Javassist庫基礎使用。

4.核心代碼

編寫埋點邏輯:

public class AnalyticsEventsBridge {


  /**
   * Hook onClick(View view)方法,並調用此方法
   */
  public static void onViewClick(@Nullable View view) {
    // 獲取view的惟一標記等相關信息
    // 生成一條埋點日誌並寫入
  }
}

編寫插件:
1.插件項目目錄結構
圖片描述

2.build.gradle修改

apply plugin: 'groovy'

dependencies {
  compile gradleApi()

  compile 'com.android.tools.build:gradle:2.3.3'
  compile 'org.javassist:javassist:3.21.0-GA'
}

apply from: './gradle-mvn-push.gradle'
apply plugin: 'maven-publish'

publishing {
  publications {
    mavenJava(MavenPublication) {
      groupId PROJ_GROUP
      artifactId PROJ_ARTIFACTID
      version PROJ_VERSION
      from components.java
    }
  }
}

3.插件執行入口,至關於Main函數

class AnalyticsPlugin implements Plugin<Project> {

  @Override
  void apply(Project project) {
    InjectAndJarMergingTransform transform = new InjectAndJarMergingTransform()
    android.registerTransform(transform)
  }
}

transform侵入編譯流程:

public class InjectAndJarMergingTransform extends Transform {
  @Override public void transform(@NonNull TransformInvocation invocation)
      throws TransformException, IOException {
    println("LianjiaJarMergingTransform, begin");
    //這裏能夠獲取到文件的輸入/輸出信息,並對其作相應的更改,核心抽象爲1個方法
    processClass(inputStream, outputStream);
    println("LianjiaJarMergingTransform, end");
  }
}

Javassist修改字節碼:

private void processClass(InputStream inputStream, OutputStream outputStream) throws IOException {
    final ClassPool classPool = AndroidClassPool.getClassPool()
    final CtClass clazz = classPool.makeClass(inputStream)
    final CtMethod ctMethod
    try {
      ctMethod = ctClass.getMethod(targetMethodName, targetMethodDescriptor);
    } catch (NotFoundException e) {
      xxxxxxxx
    }
    //經過過濾器,找到android.view.View$OnClickListener的onClick(View view)方法,略
    //Hook調用AnalyticsEventsBridge.onViewClick(view)方法
 ctMethod.insertBefore("""com.lianjia.sdk.analytics.gradle.AnalyticsEventsBridge.onViewClick(\$1);""")
    //其中$0=this, $1=$args[0]表示方法的第一個參數
}
相關文章
相關標籤/搜索