排障困难?给你的应用嵌入一个Logcat吧!

前言

利用日志埋点,以排查由于逻辑出错而引发的Bug,是我们常用的排障手段。(什么?你还在通过Debug断点调试?),但在生产环境下出于安全性考虑,往往Release包需要将日志输出到屏幕的功能关闭。

于是常会出现一种很尴尬的场景,你需要用电脑重新在手机上打一个Debug包覆盖才能重新看到日志输出,而如果恰巧事故发生在周末,你手上只有一台手机,一个APP,你该如何快速定位问题呢?

你是否会想,要是在手机上能有一个Logcat就好了,发生故障时立马就能快速定位问题,而这,正是本文想与你分享的。

偷偷先瞄一眼效果:


lAHPBG1Q6B4q33nNAwzNAWg_360_780.gif

等不及要立即使用了?我已将该库上传到远程仓库了,可以通过以下方式引入:
在项目级build.gradle添加:

allprojects {
    repositories {
        ...
        maven{ url "https://dl.bintray.com/madchan/maven" }
    }
}

以及在模块级build.gradle添加:

implementation 'com.madchan.library:embeddedlogcat:1.0.0'

使用也很容易,以正常启动Activity的方式跳转即可:

startActivity(Intent(this, LogcatActivity::class.java))

同时示例源码也发布到GitHub了,如果对你有帮助,给点个Star_吧~
https://github.com/madchan/EmbeddedLogcat

知识储备

Logcat窗口布局讲解

既然是要模仿Android Studio的Logcat功能,我们自然需要先分析Logcat窗口的功能布局,并评判哪一些功能是我们需要抄袭(参考)的,如图:

clipboard.png

先从顶部栏开始分析,从左到右依次为:

设备:默认情况下,Logcat 仅显示在指定设备上运行的应用的日志消息。

进程:通常情况下,我们只关心自身应用进程的日志消息,但需考虑到应用可能存在多进程的情况。

日志级别:通常情况下,我们通过设置日志级别来表示对不同信息的关心程度。

具体的级别分布如下:

  • Verbose:显示所有日志消息(默认值)。
  • Debug:显示仅在开发期间有用的调试日志消息,以及此列表中较低的消息级别。
  • Info:显示常规使用情况的预期日志消息,以及此列表中较低的消息级别。
  • Warn:显示尚不是错误的潜在问题,以及此列表中较低的消息级别。
  • Error:显示已经引发错误的问题,以及此列表中较低的消息级别。
  • Assert:显示开发者预计绝不会发生的问题。

搜索字段:搜索包含特定字段的日志,支持正则表达式。

过滤器:过滤器菜单中,包含以下三个过滤选项:

  • Show only selected application:仅显示通过应用代码生成的消息(默认选项)。Logcat 使用正在运行的应用的 PID 来过滤日志消息。
  • No Filters:不应用过滤器。无论您选择哪个进程,logcat 都会显示设备中的所有日志消息。
  • Edit Filter Configuration:创建或修改自定义过滤器。例如,您可以创建一个过滤器,以同时查看两个应用中的日志消息。

再从左侧继续分析,从上到下。。。我们只挑几个常用的讲吧:

清除日志:通常是为了排除之前的日志消息的干扰。需加入。
滚动到底部:可以跳转到日志底部并查看最新的日志消息。需加入。

Logcat 命令行工具

可通过adb shell运行Logcat命令行,该命令行用于转储系统消息日志,包括设备抛出错误时的堆栈轨迹,以及应用使用Log类写入的消息

Logcat包含了许多命令行选项,用以查看不同过滤条件下的日志输出,如需获取 logcat 在线帮助,可执行以下命令,此处不具体展开:

adb logcat --help

方案实现

聪明的同学可能已经猜到,Logcat命令行工具即是我们实现本主题的主要途径,而Logcat窗口布局控件的选择结果则是为命令行添加不同的过滤选项,下面我们来逐步实现。

首先,定义一个Command数据类,每一个过滤选项都作为该类的属性之一,toString方法负责将该类转换为一个完整的命令行。

