调试与日志
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 |
严重错误,不应发生 |
日志的常见坑
String拼接:Log.d(TAG, "user=" + user.toString())即使日志被关闭,toString和拼接仍执行,影响性能;- Tag 重复:每个类硬编码
TAG,重构时易遗漏; - 生产包未关日志:发布版应该剔除
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 火焰图,定位主线程热点:
- 顶部选 CPU 标签;
- 点 Record → 触发要分析的操作(如点击某个按钮加载列表);
- 点 Stop,得到 Trace 文件;
- 切换
Flame Chart视图,最宽的函数即占用最多。
典型场景:
- 滑动卡顿 → 找到
onBindViewHolder内的耗时方法; - 启动慢 → 找到 Application.onCreate 链路;
- ANR → 找到主线程被锁等待的位置。
Memory Profiler
内存分析面板。每秒采样堆使用,可强制 GC、dump hprof、查泄漏。
操作流程:
- 操作应用若干轮(如打开关闭页面 5 次);
- 点 Dump Java Heap,得到 hprof 快照;
- 按
Dominator Tree看 Retained Size 最大对象; - 找到 Activity / Fragment 实例 > 1 的,那就是泄漏对象;
- 右键
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);
// ...
}
}
常见坑与最佳实践
Log.d(TAG, "x=" + obj):参数拼接在日志关闭时也执行,浪费 CPU。用 Timber 的%s占位符或包裹if (BuildConfig.DEBUG)。- 生产包未关日志:发布版仍输出 DEBUG / VERBOSE 日志,可能泄漏用户隐私。release 包通过 ProGuard 或自定义 Tree 屏蔽。
- TAG 硬编码:每个类写
private static final String TAG = "Xxx",重构时易遗漏。Timber 自动从调用栈取类名作为 TAG。 - Profiler 录制太久:CPU Trace 文件巨大且 GC 频繁,应只录制关键时段(几秒)。
- LeakCanary 报告未跟进:每次 debug 都看到泄漏通知但顺手忽略。每次都应排查并修复。
- 方法断点滥用:方法断点会让 JVM 不能 JIT 优化该方法,全方法断点 = 慢得跑不动。慎用。
- 异常断点设全局 NPE:每次任何代码触发 NPE 都暂停,会卡在 framework 内部。给特定 package 范围。
- 直接看 logcat 找问题:未过滤的 logcat 满屏无意义日志。用
package:mine+level:过滤。 - 忘记关闭 StrictMode:上线仍开启 penaltyDialog 会弹窗影响用户体验。仅在 debug 包启用。
- hprof 不分析直接删:每次 dump 几十 MB,不分析是浪费。引入 MAT 工具链路。
存量项目维护要点:阅读与调试老代码
维护存量项目的第一步是读懂老代码,这往往比写新代码更难:
- 先跑起来再说:用第 1 章的环境搭建知识把项目本地编译运行,读静态代码不如动态调试
- 用日志画执行路径:在不熟悉的模块关键方法入口加 Log 打点,跑一遍核心流程,用 logcat 还原调用链
- 善用 Debugger:Android Studio 断点调试能让你看到运行时状态,比读代码推断更准确
- git blame 了解背景:对存疑代码用 git blame 查看提交记录与 commit message,理解当年为什么这么写
- 写注释再改代码:读懂一段老代码后,先补注释记录理解,再动手改,避免下次又得重新理解一遍
- 区分“必须改”与“能动”:不是所有老代码都需要改,有测试覆盖且行为正确的模块优先保留
关键原则:阅读老代码是一项独立技能,值得投入时间。急于改代码往往引入新 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 应用的全部知识。下一步建议:动手做一个端到端项目,把每章的内容串起来,并在真实用户场景中迭代打磨。