写日志这事,在 Android 开发里简直太日常了,日常到很多新手根本不把它当回事。可真到了联调、排查线上事故、给测试同学定位问题的时候,日志写得好不好、能不能快速从 Android Studio 的 Logcat 里捞到有用的信息,直接决定你是一个小时定位问题,还是一整天都在抓瞎。说白了,Logcat 就像飞机驾驶舱里的仪表盘,平时不看它也能飞,但一旦出状况,它就是那个帮你找出真相、甚至保命的东西。
这篇东西主要围绕在 Android Studio 里如何高效地写入日志、查看日志来聊,核心是 Logcat 这个自带工具。我会从最基础的界面操作讲起,一直聊到代码里怎么写规范日志、怎么用命令行抓取日志、遇到日志不显示或者丢掉这种坑怎么排查,适合刚入门 Android 开发的新手,也适合那些写了好一阵子代码但一直在用“土办法”看日志的同学。看完你会明白,日志不该是乱打的,Logcat 也不该是随便看一眼就切走的工具。
1. 内容整体设计与思路拆解
1.1 为什么要专门讲 Logcat 的“写入”和“查看”
一句话解释 Logcat:它是 Android 系统提供的一个环形日志缓冲区,把系统进程和应用进程通过内核 logger 写出来的日志统一收集起来,开发者可以通过 adb 命令或者 Android Studio 的 Logcat 窗口实时读取。这个“环形缓冲区”的机制有点像行车记录仪的存储卡,存满了就覆盖最老的记录,只保留最近一段时间的数据,所以抓日志讲究时效性。
在 Android Studio 里,Logcat 窗口的定位不是“看报错的小黑框”,而是开发者的主战场之一。为什么这么讲?因为一个应用从启动、布局渲染、网络请求到崩溃,所有关键事件都可以通过日志串起来。比如你在某个页面发现数据没有加载出来,第一件事不该是去看代码逻辑对不对,而是先看 Logcat 里网络层有没有打印出请求失败的信息、有没有异常堆栈,先锁定问题的大致范围,再去定位代码里的具体位置。而要做到这一步,前提有两个:第一,代码里得有足够的日志输出;第二,你得会用工具把日志筛出来看明白。这两件事,恰好对应标题里的“写入”和“查看”,是相辅相成的两块。
1.2 常规方案与备选工具的取舍
写日志的方式不止一种,市面上也有第三方日志框架,比如最常用的 Timber、Logger,还有早期很多人用的 Log4j 移植版。这些框架底层封装的依然是 android.util.Log,只是在上层做了 Tag 管理、格式化、日志文件输出等增强。实际开发中我的建议是:不要一上来就引入框架,先把系统自带的 android.util.Log 用熟练,理解了 Tag 和 Level 这两个核心概念之后,再去根据项目需要选框架。因为框架解决的是“日志写起来更方便、管理更规范”,但不会替你解决“日志怎么看、怎么分析”的问题。
查看日志的工具同样不止 Android Studio 自带的 Logcat 面板,还有 adb logcat 命令行、DDMS(旧版调试工具)、第三方的 Logcat 可视化工具,以及像 Android Studio 里的 Profiler 可以配合抓取 CPU、内存等数据。这里面的核心原则是:能直接用 Android Studio 自带功能解决的问题,就不额外安装第三方工具;需要脱离 IDE 在真机现场抓数据的时候,用 adb 命令行更靠谱;需要拿到系统级别的完整日志(比如 SystemServer、Input 系统、重启原因),才考虑 adb bugreport。这篇文章的重点放在最常用、最核心的 Logcat 窗口和 adb logcat 命令上,这两块用好了,日常开发基本就能覆盖九成场景。
2. 核心细节解析与实操要点
2.1 Logcat 窗口核心概念:Tag、Level、Message
在 Android Studio 的 Logcat 窗口里,任何一条日志都有三个关键元素:TAG(标签)、LEVEL(级别)、MESSAGE(消息内容)。很多人看日志只盯着右下角的红色报错看,这是不对的。红色报错只是结果,中间的级别和它们之间的关系才是关键。
TAG 是日志的来源标识,通常是类名或者某个业务模块名。它的作用就是给日志做分类,让你能快速筛选出某一个模块的输出。比如网络请求库一般会打一个 OkHttp 或 Retrofit 的 Tag,你用 Logcat 的过滤条件输入这个 Tag,就能只看网络层的日志,其他全部隐藏。
LEVEL 是日志的优先级,从低到高分别是 Verbose(冗余)、Debug(调试)、Info(信息)、Warn(警告)、Error(错误)、Assert(断言失败)。这 6 个级别的设计和日志保留策略有关。Verbose 和 Debug 级别通常只在开发阶段开启,发布到线上的包应该把它们关掉,否则会泄露出大量代码内部细节;Info 级别的日志适合记录一个关键操作的结果,比如“用户登录成功”;Warn 是有些隐患但不至于报错的情况;Error 则是必须关注的错误信息。
MESSAGE 就是日志的具体内容,是你在代码里拼接的那个字符串。三者的关系可以用生活化的方式理解:TAG 像是快递包裹上的收件人标签,Level 像是包裹上的“易碎品 / 普通件”标识,MESSAGE 才是包裹里面的实际货物。看日志的时候,不要只盯着某一行看,要把同一个 Tag 下面一段时间的日志连在一起读,还原当时的执行顺序。
2.2 日志级别的选择标准:什么时候用什么级别
这一点值得单独拿出来聊,因为很多新手的日志级别用得一团乱。最常见的现象是:所有日志不分青红皂白都是 Log.e,或者全用 Log.i,导致最后日志里全是垃圾信息,真正出错的地方反而被淹没了。日志级别的选择,有一个很朴素的标准——这条日志是给谁看、在什么场景下起作用。
Verbose:最细粒度的信息,比如 for 循环里每一轮处理的具体值。这个级别在连载 CPU 占用高、耗时长的任务时有奇效,但通常只在开发环境开。Android 官方建议,Verbose 日志不应该被编译进正式发布的应用里,因为它影响性能且没有保留价值。
Debug:调试用的信息,用于确认某个分支走没走、某个变量的值对不对。这类日志在开发时最常用,可以用 BuildConfig.DEBUG 控制只让 debug 包输出。
Info:表达一个有意义的事件发生了,比如 Activity 生命周期切换、网络请求发出去了、数据库迁移完成,不对应任何错误,但事后可以通过这些信息把执行路径还原出来。
Warn:某些情况发生了但程序还能继续跑,比如缓存文件不存在自动重建、用户输入了 null 值走了默认分支,这类日志值得保留,因为它们往往暗示逻辑上有潜在问题。
Error:这个级别只留给真正的异常和错误,比如网络请求失败返回错误码、JSON 解析抛异常、某个关键的初始化步骤失败。Error 日志一定要带上尽量完整的上下文信息,比如请求 URL、状态码、异常堆栈,别只丢一句“failed”。
Assert:极少用到,一般配合断言使用,表示“程序走到这里一定是出了问题,不能继续执行”。
一个容易踩的坑是:有些人觉得 Warn 比 Error 级别低,所以把不严重的错都打成 Warn,严重的打成 Error。这样分类其实没问题,但要注意,Error 日志会让 Android Studio 的 Logcat 自动聚焦到它上面,如果你把 Error 滥用成输出普通信息的渠道,Logcat 的“红色报警”就会失去意义,等真正出问题的时候反而不容易发现。我的习惯是,Error 一年到头出现的次数应该很少,如果发现某个类里全是 Log.e,说明日志级别用错了,要回头改。
2.3 在代码里写入日志:从 Log.v 到 Log.wtf
写入日志是代码层面最简单、但也最考验纪律的事情。一个正确的日志调用,要包含合适的 Tag、合理的级别、清晰的描述信息。具体到代码里,写法不难,核心是“知道该在哪里打、打什么内容”。
先看一个基础的完整示例:
import android.util.Log; public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(TAG, "onCreate called, savedInstanceState = " + savedInstanceState); setContentView(R.layout.activity_main); String userName = loadUserName(); if (userName == null) { Log.w(TAG, "userName is null, use default value"); userName = "default_user"; } Log.i(TAG, "userName loaded: " + userName); } private String loadUserName() { try { // 模拟从本地读取用户信息 return null; } catch (Exception e) { Log.e(TAG, "loadUserName failed", e); return null; } } }代码里值得注意的几个点:
第一,Tag 的定义。我习惯用private static final String TAG = "MainActivity"这种写法,类名做 Tag。不要直接在方法里写字符串字面量,不然后续要改 Tag 名字的时候,得全文搜索一堆散落的字符串。如果你的类名特别长,可以用业务简写,比如支付模块统一用Payment,但一个项目里 Tag 的命名规则要统一,否则后期按 Tag 过滤日志会很痛苦。
第二,字符串拼接和参数占位。上面示例里用了"onCreate called, savedInstanceState = " + savedInstanceState这种字符串拼接,这在 Java 里其实会有一定的性能开销,但在开发阶段打印 Debug 级别日志时完全够用,不必过度优化。如果你用 Kotlin 写,可以用Log.d(TAG, "onCreate called, savedInstanceState = $savedInstanceState"),更简洁。要打印对象的多个字段,建议打印 JSON 字符串或toString()方法返回的内容,便于阅读。
第三,捕获异常时打日志的姿势。Log.e(TAG, "loadUserName failed", e)这里的第三个参数是 Throwable,也就是异常对象。这个参数的重要性很容易被忽略——如果只写Log.e(TAG, "loadUserName failed"),你只能看到一句自定义描述,看不到异常堆栈,排查问题时等于没有日志。把 throwable 传进去,Logcat 会以完整堆栈的形式把异常打印出来,很多时候问题原因直接就在堆栈的某一行里。
第四,还有一个很少有人用的 API:Log.wtf。它的全称是 What a Terrible Failure,中文意思是“什么样的可怕错误”。它打印的日志级别是 Assert,不仅仅把日志写进缓冲区,还会在控制台震动或者弹窗提醒,甚至在特定 ROM 上会导致进程崩溃。这个 API 适合用在“绝对不可能发生、一旦发生就等于代码逻辑写错了”的场景,比如一个仅允许被某类调用、结果却被别的模块调用了的接口,可以在里面加一句Log.wtf。日常开发不用频繁使用,但要记住它比Log.e严重得多,不要随手打。
2.4 日志的格式化与可读性设计
日志不是写给别人看作文,而是写给自己和队友排查问题用的,所以格式非常重要。同一个项目里,如果每个人打日志的格式都不同,那 Logcat 看下来就是一场灾难。一个好的日志格式应该包含:时间、线程、Tag、级别、具体内容。Android Studio 的 Logcat 窗口默认会展示这些信息,但代码里打日志的时候,我们还需要做两件事来提高可读性。
第一件事,是多行日志怎么打。不要一次性Log.d(TAG, "line1\nline2\nline3"),这样倒是省事,但 Logcat 会把多行日志的后续行标记为“continuation”并把它们【折叠】起来,在 Android Studio 里默认只显示第一行,你要手动展开才能看到剩余内容,排查起来很容易漏掉。正确做法是把每条信息拆成单独一条日志,或者用统一的“key = value”格式拼成一行,比如:
Log.i(TAG, "request start, url=" + url + ", method=" + method + ", headers=" + headers); Log.i(TAG, "request success, code=" + code + ", costMs=" + costMs);第二件事,是结构化地打印 JSON 或网络响应。如果是网络请求,直接用Log.d(TAG, responseBody)把整个 JSON 打印出来,在 Logcat 里会是一长串没有换行的字符串,看起来极其痛苦。你可以在打印前先对 JSON 字符串做一次格式化,用org.json.JSONObject的toString(2)方法,输出带缩进的格式化文本。但这又涉及上面说的多行折叠问题——Android Studio 对折叠处理得还行,点击即可展开,所以在可读性和可折叠之间做个取舍即可。我个人建议是:网络响应这种大段内容,单独打一个 Tag,比如NetResponse,不要混在OkHttp这种通用 Tag 里,这样方便在 Logcat 里单独筛出来看。
3. 实操过程与核心环节实现
3.1 Android Studio 里 Logcat 窗口的完整操作指南
先讲怎么打开和基本布局。在 Android Studio 中,底部工具栏默认有 Logcat 窗口,如果没有显示,用快捷键打开:Windows 上是 Alt+6,macOS 上是 Cmd+6。窗口打开之后,你会看到右上角有几个关键控件,从左到右依次是设备下拉框、进程下拉框、日志级别过滤器下拉框、搜索框、暂停按钮、清空按钮。刚接触的人经常搞混“设备下拉框”和“进程下拉框”,这两个东西第一个管的是选择哪台真机或者模拟器,第二个管的是选择这台设备上的哪一个进程,尤其是多开应用的时候,进程选错了,日志自然对不上。
日志级别过滤器下拉框,是所有新手最先要学会用的东西。Android Studio 的 Logcat 窗口升级到新版之后,这个下拉框自带了一个“正则表达式显示所有级别”的特性,你可以选择只显示某些级别,比如只显示 Warn 和 Error,这样能快速把刷屏的日志过滤掉,只看重点关注的内容。但在开发时注意,如果过滤只显示 Error,那些起了警告作用但还不报错的 Warn 日志就看不到了,定位问题有时候反而会漏掉线索。我的习惯是默认选 Info,通过搜索框二次过滤,要用时再切级别。
搜索框是 Logcat 里最高频用到的功能。它的逻辑是先通过设备选择确认想看的设备,再用搜索框按关键字把日志筛出来。搜索结果默认是“包含关键字”的逻辑,但点击搜索框左侧的漏斗图标可以选择“正则表达式匹配”“忽略大小写”等高级模式。比如你想把所有 Activity 的生命周期日志和不含特定 Tag 的日志都筛出来,就可以用正则(MainActivity|SecondActivity).*(onCreate|onResume)来匹配。不过对于大多数场景,直接用普通关键字就够。
还有一个特别有用、但很多人不知道的操作:在日志上单击左键,可以直接在搜索框里以该日志的 Tag 为关键字进行过滤;右键单击日志,可以把该行复制出来,或者复制它的原始日志内容。如果你要跟同事沟通一个问题,把复制的日志粘贴到聊天工具里,比发一张截图更有用,因为对方可以直接在本地检索关键字。
3.2 用 Logcat 查看崩溃日志与异常堆栈
看崩溃日志是 Logcat 的高频场景,也是新手最容易手足无措的场景。当应用崩溃时,Logcat 里会出现类似FATAL EXCEPTION: main的一段日志,后面跟着Process: com.example.app, PID: 12345以及一大串java.lang.NullPointerException之类的堆栈。这里的要点是:不要看第一行报错信息就慌,要把整段堆栈从头到尾看完,重点找“我们自己代码的类名出现的那一行”,我们习惯称之为“堆栈里离自己最近的那个引用”。
为什么会这样?因为一个异常堆栈可能夹杂了几十行系统框架的调用,它们只是帮你定位的辅助信息,真正引起异常的是堆栈上最近一个出现在你自己代码里的方法调用。举个例子,如果异常是空指针,堆栈底部会告诉你at com.example.MainActivity.onCreate(MainActivity.java:20),这一行直接指向了你代码里的具体位置,其他什么at android.app.Activity.performCreate之类的基本可以忽略。如果堆栈里找不到自己写的类,那才需要担心是不是系统层面的框架 bug 或者第三方库的调用问题。
看崩溃日志时,还有两个小技巧。第一,崩溃之后日志会被应用的进程缓存,但如果在 Android Studio 里切走了设备,回来可能看不到最近的崩溃日志,这时可以用 adb 命令把日志 dump 出来看,而不是只依赖 IDE 界面。第二,Android 的异常捕获机制可能会导致 Logcat 里有多个“FATAL EXCEPTION”,比如主线程崩了还有 ANR (Application Not Responding,应用无响应)日志,要认清哪个才是导致用户看到“XX 已停止运行”的那一条。一般来说,Process: <应用包名>那一条就是目标异常。
3.3 用 Logcat 分析网络请求与业务逻辑
网络请求的排查,是 Logcat 应用最广泛、最有价值的场景。开发一个需要联网的应用时,日志打得够不够细,直接决定你调试网络请求的速度。先看一段模拟网络请求的日志输出长什么样:
D/OkHttp: --> GET http://api.example.com/v1/user/info D/OkHttp: Accept: application/json D/OkHttp: User-Agent: DemoApp/1.0.0 (Android 14; samsung SM-S918B) D/OkHttp: --> END GET D/NetResponse: {"code":0,"message":"success","data":{"userName":"张三","avatar":"http://avatar.example.com/1.png"}}这条日志包含的信息足够多,你一眼就能看到请求发了什么地址、带了什么请求头、服务器返回了什么 JSON。排查问题的顺序也因此变得简单:如果响应里 code 不等于 0,基本是业务逻辑或参数问题,去看后端接口文档;如果连日志都没有,那就是请求根本没发出去,要检查网络权限、域名、拦截器等;如果有响应但 JSON 解析报错,那就是序列化层的问题。
在实际项目中,我用过很多方案来打印这种网络日志。最省事的是在 OkHttp 的拦截器里统一打印,而不是在每个请求代码里手动加日志。它的好处是日志集中、格式统一、Tag 固定,排查时只要筛选这一个 Tag 就行。不过要注意,不要在生产环境中把包括请求体、响应体在内的完整日志打印出来,因为请求参数里可能包含手机号、身份证、Token 等敏感信息,这类数据一旦进入日志,就可能被日志系统、临时文件等渠道泄露出去。线上包可以只打印请求地址和状态码,把 body 打码。
3.4 adb logcat 命令行抓取日志:脱离 IDE 的保命技能
离开 Android Studio 之后,用 adb logcat 命令抓日志是每个 Android 开发者都必须掌握的技能。因为它不仅能抓应用日志,还能抓系统日志、内核日志,甚至能配合 dumpsys、bugreport 抓取更完整的信息。下面列几个最常用的 adb logcat 用法:
# 查看实时日志(会持续输出,Ctrl+C 停止) adb logcat # 根据关键字过滤(只显示包含关键字 Activity 的日志) adb logcat | grep Activity # 输出到文件(方便保存分析) adb logcat > app.log # 清空日志缓冲区(在多轮抓取时非常有用) adb logcat -c # 输出当前缓冲区已有日志后退出(不阻塞) adb logcat -d # 根据 Tag 和级别过滤(比如只看 MainActivity 的 Error 日志) adb logcat -s MainActivity:E # 带时间戳输出(推荐,方便和业务时间点对齐) adb logcat -v threadtime这里解释几个容易困惑的点。第一,adb logcat和adb logcat -d的区别是前者实时刷新,后者打印缓冲区当前内容后自动退出;如果你想拿到崩溃前最后一刻的日志,常用adb logcat -d把现存的日志 dump 出来。第二,-s MainActivity:E这种写法是“静默模式 + 指定过滤”,意思是除了 MainActivity 这个 Tag 的 Error 级别日志,其他全部不显示,用来精确定位单个模块的问题时非常高效。第三,如果同时接入了多台设备,需要在命令前指定设备序列号:adb -s <device_serial> logcat,否则 adb 会报“more than one device”错误。
把日志保存到文件时,尽量用-v threadtime先把线程号和时间戳加上。没有时间戳的日志在分析“异常是先发生还是后发生”时毫无价值。另外提醒一个坑:直接用adb logcat > app.log重定向标准输出时,可能出现日志乱码的问题,尤其是中文日志,建议在命令前设置adb logcat -v threadtime > app.log 2>&1把错误输出也一起重定向,确保文件完整性。
3.5 bugreport 抓取完整日志:定位系统级疑难杂症
有些问题不是应用代码的问题。比如应用闪退但 Logcat 里找不到任何异常、设备重启后应用崩溃、低内存时进程被系统杀死,这种时候单靠 Logcat 就有点乏力了,需要用到 Android 系统自带的 bugreport 工具。它会把当前设备上包括 Logcat、系统事件缓存、CPU/内存占用、GPU 渲染信息、网络状态、正在运行的服务在内的所有诊断信息打包成一个文件。
抓取 bugreport 的命令很简单:
# 生成 bugreport 文件并拉取到电脑 adb bugreport这个命令执行之后,在较新的 adb 版本里会自动把生成的 .zip 文件保存到当前电脑目录。这个 zip 文件打开后,里面有bugreport-*.txt(可读文本)、FS目录(文件系统信息)、traces目录(ANR 堆栈)等。想快速找到应用相关的信息,可以在生成的 txt 文件里搜索应用包名,应用配套设施、死锁信息、内存快照等都会以不同形式出现。比如排查“后台时被系统杀掉”的问题,可以看lowmemorykiller相关的日志;排查“系统重启原因”,看SYSTEM_BOOT和last_kmsg,后者需要 root 权限,但部分设备在 bugreport 里会带上 Restart 相关的事件记录。
需要记住的是,bugreport 的抓取是一个重度操作,会消耗较长的时间,文件体积也可能到几百兆,不要在没必要时经常抓。它的定位是“Logcat 不够用、需要系统全景视角”时才上的杀手锏。
4. 常见问题与排查技巧实录
4.1 日志不显示,先检查设备、进程和级别过滤器
很多开发者在 Logcat 里看不到自己打印的日志,第一反应是代码写错了,但实际上大部分原因是过滤条件没设对。花两分钟按下面顺序排查,基本都能解决。
第一步,确认设备下拉框选的是当前连接的手机,而不是某个很久以前挂着的模拟器;在多设备环境下这个错误极其常见,选了旧设备,新日志当然看不到。
第二步,确认进程下拉框选的是当前应用包名。如果你同时调试的是双开应用或一个进程里有多个渠道包,进程选错会导致该进程的日志完全被过滤掉。
第三步,确认级别过滤器不是选在 “Error” 之上。比如你打的是Log.d,但过滤器默认只显示 Warn 及以上级别,那 Debug 日志就不会出现,这是新手最常见的原因。
第四步,确认 Android Studio 的 Logcat 窗口没有被正则表达式搜索框里的旧关键字锁死。搜索框里留着一个你上一次调试用过的关键字,再往下滚日志,自然什么都看不到。
如果以上四项都检查过还是没日志,那才轮到代码层面怀疑——是不是这个类的日志没执行到,或者是这个类所在进程压根没启动。
4.2 日志文件已保存但内容为空或不全
用 adb logcat 重定向到文件时,输出的日志不全甚至为空,这种情况一般有两个原因。第一个原因是 shell 的重定向缓冲机制,当你用adb logcat > app.log并提前中断命令(比如 Ctrl+C)时,缓冲区的数据可能还没来得及写入文件,导致最终文件缺失一部分。解决办法是用stdbuf(如果环境支持)或者在停止前用adb logcat -d > app.log这种一次性导出方式,导出完成后自动退出,数据完整。
第二个原因是日志缓冲区被截断或者覆盖。Android 的 logcat 环形缓冲区默认大小有限,不同系统上可能只有几百 KB 到几 MB。如果在日志量特别大的模块(比如系统日志、网络打印)里持续打日志,早期内容会被覆盖。可以通过以下命令调整缓冲区大小:
# 查看当前缓冲区状态 adb logcat -g # 设置 main 缓冲区为 16MB(重启后失效) adb logcat -G 16M开发调试时建议直接把缓冲区调大,特别在要复现一个偶现 bug、等它出现的场景里,缓冲区太小的后果是你等了半天 bug 终于复现了,想回看日志时关键部分已经被刷掉了。
4.3 日志太多刷屏,如何快速过滤出关键信息
日志刷屏是开发中绕不开的问题。系统的高频日志、其他应用的日志、自己的冗余日志混在一起,让人很难快速定位。这里有三个亲测有效的过滤思路。
第一个思路是多 Tag 过滤。新版本 Android Studio 的 Logcat 支持在搜索框里通过添加过滤标签的方式实现多 Tag 联合过滤。比如你想同时看 MainActivity 和 NetResponse 两个 Tag 的日志,可以在过滤配置里分别添加两个过滤条件,而不是手动输入正则表达式。这样 Logcat 只显示这两个 Tag 的日志,其他全部屏蔽。
第二个思路是利用日志级别配合关键字。比如你怀疑是网络问题,可以搜索url或者code=之类的关键字,然后再通过级别过滤器把范围缩小到 Warn 和 Error,往往能快速定位异常在哪。
第三个思路是利用adb logcat -s在命令行直接做白名单过滤。这种方式尤其适合真机联调时用,因为命令行可以配合 shell 脚本做更复杂的处理,比如把同一个 Tag 的日志按时间排序、统计出现的频率等。不过需要注意,-s后面可以跟多个 Tag:adb logcat -s MainActivity:D OkHttp:W表示 MainActivity 的 Debug 和 OkHttp 的 Warn 日志都保留,其余丢弃。
4.4 关于日志性能与隐私的禁忌
日志虽然好用,但有几个场景下要严格约束自己。第一个是性能问题。打印日志本身是 IO 操作,单条日志的成本不高,但如果在一个被频繁调用(比如 for 循环里、onDraw 里、RecyclerView 的 onBindViewHolder 里)的方法里打印日志,累积起来会导致明显的卡顿。在开发环境或许感觉不到,但把 Debug 日志带到线上包,等于每分每秒都在为日志付性能账单。解决办法是用BuildConfig.DEBUG包一层开关,比如:
if (BuildConfig.DEBUG) { Log.d(TAG, "bind item: " + position); }Kotlin 里可以用更优雅的方式:
if (BuildConfig.DEBUG) Log.d(TAG, "bind item: $position")或者直接依赖android.util.Log.isLoggable(TAG, Log.DEBUG)在系统层面控制。这些开关可以在 debug 包打开所有日志,release 包自动关闭。
第二个是隐私问题。日志里绝对不能打印密码、短信验证码、支付密码、完整身份证号、完整手机号、Token、Cookie 等敏感信息。即使是在开发环境中,这些信息一旦被日志系统收集或者流传出去,风险都极高。如果非要打印,也要做脱敏处理。最简单的脱敏是只保留前三位和后四位,中间用星号代替;更稳妥的做法是不要打印到日志里,而是用断点来看。
4.5 观察 Logcat 里 Binder、ANR 与系统日志的细节
在实际联调过程中,Logcat 里偶尔会出现一些平时很少见的日志内容,需要知道它们大概表示什么,否则容易一头雾水。
第一类是 Arrow(->)和 Binder 相关的日志。Logcat 中经常出现形如Process: com.example.app和at com.example.MainActivity.onResume(MainActivity.java:20)这种带->箭头的内容,比如Binder:12345_C之类,它们表示不同进程之间通过 Binder 通信。当你看到自己应用的日志里混入系统进程的输出,别急着怀疑日志怎么看错了——这大概率是应用调用了系统服务(比如 ActivityManager、PackageManager),系统服务在处理过程中也打印了日志。这类日志有助于判断某个系统调用是不是卡住了。
第二类是 ANR(Application Not Responding)日志。应用长时间无响应时,系统会打出一条ANR in com.example.app的 FATAL 日志。如果只盯着 Logcat 看,可能漏掉 ANR 的核心信息。配合 bugreport 或者/data/anr/traces.txt才能拿到线程栈,从而判断是主线程被 IO 阻塞,还是死锁,或者有广播接收器执行超时。我的建议是:遇到 ANR,不要只依赖 Logcat,立刻用 bugreport 抓取现场,这是最接近事实真相的材料。
第三类是 “Input dispatching timed out”(输入事件分发超时)日志。它表示用户触摸事件在指定时间内没有被处理完,往往意味着主线程被卡住了。排查思路是:找到超时日志对应的时间戳,往前推几百毫秒,看主线程那段日志里在执行什么任务。很多情况下会发现,主线程在做文件读写、网络请求、JSON 解析这类本不该在主线程做的事,日志里会直接体现出来。
5. 从入门到进阶的一条建议路线
讲了一堆具体的操作,最后想说说学 Logcat 的整体思路,给不同阶段的读者一个参考路径。
刚入门的同学,先不要急着去研究各种高级过滤规则和命令行的花活,把最核心的三件事做好:能合理选择日志级别、能规范地用一个 Tag、能在 Logcat 窗口里通过关键字和级别过滤器快速定位自己打印的日志。这三点做到,日常调试就够用了。
写了两三个月代码之后,可以开始刻意练习一些进阶用法:用 OkHttp 拦截器统一打印网络日志、为不同的业务模块规划统一的 Tag 规范、用 adb logcat 把日志导出成文件做更长时间维度的分析。这时候的重点是“从点状看日志过渡到线状看日志”,也就是不再只看某一个时刻的那一行报错,而是能把一段时间内多个模块的日志串联成一整个执行流程。
有经验之后,再学习怎么把日志、崩溃堆栈、bugreport、系统事件关联起来分析复杂问题。比如同时篡改多份材料去判断一个线上问题,这类分析能力和工具本身的熟练程度关系不大,更看重平时积累的“对系统运行原理的理解”。
6. 写在最后:日志这件事值得认真对待
我个人在做项目的时候,吃过不少日志不规范带来的亏,也见过团队里因为日志打得差,一个非常简单的历史问题查了整整一天的场景。回想原因,很多时候不是问题本身有多难,而是日志里根本找不到有效信息——要么没打、要么打了但被垃圾日志淹没、要么打在了根本没执行到的分支里。后来在团队里推动“写日志要像写代码一样有规范”,从 Tag 命名、级别选择、关键节点的落盘方式都统一了一遍,排查问题的时间肉眼可见地降了下来。
最后再分享一个小习惯:每写完一个功能模块,花十分钟顺手把日志过一遍,想象一下未来某天这个模块出了问题,你希望当时的自己留下哪些线索。按着这个标准去补日志,比事后靠猜省太多时间。日志这东西,平时看着不起眼,真到关键时刻就是排障的脐带,别让它断了。