好用的 Android 日志工具

本文介绍了一个简洁而强大的Android日志工具,设计包括4个日志级别,提供线程名、调用栈信息,并确保在Release版中日志内容被优化。通过日志TAG的分层设计,实现模块化的日志输出。文章还探讨了系统Log的优缺点,并给出了详细的实现和使用方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

好用的 Android 日志工具

简介

分享一个 Android 日志工具(Java 层),几乎我的每个项目都会用到,自认为非常好用,这里描述一下它的设计和实现。

它有如下几个特点:

  1. 简单,仅由一个 100 余行的 Java 类实现,猴子都能看懂 _
  2. 额外可选日志内容,提供线程名信息和调用栈,提供当前日志打印所在类以及所在代码行数;
  3. 方便,包含栈信息,直接用鼠标即可点击到日志打印所在行;
  4. 安全,保证日志字符串完全被优化掉,而不是留在代码中,下面会分析;
  5. 灵活,提供二次封装。

开源仓库地址在文末给出。

背景

日志是工程开发中必不可少的调试工具之一,良好的日志代码可以清晰的反映逻辑流程和关键变量的值,任何程序开发框架都会提供内置的日志类库供开发者使用。

Android SDK 也不例外,它提供了 android.util.Log 来打印日志,包含了 6 个级别的日志。

分别是:AssertDebugErrorInfoVerboseWarning

通常我们在使用 Log 时,几乎一定是需要包装的,最常见的需求就是发布 Release 包时去除所有打印的日志,防止泄露关键信息以及影响应用执行效率。

下面分析系统 Log 类中的几个我个人认为的优缺点。

优点:

  1. 足够简单,在临时测试时可以随手打印出日志;
  2. 自带相关调式信息,例如时间、进程/线程ID、应用包名,日志级别。

缺点:

  1. 没有应用内的日志开关,几乎一定需要封装;
  2. 调试信息不够丰富,例如线程以 ID 形式显示不够直观、无法知道日志调用所在代码行;
  3. 日志级别冗余,提供 6 个级别的日志,让人不知道该选择哪一个(我从没用过所有级别);
  4. 未提供日志格式化方法,只能使用字符串相加,这样会导致日志字符串无法被优化(下面会分析)。

由于系统 API 要设计的足够通用,必须要足够简单,所以不会提供那么多附加内容,那是框架需要做的。

那么下面就用自己的代码来弥补它的缺点。

设计

日志级别设计

我认为 4 个级别已经足够覆盖 Java 层逻辑的所有的情况。

  1. Error 级别,使用红色标记,表示严重错误,不应该出现的、非预料中的错误。一旦出现,应用是无法正常执行的。

  2. Warn 级别,使用橙色标记,表示警告,预料内的异常。例如网络异常,通常出现警告级别的异常,需要考虑替方案,例如没有网络,则从本地缓存读取。

  3. Debug 级别,使用绿色标记,表示调试信息,打印关键逻辑相关的变量数值的信息。

  4. Info 级别,使用蓝色标记,表示和流程相关的信息,例如执行进入了某个函数,某个服务被启动,通常不包含变量。

日志格式设计

首先分析系统日志包含的信息,如下:

2020-07-17 14:15:18 21194-21194/io.l0neman.example I/MainActivity: onResume: initViews 
 \               /   \       /   \              / / \  \        /   \                /
      [ 时间 ]    [ 进程-线程 ID ]    [ 包名 ]     [ 级别 ] [ TAG ]       [ 日志内容 ]

发现系统日志已经包含了相关信息,那么我们就不考虑添加这些信息了。

需要添加如下内容:

  1. 线程名字,了解实时的逻辑执行线程信息;
  2. 栈信息,当前调用栈信息,可像异常抛出时可用鼠标点击跳转至调用处。

例如,异常调用栈后面表示代码具体调用行,可以用鼠标定位到具体位置:

Caused by: java.lang.RuntimeException: demo
        at io.l0neman.example.MainActivity.onCreate(MainActivity.java:17)
        at android.app.Activity.performCreate(Activity.java:7802)
        ...

通常我们可以定制的部分只有日志的 TAG 和后面的内容。

TAG 官方建议不超过 20 个字符,避免影响搜索效率,所以不考虑在这里添加过多内容。

