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_SERVICEBLUETOOTH_SERVICE
  • 不要用太短的标签,比如 SVCSRV,太容易撞车
场景 推荐标签 不推荐标签
自定义服务 MyCustomService SVC
网络相关 NetworkMonitor NET
传感器服务 SensorService SENS

日志输出的最佳实践

光会写日志还不够,还得知道怎么写才有效。我总结了几条经验:

  1. 关键路径必须打日志:比如服务启动、绑定、销毁,这些地方一定要有 Info 级别的日志
  2. 异常分支要打 Error:catch 到异常时,把堆栈也打出来,别只打个 "error occurred"
  3. 性能敏感处用 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 日志级别控制 Verbose(调试) → Debug(开发) → Info(关键) → Warning(警告) → Error(错误) system buffer(logcat -b system) main buffer(logcat -b main) adb logcat -b system -s TAG adb logcat -b main -s TAG

从这张图可以看出来,系统服务日志的流向很清晰:代码 → 工具类 → Slog API → 日志级别过滤 → 输出到对应 buffer → 开发者通过 logcat 查看。每一步都有讲究,每一步都值得认真对待。


好了,关于系统服务中如何添加自定义日志,我就讲这么多。记住一句话:日志不是写给机器看的,是写给未来的自己看的。把日志写好了,调试效率能翻一倍。

公众号:蓝海资料掘金营,微信deep3321