A
第 36 章JAVA40 分钟

调试与日志

Android 调试与日志:Log 与 Timber 日志库、Android Studio Profiler(CPU / Memory / Energy)、Layout Inspector、LeakCanary 集成、调试技巧(断点 / 日志点 / 条件断点 / 异常断点)。

学习目标

  • 掌握 Log 各级别 API 与最佳实践
  • 能够集成并使用 Timber 日志库
  • 了解 Android Studio Profiler 与 Layout Inspector
  • 能够集成 LeakCanary 排查内存泄漏
  • 掌握断点 / 条件断点 / 日志点等调试技巧

学习目标

  • 掌握 Log 各级别 API 与最佳实践;
  • 能够集成并使用 Timber 日志库;
  • 了解 Android Studio Profiler 与 Layout Inspector;
  • 能够集成 LeakCanary 排查内存泄漏;
  • 掌握断点 / 条件断点 / 日志点等调试技巧。

调试与日志

调试与日志贯穿整个开发周期。本章从最基础的 Log 开始,逐步到 Timber、Profiler、Layout Inspector、LeakCanary 和断点技巧。

Android Log

android.util.Log 是最基础的日志 API:

public class Log {
    public static int v(String tag, String msg);   // VERBOSE
    public static int d(String tag, String msg);   // DEBUG
    public static int i(String tag, String msg);   // INFO
    public static int w(String tag, String msg);   // WARN
    public static int e(String tag, String msg);  // ERROR
    public static int wtf(String tag, String msg); // ASSERT
}

用法:

public class UserService {
    private static final String TAG = "UserService";

    public void login(String username) {
        Log.d(TAG, "login start, user=" + username);
        try {
            doLogin(username);
            Log.i(TAG, "login success");
        } catch (IOException e) {
            Log.e(TAG, "login failed", e);
        }
    }
}

级别语义

级别 用途
Log.v 最详细,通常发布版关闭
Log.d 调试信息
Log.i 关键流程节点
Log.w 警告,可恢复
Log.e 错误,影响业务
Log.wtf 严重错误,不应发生

日志的常见坑

  1. String 拼接:Log.d(TAG, "user=" + user.toString()) 即使日志被关闭,toString 和拼接仍执行,影响性能;
  2. Tag 重复:每个类硬编码 TAG,重构时易遗漏;
  3. 生产包未关日志:发布版应该剔除 Log.v / Log.d,避免敏感信息泄漏。

BuildConfig.DEBUG 控制日志

public class MyLog {
    public static void d(String tag, String msg) {
        if (BuildConfig.DEBUG) {
            Log.d(tag, msg);
        }
    }
}

这种方式仍会拼接字符串,因为调用方在传参时已经构造好。更优方案是 Timber。

Timber 日志库

Timber 是 Jake Wharton 写的日志门面库,带占位符、自动 Tag、可插拔 Tree(按 Build 类型路由)。

依赖

dependencies {
    implementation "com.jakewharton.timber:timber:5.0.1"
}

初始化

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        if (BuildConfig.DEBUG) {
            Timber.plant(new Timber.DebugTree());
        } else {
            // 上线时改用 Crashlytics 上报
            Timber.plant(new CrashlyticsTree());
        }
    }
}

使用

Timber.d("login start, user=%s", username);
Timber.i("login success");
Timber.w("rate limit hit, retry later");
Timber.e(e, "login failed");
Timber.tag("MyTag").d("custom tag");
  • 占位符 %s 让参数在日志关闭时不拼接;
  • 自动从调用栈中提取类名作为 Tag;
  • Timber.tag(...) 可临时覆盖。

自定义 Tree

public class CrashlyticsTree extends Timber.Tree {
    @Override
    protected void log(int priority, String tag, String message, Throwable t) {
        if (priority < Log.WARN) return;
        // 仅 WARN / ERROR / ASSERT 上报
        FirebaseCrashlytics crash = FirebaseCrashlytics.getInstance();
        crash.log(tag + ": " + message);
        if (t != null) {
            crash.recordException(t);
        }
    }
}

