21、Logcat 与自定义日志:在系统服务中如何添加自己的日志输出
系统服务跑在系统进程里,平时调试起来最头疼的就是看不到内部状态。你想想看,一个服务几百行代码,出问题了连个日志都没有,那跟盲人摸象有什么区别?
我个人习惯,在写任何一个系统服务之前,先把日志框架搭好。这不是矫情,是血的教训换来的经验。今天我们就聊聊,怎么在系统服务里优雅地添加自己的日志输出。
为什么系统服务的日志不能随便写?
很多人觉得日志嘛,不就是 Log.d() 一下的事?在普通 App 里确实如此。但在系统服务里,情况要复杂得多。
我遇到过一个问题:某个系统服务在线上环境疯狂输出日志,结果 logd 进程 CPU 飙升,整个系统都卡顿了。原因很简单——日志量太大,把 log buffer 撑爆了。
所以,系统服务的日志输出,有几个硬性要求:
- 可控性:必须能动态开关,不能硬编码
- 分级:Verbose、Debug、Info、Warning、Error 一个都不能少
- 标签规范:标签要统一,方便 grep 过滤
- 性能开销:日志拼接不能影响主流程性能
核心原则:系统服务的日志,是给开发者看的,不是给用户看的。所以不要怕日志多,但要怕日志乱。
标准做法:使用 Slog
Android 系统服务里,官方推荐用 Slog 而不是 Log。为什么?
说白了,Slog 是专门为系统服务设计的日志工具。它会把日志输出到 system buffer,而不是 main buffer。这样你用 logcat -b system 就能单独过滤出系统服务的日志,不会被 App 日志淹没。
用法很简单:
import android.util.Slog;
public class MyCustomService extends IMyCustomService.Stub {
private static final String TAG = "MyCustomService";
public void doSomething(String input) {
Slog.d(TAG, "doSomething called with: " + input);
// 业务逻辑...
Slog.i(TAG, "doSomething completed");
}
}
嗯,这里要注意:Slog 的日志级别和 Log 完全一致。但 Slog 默认会把日志写到 /dev/log/system 节点,而 Log 写的是 /dev/log/main。
进阶:自定义日志工具类
光用 Slog 还不够。我习惯封装一个自己的日志工具类,加一些实用功能。
比如,动态控制日志开关:
public class MyServiceLogger {
private static final String TAG = "MyCustomService";
// 这个值可以通过 Settings.Global 动态修改
private static boolean sDebug = false;
public static void setDebug(boolean enable) {
sDebug = enable;
}
public static void v(String msg) {
if (sDebug) {
Slog.v(TAG, msg);
}
}
public static void d(String msg) {
if (sDebug) {
Slog.d(TAG, msg);
}
}
public static void i(String msg) {
Slog.i(TAG, msg);
}
public static void w(String msg) {
Slog.w(TAG, msg);
}
public static void e(String msg) {
Slog.e(TAG, msg);
}
}
为什么要这么做?
我曾经在调试一个定位服务时,Verbose 日志每秒输出几百条。如果不开开关控制,线上环境直接炸裂。有了这个开关,我可以在 adb shell setprop 或者通过 Settings 动态开启,调试完再关掉,非常灵活。
小技巧:可以用 SystemProperties.getBoolean("persist.sys.mydebug", false) 来读取属性值,这样不用编译就能开关日志。
日志标签的命名规范
标签这东西,看似简单,其实坑很多。我见过有人用 TAG = "MyService",结果跟系统里其他服务的标签冲突了,grep 的时候一堆干扰。
我的建议是:
- 用包名+类名,比如
com.android.server.MyCustomService - 或者用模块缩写,比如
WIFI_SERVICE、BLUETOOTH_SERVICE - 不要用太短的标签,比如
SVC、SRV,太容易撞车
| 场景 | 推荐标签 | 不推荐标签 |
|---|---|---|
| 自定义服务 | MyCustomService | SVC |
| 网络相关 | NetworkMonitor | NET |
| 传感器服务 | SensorService | SENS |
日志输出的最佳实践
光会写日志还不够,还得知道怎么写才有效。我总结了几条经验:
- 关键路径必须打日志:比如服务启动、绑定、销毁,这些地方一定要有 Info 级别的日志
- 异常分支要打 Error:catch 到异常时,把堆栈也打出来,别只打个 "error occurred"
- 性能敏感处用 isLoggable:如果日志拼接本身很耗时,先判断一下级别
if (Slog.isLoggable(TAG, Slog.VERBOSE)) {
// 这里的字符串拼接只在 Verbose 开启时才执行
Slog.v(TAG, "Detailed state: " + dumpInternalState());
}
注意:不要在日志里打印用户敏感信息,比如密码、Token、电话号码。系统日志可能会被其他 App 通过 READ_LOGS 权限读取。
实战:在系统服务中集成自定义日志
好了,理论说完了,我们来看一个完整的例子。假设我们要写一个 BatteryMonitorService,用来监控电池状态。
public class BatteryMonitorService extends IBatteryMonitor.Stub {
private static final String TAG = "BatteryMonitorService";
private static final boolean DEBUG = true; // 编译时开关
private Context mContext;
public BatteryMonitorService(Context context) {
mContext = context;
Slog.i(TAG, "BatteryMonitorService created");
}
public void start() {
Slog.d(TAG, "start() called");
// 注册广播接收器
IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
mContext.registerReceiver(mBatteryReceiver, filter);
Slog.i(TAG, "Battery receiver registered");
}
private BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
float percent = level * 100.0f / scale;
if (DEBUG) {
Slog.v(TAG, "Battery level: " + percent + "%");
}
if (percent < 15.0f) {
Slog.w(TAG, "Battery low! Only " + percent + "% remaining");
}
}
};
}
你看,这个例子里:
- 服务启动时打了 Info 日志
- 广播接收时打了 Verbose 日志(受 DEBUG 控制)
- 低电量时打了 Warning 日志
这样分层之后,线上环境只看 Warning 和 Error,调试时打开 DEBUG 看所有细节,非常清晰。
知识体系总览
下面这张图,帮你快速理清系统服务日志的核心逻辑:
从这张图可以看出来,系统服务日志的流向很清晰:代码 → 工具类 → Slog API → 日志级别过滤 → 输出到对应 buffer → 开发者通过 logcat 查看。每一步都有讲究,每一步都值得认真对待。
好了,关于系统服务中如何添加自定义日志,我就讲这么多。记住一句话:日志不是写给机器看的,是写给未来的自己看的。把日志写好了,调试效率能翻一倍。
公众号:蓝海资料掘金营,微信deep3321