日志在应用开发中有着非比寻常的作用。很多人觉得日志不太重要,只要掌握调试技术即可。这种认识是错误的。 调试只能在开发过程中使用,一旦给测试人员打包了,我们就没法调试了,这时候就得靠日志发挥作用。如果没有日志, 出现问题后的排错将会是非常困难的。你看看那些做的好的App
,日志做的非常好,比如我们常用的nginx、Tomcat等, 出现问题后,一看日志就能立马知道出错原因。
在开发和内侧阶段,我们可以直接将日志输出到控制台,这样方便我们观察。
APP
上线后,为了安全性,日志是不会输出到控制台的,一般可以写文件缓存一下,然后等到某个条件成熟, 上传到服务器,然后服务器通过自动化的分析程序对日志进行分析或者展示到Web
界面上。
android.util.Log
是Android Framework
提供给开发者的日志输出功能。 这些日志被缓存在一个缓存区中,我们通过adb logcat
命令查看。
很多日志只是在开发过程中打印,真正上线后是不会打印日志的,对这种策略,我们通常对做法有两个:
我们可以自己定义一个日志开关。也可以使用Android
工程的。 我们知道,在AndroidManifest.xml
中application
标签有一个开关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;
}
}
我们一般在打包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
失效。
在GNU/Linux和macOS系统中, 我们通常会配合上grep
命令进行过滤包含我们感兴趣的关键字的日志。
adb logcat | grep "<关键字>"
在Windows系统中,我们通常会配合上find
命令进行过滤包含我们感兴趣的关键字的日志。
adb logcat | find "<关键字>"
常用的关键字有下面这些:
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
一旦出现没有捕获的异常,程序会崩溃掉。在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());
}
});
}
}
在Application
的onCreate()
中注册:
Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(this));
对于上线后的App的崩溃日志收集,我们通常会使用友盟的组建或者是腾讯的Bugly
。
我们通过打印异常链,观察对象和方法的调用过程。我们通常会自己造一个异常:
Log.e("TAG", "method()", new Exception("message"));
这对于学习非常重要。