Android日志

日志在应用开发中有着非比寻常的作用。很多人觉得日志不太重要,只要掌握调试技术即可。这种认识是错误的。 调试只能在开发过程中使用,一旦给测试人员打包了,我们就没法调试了,这时候就得靠日志发挥作用。如果没有日志, 出现问题后的排错将会是非常困难的。你看看那些做的好的App,日志做的非常好,比如我们常用的nginxTomcat等, 出现问题后,一看日志就能立马知道出错原因。

在开发和内侧阶段,我们可以直接将日志输出到控制台,这样方便我们观察。

APP上线后,为了安全性,日志是不会输出到控制台的,一般可以写文件缓存一下,然后等到某个条件成熟, 上传到服务器,然后服务器通过自动化的分析程序对日志进行分析或者展示到Web界面上。

1.1、android.util.Log

android.util.LogAndroid Framework提供给开发者的日志输出功能。 这些日志被缓存在一个缓存区中,我们通过adb logcat命令查看。

很多日志只是在开发过程中打印,真正上线后是不会打印日志的,对这种策略,我们通常对做法有两个:

  • 设置一个日志开关,我们在打包的时候修改这个开关,就可以做到日志是否打印了。
  • 在编译的时候,把打印日志相关的代码用程序删除掉,这样更好,还可以顺便减少很多的指令,性能更好。ProGuard就能做到这一点。
1.1.1、日志开关

我们可以自己定义一个日志开关。也可以使用Android工程的。 我们知道,在AndroidManifest.xmlapplication标签有一个开关android:debuggable控制着Android Device Monitor能否显示某个app的进程。如下:

<application
    android:name="com.fpliu.newton.MyApplication"
    android:debuggable="false"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme">
</application>

android:debuggable这个开关,在编译的时候,自动生成了一个Java类,叫做BuildConfig, 内容可能如下:

/**
 * Automatically generated file. DO NOT MODIFY
 */
package com.fpliu.newton;

public final class BuildConfig {
    public static final boolean DEBUG = Boolean.parseBoolean("false");
    public static final String APPLICATION_ID = "com.fpliu.newton";
    public static final String BUILD_TYPE = "debug";
    public static final String FLAVOR = "";
    public static final int VERSION_CODE = 1;
    public static final String VERSION_NAME = "1.0";
}

这里的DEBUG常量是一定会有的。它就是根据application标签有的android:debuggable属性得到的, 如果没有配置这个属性,默认是false

BuildConfig中的其他属性是通过build.gradle配置文件获得的。

我们对android.util.Log进行一个封装,如下:

package com.fpliu.newton;

public final class DebugLog {

    public static final boolean ENABLED = BuildConfig.DEBUG;

    /**
     * TAG的前缀,便于过滤
     */
    public static final String PREFIX = "XX_";

    private DebugLog() {
    }

    public static int v(String tag, String msg) {
        return ENABLED ? Log.v(PREFIX + tag, "" + msg) : 0;
    }

    public static int v(String tag, String msg, Throwable throwable) {
        return ENABLED ? Log.v(PREFIX + tag, msg, throwable) : 0;
    }

    public static int d(String tag, String msg) {
        //华为的这款手机只能打印information信息
        if ("GEM-703L".equals(Build.MODEL)
                || "H60-L11".equals(Build.MODEL)) {
            return i(tag, msg);
        }
        return ENABLED ? Log.d(PREFIX + tag, "" + msg) : 0;
    }

    public static int d(String tag, String msg, Throwable throwable) {
        return ENABLED ? Log.d(PREFIX + tag, msg, throwable) : 0;
    }

    public static int i(String tag, String msg) {
        return ENABLED ? Log.i(PREFIX + tag, "" + msg) : 0;
    }

    public static int i(String tag, String msg, Throwable tr) {
        return ENABLED ? Log.i(PREFIX + tag, msg, tr) : 0;
    }

    public static int w(String tag, String msg) {
        return ENABLED ? Log.w(PREFIX + tag, "" + msg) : 0;
    }

    public static int w(String tag, String msg, Throwable tr) {
        return ENABLED ? Log.w(PREFIX + tag, msg, tr) : 0;
    }

    public static int w(String tag, Throwable tr) {
        return ENABLED ? Log.w(PREFIX + tag, tr) : 0;
    }

    public static int e(String tag, String msg) {
        return ENABLED ? Log.e(PREFIX + tag, "" + msg) : 0;
    }

    public static int e(String tag, String msg, Throwable tr) {
        return ENABLED ? Log.e(PREFIX + tag, msg, tr) : 0;
    }
}
1.1.2、通过ProGuard删除掉日志

我们一般在打包APK的时候,会使用ProGuard混淆Java代码,实际上,ProGuard不仅仅会混淆代码,还可以进行优化,比如删除指定的类、指定的类的方法等。 我们就是利用它的这个优化功能删除我们的日志打印的类的。