通常最关心的还是日志的内容,所以添加的线程信息和栈信息不考虑放在日志最前面,那么日志格式设计如下。

${log content} [thread name](Class:${line number})

对应实际日志为:

2020-07-17 14:15:18 21194-21194/io.l0neman.example I/MainActivity: onResume: initViews [main](MainActivity.java:24)

这样就设计好了日志格式,继续其他部分的设计。

日志 TAG 设计

TAG 是一个标签用于快速搜索,通常为一个类名或一个模块的标签,便于识别日志所处模块环境。

Android 系统源代码中通常一个类表示一项功能,那么此类中都有一个 TAG 常量,值和类名相同;

当项目复杂时,包含多个模块,每个模块包含多个类,一个类表示一个功能,那么我认为 TAG 需要分层。

由于 TAG 不宜过长,那就设计两层,如下。

TAG 分为一个主 TAG 和子 TAG,两个 TAG 之间使用 # 相连,格式如下。

Primary#Secondary

那么,当应用简单时,只有一个应用模块,主 TAG 就可以表示整个应用,例如程序为 LoggerExample。

那么可以使用 LE 作为主 TAG,用于和其他应用区分,每个类的名字作为子 TAG,用于表示具体类的功能标签:

LE#MainActivity

当应用复杂,具有多个模块,那么,主 TAG 就可以表示每个模块,例如模块为 Foo,Bar。

每个类的名字依然作为子 TAG,打印日志的时候就可以区分模块了:

Foo#FileManager

Bar#ImageFactory

至于更细粒度的 Java 方法,TAG 并不是必须的,那么可以在日志内容中进行补充。

日志安全性设计

日志的安全性体现如下:

  1. 日志内容不允许在 release 版本中,因为打印时会泄露关键变量信息。
  2. 日志内容不允许出现在 release 版本的 DEX 文件中,因为会给逆向分析者提供流程逻辑信息,通常逆向分析者通过静态分析工具将 DEX 反编译为类 Java 代码,如果看到日志中的详细信息,相当于了解了当前函数的作用。

下面是实例分析,通常封装日志时都采用如下 logD 的方法,直接使用 BuildConfig.DEBUG 变量来包装一下,打印日志时使用 + 号连接字符串内容:

private void testAndroidLog() {
   
  logD(TAG, "#testAndroidLog mId=" + mId);
}

private static void logD(String tag, String log) {
   
  if (BuildConfig.DEBUG) {
   
    Log.d(TAG, log);
  }
}

如果在 Debug 版本调用 testAndroidLog 方法,将会正常打印日志,在 Release 中则 logcat 中看不到任何日志。

通常在打包 Release 版本时,会打开压缩选项 minifyEnabled true,混淆器会对未使用的参数进行优化移除。

那么使用静态反编译器 jadx 打开 APK 查看这段代码,如下:

...
public final void r() {
   
  "#testAndroidLog mId=" + this.p;
  t();
}

public static void t() {
   
}

发现字符串组合的代码依然存在,因为当混淆器发现 logD 函数里面是空内容(由于 BuildConfig.DEBUG 为常量 false,所以编译器在编译成字节码时直接把此句 if 语句移除了),没有用到 taglog 参数,所以移除了 taglog 参数,然后将 logD 函数混淆为 t,但是在原始代码传递 log 时,做了字符串相加的运算,需要生成临时变量,则混淆器认为此句逻辑为有效逻辑,则不优化。

虽然从逻辑角度,这句代码完全可以优化掉,但是对于混淆器来说,它分析代码时需要更保守,防止优化掉有用的代码。

那么当逆向人员静态分析时,一下就能看出这个函数的用意是 testAndroidLog,即为了测试 Android Log,且 mId 变量和此方法紧密想关,当大量此类型的日志留在代码中,会对逆向人员提供较大帮助。

那么效果最好的一定是将 if 语句放在外面:

private void testAndroidLog() {
   
  if (BuildConfig.DEBUG) {
   
    logD(TAG, "#testAndroidLog mId=" + mId);
  }
}

在编译时就全部优化掉了,然而每打印一次日志都得写,非常麻烦,所以不考虑。

那么最终采用如下方案,使用字符串格式化的方式解决此问题。

private void testLogger() {
   
  loggerD(TAG, "#testAndroidLog mId=%d", mId);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值