data class Command(var level: String = " *:V") {    // 级别

    var pid: Int? = 0        // 进程ID
    var expr: String? = null    // 关键词

    override fun toString(): String {
        val builder = StringBuilder("logcat -d -v time $level")

        pid?.let {
            builder.append(" --pid=$pid")
        }

        if (!TextUtils.isEmpty(expr)) {
            builder.append(" -e $expr+")
        }

        return builder.toString()
    }
}

-d选项

接着,介绍一个最基本的命令行,此命令行将日志转储到屏幕并退出

adb logcat -d

我们在执行完该命令行后,逐行读取日志信息并输出到TextView:

// LogcatExecutor.kt
...
private fun execOutputCommand(command: Command?) {
    try {
        val command = command?.toString() ?: "logcat -d"
        val process = Runtime.getRuntime().exec(command)
        val bufferedReader = BufferedReader(InputStreamReader(process.inputStream))

        val log = StringBuilder()
        var line: String? = bufferedReader.readLine()
        while (line != null) {
            log.append(line)
            log.append("\n\n")

            line = bufferedReader.readLine()
        }

        callback?.onLogOutput(log.toString())

    } catch (e: IOException) {
        Log.e("LogcatHandler", "执行Logcat命令行失败:" + e.message)
    }

}
...

--pid=<pid>选项

此命令行仅输出来自给定 PID 的日志。由进程Spinner选中指定选项后,为Command类的pid属性赋值,并重新执行此命令行输出日志:

// LogcatActivity.kt
...
private lateinit var process: Spinner
...
process.onItemSelectedListener = object : OnItemSelectedListener {
    override fun onItemSelected(
        adapterView: AdapterView<*>?,
        view: View,
        position: Int,
        l: Long
    ) {
        command.pid = processMap[(process.adapter.getItem(position) as String)]
        startOutput()
    }

    override fun onNothingSelected(adapterView: AdapterView<*>?) {}
}
...

*:S选项

此命令行用于指示最低优先级,不低于指定优先级的标记的消息会写入日志。与上面步骤相似,只不过赋值的是level属性。

// LogcatActivity.kt
...
private lateinit var level: Spinner
...
level.onItemSelectedListener = object : OnItemSelectedListener {
    override fun onItemSelected(
        adapterView: AdapterView<*>?,
        view: View,
        i: Int,
        l: Long
    ) {
        when (i) {
            0 -> command.level = "*:V"
            1 -> command.level = "*:D"
            2 -> command.level = "*:I"
            3 -> command.level = "*:W"
            4 -> command.level = "*:E"
            else -> {
            }
        }
        startOutput()
    }

    override fun onNothingSelected(adapterView: AdapterView<*>?) {}
}
...

-e <expr>选项

此命令行只输出日志消息与 <expr> 匹配的行,其中 <expr> 是正则表达式。

// LogcatActivity.kt
...
private lateinit var search: EditText
...
search.addTextChangedListener (object : TextWatcher{

    override fun afterTextChanged(s: Editable?) {
        command.expr = s.toString().trim()
        startOutput()
    }

    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}

})
...

其他的如清除日志、滚动到底部、高亮ERROR级别日志是属于交互的优化,不在本文介绍的范围之内,感兴趣的可以阅读源码。

使用场景

可以参考我之前写的文章《Preference库:为你的应用快速搭建一个「开发者选项」》,为你的应用添加调试入口,并增加「进入日志调试页」的调试选项。

文章链接:https://www.jianshu.com/p/6ae1794d8fca

总结

本文以Android Studio的Logcat功能为参考模板,使用Logcat 命令行工具搭配合适的交互控件,在应用内搭建了一个类似的功能,可帮助开发者根据日志信息快速定位问题,快跟我来一起使用吧!

参考

Logcat 命令行工具
https://developer.android.google.cn/studio/command-line/logcat
使用 Logcat 写入和查看日志
https://developer.android.google.cn/studio/debug/am-logcat

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266