Tree 让日志在不同环境走不同路径:debug 包打印到 logcat,release 包上报到 Crashlytics。

排除敏感日志

Timber.tag("API").d("token=%s", BuildConfig.DEBUG ? token : "***");

发布版本会走 CrashlyticsTree,把 token 显式打码再上报。

Android Studio Profiler

Android Studio 自带 Profiler,能实时分析 CPU、内存、电量、网络使用。打开方式:View → Tool Windows → Profiler,连接设备后自动检测运行中的进程。

CPU Profiler

录制一个时段的 CPU 火焰图,定位主线程热点:

  1. 顶部选 CPU 标签;
  2. 点 Record → 触发要分析的操作(如点击某个按钮加载列表);
  3. 点 Stop,得到 Trace 文件;
  4. 切换 Flame Chart 视图,最宽的函数即占用最多。

典型场景:

  • 滑动卡顿 → 找到 onBindViewHolder 内的耗时方法;
  • 启动慢 → 找到 Application.onCreate 链路;
  • ANR → 找到主线程被锁等待的位置。

Memory Profiler

内存分析面板。每秒采样堆使用,可强制 GC、dump hprof、查泄漏。

操作流程:

  1. 操作应用若干轮(如打开关闭页面 5 次);
  2. 点 Dump Java Heap,得到 hprof 快照;
  3. 按 Dominator Tree 看 Retained Size 最大对象;
  4. 找到 Activity / Fragment 实例 > 1 的,那就是泄漏对象;
  5. 右键 Show nearest GC Root 看泄漏路径。

Energy Profiler

  • CPU、网络、GPS 的能耗可视化;
  • 查看后台任务、Alarm、Job 是否在不应唤起时段运行;
  • 优化省电核心:减少长闹钟、按电量状态推迟任务。

Layout Inspector

布局调试工具,可视化当前 View 树与属性。

打开:Tools → Layout Inspector。功能:

  • 选择进程,看实时 Activity 的 View 树;
  • 单击截图中的 View → 高亮其布局位置 + 节点;
  • 查看属性 margin、padding、textSize;
  • 开 3D 模式:层叠展示,发现过度绘制;
  • 与之前对比:在 Studio Jellyfish+ 版本支持 Live Updates。

典型场景:

  • 布局错位 → 看 View 实际 x / y;
  • 间距错误 → 看 marginStart;
  • 文字溢出 → 看 width 是否 wrap_content。

LeakCanary 集成

见上一章,这里给完整集成步骤:

dependencies {
    debugImplementation "com.squareup.leakcanary:leakcanary-android:2.14"
}
// Application 不需要写代码,LeakCanary 2.x 内置 ContentProvider 自动安装
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // 监听泄漏事件
        LeakCanary.config.setRetainedInstanceCountThreshold(5);
    }
}

LeakCanary.config 允许自定义:

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // 自定义分析监听
        LeakCanary.config = LeakCanary.config.copy(
                this,
                null,                                       // 持有对象
                null,
                null,
                5,                                          // 阈值
                true,
                null
        );
    }
}

每次 Activity / Fragment 销毁,LeakCanary 在 5 秒后检查其弱引用是否被回收。如果仍存在,dump hprof 并分析泄漏链,发出通知点击后展示。

调试技巧

断点

最基本调试方式。点击代码行号左侧灰色区域设置红色圆点,运行到此处暂停。

  • Resume(F9):继续到下一个断点;
  • Step Over(F8):执行当前行不进入函数;
  • Step Into(F7):进入函数;
  • Step Out(Shift+F8):跳出函数到调用处;
  • Run to Cursor(Alt+F9):直接运行到光标位置。

条件断点

右键断点 → Condition,输入布尔表达式:

if (user.id == 42) {
    // 仅当 user.id == 42 时断点命中
}

示例:循环里只关心第 1000 个:

