Android 打造异常崩溃捕获工具
轉載請標明出處:http://blog.csdn.net/zhaoyanjun6/article/details/112476811
本文出自【趙彥軍的博客】
文章目錄
- 前言
- Thread.dumpStack()
- 如何把線程堆棧日志保存到文件
- 如何捕捉Crash
- 異常傳遞
- 驚喜
- 擴展設備信息
前言
因為疫情原因,今年的年會取消了,對于這個年會期待已久,心里還是有點失落。疫情當前,祝福所有人平安。
本文所有代碼示例都上傳至:https://github.com/zyj1609wz/AndroidCrash
Thread.dumpStack()
打印當前線程調用堆棧, 這個在調試時特別好用,舉例如下:
Util.java
public class Util {public static void print(){Thread.dumpStack();} }MainActivity.java
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)Util.print()} }效果如下:
看到這個日志,你想到什么? 肯定是想到崩潰日志,是吧?
線程堆棧日志清晰的標明了當前發生的類及其行號,更重要的是顯示了方法調用路徑。這個很重要,對于排查問題,調試項目提供了很好的幫助。
下次遇到問題需要調試時,可以試試這個方法,很有用?
如何把線程堆棧日志保存到文件
我們先看看 Thread.dumpStack() 源碼
很簡單,其實就是調用了 Throwable 的 printStackTrace 方法。
除此之外, Throwable 還有一個方法,允許外部傳入一個 PrintWriter
完整的代碼如下:
MainActivity 如下:
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)Util.print(this)} }允許起來,看一下效果:
可以看到文件已經寫入了,打開看看 :
很完美啊 !!!
如何捕捉Crash
沒有 try…catch 住的異常,即 Uncaught 異常,都會導致應用程序崩潰。那么面對崩潰,我們是否可以做些什么呢?比如程序退出前,彈出個性化對話框,而不是默認的強制關閉對話框,或者彈出一個提示框安慰一下用戶,甚至重啟應用程序等。
其實Java提供了一個接口給我們,可以完成這些,這就是 UncaughtExceptionHandler,該接口含有一個純虛函數:public abstract void uncaughtException (Thread thread, Throwableex)。
Uncaught 異常發生時會終止線程,此時,系統便會通知 UncaughtExceptionHandler ,告訴它被終止的線程以及對應的異常,然后便會調用 uncaughtException 函數。如果該 handler 沒有被顯式設置,則會調用對應線程組的默認 handler 。如果我們要捕獲該異常,必須實現我們自己的handler,并通過以下函數進行設置:
public static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler)實現自定義的 handler,只需要繼承UncaughtExceptionHandler該接口,并實現uncaughtException方法即可。
package com.cootek.remoteapp;import android.content.Context; import android.os.Environment;import androidx.annotation.NonNull;import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.text.Format; import java.text.SimpleDateFormat; import java.util.Locale;/*** @author yanjun.zhao* @time 2021/1/11 4:44 PM* @desc*/ public class CrashUtil implements Thread.UncaughtExceptionHandler {private static final Format FORMAT = new SimpleDateFormat("MM-dd HH-mm-ss", Locale.getDefault());private Context mContext;private static CrashUtil INSTANCE = new CrashUtil();/*** 保證只有一個CrashHandler實例*/private CrashUtil() {}/*** 獲取CrashHandler實例 ,單例模式*/public static CrashUtil getInstance() {return INSTANCE;}public void init(Context context) {this.mContext = context.getApplicationContext();//這一句,至關重要,一定要設置 Thread.setDefaultUncaughtExceptionHandler(this);}/*** 打印當前線程堆棧,保存到本地文件*/public void print(Throwable throwable) {String fileName = getFileDir(mContext) + FORMAT.format(System.currentTimeMillis()) + ".txt";if (createOrExistsFile(fileName)) {PrintWriter pw = null;try {pw = new PrintWriter(new FileWriter(fileName, false));throwable.printStackTrace(pw);} catch (IOException ioException) {} finally {if (pw != null) {pw.close();}}}}/*** 創建文件** @param filePath* @return*/private static boolean createOrExistsFile(String filePath) {File file = new File(filePath);if (file.exists()) {return file.isFile();}if (!createOrExistsDir(file.getParentFile())) {return false;}try {return file.createNewFile();} catch (IOException e) {e.printStackTrace();return false;}}private static boolean createOrExistsDir(File file) {return file != null && (file.exists() ? file.isDirectory() : file.mkdirs());}/*** 獲取堆棧日志存儲目錄** @param context* @return*/private static String getFileDir(Context context) {if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())&& context.getExternalCacheDir() != null) {return context.getExternalCacheDir() + File.separator + "crash" + File.separator;} else {return context.getCacheDir() + File.separator + "crash" + File.separator;}}@Overridepublic void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {print(e);} }MyApp 代碼:
/*** @author yanjun.zhao* @time 2021/1/11 7:52 PM* @desc*/ public class MyApp : Application() {override fun onCreate() {super.onCreate()CrashUtil.getInstance().init(this)} }主要代碼,我們就寫完了,下面我們來測試一下:
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)findViewById<TextView>(R.id.bt).setOnClickListener {//制造一個crash5 / 0}} }允許起來,點擊 button , 人為制造一個 crash ,發現 Android 應用程序沒有崩潰,再點開crash 日志目錄,發現已經生成了日志,打開看看:
異常傳遞
在上面的例子中,我們人為的制造了一個 crash , 并且成功的捕捉了,把 crash 日志寫入本地文件。
一個直觀的感覺是:app 不會崩潰了。
但是也有一個問題,其他 crash 捕捉器就捕捉不到了,比如 :bugly 。如何才能解決這個問題。
第一步,在 Thread.setDefaultUncaughtExceptionHandler 之前,先獲取 defaultUncaughtExceptionHandler
public void init(Context context) {this.mContext = context.getApplicationContext();defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();Thread.setDefaultUncaughtExceptionHandler(this); }在處理異常的地方,先處理自己的邏輯,然后把異常向后傳遞
@Override public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {//優先處理自己的邏輯,把crash日志存起來print(e);if (defaultUncaughtExceptionHandler != null) {//如果原來的 Thread 有自己的 handler , 就把 crash 傳遞下去,//比如:如果集成了bugly , 那就傳給bugly 處理defaultUncaughtExceptionHandler.uncaughtException(t, e);} else {} }完整的 CrashUtil 類如下:
package com.cootek.remoteapp;import android.content.Context; import android.os.Environment;import androidx.annotation.NonNull;import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.text.Format; import java.text.SimpleDateFormat; import java.util.Locale;/*** @author yanjun.zhao* @time 2021/1/11 4:44 PM* @desc*/ public class CrashUtil implements Thread.UncaughtExceptionHandler {private static final Format FORMAT = new SimpleDateFormat("MM-dd HH-mm-ss", Locale.getDefault());private Context mContext;private static CrashUtil INSTANCE = new CrashUtil();private Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler;/*** 保證只有一個CrashHandler實例*/private CrashUtil() {}/*** 獲取CrashHandler實例 ,單例模式*/public static CrashUtil getInstance() {return INSTANCE;}public void init(Context context) {this.mContext = context.getApplicationContext();defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();Thread.setDefaultUncaughtExceptionHandler(this);}/*** 打印當前線程堆棧,保存到本地文件*/public void print(Throwable throwable) {String fileName = getFileDir(mContext) + FORMAT.format(System.currentTimeMillis()) + ".txt";if (createOrExistsFile(fileName)) {PrintWriter pw = null;try {pw = new PrintWriter(new FileWriter(fileName, false));throwable.printStackTrace(pw);} catch (IOException ioException) {} finally {if (pw != null) {pw.close();}}}}/*** 創建文件** @param filePath* @return*/private static boolean createOrExistsFile(String filePath) {File file = new File(filePath);if (file.exists()) {return file.isFile();}if (!createOrExistsDir(file.getParentFile())) {return false;}try {return file.createNewFile();} catch (IOException e) {e.printStackTrace();return false;}}private static boolean createOrExistsDir(File file) {return file != null && (file.exists() ? file.isDirectory() : file.mkdirs());}/*** 獲取堆棧日志存儲目錄** @param context* @return*/private static String getFileDir(Context context) {if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())&& context.getExternalCacheDir() != null) {return context.getExternalCacheDir() + File.separator + "crash" + File.separator;} else {return context.getCacheDir() + File.separator + "crash" + File.separator;}}@Overridepublic void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {//優先處理自己的邏輯,把crash日志存起來print(e);if (defaultUncaughtExceptionHandler != null) {//如果原來的 Thread 有自己的 handler , 就把 crash 傳遞下去,//比如:如果集成了bugly , 那就傳給bugly 處理defaultUncaughtExceptionHandler.uncaughtException(t, e);} else {}} }這樣就同時兼容了 bugly 等 crash捕捉工具。
驚喜
最近看到一個很出名的異常捕獲工具,翻了它的源碼,發現和我的做法一致,因此可以證明,我的做法沒有問題。下面截一個圖給大家看看,這個工具是怎么處理的?
擴展設備信息
像 bugly 一樣,每一個 crash日志都會包含設備信息,app 版本號等。其實這個也很簡單,把設備信息寫入文件就行了,我在 github 上已經完善了,這里就不展示了。
這里展示一個完整的 crash日志:
總結
以上是生活随笔為你收集整理的Android 打造异常崩溃捕获工具的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android 如何做一次内存泄漏大排查
- 下一篇: Android 如何正确统计页面停留时长