Hook植入日志协助定位问题方案

一、方案概述

1.1 背景

在Android开发中,经常会遇到一些难以定位的Crash问题,特别是:

  • 无系统堆栈的异常 :如BadTokenExceptionWindowManager$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 关键设计点

  1. 时间窗口控制:只在特定时间段内记录日志,减少日志量

  2. 异常保护:所有监控逻辑包裹在try-catch中,避免影响业务

  3. 调用栈过滤:只记录项目相关的调用栈,避免系统调用栈干扰

  4. 信息完整性:记录方法名、参数、对象状态、调用时间等关键信息

三、实例: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时:

  1. 查看异常时间:从异常日志中获取发生时间

  2. 查找对应日志:在启动后3秒内的AddView监控日志中找到对应时间点的操作

  3. 定位问题:通过记录的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 日志信息记录原则

  1. 关键参数:记录可能导致问题的关键参数

  2. 对象状态:记录对象的关键状态信息(如Activity的isFinishing、View的isAttachedToWindow等)

  3. 调用栈:过滤项目相关的调用栈,避免系统调用栈干扰

  4. 时间戳:记录相对启动时间,便于与异常时间对应

5.3 性能优化

  1. 异常处理:所有监控逻辑都要包裹在try-catch中,避免影响业务

  2. 条件判断:先进行快速判断(时间窗口、启动时间),再执行耗时操作(获取调用栈)

  3. 日志量控制:合理设置时间窗口,避免产生过多日志

  4. 异步处理:对于耗时操作,可以考虑异步记录日志

5.4 调试技巧

  1. 添加调试日志:在关键判断点添加Timber调试日志,确认监控是否生效

  2. 日志级别:使用合适的日志级别(online/offline),线上使用online避免日志丢失

  3. 日志过滤:使用统一的Tag便于过滤和查找

  4. 日志关联:通过时间戳关联异常日志和监控日志

六、使用指南

6.1 前置条件

  1. 启动时间设置 :确保APP_START_TIME在应用启动时已正确设置

  2. Logger系统:确保Logger系统正常工作

  3. Hook系统:确保Hook系统已正确配置和启用

6.2 查看日志

日志通过Logger.online(LogType.OTHER, ...)输出,可以在以下位置查看:

  • 开发环境 :Logcat中过滤LogType.OTHER或搜索"监控"

  • 线上环境:通过日志上报系统查看,搜索对应的监控Tag

6.3 问题排查流程

  1. 获取异常信息:从Crash上报系统获取异常类型、时间和堆栈

  2. 查找监控日志:在对应时间点查找监控日志

  3. 分析日志信息:通过记录的参数、状态和调用栈分析问题

  4. 定位代码:根据调用栈定位到具体代码位置

  5. 修复问题:根据分析结果修复问题

6.4 常见问题

Q: 为什么没有看到监控日志?

A: 检查以下几点:

  1. APP_START_TIME是否已设置(会有Timber调试日志)

  2. 是否在监控时间窗口内调用了目标方法

  3. Logger系统是否正常工作

  4. Hook是否已正确配置

Q: 日志太多怎么办?

A: 可以:

  1. 缩短时间窗口

  2. 增加更精确的条件判断

  3. 只监控特定场景(如特定Activity、特定操作)

Q: 如何添加新的监控点?

A: 参考本文档的模板和示例,按照以下步骤:

  1. 确定要Hook的方法

  2. 编写Hook方法

  3. 实现日志记录逻辑

  4. 测试验证

七、总结

通过Hook植入日志的方案,我们可以:

  1. 快速定位问题:在发生无堆栈异常时,通过历史日志找到问题根源

  2. 预防问题:发现可能导致问题的操作模式

  3. 分析流程:了解应用启动或运行过程中的关键操作序列

  4. 提升效率:减少问题排查时间,提高开发效率

该方案具有以下优点:

  • 非侵入性:通过AOP实现,不需要修改业务代码

  • 灵活配置:可以根据需要调整监控范围和时间窗口

  • 性能友好:监控逻辑轻量,对性能影响极小

  • 易于扩展:可以快速添加新的监控点

  • 通用性强:适用于各种难以定位的Crash问题

相关推荐
FlightYe1 小时前
Android投屏MirrorCast全链路
android
Ehtan_Zheng2 小时前
Kotlin const val vs val:字节码、性能与隐藏陷阱详解
android·kotlin
墨狂之逸才2 小时前
Android TV 垃圾应用清理指南
android
源来猿往2 小时前
记ffmpeg-8.1.1 之Android库编译(window)
android·ffmpeg
恋猫de小郭3 小时前
Android 17 正式版发布,全新 AI 和各种破坏性更新
android·前端·flutter
我命由我123453 小时前
Jetpack Room - Room 查询返回列表无需判空、LIKE 关键字
android·java·开发语言·java-ee·android jetpack·android-studio·android runtime
朝星3 小时前
Android开发[14]:网络优化之OkHttp
android·okhttp·kotlin
私人珍藏库3 小时前
[Android] FX Player-安卓全格式播放器-比MX播放器好用
android·学习·工具·软件·多功能
写点啥呢4 小时前
车机 Android 开机优化复盘:我怎么和 AI 一起把问题定位到 SystemUI
android·人工智能