for (int i = 0; i < list.size(); i++) {
    process(list.get(i)); // 断点 + 条件 i == 1000
}

日志点(Logging Breakpoint)

不暂停程序但输出 log。右键断点取消 Suspend,勾选 Evaluate and log:

"processing " + i + ", size=" + list.size()

适合不想中断流程但想观察中间状态。

异常断点

Run → View Breakpoints → Add Exception Breakpoint。可以指定异常类:

  • NullPointerException:任何 NPE 抛出时暂停;
  • ClassCastException;
  • 自定义异常。

异常断点对查 “异常发生源头” 极有用,比 stacktrace 更早抓到抛出点。

方法断点

行号左点 Method Breakpoint,进入或退出方法时暂停。慎用——会显著拖慢运行,因为 JIT 无法优化该方法。

字段断点

字段读写时暂停。View Breakpoints → Add Field Watchpoint,可选读 / 写 / 读写。

Evaluate Expression

断点暂停时按 Alt+F8 输入表达式,能调用任意方法:

list.get(0).getName().toUpperCase()

修改运行时状态:

this.list = new ArrayList<>()

这在排查“为什么这个变量是这个值”时极有用。

通过 ADB 调试

某些场景无法用断点(如启动期、后台进程):

# 启动时等调试器
adb shell am set-debug-app -w com.example.myapp

# 用 gdb 调试 native
adb pull /data/anr/traces.txt

# 启动 Activity 并输出 trace
adb shell am start -W -n com.example.myapp/.MainActivity

# 查看所有运行中的进程
adb shell ps | grep myapp

# 抓 logcat 过滤 tag
adb logcat -s UserService:D Crashlytics:W

通过 Android Studio Logcat

新版 Android Studio 的 Logcat 面板(Dolphin+ 起)支持复杂过滤:

package:mine level:DEBUG tag~^My.*
  • package:mine 仅当前应用;
  • level:DEBUG 仅 DEBUG 及以上;
  • tag~^My.* Tag 正则匹配;
  • 多条件用空格连。

systrace / Perfetto

系统级 trace:

adb shell perfetto -o /sdcard/trace.perfetto-trace \
  --txt -c - <<EOF
buffers { size_kb: 8960 }
data_sources {
  config { name: "linux.ftrace" ftrace_config {
    ftrace_events: "sched/sched_switch"
    ftrace_events: "sched/sched_waking"
  } }
}
EOF
adb pull /sdcard/trace.perfetto-trace

用 Perfetto UI 打开。可看到:

  • 主线程是否在 16ms 内做完一帧;
  • Binder 调用等待了多久;
  • 哪个进程在抢 CPU。

内存分析 (hprof)

从 Profiler dump hprof → 用 MAT 或 LeakCanary 分析:

  • Shallow Size:对象本身大小;
  • Retained Size:对象被回收能释放的总大小(含其持有的引用链);
  • Dominator Tree:按 Retained Size 排序,看谁主导。

常见排查:搜 MainActivity → 看 mContext 引用链是否被长生命周期对象(static、Singleton、Application)持有。

代码示例:自定义调试入口

把日志、性能、调试集中在 debug 包,release 包静默:

public class DebugHost {
    private static boolean sEnabled;

    public static void init(Application app) {
        sEnabled = BuildConfig.DEBUG;
        if (sEnabled) {
            Timber.plant(new Timber.DebugTree());
            if (LeakCanary.isInAnalyzerProcess(app)) return;
            installStrictMode();
        }
    }

    public static void log(String msg, Object... args) {
        if (sEnabled) Timber.d(msg, args);
    }

    public static void logError(Throwable e, String msg) {
        if (sEnabled) {
            Timber.e(e, msg);
        } else {
            Crashlytics.log(msg);
            Crashlytics.recordException(e);
        }
    }

