
一、方案概述
1.1 背景
在Android开发中,经常会遇到一些难以定位的Crash问题,特别是:
-
无系统堆栈的异常 :如
BadTokenException、WindowManager$BadTokenException等,系统堆栈信息不完整 -
难以复现的问题:偶发性问题,需要记录关键操作的历史信息才能分析
-
启动阶段的问题:应用启动早期发生的异常,缺少上下文信息
传统的日志和异常堆栈往往无法提供足够的信息来定位这类问题。

1.2 解决方案
通过Hook技术植入日志,在关键方法调用时记录详细信息,包括:
-
方法调用时间点
-
关键参数和对象状态
-
调用栈信息
-
上下文环境
这样即使发生异常时没有完整的系统堆栈,也能通过历史日志追溯到问题根源。
传统方案 vs Hook方案对比

1.3 技术原理
使用AOP(面向切面编程)技术,通过@HookObject注解Hook目标方法,在方法调用前后植入日志记录逻辑。该方案具有以下特点:
-
非侵入性:不需要修改业务代码
-
灵活配置:可以控制监控范围和时间窗口
-
性能友好:监控逻辑轻量,对性能影响极小
-
易于扩展:可以快速添加新的监控点
-
工作原理流程图:

- 详细问题排查流程图

二、实现框架
2.1 Hook方法模板
Java
*/**
* Hook [目标类].[目标方法] 方法,用于记录[操作类型]的详细信息
** */*@Keep@HookObject(hookClass = [目标类].class, hookOpcode = HookObject.MethodOpcode.[调用类型])public static [返回类型] [方法名]([参数列表]) {
*// 记录日志*
logOperationIfNeeded("[方法描述]", [参数]);
*// 执行原方法*return [原方法调用];
}
Hook类型说明:
-
INVOKEVIRTUAL:普通实例方法 -
INVOKESTATIC:静态方法 -
INVOKEINTERFACE:接口方法 -
INVOKESPECIAL:构造函数或super方法
2.2 日志记录模板
Java
*/**
* 检查是否在监控时间窗口内,如果是则记录详细信息
** */*private static void logOperationIfNeeded(String methodName, [参数列表]) {
try {
long currentTime = System.currentTimeMillis();
long startTime = BaseCacheKeyKt.getAPP_START_TIME();
*// 如果启动时间未设置,跳过日志*if (startTime <= 0) {
Timber.tag(TAG).d("logOperationIfNeeded: APP_START_TIME未设置,跳过日志");
return;
}
long elapsed = currentTime - startTime;
*// 只在指定时间窗口内记录(可根据需要调整)*if (elapsed > 0 && elapsed <= [时间窗口毫秒数]) {
StringBuilder sb = new StringBuilder();
sb.append("【[操作类型]监控】启动后").append(elapsed).append("ms: ");
sb.append(methodName).append("\n");
*// 记录关键参数信息// [根据实际情况记录参数]// 记录调用栈(过滤项目相关类)*
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
sb.append(" StackTrace:\n");
int stackDepth = Math.min(stackTrace.length, 8); *// 最多8层*for (int i = 4; i < stackDepth; i++) { *// 跳过hook相关的前4层*StackTraceElement element = stackTrace[i];
if (element.getClassName().startsWith("cn.huolala") ||
element.getClassName().startsWith("com.lalamove")) {
sb.append(" ").append(element.toString()).append("\n");
}
}
Logger.online(LogType.OTHER, sb.toString());
}
} catch (Exception e) {
*// 记录异常,避免影响正常流程*
Timber.tag(TAG).e(e);
}
}
2.3 关键设计点
-
时间窗口控制:只在特定时间段内记录日志,减少日志量
-
异常保护:所有监控逻辑包裹在try-catch中,避免影响业务
-
调用栈过滤:只记录项目相关的调用栈,避免系统调用栈干扰
-
信息完整性:记录方法名、参数、对象状态、调用时间等关键信息
三、实例:AddView监控
3.1 问题场景
在应用启动过程中,某些addView操作可能会触发BadTokenException,但异常堆栈往往不完整,无法定位是哪个addView操作导致的问题。
3.2 实现方案
3.2.1 Hook ViewGroup.addView方法
ViewGroup.addView有4个重载方法,需要全部Hook:
Java
*// Hook ViewGroup.addView(View)*@Keep@HookObject(hookClass = ViewGroup.class, hookOpcode = HookObject.MethodOpcode.INVOKEVIRTUAL)public static void addView(ViewGroup viewGroup, View child) {
logAddViewIfNeeded("ViewGroup.addView(View)", viewGroup, child, null);
viewGroup.addView(child);
}
*// Hook ViewGroup.addView(View, int)*@Keep@HookObject(hookClass = ViewGroup.class, hookOpcode = HookObject.MethodOpcode.INVOKEVIRTUAL)public static void addView(ViewGroup viewGroup, View child, int index) {
logAddViewIfNeeded("ViewGroup.addView(View, int)", viewGroup, child, null);
viewGroup.addView(child, index);
}
*// Hook ViewGroup.addView(View, LayoutParams)*@Keep@HookObject(hookClass = ViewGroup.class, hookOpcode = HookObject.MethodOpcode.INVOKEVIRTUAL)public static void addView(ViewGroup viewGroup, View child, ViewGroup.LayoutParams params) {
logAddViewIfNeeded("ViewGroup.addView(View, LayoutParams)", viewGroup, child, params);
viewGroup.addView(child, params);
}
*// Hook ViewGroup.addView(View, int, LayoutParams)*@Keep@HookObject(hookClass = ViewGroup.class, hookOpcode = HookObject.MethodOpcode.INVOKEVIRTUAL)public static void addView(ViewGroup viewGroup, View child, int index, ViewGroup.LayoutParams params) {
logAddViewIfNeeded("ViewGroup.addView(View, int, LayoutParams)", viewGroup, child, params);
viewGroup.addView(child, index, params);
}
3.2.2 Hook WindowManager.addView方法
WindowManager.addView是接口方法,可能导致BadTokenException:
Java
*// Hook WindowManager.addView(View, LayoutParams)*@Keep@HookObject(hookClass = WindowManager.class, hookOpcode = HookObject.MethodOpcode.INVOKEINTERFACE)public static void addView(WindowManager windowManager, View view, WindowManager.LayoutParams params) {
logAddViewIfNeeded("WindowManager.addView(View, LayoutParams)", null, view, params);
windowManager.addView(view, params);
}
3.2.3 日志记录实现
Java
*/**
* 检查是否在启动3秒内,如果是则打印addView信息
** */*private static void logAddViewIfNeeded(String methodName, ViewGroup parent, View view, Object params) {
try {
long currentTime = System.currentTimeMillis();
long startTime = BaseCacheKeyKt.getAPP_START_TIME();
*// 如果启动时间未设置,不打印*if (startTime <= 0) {
Timber.tag(TAG).d("logAddViewIfNeeded: APP_START_TIME未设置,跳过日志");
return;
}
long elapsed = currentTime - startTime;
*// 只在启动前3秒内打印*if (elapsed > 0 && elapsed <= 3000) {
StringBuilder sb = new StringBuilder();
sb.append("【AddView监控】启动后").append(elapsed).append("ms: ");
sb.append(methodName).append("\n");
*// 记录View信息*if (view != null) {
sb.append(" View: ").append(view.getClass().getName());
if (view.getId() != View.NO_ID) {
try {
String resName = view.getResources().getResourceEntryName(view.getId());
sb.append(" (id=").append(resName).append(")");
} catch (Exception e) {
sb.append(" (id=").append(view.getId()).append(")");
}
}
sb.append("\n");
}
*// 记录父容器信息*if (parent != null) {
sb.append(" Parent: ").append(parent.getClass().getName()).append("\n");
}
*// 记录LayoutParams信息*if (params != null) {
sb.append(" Params: ").append(params.getClass().getSimpleName());
if (params instanceof WindowManager.LayoutParams) {
WindowManager.LayoutParams wmParams = (WindowManager.LayoutParams) params;
sb.append(" type=").append(wmParams.type)
.append(" flags=0x").append(Integer.toHexString(wmParams.flags));
}
sb.append("\n");
}
*// 记录调用栈*
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
sb.append(" StackTrace:\n");
int stackDepth = Math.min(stackTrace.length, 8);
for (int i = 4; i < stackDepth; i++) {
StackTraceElement element = stackTrace[i];
if (element.getClassName().startsWith("cn.huolala") ||
element.getClassName().startsWith("com.lalamove")) {
sb.append(" ").append(element.toString()).append("\n");
}
}
Logger.online(LogType.OTHER, sb.toString());
}
} catch (Exception e) {
*// 记录异常,帮助调试*
Timber.tag(TAG).e(e);
}
}
3.3 日志输出示例
Plain
【AddView监控】启动后1234ms: ViewGroup.addView(View, LayoutParams)
View: android.widget.TextView (id=title_text)
Parent: android.widget.LinearLayout
Params: LinearLayout$LayoutParams
StackTrace:
cn.huolala.porter.biz.home.HomeActivity.onCreate(HomeActivity.kt:123)
cn.huolala.porter.biz.home.HomeActivity.initView(HomeActivity.kt:456)
cn.huolala.porter.biz.home.HomeActivity.setupViews(HomeActivity.kt:789)
3.4 使用效果
当发生BadTokenException时:
-
查看异常时间:从异常日志中获取发生时间
-
查找对应日志:在启动后3秒内的AddView监控日志中找到对应时间点的操作
-
定位问题:通过记录的View信息、Parent信息和调用栈,快速定位问题代码
应用启动过程中的AddView监控时间线

