使用场景
- 同步数据库时,有些特定的情况导致线程停止,但数据库数据还没有同步完,可以再重新打开线程,再同步完剩下的数据。
- 移动端安卓客户端,不想接入如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;
}
}