kotlin
xxFragment.kt:
PerformancePointUtil.getInstance().startUiLog(currentPageName)
....
PerformancePointUtil.getInstance().endUiLog(currentPageName)
xxUtils.kt:
private Logger mLogger;
.....
private String getUiMsg(boolean isStart, String pageName) {
final String flag = isStart ? START : END;
final StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(mProjectName)
.append(UNDER_LINE)
.append(UI)
.append(UNDER_LINE)
.append(pageName)
.append(UNDER_LINE)
.append(flag)
.append(COLON)
.append(System.currentTimeMillis());
return stringBuilder.toString();
}
/***
* when UI on create start output log
* @param pageName UI page name,now is class name.
*/
public void startUiLog(@NonNull String pageName) {
logMsg(getUiMsg(true, pageName));
}
/***
* when UI data set finished and draw completed output log.
* @param pageName current UI page name,now is class simple name.
*/
public void endUiLog(@NonNull String pageName) {
logMsg(getUiMsg(false, pageName));
}
public void logMsg(String msg) {
if (mLogger == null){
return;
}
if (mLogSwitch) {
mLogger.log(LOG_LEVEL, msg);
} else {
mLogger.log(LOG_LEVEL, LOG_FAILED_MSG);
}
}
分析
1. endUiLog 目前具体应该写在哪个回调方法里?
在您的 xxFragment.kt 架构中,endUiLog 必须精准挂载在 LoadState.LoadSuccess 分支触发的 UI 渲染完成时。
然而,仅仅写在 updateUI(state.data) 后面是不够精准的。因为 RecyclerView.Adapter.notifyDataSetChanged() 或 setNewList() 只是把数据提交给了内存,此时 CPU 还在计算布局,GPU 还没有完成首帧的绘制。
💻 完美对齐"首帧渲染完成"的最佳写法
利用 View.post 机制,将 endUiLog 放入下一帧的队列中,确保日志输出时 RecyclerView 已经完成了在屏幕上的第一帧绘制:
kotlin
private fun handleUiState(state: ExploreUiState) {
when (state.loadState) {
is LoadState.Loading -> {
exploreAdapter?.setNewList(emptyList())
showLoading()
// 页面生命周期起点已经由外层触发,此处可执行通用动作
onUiStart(javaClass.simpleName)
}
is LoadState.LoadSuccess -> {
setGridLayoutManager(state.bannerWidgetStyle)
updateUI(state.data)
// 🎯 核心:在视图队列最后执行,确保 RecyclerView 渲染绘制完毕
binding?.exploreRvList?.post {
PerformancePointUtil.getInstance().endUiLog(javaClass.simpleName)
}
}
// ... 其他分支
}
}
2. 如果接口请求失败(LoadError),是否也需要调用 endUiLog?
结论:不应该调用 endUiLog,而应该调用代表异常或中断的独立埋点函数。
❌ 为什么不能在 LoadError 中调用 endUiLog?
如果请求失败时也调用 endUiLog,后台系统通过 END时间戳 - START时间戳 计算出的耗时,就会把"接口报错卡顿的时间"误认为"页面成功加载的时间"。这会导致线上真实的页面渲染耗时数据被严重污染,无法筛选出真正因为 UI 卡顿导致的性能问题。
🛠️ 推荐的异常处理策略(3种方案)
根据车机系统(Volvo 级别)的严谨监控规范,面对 LoadError 应该采用以下策略:
| 方案 | 具体做法 | 适用场景 |
|---|---|---|
| 方案 A:单独定义取消/错误标签(最推荐) | 在 PerformancePointUtil 中扩展一个 cancelUiLog 或 errorUiLog 方法,输出 _ERROR 或 _CANCEL 标记。 | 需要统计接口失败率和异常等待耗时。 |
| 方案 B:直接舍弃当前耗时 | 在 LoadError 中只处理异常 UI 展示(showExceptionLayout),不调用任何 end 埋点。 | 后台分析工具会自动过滤掉只有 START 没有 END 的死链数据。 |
| 方案 C:上报错误码(Error Code) | 在日志拼装时追加错误码:项目_UI_页面_ERROR:时间戳:错误码。 | 便于自动化脚本一眼看出是因为网络超时的白屏,还是数据解析失败的白屏。 |
💻 方案 A 在 handleUiState 中的代码演进示例
kotlin
is LoadState.LoadError -> {
showExceptionLayout(state.loadState.exception)
onUiReady()
// ⚠️ 报错时不调 endUiLog,而是调专门的异常埋点
PerformancePointUtil.getInstance().errorUiLog(javaClass.simpleName)
}