四、扩展应用场景
4.1 Toast显示监控(BadTokenException)
问题 :Toast在Activity销毁后显示可能触发BadTokenException
实现:
Java
@Keep@HookObject(hookClass = Toast.class, hookOpcode = HookObject.MethodOpcode.INVOKEVIRTUAL)public static void show(Toast toast) {
logToastIfNeeded("Toast.show()", toast);
toast.show();
}
private static void logToastIfNeeded(String methodName, Toast toast) {
try {
long currentTime = System.currentTimeMillis();
long startTime = BaseCacheKeyKt.getAPP_START_TIME();
if (startTime <= 0) return;
long elapsed = currentTime - startTime;
if (elapsed > 0 && elapsed <= 5000) { *// 启动后5秒内*StringBuilder sb = new StringBuilder();
sb.append("【Toast监控】启动后").append(elapsed).append("ms: ");
sb.append(methodName).append("\n");
if (toast != null) {
View view = toast.getView();
if (view != null) {
sb.append(" View: ").append(view.getClass().getName()).append("\n");
}
sb.append(" Duration: ").append(toast.getDuration()).append("\n");
}
*// 记录调用栈...*
Logger.online(LogType.OTHER, sb.toString());
}
} catch (Exception e) {
Timber.tag(TAG).e(e);
}
}
4.2 Dialog显示监控
问题:Dialog在Activity销毁后显示可能触发异常
实现:
Java
@Keep@HookObject(hookClass = Dialog.class, hookOpcode = HookObject.MethodOpcode.INVOKEVIRTUAL)public static void show(Dialog dialog) {
logDialogIfNeeded("Dialog.show()", dialog);
dialog.show();
}
private static void logDialogIfNeeded(String methodName, Dialog dialog) {
try {
long currentTime = System.currentTimeMillis();
long startTime = BaseCacheKeyKt.getAPP_START_TIME();
if (startTime <= 0) return;
long elapsed = currentTime - startTime;
if (elapsed > 0 && elapsed <= 5000) {
StringBuilder sb = new StringBuilder();
sb.append("【Dialog监控】启动后").append(elapsed).append("ms: ");
sb.append(methodName).append("\n");
if (dialog != null) {
sb.append(" Dialog: ").append(dialog.getClass().getName()).append("\n");
Window window = dialog.getWindow();
if (window != null) {
sb.append(" Window: ").append(window.getClass().getName()).append("\n");
}
Context context = dialog.getContext();
if (context instanceof Activity) {
Activity activity = (Activity) context;
sb.append(" Activity: ").append(activity.getClass().getName())
.append(" isFinishing=").append(activity.isFinishing())
.append(" isDestroyed=").append(activity.isDestroyed()).append("\n");
}
}
*// 记录调用栈...*
Logger.online(LogType.OTHER, sb.toString());
}
} catch (Exception e) {
Timber.tag(TAG).e(e);
}
}
4.3 PopupWindow显示监控
问题:PopupWindow在View detached后显示可能触发异常
实现:
Java
@Keep@HookObject(hookClass = PopupWindow.class, hookOpcode = HookObject.MethodOpcode.INVOKEVIRTUAL)public static void showAtLocation(PopupWindow popupWindow, View parent, int gravity, int x, int y) {
logPopupWindowIfNeeded("PopupWindow.showAtLocation()", popupWindow, parent);
popupWindow.showAtLocation(parent, gravity, x, y);
}
private static void logPopupWindowIfNeeded(String methodName, PopupWindow popupWindow, View parent) {
try {
long currentTime = System.currentTimeMillis();
long startTime = BaseCacheKeyKt.getAPP_START_TIME();
if (startTime <= 0) return;
long elapsed = currentTime - startTime;
if (elapsed > 0 && elapsed <= 5000) {
StringBuilder sb = new StringBuilder();
sb.append("【PopupWindow监控】启动后").append(elapsed).append("ms: ");
sb.append(methodName).append("\n");
if (popupWindow != null) {
sb.append(" PopupWindow: ").append(popupWindow.getClass().getName()).append("\n");
View contentView = popupWindow.getContentView();
if (contentView != null) {
sb.append(" ContentView: ").append(contentView.getClass().getName()).append("\n");
}
}
if (parent != null) {
sb.append(" Parent: ").append(parent.getClass().getName())
.append(" attached=").append(parent.isAttachedToWindow()).append("\n");
}
*// 记录调用栈...*
Logger.online(LogType.OTHER, sb.toString());
}
} catch (Exception e) {
Timber.tag(TAG).e(e);
}
}
4.4 其他场景
可以根据实际需要Hook其他关键方法,如:
-
Activity生命周期方法(onCreate、onResume等)
-
Fragment生命周期方法
-
网络请求方法
-
文件操作方法
-
数据库操作方法
五、最佳实践
5.1 时间窗口选择
-
启动阶段问题:3-5秒,覆盖应用启动早期
-
特定操作监控:根据操作特点选择合适的时间窗口
-
长期监控:可以设置较大的时间窗口或移除时间限制
-
动态调整:根据实际日志量调整,避免产生过多日志
5.2 日志信息记录原则
-
关键参数:记录可能导致问题的关键参数
-
对象状态:记录对象的关键状态信息(如Activity的isFinishing、View的isAttachedToWindow等)
-
调用栈:过滤项目相关的调用栈,避免系统调用栈干扰
-
时间戳:记录相对启动时间,便于与异常时间对应
5.3 性能优化
-
异常处理:所有监控逻辑都要包裹在try-catch中,避免影响业务
-
条件判断:先进行快速判断(时间窗口、启动时间),再执行耗时操作(获取调用栈)
-
日志量控制:合理设置时间窗口,避免产生过多日志
-
异步处理:对于耗时操作,可以考虑异步记录日志
5.4 调试技巧
-
添加调试日志:在关键判断点添加Timber调试日志,确认监控是否生效
-
日志级别:使用合适的日志级别(online/offline),线上使用online避免日志丢失
-
日志过滤:使用统一的Tag便于过滤和查找
-
日志关联:通过时间戳关联异常日志和监控日志
六、使用指南
6.1 前置条件
-
启动时间设置 :确保
APP_START_TIME在应用启动时已正确设置 -
Logger系统:确保Logger系统正常工作
-
Hook系统:确保Hook系统已正确配置和启用
6.2 查看日志
日志通过Logger.online(LogType.OTHER, ...)输出,可以在以下位置查看:
-
开发环境 :Logcat中过滤
LogType.OTHER或搜索"监控" -
线上环境:通过日志上报系统查看,搜索对应的监控Tag
6.3 问题排查流程
-
获取异常信息:从Crash上报系统获取异常类型、时间和堆栈
-
查找监控日志:在对应时间点查找监控日志
-
分析日志信息:通过记录的参数、状态和调用栈分析问题
-
定位代码:根据调用栈定位到具体代码位置
-
修复问题:根据分析结果修复问题
6.4 常见问题
Q: 为什么没有看到监控日志?
A: 检查以下几点:
-
APP_START_TIME是否已设置(会有Timber调试日志) -
是否在监控时间窗口内调用了目标方法
-
Logger系统是否正常工作
-
Hook是否已正确配置
Q: 日志太多怎么办?
A: 可以:
-
缩短时间窗口
-
增加更精确的条件判断
-
只监控特定场景(如特定Activity、特定操作)
Q: 如何添加新的监控点?
A: 参考本文档的模板和示例,按照以下步骤:
-
确定要Hook的方法
-
编写Hook方法
-
实现日志记录逻辑
-
测试验证
七、总结
通过Hook植入日志的方案,我们可以:
-
快速定位问题:在发生无堆栈异常时,通过历史日志找到问题根源
-
预防问题:发现可能导致问题的操作模式
-
分析流程:了解应用启动或运行过程中的关键操作序列
-
提升效率:减少问题排查时间,提高开发效率
该方案具有以下优点:
-
非侵入性:通过AOP实现,不需要修改业务代码
-
灵活配置:可以根据需要调整监控范围和时间窗口
-
性能友好:监控逻辑轻量,对性能影响极小
-
易于扩展:可以快速添加新的监控点
-
通用性强:适用于各种难以定位的Crash问题