    private static void installStrictMode() {
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                .detectDiskReads().detectDiskWrites().detectNetwork()
                .penaltyLog().penaltyDialog().build());
        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                .detectActivityLeaks()
                .detectLeakedClosableObjects()
                .penaltyLog().build());
    }
}

在 MyApplication.onCreate 调用:

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        DebugHost.init(this);
        // ...
    }
}

常见坑与最佳实践

  1. Log.d(TAG, "x=" + obj):参数拼接在日志关闭时也执行,浪费 CPU。用 Timber 的 %s 占位符或包裹 if (BuildConfig.DEBUG)。
  2. 生产包未关日志:发布版仍输出 DEBUG / VERBOSE 日志,可能泄漏用户隐私。release 包通过 ProGuard 或自定义 Tree 屏蔽。
  3. TAG 硬编码:每个类写 private static final String TAG = "Xxx",重构时易遗漏。Timber 自动从调用栈取类名作为 TAG。
  4. Profiler 录制太久:CPU Trace 文件巨大且 GC 频繁,应只录制关键时段(几秒)。
  5. LeakCanary 报告未跟进:每次 debug 都看到泄漏通知但顺手忽略。每次都应排查并修复。
  6. 方法断点滥用:方法断点会让 JVM 不能 JIT 优化该方法,全方法断点 = 慢得跑不动。慎用。
  7. 异常断点设全局 NPE:每次任何代码触发 NPE 都暂停,会卡在 framework 内部。给特定 package 范围。
  8. 直接看 logcat 找问题:未过滤的 logcat 满屏无意义日志。用 package:mine + level: 过滤。
  9. 忘记关闭 StrictMode:上线仍开启 penaltyDialog 会弹窗影响用户体验。仅在 debug 包启用。
  10. hprof 不分析直接删:每次 dump 几十 MB,不分析是浪费。引入 MAT 工具链路。

存量项目维护要点:阅读与调试老代码

维护存量项目的第一步是读懂老代码,这往往比写新代码更难:

  1. 先跑起来再说:用第 1 章的环境搭建知识把项目本地编译运行,读静态代码不如动态调试
  2. 用日志画执行路径:在不熟悉的模块关键方法入口加 Log 打点,跑一遍核心流程,用 logcat 还原调用链
  3. 善用 Debugger:Android Studio 断点调试能让你看到运行时状态,比读代码推断更准确
  4. git blame 了解背景:对存疑代码用 git blame 查看提交记录与 commit message,理解当年为什么这么写
  5. 写注释再改代码:读懂一段老代码后,先补注释记录理解,再动手改,避免下次又得重新理解一遍
  6. 区分“必须改”与“能动”:不是所有老代码都需要改,有测试覆盖且行为正确的模块优先保留

关键原则:阅读老代码是一项独立技能,值得投入时间。急于改代码往往引入新 bug,先理解再动手是存量维护的黄金法则。

章节小结

本章覆盖了 Android 调试与日志体系:基础 Log API 与级别语义(V/D/I/W/E/WTF)、Timber 日志库(占位符、自动 Tag、自定义 Tree、与 Crashlytics 路由)、Android Studio Profiler(CPU 火焰图、Memory Heap Dump、Energy)、Layout Inspector(实时 View 树、3D 过度绘制)、LeakCanary 自动检测泄漏与配置、调试技巧(条件断点、日志点、异常断点、字段断点、Evaluate Expression、ADB trace 与 Perfetto、hprof MAT 分析)。核心要点:日志统一走 Timber 不直接用 Log,发布包屏蔽敏感日志;性能问题用 Profiler 与 Perfetto 定位;泄漏交给 LeakCanary;异常断点抓“第一现场”。

下一章预告

Part 6 至此结束。Android 开发教程系列至此完成。回顾整个系列,从搭建环境、Java 基础、UI 基础、四大组件、数据与网络、架构与工程化,到本部分体验优化,你已经具备了独立完成一款高质量 Android 应用的全部知识。下一步建议:动手做一个端到端项目,把每章的内容串起来,并在真实用户场景中迭代打磨。