埋點的核心邏輯抽象:將「APP生產」的「用戶數據」組織「發送給服務器」。
1.Producer是APP,生產各類用戶數據。
2.Consumer是埋點系統的數據上傳模塊,把各類用戶數據上傳給服務器。
3.MetaData是對用戶數據的抽象。
4.Queue是存儲用戶數據的隊列。java
將抽象邏輯分解爲各個子模塊並拼裝,造成最終的架構。
1.MetaData模塊:將用戶行爲抽象爲數據描述。
2.Producers模塊:生產用戶數據,將用戶行爲轉化爲數據集。
3.Consumers模塊:消費用戶數據,將用戶數據上傳給服務器。
4.Storage模塊:存儲用戶數據,將用戶數據暫存在文件中。
5.Broker模塊:管理用戶數據。android
關鍵是特徵值的提取。
設備基礎屬性:
設備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
1.內存緩存(List):每生產一條數據都會先進入內存緩存
2.數據庫存儲(DataBase):提供對數據庫的操做接口數據庫
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
申請(aquire)數據,消費(upload)數據,釋放(release)數據
建議:LJ目前的代碼能夠參考此設計進行代碼優化緩存
普通埋點數據生產
Producers模塊對外提供各類封裝好的add方法供開發者調用,如addClickEvent(), addPageEnterEvent(), addPageLeaveEvent()等。服務器
無埋點數據生產
Producers模塊自動生產各類埋點數據,如app啓動/退出,頁面進入/退出/滑動,頁面元素點擊,push到達/點擊等。架構
因爲歷史緣由,LJ總共有2個埋點庫:
dig庫:LJ-APP&BK-APP均在使用,用於對無埋點沒法處理的特殊數據進行補充
無埋點庫:LJ-APP用的老版本(pid沒法自定義,主工程和插件共用主工程的pid),BK-APP用的新版本(pid能夠自定義,主工程和插件工程能夠分別定義本身的pid)
建議:兩庫合併
無埋點的核心是,如何經過代碼自動蒐集想要的信息:
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文件。
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; } }
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]表示方法的第一個參數 }