18、Launcher的日志系统:Logcat的Tag规范、自定义Logger工具、关键节点的日志埋点

做Launcher开发,最头疼的是什么?

我个人觉得,不是UI布局,也不是动画卡顿,而是——出了问题你根本不知道它发生在哪一步。Launcher这东西,启动流程长、事件交互多、状态切换频繁。没有一套好的日志系统,你就像在黑夜里摸路。

今天我们就来聊聊,怎么给Launcher装上一双“眼睛”。

18.1 为什么Launcher需要一套独立的日志体系?

你可能会想:Android不是自带Logcat吗?直接用不就行了?

嗯,理论上可以。但实际项目中你会发现,Launcher的日志量太大了。系统服务、输入事件、窗口管理、应用启动……各种日志混在一起。你想看Launcher自己的行为,得从几千行日志里翻,效率极低。

我在项目中遇到过这种情况:线上用户反馈说“桌面偶尔卡死”,但我们抓到的日志全是系统级的,根本定位不到Launcher内部哪个环节出了问题。后来我们花了三天时间,把日志体系重新梳理了一遍,问题才浮出水面。

所以,Launcher必须有自己的日志规范。说白了,就是三个目标:

  • 可过滤:一眼就能从Logcat里挑出Launcher的日志
  • 可分级:区分DEBUG、INFO、WARN、ERROR,别把调试信息当错误打
  • 可追溯:关键节点必须有日志,能还原整个执行链路

18.2 Logcat的Tag规范:给日志打上“标签”

Logcat里过滤日志靠什么?靠Tag。Tag就是日志的身份证。

我建议Launcher项目里统一使用这样的Tag命名规则:

模块 Tag格式 示例
桌面启动 Launcher_Startup Launcher_Startup_onCreate
工作区 Launcher_Workspace Launcher_Workspace_addItem
拖拽 Launcher_Drag Launcher_Drag_startDrag
应用列表 Launcher_AllApps Launcher_AllApps_onSearch
数据库 Launcher_DB Launcher_DB_queryFavorites
动画 Launcher_Anim Launcher_Anim_springTo

为什么要加“Launcher_”前缀?因为这样你在Logcat里直接搜“Launcher_”就能过滤出所有Launcher相关日志。简单粗暴,但有效。

小技巧: 我习惯在Logcat里保存一个过滤表达式:tag:~Launcher_。这样每次打开Android Studio,直接点一下就能看到Launcher的日志,不用重新输入。

18.3 自定义Logger工具:让日志更好用

Android自带的Log类功能太基础了。我们得自己封装一个Logger工具,加一些“私货”。

来看看我项目中用的Logger工具长什么样:

public class LauncherLogger {

    private static final String TAG_PREFIX = "Launcher_";
    private static final boolean DEBUG = BuildConfig.DEBUG;

    public static void v(String tag, String msg) {
        if (DEBUG) Log.v(TAG_PREFIX + tag, msg);
    }

    public static void d(String tag, String msg) {
        if (DEBUG) Log.d(TAG_PREFIX + tag, msg);
    }

    public static void i(String tag, String msg) {
        Log.i(TAG_PREFIX + tag, msg);
    }

    public static void w(String tag, String msg) {
        Log.w(TAG_PREFIX + tag, msg);
    }

    public static void e(String tag, String msg) {
        Log.e(TAG_PREFIX + tag, msg);
    }

    public static void e(String tag, String msg, Throwable tr) {
        Log.e(TAG_PREFIX + tag, msg, tr);
    }

    // 带方法名的日志,方便追踪调用位置
    public static void dWithMethod(String tag, String msg) {
        if (DEBUG) {
            StackTraceElement[] stack = Thread.currentThread().getStackTrace();
            String methodName = stack.length > 4 ? stack[4].getMethodName() : "unknown";
            Log.d(TAG_PREFIX + tag, "[" + methodName + "] " + msg);
        }
    }
}

这个工具有几个好处:

  • 自动加前缀,不用每个地方都写“Launcher_”
  • DEBUG模式下才输出Verbose和Debug级别日志,Release包自动关闭
  • 带方法名的版本,能直接看到是哪个函数打的日志
重点: Release包一定要关掉Verbose和Debug日志。我曾经见过一个项目,Release包没关日志,结果用户手机里一天写了几百MB的日志文件,直接把存储撑爆了。

18.4 关键节点的日志埋点:把“眼睛”装对地方

日志工具有了,接下来就是埋点。埋点不是到处乱打,而是挑关键节点。

我总结了一下,Launcher里必须埋点的几个位置:

18.4.1 启动流程

Launcher启动是最容易出问题的环节。我建议在以下位置打日志:

