Android 开发圈里被问过的问题里,"logcat 怎么看 Log.i 的日志"绝对排得上前几。很多人第一次跑adb logcat的时候都犯过嘀咕:明明代码里写了Log.i("TAG", "hello"),终端里却什么都没看到,于是去各个社区问"要加什么参数才能显示出来"。这个问法本身其实藏着一个对 logcat 过滤机制的误解。先把答案说清楚:默认情况下你不需要加任何东西,Log.i 日志一直都在输出,你只是被大量 Verbose 和 Debug 信息淹没了;如果你确实只想看 Info 级别及以上的日志,那么要加的是*:I这样的优先级过滤表达式,不是-v I,更不是-i。这篇文章把这条命令背后的语法、容易踩的坑,以及实际开发里的组合用法一次讲透。
1. Log.i 的过滤逻辑:先搞清楚"加什么"到底是什么意思
1.1 答案先放出来:*:I就是你要加的
直接命令行操作一下。假设代码里有这样一条日志:
Log.i("MarketDemo", "onCreate: user clicked login button"); Log.d("MarketDemo", "onCreate: debug detail info");如果你执行的是普通adb logcat,终端会被各种进程的日志刷屏,V 和 D 级别的占大多数,Log.i 很容易被淹没在几十行里,肉眼根本注意不到。这时候想要"只看 Info 及以上的日志",标准写法是:
adb logcat *:I*表示匹配所有 tag,冒号后面跟的是优先级I,意思是"所有标签下,Info 及以上级别的日志都显示出来"。所以你会看到I MarketDemo这一行出现在屏幕上,而D MarketDemo这一行被过滤掉了。
有人可能会觉得奇怪:为什么不直接写adb logcat I?这就是 logcat 语法的重点了——单独一个I在这里不是优先级,而是被当成标签名去匹配,你看到的会是 tag 名恰好叫 "I" 的日志,跟 Info 级别没有半点关系。想过滤级别,必须先写tag:再写优先级,*:I是整个规则里最简单的一种。
1.2 logcat 的默认行为:不是看不见,是信息太多
其实 logcat 的默认行为非常"宽容":只要没有指定过滤规则,它会把所有优先级、所有 tag 的日志全部吐出来,从 Verbose 到 Fatal 一个不漏。所以你问"要加什么才能显示 Log.i",从严格意义上说答案是"不需要加任何参数,Log.i 本来就会显示"。
那为什么实践中总觉得 Log.i "不显示"?主要有两个原因。
第一个原因是信息过载。一个正常 App 跑起来,周围的系统进程、服务进程、你依赖的第三方库,每秒都能产生几十行 V/D 日志。Log.i 在里面既不靠前也不显眼,轻轻松松就被刷没了。尤其当你用 USB 连接真机、日志量特别大的时候,终端滚屏速度非常快,想靠肉眼抓到一条自己的日志几乎不可能。
第二个原因是 tag 没配对。logcat 的输出里每一行都会带优先级/Tag,如果你在代码里写的是Log.i("MyApp", "hello"),但在adb logcat里直接搜 "hello" 是没问题的,可如果你下意识搜的是别的关键词,自然觉得"没有输出"。
所以真正的问题通常不是"Log.i 没显示",而是"怎么让 Log.i 从海量日志里跳出来"。*:I这种优先级过滤就是最直接的手段。
2. 日志优先级与 logcat 的 tag:priority 语法
2.1 Android 日志级别都有哪些
要彻底看明白这条命令,先得把 Android 的日志级别体系过一遍。Android 的android.util.Log类提供了五个静态方法,对应五个优先级:
| 方法 | 优先级 | 含义 | 常见用途 |
|---|---|---|---|
Log.v() | V / VERBOSE | 啰嗦级别 | 最细粒度的调试信息,平时基本不输出 |
Log.d() | D / DEBUG | 调试级别 | 开发阶段排查问题的细节输出 |
Log.i() | I / INFO | 信息级别 | 关键节点、业务流程、事件通知 |
Log.w() | W / WARN | 警告级别 | 有潜在问题但不影响运行的提示 |
Log.e() | E / ERROR | 错误级别 | 出错、异常、崩溃前的关键日志 |
在 logcat 内部其实还有一个 FATAL 级别,对应 F,通常由系统处理致命错误时使用,普通 App 代码里一般不会主动打。六个级别的优先级从低到高是:V < D < I < W < E < F。
这里最容易被忽略的一点是:logcat 按优先级过滤时,遵循的是"大于等于"的规则。也就是说*:I过滤出来的不只是 Log.i,还包括 Log.w、Log.e 这些比 I 更高的级别。如果你只想看 Log.i 本身,不想看 W 和 E,单纯靠 logcat 的优先级过滤是做不到的,需要配合 grep 做精确文本匹配,这个后面会专门讲。
2.2tag:priority过滤规则的运作原理
logcat 的过滤规则语法可以概括成一句话:<tag>:<priority>,多个规则用空格分隔。比如:
adb logcat ActivityManager:I MyApp:D *:S这条命令的意思是:输出ActivityManager标签下 Info 及以上级别,再加MyApp标签下 Debug 及以上级别,其余所有标签一律静默。多个规则之间是"并集"关系,不是"交集"关系。
这里的*:S需要单独解释一下。S代表 SILENT,也就是静默。*:S表示所有标签都静默,这通常会作为"兜底规则"跟具体 tag 的规则一起使用,用来把其他所有进程的日志全部关掉,只留下你关心的那部分。
比如你只想看MyApp这个 tag 的日志,最干净的写法是:
adb logcat MyApp:* *:S或者简写成adb logcat -s MyApp。-s参数是 logcat 提供的一个快捷方式,含义是"把所有标签设为静默,只显式指定标签"。
讲到这儿顺便多说一句:日志的过滤不是在 PC 端"事后删除"的,而是 adb 服务端把过滤条件传给设备上的logd守护进程,由 logd 在日志进入缓冲区之前就决定要不要保留。所以使用*:I这种原生过滤能显著降低 USB 传输和终端的显示压力,这也是我优先推荐原生过滤而不是| grep的原因。
3. 实战:三种最常见的"只看 Log.i"场景
3.1 全局过滤:adb logcat *:I
最简单粗暴的用法,适合不关心具体 tag、只想把日志量压下去的场景。
adb logcat *:I执行之后,终端输出会明显干净很多,原本满屏的 V/D 都没了,剩下 I、W、E、F 四条级别。如果这个时候你的Log.i还在刷屏,那说明你的 Info 日志确实打得太多了,需要反思一下是不是有的地方应该用Log.d。
实际开发里,我经常在需要跟别人远程协作、一边操作 App 一边看日志的时候用它。因为 W 和 E 一般量不大,I 覆盖了业务关键节点,信息密度刚刚好,不会漏掉重要内容,也不至于被 Debug 日志淹没。
3.2 只看某个 tag:adb logcat MyTag:I *:S
实际项目里,你关心的往往不是整个系统的 Info 日志,而是自己某个模块、某个 tag 下的日志。举个例子,代码里所有网络请求日志都打成了Log.i("Network", "..."),你想只看 Network 相关,那就跑:
adb logcat Network:I *:S这条命令的效果是:只显示 tag 为 Network、级别为 Info 及以上的日志,其他 tag 一律不显示。
如果想同时保留多个 tag,可以这样写:
adb logcat Network:I Push:I *:S如果觉得每次敲一长串太麻烦,用-s更短:
adb logcat -s Network:I Push:I但注意-s的语义是"静默其他所有",所以它后面的规则里可以省略*:S。两种写法等价,我个人习惯用-s,少敲几个字符。
3.3 顺手把格式和缓冲区调好:-v threadtime和-c
光有过滤还不够,日常抓 Log.i 的时候,我基本都会把输出格式和缓冲区一起安排好。
输出格式用-v threadtime:
adb logcat -v threadtime Network:I *:Sthreadtime格式会带上日期时间、进程 PID、线程 TID,比如:
03-20 15:39:50.689 1234 5678 I Network: request url=https://api.example.com/login这样你能清楚看到这条 Log.i 是哪个进程、哪个线程打出来的,排查并发问题的时候特别有用。关于-v的更多细节,下一章专门讲。
缓冲区用-c先清空:
adb logcat -c adb logcat -v threadtime Network:I *:S-c的作用是清空设备上的 logcat 环形缓冲区。为什么要先清?因为 logcat 缓冲区是有限的环形存储,里面可能残留了很多旧的日志。你连接 adb 之后如果不清空,新日志会排在旧日志后面,干扰判断。通常的操作流程是:先清空缓冲区,然后在 App 里复现一次问题,再回来过滤查看,这样输出里基本就是你这次操作产生的日志。
一条龙组合命令:
adb logcat -c && adb logcat -v threadtime Network:I *:S4.-v不是"版本",它控制的是输出格式
4.1-v与*:I的区别
这一节必须单独拎出来说,因为我在各种技术群里见过太多人把-v和"版本"联系到一起,或者以为-v I是过滤 Info 的写法。其实-v是--format的短参数,后面的值控制的是"每条日志长什么样",而不是"显示哪些级别"。
-v支持的常见格式如下:
| 格式 | 输出示例 | 特点 |
|---|---|---|
brief | I/Network( 1234): message | 默认格式之一,紧凑,带优先级和 tag |
time | 03-20 15:39:50.689 I/Network( 1234): message | 带时间,不带线程号 |
threadtime | 03-20 15:39:50.689 1234 5678 I Network: message | 带时间、PID、TID,最常用 |
process | I( 1234) message (Network) | 以进程为单位,紧凑 |
tag | I/Network: message | 只保留优先级和 tag,最干净 |
raw | message | 只输出消息本体,什么都不带 |
long | [ 03-20 15:39:50.689 1234: 5678 I/Network ]加多行消息 | 适合长日志,会分行显示 |
-v后面跟的只能是上述这类格式名。所以adb logcat -v I会直接解析失败,终端报:
Unknown argument for -v: I因为 logcat 在解析参数时把I当成了一种未知的输出格式,而不是优先级。这跟*:I完全是两码事。
4.2 为什么adb logcat -v I会报错
把 logcat 的参数分类想清楚,就能避免这类错误。logcat 命令行参数大致分三类:
第一类是"动作类"参数,比如-c(清空缓冲区)、-g(查看缓冲区大小)、-d(导出当前缓冲区内容后退出)、-h(查看帮助)。这一类命令通常单独使用,不会跟过滤规则混在一起。
第二类是"格式类"参数,就是-v加各种格式名,影响输出的排版。这一类可以跟过滤规则共存,互不干扰,比如adb logcat -v threadtime *:I就是"以 threadtime 格式输出 Info 及以上日志"。
第三类是"过滤规则",也就是tag:priority这种表达式。它不跟在任何参数后面,直接作为命令的尾部参数出现。这里容易出错的是,adb logcat -v threadtime Network:I *:S中Network:I和*:S都是过滤规则,而-v threadtime是格式设置,二者各管各的。
理清这三类之后,再看到adb logcat -v I报错就不奇怪了:-v后面需要一个格式名,你给了个优先级字母,解析器自然不认。想过滤 Info,就把I放到尾部的*:I里,别放在-v后面。
5. 用 grep 抓 Log.i 的替代方案与真实区别
5.1| grep " I/"到底行不行
除了 logcat 原生的*:I,很多老司机还喜欢用管道加 grep:
adb logcat | grep " I/"这种写法能不能抓到 Log.i?能,但有个前提:grep 匹配的是 logcat 输出的文本行,而文本格式受-v影响。默认的 brief 格式下,Info 日志长这样:
I/Network( 1234): request url=https://api.example.com/login所以grep " I/"能匹配到。但如果你用了-v threadtime,Info 日志变成:
03-20 15:39:50.689 1234 5678 I Network: request url=https://api.example.com/login这里 tag 前面是一个空格加I,再跟一个空格加Network,没有斜杠了。这时候你要想匹配 Info 级别,应该写:
adb logcat -v threadtime | grep " I Network"或者更稳妥一点,用正则匹配I/和I两种:
adb logcat -v threadtime | grep -E "I/(Network|MyApp)| I (Network|MyApp)"所以说,用 grep 本身没错,但一定要先确认你用的-v格式是什么,否则匹配条件对不上,输出就是空的。
5.2 什么时候该用 logcat 原生过滤,什么时候该用 grep
这里给一张对比表,看完你就知道怎么选了:
| 维度 | 原生*:I过滤 | | grep " I/" |
|---|---|---|
| 过滤位置 | 设备端 logd,入口处丢弃 | PC 端,日志已经传过来再筛 |
| USB/终端压力 | 低,不显示的不传输 | 高,海量日志先传过来再说 |
| 是否能拦截 V/D | 能,直接在源头丢了 | 不能,先输出再删 |
| 能否匹配"只显示 I 不含 W/E" | 不能,*:I包含 W/E | 能,grep 按文本精确匹配 |
| 正则/复杂匹配 | 不支持 | 支持 |
| 系统属性过滤影响 | 受persist.log.tag等限制 | 同样受影响,因为源头就已经没了 |
从这张表能看出一个关键结论:如果你的目的是"减少日志量,让 Log.i 从噪音中露出来",优先用原生*:I;如果你的目的是"在已经确定日志量可控的前提下,做更精细的文本筛选",比如只提取某个请求 URL、某个异常关键字,再上 grep。
还有个常见组合是前后搭配:先用*:I把日志量压下来,再管道给 grep 做关键词匹配。
adb logcat -v threadtime *:I | grep "login"这样即使 grep 支持的匹配很精确,输入给它处理的数据量也小了很多,效率更高。
6. 日志明明打了却没输出的几个隐藏坑
6.1 真机上把日志级别给改了:persist.log.tag
很多真机、尤其是某些定制 ROM 上,会存在一个隐藏的"全局日志级别开关",它就是系统属性persist.log.tag。如果这个属性被设成了W或者E,那么 logd 在入口处就会把所有Log.i甚至Log.w都丢掉,你代码里怎么写都打不出来,PC 端怎么过滤都白搭。
检查方法:
adb shell getprop persist.log.tag如果返回值是W、E、S等高位级别,那就说明系统在源头做了限制。可以临时改低试试:
adb shell setprop persist.log.tag V这里V是因为 V 是最低级别,能放行所有日志。有些系统还支持按 tag 设置,比如只针对某个 tag 放行:
adb shell setprop log.tag.MyApp D意思是 tag 为 MyApp 的日志,Debug 及以上级别都能正常输出。
这类属性一般需要 adb root 权限或者 debuggable 的 userdebug 版本才能改。遇到 Log.i 打了却不显示的情况,第一步就该查这个,别急着怀疑自己的过滤命令写错了。
6.2 日志缓冲区太小,Log.i 被刷掉
logcat 的日志缓冲区是环形结构,大小有限。如果你在开发者选项里把"日志记录器缓冲区大小"设得太小,比如 64KB,那么日志量稍微一冲,旧的 Log.i 就会直接被新的日志顶掉,等你打开终端看的时候,刚才打印的 Info 已经无影无踪了。
查看当前缓冲区状态用:
adb logcat -g输出类似:
main: ring buffer is 256Kb (239Kb consumed), max entry is 5120, max payload is 10240 system: ring buffer is 256Kb (0Kb consumed), max entry is 5120, max payload is 10240如果你发现consumed经常接近上限,说明日志在持续刷新,旧数据很容易被覆盖。这时候除了调大缓冲区,还可以用-b all一次性查看所有缓冲区:
adb logcat -b all -v threadtime *:I这里-b选择缓冲区,all表示 main、system、events、crash 全部输出。绝大多数 App 用Log.i打出来的日志都在main缓冲区里,但系统组件的日志可能落在system,崩溃堆栈在crash,多缓冲区一起看能避免漏掉关键上下文。
6.3 用 --pid 精确锁定进程
有时候你过滤了 tag,也过滤了级别,输出里还是混着多进程的日志,干扰判断。这时候最干净的办法是按 PID 过滤,直接从进程维度锁死。
命令是这样:
adb logcat --pid=$(adb shell pidof -s com.example.app)pidof -s会返回指定包名进程的 PID,然后--pid参数让 logcat 只输出这个进程的日志。如果你还想加上 Info 过滤,可以这样组合:
adb logcat --pid=$(adb shell pidof -s com.example.app) *:I这条命令是我抓应用启动日志最常用的方案:先-c清空缓冲区,重启 App,然后执行上面这行,输出里基本就是你这个进程的所有 Info 及以上日志,干净、精准、不吵。
需要注意的是,如果应用是多进程架构,pidof -s返回的只是其中一个进程的 PID,其他进程的日志看不到。需要多进程一起看的时候,可以去掉-s拿到多个 PID,或者干脆用--pid的多次调用分别开终端。
最后还有一个小经验:Log.i的 tag 命名尽量规范,别带空格、冒号这类特殊字符。因为 logcat 的tag:priority语法是按冒号切分 tag 和优先级的,tag 里一旦出现冒号,过滤规则就可能解析错位,明明日志打了,你也看不到。我见过有人把 tag 写成"Time:Cost",然后adb logcat Time:Cost:I *:S怎么跑都不对,最后才发现是冒号把规则拆成了三段。这些细节在你排查"Log.i 不显示"的时候往往比命令本身更关键。