使用場景
- 同步數據庫時,有些特定的情況導致線程停止,但數據庫數據還沒有同步完,可以再重新打開線程,再同步完剩下的數據。
- 移動端安卓客戶端,不想接入如Bugly或者啄木鳥這種功能完善的第三方SDK,而只是想普通記錄應用奔潰時候的日誌。
- 其他種種情況
測試人員告訴我,有一些難以復現的bug,可不可以加個日誌記錄,那好吧,反正也是小項目,不接第三方,自己寫好了。
思路
我準備在工程類Application類里加上異常處理器,如果出現造成閃退的異常,就獲取異常,寫入一個txt文件內,並存到本地。(如果外存儲可寫,就存到 手機存儲/項目名/cache目錄下。不可寫就存到/data/data/包名/files/cache目錄下)詳情見下文的文件存儲工具類。
1、自定義一個處理器,實現接口Thread.UncaughtExceptionHandler
/**
* @author haizhuo
* @introduction 崩潰異常處理器
*/
public class MyCrashExceptionHandler implements Thread.UncaughtExceptionHandler {
private static MyCrashExceptionHandler instance;
private Context mContext;
private MyCrashExceptionHandler() {
}
public static MyCrashExceptionHandler getInstance() {
if (instance == null) {
synchronized (MyCrashExceptionHandler.class) {
if (instance == null) {
instance = new MyCrashExceptionHandler();
}
}
}
return instance;
}
public void init(Context context) {
if (context == null) {
throw new IllegalArgumentException("Context is null!!!");
}
mContext = context.getApplicationContext();
Thread.setDefaultUncaughtExceptionHandler(this);
}
@Override
public void uncaughtException(Thread t, Throwable e) {
if (e == null) {
//讓系統默認的異常處理器來處理
Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, e);
} else {
new Thread(() -> {
Looper.prepare();
Toast.makeText(mContext, "程序發生異常,即將退出", Toast.LENGTH_SHORT).show();
Looper.loop();
}).start();
printCrashInfo(e);
SystemClock.sleep(2000);
//清棧的原因是,有時候出現崩潰會重啓app,造成不能正常退出,體驗不好
AppManager.getInstance().AppExit();
//android.os.Process.killProcess(android.os.Process.myPid());
//System.exit(0);
}
}
public void printCrashInfo(Throwable ex) {
String time = TimeUtil.getCurrentTime();
File crashFile = new File(StorageUtil.getCacheDir(), time + ".txt");
try {
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(crashFile)));
pw.println(time);
PackageManager pm = mContext.getPackageManager();
PackageInfo packageInfo = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
pw.println("packageName:" + mContext.getPackageName());
pw.println("APK Ver:" + packageInfo.versionName + " _ " + packageInfo.versionCode);
pw.println("---------------------------------------------------");
ex.printStackTrace(pw);
pw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
2、在Application的onCreate方法中初始化
/**
* @author haizhuo
*/
public class MyApplication extends Application{
private static MyApplication myApplication;
/**
* 崩潰異常處理器
*/
private MyCrashExceptionHandler myCrashExceptionHandler;
/**
* 獲取實例
*
* @return myApplication
*/
public static MyApplication getInstance() {
return myApplication;
}
@Override
public void onCreate() {
super.onCreate();
myApplication = this;
myCrashExceptionHandler=MyCrashExceptionHandler.getInstance();
myCrashExceptionHandler.init(this);
}
}
堆棧管理工具類
/**
* @author haizhuo
* @description activity堆棧式管理
*/
public class AppManager {
private final String TAG="AppManager";
private Stack<Activity> activityStack;
private static AppManager mInstance;
private AppManager() {
activityStack= new Stack<>();
}
/**
* 單一實例
*/
public static AppManager getInstance() {
if (mInstance == null) {
mInstance = new AppManager();
}
return mInstance;
}
/**
* 添加Activity到堆棧
*/
public void addActivity(Activity activity) {
if (activityStack == null) {
activityStack = new Stack<>();
}
activityStack.add(activity);
}
/**
* 從堆棧中刪除Activity
*/
public void removeActivity(Activity activity){
if (activity == null || activityStack.isEmpty()) {
return;
}
activityStack.remove(activity);
}
/**
* 獲取當前Activity(堆棧中最後一個壓入的)
*/
public Activity currentActivity() {
Activity activity = null;
if (!activityStack.empty()) {
activity = activityStack.lastElement();
}
return activity;
}
/**
* 結束當前Activity(堆棧中最後一個壓入的)
*/
public void finishActivity() {
Activity activity = activityStack.lastElement();
finishActivity(activity);
}
/**
* 結束指定的Activity
*/
public void finishActivity(Activity activity) {
if (activity == null || activityStack.isEmpty()) {
return;
}
if (!activity.isFinishing()) {
activity.finish();
}
activityStack.remove(activity);
}
/**
* 結束指定類名的Activity
*/
public void finishActivity(Class<?> cls) {
for (Activity activity : activityStack) {
if (activity.getClass().equals(cls)) {
finishActivity(activity);
break;
}
}
}
/**
* 結束所有Activity
*/
public void finishAllActivity() {
try {
while (true) {
Activity activity = currentActivity();
if (activity == null) {
break;
}
finishActivity(activity);
}
} catch (Exception e) {
Logger.log(Logger.ERROR,TAG,"關閉所有Activity錯誤"+e.getMessage(),null);
}finally {
activityStack.clear();
}
}
/**
* 獲取指定的Activity
*
*/
public Activity getActivity(Class<?> cls) {
if (activityStack != null){
for (Activity activity : activityStack) {
if (activity.getClass().equals(cls)) {
return activity;
}
}
}
return null;
}
/**
* 退出應用程序
*/
public void AppExit() {
try {
finishAllActivity();
// 殺死該應用進程
/*android.os.Process.killProcess(android.os.Process.myPid());*/
//終止當前JVM虛擬機,效果和上面的殺死進程差不多
/*System.exit(0);*/
} catch (Exception e) {
e.printStackTrace();
}
}
}
存儲文件工具類
/**
* @author haizhuo
* @introduction 工具類,存儲文件
*/
public class StorageUtil {
private StorageUtil() {
}
/**
* 判斷外存儲是否可寫
*
* @return
*/
public static boolean isExternalStorageWritable() {
return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
}
private static File getAppDir() {
File rootDir;
if (isExternalStorageWritable()) {
rootDir = new File(Environment.getExternalStorageDirectory(),MyApplication.getInstance().getAppName());
} else {
rootDir = MyApplication.getInstance().getFilesDir();
}
if (!rootDir.exists()) {
rootDir.mkdirs();
}
return rootDir;
}
/**
* 獲取當前app文件存儲目錄
*
* @return
*/
public static File getFileDir() {
File fileDir = new File(getAppDir(), "file");
if (!fileDir.exists()) {
fileDir.mkdirs();
}
return fileDir;
}
/**
* 獲取當前app圖片文件存儲目錄
*
* @return
*/
public static File getImageDir() {
File imageDir = new File(getAppDir(), "image");
if (!imageDir.exists()) {
imageDir.mkdirs();
}
return imageDir;
}
/**
* 獲取當前app緩存文件存儲目錄
*
* @return
*/
public static File getCacheDir() {
File cacheDir = new File(getAppDir(), "cache");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
return cacheDir;
}
/**
* 獲取當前app音頻文件存儲目錄
*
* @return
*/
public static File getAudioDir() {
File audioDir = new File(getAppDir(), "audio");
if (!audioDir.exists()) {
audioDir.mkdirs();
}
return audioDir;
}
/**
* @param context
* @return "/storage/emulated/0/Android/data/com.xxx.xxx/cache"目錄
*/
public static String getExternalCacheDir(Context context) {
return context.getExternalCacheDir().getAbsolutePath();
}
/**
* 創建一個文件夾, 存在則返回, 不存在則新建
*
* @param parentDirectory 父目錄路徑
* @param directory 目錄名
* @return 文件,null代表失敗
*/
public static File getDirectory(String parentDirectory, String directory) {
if (TextUtils.isEmpty(parentDirectory) || TextUtils.isEmpty(directory)) {
return null;
}
File file = new File(parentDirectory, directory);
boolean flag;
if (!file.exists()) {
flag = file.mkdir();
} else {
flag = true;
}
return flag ? file : null;
}
/**
* 根據輸入流,保存文件
* 類型:直接覆蓋文件
*
* @param file
* @param is
* @return
*/
public static boolean writeFile(File file, InputStream is) {
OutputStream os = null;
try {
//在每次調用的時候都會覆蓋掉原來的數據
os = new FileOutputStream(file);
byte data[] = new byte[1024];
int length = -1;
while ((length = is.read(data)) != -1) {
os.write(data, 0, length);
}
os.flush();
return true;
} catch (FileNotFoundException e) {
e.printStackTrace();
Logger.e(new RuntimeException("FileNotFoundException occurred. ", e), "發生錯誤", "");
return false;
} catch (IOException e) {
e.printStackTrace();
Logger.e(new RuntimeException("IOException occurred. ", e), "發生錯誤", "");
return false;
} finally {
closeStream(os);
closeStream(is);
}
}
/**
* 刪除文件或文件夾
*
* @param file
*/
public static void deleteFile(File file) {
try {
if (file == null || !file.exists()) {
return;
}
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files != null && files.length > 0) {
for (File f : files) {
if (f.exists()) {
if (f.isDirectory()) {
deleteFile(f);
} else {
f.deleteOnExit();
Logger.d("StorageUtil", "刪除文件 " + f.getAbsolutePath());
}
}
}
}
} else {
file.deleteOnExit();
Logger.d("StorageUtil", "刪除文件 " + file.getAbsolutePath());
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 保存文件
*
* @param inputStream 輸入流,比如獲取網絡下載的字節流 ResponseBody.byteStream()
* @param outputStream 輸出流,比如FileOutputStream則是保存文件
* @return
*/
public static boolean saveFile(InputStream inputStream, OutputStream outputStream) {
if (inputStream == null || outputStream == null) {
return false;
}
try {
try {
byte[] buffer = new byte[1024 * 4];
while (true) {
int read = inputStream.read(buffer);
if (read == -1) {
break;
}
outputStream.write(buffer, 0, read);
}
outputStream.flush();
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
inputStream.close();
outputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
/**
* 關閉流
*
* @param closeable
*/
public static void closeStream(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
throw new RuntimeException("關閉流失敗!", e);
}
}
}
/**
* 通過uri拿到文件真實路徑
* @param context
* @param uri
* @return
*/
public static String getRealFilePath(final Context context, final Uri uri) {
if (null == uri) {return null;}
final String scheme = uri.getScheme();
String data = null;
if (scheme == null)
{data = uri.getPath();}
else if (ContentResolver.SCHEME_FILE.equals(scheme)) {
data = uri.getPath();
} else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
final Cursor cursor = context.getContentResolver().query(uri, new String[]{MediaStore.Images.ImageColumns.DATA}, null, null, null);
if (null != cursor) {
if (cursor.moveToFirst()) {
final int index = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
if (index > -1) {
data = cursor.getString(index);
}
}
cursor.close();
}
}
return data;
}
}