Android全量埋點實踐
一、專案背景
產品又提了一堆埋點需求,有簡單的,如點選的。有複雜的,如統計頁面A到頁面B再到頁面C的。有的還是介面相關的,本來以為後臺能埋點的(我都調後臺介面了,不能後臺埋嗎?),因為無法解釋的原因,最終還是app端埋。
部分點位如下:
雖然之前通過 Tracker框架 可以實現長路徑的埋點,但因為點位太多了,業務層寫的也煩,所以就想著必須得做全量埋點。解放雙手,讓產品不用再找我們了。
全量埋點大致框架圖:
1、Tracker負責全域性事件監聽,但不做任何邏輯,由上層實現
2、Apt 註解處理器,用於在編譯時生成 extFieldMapping.csv
3、FullPointer依賴於Tracker,事件發生後,收集資料。還包括解析mapping檔案,引數->ext 對映,資料上傳等。
4、APP 業務層負責 對一些關鍵點位繫結業務引數
根據產品需求分析,我們拆分為三種大的事件。
eventType 事件型別:
1、頁面事件
2、互動事件
3、介面事件
二、頁面page事件
頁面的定義是由產品決定的,Android 端一般為Activity,Fragemnt。Activity 預設為一個頁面,Fragement預設不是頁面,需要業務層對 fragemnt 加上pageId,框架才會認為是頁面,之後此 fragment 裡的view 的點選事件,獲取到的 pageId 就是 fragment的pageId,而不是Activity 的了。而Activity也可以加pageId,有pageId會取pageId,否則會取activity.className
page 會產生四種小事件,對應 頁面 的 生命週期 lifecycle。(0 enter、1 show 、2 hide、 3 exit) 其中 enter與exit 一個頁面存活時只會觸發一次,而show與hide 會多次觸發。
下面分別對activity、fragment 來實現四種事件說明
2.1 Activity 為 page
enter 進入
對應 activity.onCreate
show 對使用者可見
對應 activity.onResume
hide 對使用者不可見
對應 activity.onPause
exit 頁面退出
對應 activity.onDestory
使用 Tracker 全域性監聽
//頁面 activity show/hide
Track.fromAnyActivity().activityOnCreated().subscribe(activity -> {
onPageLifeCycle(activity, LIFECYCLE_ENTER);
}).activityOnResumed().subscribe(activity -> {
onActivityShowHide(activity, true);
}).activityOnPaused().subscribe(activity -> {
onActivityShowHide(activity, false);
}).activityOnDestroyed().subscribe(activity -> {
onPageLifeCycle(activity, LIFECYCLE_EXIT);
TrackTools.cleanPageParams(activity);
});
2.2 Fragment 為 page
enter 進入
對應 fragment.onCreateView
exit 頁面退出
對應 fragment.onDestory
因為 fragment 使用場景有多種,有ViewPage 巢狀 fragment 的,有通過 FragmentTransaction.show、hide 切換的,也可以直接add的。所以針對 fragment 的show、hide 需要多重處理。
使用 Tracker 全域性監聽
//fragment enter、exit
Track<?> fragmentTrack = Track.from(FragmentActivity.class);
fragmentTrack.fragmentOnCreateView(Fragment.class).subscribe(fragment -> {
onPageLifeCycle(fragment, LIFECYCLE_ENTER);
}).fragmentOnDestroyed(Fragment.class).subscribe(fragment -> {
onPageLifeCycle(fragment, LIFECYCLE_EXIT);
});
//fragment show、hide
fragmentTrack.fragmentOnHiddenChanged(Fragment.class).subscribe(fragment ->
onFragmentShowHide(fragment, !fragment.isHidden()));
fragmentTrack.fragmentSetUserVisibleHint(Fragment.class).subscribe(fragment -> {
if (fragment.isResumed()) {
onFragmentShowHide(fragment, fragment.getUserVisibleHint());
}
});
fragmentTrack.fragmentOnResumed(Fragment.class).subscribe(fragment -> {
if (!fragment.isHidden() && fragment.getUserVisibleHint()) {
onFragmentShowHide(fragment, true);
}
});
fragmentTrack.fragmentOnPaused(Fragment.class).subscribe(fragment -> {
if (!fragment.isHidden() && fragment.getUserVisibleHint()) {
onFragmentShowHide(fragment, false);
}
});
2.3 page 的唯一標識 pageId
如果直接拿activity、fragment的className當作頁面的id。會導致android與ios端不一致。所以我們為了方便後續BI分析資料,需要業務層對頁面setPageId,具體android端使用 @PageId("20062")
註解加在activity或fragment 上,當事件發生時,框架拿到 activity 或 fragment 物件,獲取註解中的值。而有些fragment類是通用的。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-vG2csd3R-1607917412088)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a701192b5a3d45cabf1b1af129c478b8~tplv-k3u1fbpfcp-watermark.image)]
例:一個viewPage巢狀幾個fragment,通過初始化引數展示不同的列表,產品認為它們是不同的頁面。而程式碼全在一個fragment類中,如果我們通過 class 的註解 ,無法區分不同的頁面。所以還需要有一個 setPageId(frg/act,"pageId")
方法,而且有的頁面還能攜帶引數。在上面例子中,需要攜帶頁面所處的位置,即後臺可以動態控制每個tab頁面所處的位置。
@PageId(value = "21002", remarks = "我的出價-待成交")
public class MyPricePageBase {
public int position;//頁面位置標識
public int type;//4010介面中的type欄位
}
業務層 在 onCreate 或者其它地方 呼叫 setPageId()
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
……
MyPricePageBase page=new MyPricePageBase();
page.position=position;
page.type=type;
TrackTools.setPageId(this, page);
}
將pageId 註解加在bean上。並且與頁面繫結起來,框架中當頁面事件發生時,取出pageId與引數欄位。
三、view互動事件
3.1 actionType 互動事件型別
互動事件主要是view的互動事件,分為四種小事件
1、點選事件
2、重新整理事件
3、載入事件
4、切換事件
點選就是view的點選事件。重新整理是在列表中,使用者通過手勢下拉重新整理。載入就是上滑載入更多。雖然重新整理與載入更多都會觸發介面事件,但某些情況為了能區分是 自動進入頁面重新整理 還是 使用者手動重新整理,所以重新整理、載入事件也就有必要了。切換是指在 banner、大圖瀏覽,可以左右滑動切換的事件。因為場景較少,這裡開放插入事件方法,由業務層主動插入事件。
使用Tracker 全域性監聽
//全view click 事件
Track.fromAnyActivity().anyViewClick().subscribe(view -> {
String actionId = TrackTools.getViewActionId(view);
String pageId = TrackTools.getPageId(view);
Object params = TrackTools.getViewParams(view);
Map map = extManager.getExtParamsMap(actionId, params, Block.VIEWTAG);
//插入事件...
LogUtil.i(TAG, "anyViewClick actionId: " + actionId + " pageId=" + pageId + " map=" + map);
});
//重新整理
Track.fromAnyActivity().onRefresh(TrackManager.ANY_VIEW).subscribe(view -> {
LogUtil.d(TAG, "onRefresh " + view);
//插入事件
onViewAction(view, REFRESH_ACTION);
});
//載入
Track.fromAnyActivity().onLoadMore(TrackManager.ANY_VIEW).subscribe(view -> {
LogUtil.d(TAG, "onLoadMore " + view);
//插入事件
onViewAction(view, LOAD_MORE_ACTION);
});
3.2 互動事件的唯一標識 actionId
互動事件中,主要物件是view。我們需要區分是哪個view的點選。所以就需要生成view的唯一Id。而要區分某個頁面的某個view。雖然有view.getId()方法,但是考慮到專案中,不一定所有可以點選的view都有id,而且並不能保證id在一個頁面中是唯一的。可以通過viewTree 的方式。根據view的className與在父view的位置 Button[1]
,一直到根view的層級關係。大致長這樣
CoordinatorLayout[1]/AutoConstraintLayout[1]/SmartRefreshLayout[0]/RecyclerView[0]/AutoLinearLayout[1]
根View 可以查詢到 android.R.id.content
即可。而即便是viewTree 的方式,也無法保證在不同app版本,如果改變了佈局,view的位置發生變化。那麼viewTree就會發生變化。雖然網上有通過 “相對於同類view在父view的位置,而不是直接取在父view的位置”。但依然解決不了頁面大變的情況。
所以我們對一些重要的按鈕。(目前是重要頁面,常用功能的view),對它setViewTag。就是一個別名id,並且android與ios一致,這樣會方便BI後續分析資料。當然需要 業務層 在程式碼裡手動呼叫set,業務層需要確保在佈局變化後,tag依然一樣。用於區分此頁面的此view。框架在拿到view時,getViewTag就行。而沒有setViewTag的view,我們通過viewTree的方式。雖然會有viewTree變化的問題,只不過我們選擇忽略了。。。。
TrackTools.setTag(view,"home-search");
/**
* setTag 無引數
*/
public static View setTag(View v, String viewTag) {
v.setTag(0xff000001, viewTag);
return v;
}
3.3 互動事件攜帶引數
在某些情況下,view是動態的。通過介面資料配置按鈕的背景、文字,點選跳轉的路由等。例:
那麼當這種按鈕點選的時候,需要攜帶業務引數,如id,路由地址之類的。使用如下:
//定義tag類
@ViewTag(value = "home-search",remarks = "首頁-搜尋框")
public class TestTag {
public String id;
public String url;
}
//tag與view繫結
TestTag tag=new TestTag();
tag.id=id;
tag.url=url;
TrackTools.setTag(button,tag);
TrackTools.setTag
/**
* setTag 並且攜帶引數
*/
public static View setTag(View v, Object tagObj) {
ViewTag tag = tagObj.getClass().getAnnotation(ViewTag.class);
if (tag == null || TextUtils.isEmpty(tag.value())) {
throw new RuntimeException(tagObj.getClass() + " don't has ViewTag Annotation");
}
v.setTag(0xff000001, tagObj);
return v;
}
就是通過view.setTag與物件繫結起來,在點選事件中通過view再取出物件的tag與引數。
三、介面事件
介面事件是指網路介面請求事件。當請求成功後,把 介面號(介面的唯一標識)+ 業務引數作為一個事件,有些特殊介面事件還需要 返回引數。由於每家公司都會對網路請求再封裝一層。監聽實現方式肯定無法統一,這裡只給出一個使用參考示例。
Tracker 全域性監聽 介面事件 示例
//全介面事件
Track.fromApplication().onAnyApiSuccess().subscribe(response -> {
int code = response.getServiceCode();
Object request=response.getRequest();
Object result=response.getResult();
});
四、欄位與ext的對映表
從上面三個大事件可以看出,都有可能攜帶引數。
page 事件必有 lifecycle生命週期欄位,和特殊頁面的業務id
互動事件中的點選事件 可能會攜帶業務id,位置,等
介面事件 必有 請求的業務引數,部分介面會需要 返回的業務引數
而引數的名字是各種各樣的,同一個事件的不同app版本可能會增刪引數。在與資料分析部門討論後,為了方便BI後續分析。決定將每一個事件引數固定對映到 ext1…n 上,所以需要有一張 欄位與ext的對映表 即:extFieldMapping.csv
如上圖:介面事件2005中,獲取到引數 age ,實際賦值 給 ext1,id->ext2,如果這個介面有新增欄位,向後新增,而 最大 ext的列數 為欄位引數最多的那個事件。在 home-msg 點選事件中,獲取到arg3引數,賦值給 ext1。並且不同版本的 mapping 是隻增加欄位的,不能刪除欄位(因為老資料會有值)。那麼android與ios統一按上面規則使用這張表。而BI分析時,通過事件id,可以反查到ext1對應的含義。如:2005 事件中。ext1欄位的含義就是 age。
4.1 具體實現
表的格式是 .csv,這是一種簡易的表,使用文字方式讀寫,換行符\n分隔為行,英文逗號分隔為列。第一行 為列名,後面的行 為資料。使用 excel 開啟就是如上表格的樣式,也方便程式中解析處理。文字開啟如下:
actionId,remarks,ext1,ext2,ext3,ext4,ext5,ext6,ext7,ext8
//serviceCode,,,,,,,,,
2001,,actiStatus,actiStatusText,activityTag,applicantId,applicantLink,applicantName,applicantPhone,applicantReason
2002,,age,age1,agreeType,mobile,password,testMobile,username,
2003,,age,id,,,,,,
2005,,age,id,,,,,,
//pageId,,,,,,,,,
1000001,,lifecycle,,,,,,,
1000002,,lifecycle,position,,,,,,
1000003,,lifecycle,index,,,,,,
1000004,,lifecycle,,,,,,,
//viewTag,,,,,,,,,
home-msg,首頁-訊息,arg3,arg4,,,,,,
home-search,首頁-搜尋框,arg1,arg2,arg3,,,,,
表的生成 是由Android端生成的。編譯時使用 Apt(註解處理器) 生成extFieldMapping.csv
檔案至app/src/main/assets
(一般我們在使用Apt時,都是建立 .java
檔案,其實 Apt 過程也可以做其它事,例如拿到assets目錄,讀寫檔案,然後 打包過程會把最新檔案打進app 中)
apt 獲取 assets 目錄,主要是拿到本地專案路徑
/**
* 獲取 assets 目錄
*/
private File getAssetsDirFile() {
try {
mFiler = processingEnv.getFiler();
FileObject temp = mFiler.getResource(StandardLocation.CLASS_OUTPUT, "", "META-INF");
String url = temp.toUri().toString();
//url=file:/Users/wzh/ttpc/gitlab/Android-Compiler/app/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/META-INF/test.csv
try {//mac linux
url = url.substring(0, url.indexOf("/build/")).replace("file:", "");
} catch (Exception e) {//windows
url = url.substring(0, url.indexOf("/build/")).replace("file:/", "");
}
url = url.substring(0, url.lastIndexOf("/")) + "/app/src/main/assets";//固定在app 目錄
File dir = new File(url);
if (!dir.exists() || !dir.canWrite()) {
throw new RuntimeException(dir.getAbsolutePath() + " don't exist or don't write,please check dir");
}
return dir;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
4.1.1 介面引數
介面引數是通過處理 介面類 註解,獲取所有介面方法,然後解析獲取 介面號與請求的bean,解析bean獲取欄位。
介面類使用 retrofit 定義方式,再封裝了業務邏輯,大致定義如下:
@TtpcApi
public interface BossApi {
@CODE(2005)
@POST("/testapp/")
Observable<BaseResult<LoginBean,Object>> loginBoss(@Body LoginRequest request);
@CODE(2002)
@POST("/testapp/")
HttpTask<LoginResponse,Integer> login7(@Body LoginRequest request);
}
4.1.2 page引數
而page引數的獲取則是通過解析pageId註解,pageId可以直接加在activity或fragment上,也可以加在普通bean上,如果是bean,則獲取bean的所有欄位,當做事件的引數。這也是解釋 setPageid 為什麼一定要pageId註解,也是為了表的生成。
pageId註解定義:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface PageId {
/**
* PageId 值,必填
*/
String value();
/**
* 備註
*/
String remarks() default "";
}
4.1.3 view引數
view的引數也是一樣,解析ViewTag 註解的bean,獲取欄位當做引數。通過setTag(View v, String viewTag)
的方式,表示沒有引數,也就不需要出現在表裡。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewTag {
/**
* tag 值,必填
*/
String value();
/**
* 備註
*/
String remarks() default "";
}
由於表的生成是由編譯時自動生成的,不需要人為的去修改表。這樣可以減少一些人為導致的錯誤發生。由於Android與ios用的是同一張表,執行時只需讀取,解析表即可,所以ios無需生成表,我們的策略是在由安卓打正式包時,生成後的表,上傳到區域網伺服器,ios編譯時下載放到專案裡即可。
五、總結
可以看出,這套全量埋點框架的部分工作還是需要由業務層完成,通過手動setViewTag、setPageId的方式,好像並沒有減輕業務層的工作量。但好在不多,主要還是因為需要這張ext與欄位的對映表,所以業務層使用方式也得相容表的生成功能。
5.1 上線後的後續工作
在資料傳給後臺,產品提出點位需求,BI分析階段,我們需要提供某個點位或者好幾個點位,如某個按鈕的viewTree是啥,這個點應該怎麼串。
5.2 業界方案
在view與業務引數繫結這塊,網上有方案是通過後臺下發配置,指定點選時的業務引數,app解析類似 resp.result.id
語法,這樣後續工作就是填這種語法了。而view的唯一標識,沒有通過setViewTag的view。使用viewTree 的方式,無法保證在佈局改變後生成的一致。這一點也沒什麼好辦法,歡迎大家討論。
總之,沒有完美的全量方案,也沒有一勞永逸的方案。各有優缺,需要根據 app端,分析端,產品端的 需求來一起制定。謹供參考。
5.3 關於開源
全量埋點的部分,後續有需要的話會刪除部分業務邏輯開源出來。
其中 Tracker 是開源的,可以實現全域性監聽功能,通用的可以監聽全域性 viewClick。和activity/fragment 的生命週期,頁面的跳轉。而下拉重新整理,載入,各家用的都不一樣,就沒必要開源了。當然如果你需要 很長的埋點 需求,tracker 也可以實現,具體看專案介紹。
本文同步傳送於:https://juejin.cn/post/6904900874183819277
相關文章
- 51信用卡 Android 自動埋點實踐Android
- 面向切面程式設計AspectJ在Android埋點的實踐程式設計Android
- vue宣告式埋點實踐Vue
- 無埋點統計SDK實踐
- 全自動埋點 diff 工具
- 呼叫鏈監控 CAT 之 URL埋點實踐
- Android埋點技術概覽Android
- 達達埋點遷移京東子午線實踐
- 視覺化埋點在React Native中的實踐視覺化React Native
- 如何使用Android視覺化埋點Android視覺化
- 商家視覺化埋點探索和實踐|得物技術視覺化
- 一個輕量級react埋點元件React元件
- 輕量級非侵入式埋點方案
- 埋點
- iOS全埋點解決方案-UITableView和UICollectionView點選事件iOSUIView事件
- iOS全埋點解決方案-控制元件點選事件iOS控制元件事件
- iOS全埋點解決方案-介面預覽事件iOS事件
- iOS全埋點解決方案-採集奔潰iOS
- iOS全埋點解決方案-時間相關iOS
- iOS全埋點解決方案-手勢採集iOS
- iOS全埋點解決方案-資料儲存iOS
- 軟體質量保障全流程實踐分享
- 得物技術埋點自動化驗證的探索和最佳實踐
- Android無埋點資料收集SDK關鍵技術Android
- JJEvent 一個可靠的Android端資料埋點SDKAndroid
- 小程式從手動埋點到自動埋點
- iOS全埋點解決方案-應用退出和啟動iOS
- 埋點表相關
- Android無埋點資料採集實戰(附原始碼,兩行程式碼即可接入)Android原始碼行程
- 前端埋點方案分析前端
- 埋點計算定位
- Android ItemTouchHelper 實踐Android
- 閒魚是如何實踐一套完整的埋點自動化驗證方案的?
- 網易雲音樂全鏈路埋點管理平臺建設
- iOS全埋點解決方案-APP和H5打通iOSAPPH5
- 元件中路由和埋點元件路由
- js無侵入埋點方案JS
- 不可缺少的程式埋點