// Launcher.java
@Override
protected void onCreate(Bundle savedInstanceState) {
    LauncherLogger.i("Startup", "onCreate start");
    // ... 初始化代码 ...
    LauncherLogger.i("Startup", "onCreate end");
}

@Override
protected void onResume() {
    LauncherLogger.i("Startup", "onResume start");
    // ... 恢复代码 ...
    LauncherLogger.i("Startup", "onResume end");
}

// 绑定工作区完成
private void onWorkspaceBound() {
    LauncherLogger.i("Startup", "workspace bound, items count=" + mWorkspace.getChildCount());
}

为什么要打“start”和“end”?因为这样你就能算出每个阶段耗时。如果onCreate到onResume之间超过2秒,那肯定有问题。

18.4.2 应用安装/卸载/更新

Launcher需要监听应用变化。这个环节的日志很重要:

// 应用安装
@Override
public void onPackageAdded(String packageName, UserHandle user) {
    LauncherLogger.i("Package", "added: " + packageName);
    // 重新加载图标
    LauncherLogger.d("Package", "reloading icon for " + packageName);
}

// 应用卸载
@Override
public void onPackageRemoved(String packageName, UserHandle user) {
    LauncherLogger.w("Package", "removed: " + packageName);
    // 移除桌面图标
}

// 应用更新
@Override
public void onPackageChanged(String packageName, UserHandle user) {
    LauncherLogger.i("Package", "changed: " + packageName);
}
注意: 应用卸载用w级别,因为如果卸载的是桌面上的图标,用户可能会看到图标消失。用w级别能让你在排查问题时快速定位到这类事件。

18.4.3 拖拽操作

拖拽是Launcher里交互最复杂的操作。埋点要覆盖整个生命周期:

// 开始拖拽
public void onDragStart(DragSource source, Object info, int dragAction) {
    LauncherLogger.d("Drag", "start, source=" + source.getClass().getSimpleName());
}

// 拖拽中
public void onDragMove(float x, float y) {
    // 这里不要打日志!太频繁了
}

// 拖拽结束
public void onDragEnd(boolean dropped) {
    LauncherLogger.d("Drag", "end, dropped=" + dropped);
    if (!dropped) {
        LauncherLogger.w("Drag", "item not dropped, returning to original position");
    }
}

你发现没有?onDragMove我没打日志。为什么?因为拖拽过程中坐标变化太频繁,一秒几十次,打日志会严重影响性能。我见过有人在这里打日志,结果拖拽卡成PPT。

18.4.4 数据库操作

Launcher的数据库操作虽然不频繁,但一旦出错,后果很严重:

// 查询
public ArrayList<ItemInfo> loadFavorites() {
    LauncherLogger.d("DB", "loadFavorites start");
    ArrayList<ItemInfo> items = queryFavorites();
    LauncherLogger.d("DB", "loadFavorites end, count=" + items.size());
    return items;
}

// 写入
public void addItemToDatabase(ItemInfo item) {
    LauncherLogger.i("DB", "addItem: " + item.title + ", id=" + item.id);
    try {
        // 写入操作
    } catch (Exception e) {
        LauncherLogger.e("DB", "addItem failed", e);
    }
}

18.5 知识体系总览

下面这张图,把Launcher日志系统的核心脉络画出来了:

Launcher日志系统知识体系 Tag规范 • 统一前缀 Launcher_ • 模块化命名 • 可过滤性优先 • 避免Tag冲突 • 区分日志级别 自定义Logger • 自动添加前缀 • DEBUG开关控制 • 方法名追踪 • 异常堆栈打印 • Release包过滤 日志埋点 • 启动流程 • 应用安装/卸载 • 拖拽操作 • 数据库操作 • 动画状态切换 目标:可过滤 · 可分级 · 可追溯

18.6 避坑指南

最后,分享几个我踩过的坑:

  • 别在循环里打日志:尤其是ListView的getView、动画的onAnimationUpdate这些高频回调。我曾经在getView里打了一个d级别的日志,结果列表滑动时手机发烫,帧率掉到20帧。
  • Release包一定要做日志过滤:用BuildConfig.DEBUG控制,或者用ProGuard把Log代码删掉。不然用户手机里全是你的调试日志。
  • 敏感信息别打日志:比如用户输入的搜索关键词、包名列表等。我之前见过有人把用户安装的所有应用包名打到了日志里,这其实涉及隐私问题。
  • 日志别太长:Logcat单条日志有长度限制(好像是4KB左右)。超过会被截断。如果你要打印一个列表,建议只打数量,别打全部内容。

好了,Launcher的日志系统就聊到这里。这套规范我在多个项目中实践过,效果还不错。你可以在自己的项目里试试,有问题随时交流。


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