示例:

-assumenosideeffects class android.util.Log {
    public static int v(...);
    public static int i(...);
    public static int w(...);
    public static int d(...);
    public static int e(...);
}

-keep class android.util.Log {
    *;
}

assumenosideeffects拆开是assume no side effects,假定无效的意思,就是说被标识的代码是无效代码,可以被删掉。 想要使这个指令有效的话,一定不能配置-dontoptimize-dontoptimize意思是不要优化,这会使得assumenosideeffects失效。

1.2、adb logcat

GNU/LinuxmacOS系统中, 我们通常会配合上grep命令进行过滤包含我们感兴趣的关键字的日志。

adb logcat | grep "<关键字>"

Windows系统中,我们通常会配合上find命令进行过滤包含我们感兴趣的关键字的日志。

adb logcat | find "<关键字>"

常用的关键字有下面这些:

  • Activity
  • Fragment
  • System.out
  • System.err
  • Web Console
  • chromium

Android4.4开始,WebView的内核换成了chromium, 打印WebView内部的日志就要使用chromium进行过滤了,以前的用Web Console进行过滤。

重要的一些日志信息:

Choreographer: Skipped 310 frames!  The application may be doing too much work on its main thread.
art     : Background partial concurrent mark sweep GC freed 6757(511KB) AllocSpace objects, 15(5MB) LOS objects, 39% free, 9MB/16MB, paused 19.809ms total 29.751ms
ActivityManager: Displayed com.unionx.yilingdoctor.member/com.pizidea.imagepicker.ui.activity.ImagesGridActivity: +1s545ms
SurfaceFlinger: couldn't log to binary event log: overflow.
Surface : getSlotFromBufferLocked: unknown buffer: 0xe1055000
OpenGLRenderer: Failed to set EGL_SWAP_BEHAVIOR on surface 0xe84b6160, error=EGL_SUCCESS
art     : Starting a blocking GC Explicit
art     : Explicit concurrent mark sweep GC freed 1897(172KB) AllocSpace objects, 0(0B) LOS objects, 24% free, 8MB/11MB, paused 102us total 10.440ms
art     : hprof: heap dump "/storage/emulated/0/Download/leakcanary-com.unionx.yilingdoctor.member/a6922d89-8df7-4041-809d-440e118aca26_pending.hprof" starting...
art     : hprof: heap dump completed (19MB) in 1.010s
art     : Long monitor contention event with owner method=java.util.List com.android.server.am.ActivityManagerService.getAllStackInfos() from ActivityManagerService.java:8937 waiters=0 for 4.212s
MultiDex: VM has multidex support, MultiDex support library is disabled.
dex2oat : Unexpected CPU variant for X86 using defaults: x86
Failed to create oat file: /data/dalvik-cache/x86/data@app@com.unionx.yilingdoctor.member-2@split_lib_slice_7_apk.apk@classes.dex: Permission denied
1.3、奔溃日志收集

一旦出现没有捕获的异常,程序会崩溃掉。在Java SE 5中加入了处理未捕获异常的机会。 在java.lang.Thread中加入了如下两个方法:

public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh)
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh)

UncaughtExceptionHandler接口的定义如下:

public interface UncaughtExceptionHandler {
    void uncaughtException(Thread t, Throwable e);
}

一旦出现了未捕获的异常,就会执行UncaughtExceptionHandler接口的方法。 所以,我们一般会在这个方法里保存日志、给用户提示、重新启动线程、重新启动APP等工作。

android.os.Process.killProcess(android.os.Process.myPid());可以杀死App是有条件的, 只有在Activity栈的根Activity上调用这个方法才会杀死进程,否则会先杀死再重启。因为系统认为这个 异常导致了程序退出。

public class CrashHandler implements Thread.UncaughtExceptionHandler {

    private Context appContext;

    public CrashHandler(Context appContext) {
        this.appContext = appContext;
    }

    @Override
    public void uncaughtException(Thread thread, final Throwable ex) {
        // 使用 Toast 来显示异常信息
        new Thread() {
            @Override
            public void run() {
                Looper.prepare();
                Toast.makeText(context, "程序异常,正要退出", Toast.LENGTH_LONG).show();
                Looper.loop();
            }
        }.start();
        ThreadPoolManager.EXECUTOR.execute(new Runnable() {
            @Override
            public void run() {
                //保存日志
                //TODO
                // 杀死进程
                Process.killProcess(Process.myPid());
            }
        });
    }
}

ApplicationonCreate()中注册:

Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(this));

对于上线后的App的崩溃日志收集,我们通常会使用友盟的组建或者是腾讯的Bugly

1.4、打印异常

我们通过打印异常链,观察对象和方法的调用过程。我们通常会自己造一个异常:

Log.e("TAG", "method()", new Exception("message"));

这对于学习非常重要。