前言
相信大家在调试android应用程序包的时候经常会遇到“应用程序没有响应”这种情况,也就是当我们的应用程序出现crash情况,这种提示方式会给用户带来很不好的体验。而且,通常crash导致的退出,因为没有走正常的退出流程,会导致数据没保存、线程没有杀死、缓存没有清空等问题。这个时候就需要一个全局CrashHandler。
这个时候做过项目同学就知道有优秀的第三方SDK,譬如国内有:Umeng统计,腾讯的Bugly,国外有:fabric的Crashlytics 等吧,都不一一细说了,使用起来都很方便,接入也很简单。接下来讲讲他们的基本实现方式吧。
CrashHandler的作用
CrashHandler
: 崩溃处理器,捕获Crash信息并作出相应的处理
- 测试使用:应用在日常的开发中,我们经常需要去Logcat测试我们的App,但由于很多原因,Android Monitor会闪屏或者Crash信息丢失。 这个时候就需要一个
CrashHandler
来将Crash写入到本地方便我们随时随地查看。 - 上线使用:应用的崩溃率是用户衡量筛选应用的重要标准,那么应用上线以后 我们无法向用户借手机来分析崩溃原因。为了减低崩溃率,这个时候需要
CrashHandler
来帮我们将崩溃信息返回给后台,以便及时修复。
下面我们就手把手写一个实用、本地化、轻量级的CrashHandler吧
- 实现
Thread.UncaughtExceptionHandler
接口,并重写uncaughtException
方法,此时你的CrashHandler就具备了接收处理异常的能力了。 - 调用
Thread.setDefaultUncaughtExceptionHandler(CrashHandler)
,来使用我们自定义的CrashHandler
来取代系统默认的CrashHandler
- 结合单例模式
- 总体三步: 捕获异常、信息数据获取、数据写入和上传
CrashHandler实现代码如下:
public class CrashHandler implements UncaughtExceptionHandler {
private static final String TAG = CrashHandler.class.getSimpleName();
private Context mContext;
private String mPath;
private static final String sFileName = "crash";
private static final String sFileNameSuffix = ".trace";
private DateFormat mDateFormat;
private ExecutorService mExecutor = Executors.newSingleThreadExecutor();
//private ExecutorService mExecutor = Executors.newCachedThreadPool();
private static CrashHandler mInstance;
private UncaughtExceptionHandler mUncaughtExceptionHandler;
private ConcurrentHashMap<String, Object> mInfoMap = new ConcurrentHashMap<>();
private ConcurrentHashMap<String, String> mPackageInfoMap = new ConcurrentHashMap<>();
private ConcurrentHashMap<String, String> mDeviceInfoMap = new ConcurrentHashMap<>();
private ConcurrentHashMap<String, String> mSysInfoMap = new ConcurrentHashMap<>();
private ConcurrentHashMap<String, String> mSecureInfoMap = new ConcurrentHashMap<>();
private String mMemInfo;
private String mThrowableInfo;
/** 异常信息 */
private static final String sThrowableInfoString = "throwable_info";
/** 应用包信息 */
private static final String sPackageInfoMap = "package_info_map";
/** 设备数据信息 */
private static final String sBuildInfoMap = "build_info_map";
/** 系统常规配置信息 */
private static final String sSystemInfoMap = "system_info_map";
/** 手机安全配置信息 */
private static final String sSecureInfoMap = "secure_info_map";
/** 内存情况信息 */
private static final String sMemoryInfoString = "memory_info_map";
private CrashUploader mCrashUploader;
public static CrashHandler getInstance() {
if (mInstance == null) {
synchronized (CrashHandler.class) {
if (mInstance == null) {
mInstance = new CrashHandler();
}
}
}
return mInstance;
}
public void init(Context context, CrashUploader crashUploader) {
mContext = context.getApplicationContext();
mCrashUploader = crashUploader;
mUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
mDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault());
File crashDir = mContext.getExternalFilesDir(sFileName);
if (crashDir != null){
if (!crashDir.exists()) {
crashDir.mkdirs();
}
mPath = crashDir.getAbsolutePath();
}
}
public void deleteCrashFile(){
clearInfo();
deleteDirectory(mPath);
}
/**
* 这个是最关键的函数,当程序中有未被捕获的异常,系统将会自动调用uncaughtException方法
*
* @param t 出现未捕获异常的线程
* @param e 未捕获的异常,有了这个ex,我们就可以得到异常信息
*/
@Override
public void uncaughtException(Thread t, Throwable e) {
if (!handleException(e) && mUncaughtExceptionHandler != null) {
//如果用户没有处理则让系统默认的异常处理器来处理
mUncaughtExceptionHandler.uncaughtException(t, e);
}else {
killProcess();
}
}
/**
* 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成.
*
* @param ex
* @return true:如果处理了该异常信息;否则返回false.
*/
private boolean handleException(Throwable ex) {
if (ex == null) {
return false;
}
mExecutor.execute(new Runnable() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(mContext, "Sorry, The program is abnormal and will exit soon.", Toast.LENGTH_SHORT).show();
Looper.loop();
}
});
collectInfo(ex);
saveCrashInfoToFile();
uploadCrashMessage();
return true;
}
/**
* 退出应用
*/
private void killProcess() {
MLog.e(TAG, "killProcess");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Intent startMain = new Intent(Intent.ACTION_MAIN);
startMain.addCategory(Intent.CATEGORY_HOME);
startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(startMain);
//Process.killProcess(Process.myPid());
System.exit(1);
}
/**
* 收集设备参数信息
*
* @param ex
*/
private void collectInfo(Throwable ex){
clearInfo();
collectThrowableInfo(ex);
collectPackageInfo();
collectBuildInfo();
collectSystemInfo();
collectSecureInfo();
collectMemInfo();
mInfoMap.put(sThrowableInfoString, mThrowableInfo);
mInfoMap.put(sPackageInfoMap, mPackageInfoMap);
mInfoMap.put(sBuildInfoMap, mDeviceInfoMap);
mInfoMap.put(sSystemInfoMap, mSysInfoMap);
mInfoMap.put(sSecureInfoMap, mSecureInfoMap);
mInfoMap.put(sMemoryInfoString, mMemInfo);
}
private void clearInfo(){
mMemInfo = "";
mPackageInfoMap.clear();
mDeviceInfoMap.clear();
mSysInfoMap.clear();
mSecureInfoMap.clear();
mInfoMap.clear();
mThrowableInfo = "";
}
/**
* 保存日志文件
*/
private void saveCrashInfoToFile(){
StringBuffer mStringBuffer = infoMapToStrBuffer(mPackageInfoMap);
mStringBuffer.append(infoMapToStrBuffer(mDeviceInfoMap));
mStringBuffer.append(infoMapToStrBuffer(mSysInfoMap));
mStringBuffer.append(infoMapToStrBuffer(mSecureInfoMap));
mStringBuffer.append(mMemInfo);
mStringBuffer.append(mThrowableInfo);
String mTime = mDateFormat.format(new Date());
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
try {
String filePath = addSlash(mPath) + mTime + sFileName + sFileNameSuffix;
FileOutputStream mFileOutputStream = new FileOutputStream(filePath);
mFileOutputStream.write(mStringBuffer.toString().getBytes());
mFileOutputStream.close();
MLog.d(TAG, filePath);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 上传崩溃信息
*/
private void uploadCrashMessage(){
new Thread(new Runnable() {
@Override
public void run() {
//http 上传服务端
//deleteCrashFile();
//成功
Looper.prepare();
mCrashUploader.uploadSuccess();
Looper.loop();
//失败
Looper.prepare();
mCrashUploader.uploadError("");
Looper.loop();
}
}).start();
}
/**
* 获取捕获异常的信息
*
* @param ex
*/
private void collectThrowableInfo(Throwable ex) {
Writer mWriter = new StringWriter();
PrintWriter mPrintWriter = new PrintWriter(mWriter);
ex.printStackTrace(mPrintWriter);
ex.printStackTrace();
Throwable throwable = ex.getCause();
while (throwable != null) {
throwable.printStackTrace(mPrintWriter);
mPrintWriter.append("\r\n");
throwable = throwable.getCause();
}
mPrintWriter.close();
mThrowableInfo = mWriter.toString();
}
/**
* 获取APP进程内存信息
*/
private void collectMemInfo() {
BufferedReader bufferedReader = null;
StringBuffer stringBuffer = new StringBuffer();
ArrayList<String> commandLine = new ArrayList<>();
commandLine.add("dumpsys");
commandLine.add("meminfo");
commandLine.add(Integer.toString(Process.myPid()));
try {
java.lang.Process process = Runtime.getRuntime().exec(commandLine.toArray(new String[commandLine.size()]));
bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()), 4 * 1024);
while (true) {
String line = bufferedReader.readLine();
if (line == null) {
break;
}
stringBuffer.append(line);
stringBuffer.append("\n");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
mMemInfo = stringBuffer.toString();
}
/**
* 获取系统安全设置信息
*/
private void collectSecureInfo() {
Field[] fields = Settings.Secure.class.getFields();
for (Field field : fields) {
if (!field.isAnnotationPresent(Deprecated.class) && field.getType() == String.class && field.getName().startsWith("WIFI_AP")) {
try {
String value = Settings.Secure.getString(mContext.getContentResolver(), (String) field.get(null));
if (value != null) {
mSecureInfoMap.put(field.getName(), value);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
/**
* 获取系统常规设定属性
*/
private void collectSystemInfo() {
Field[] fields = Settings.System.class.getFields();
for (Field field : fields) {
if (!field.isAnnotationPresent(Deprecated.class) && field.getType() == String.class) {
try {
String value = Settings.System.getString(mContext.getContentResolver(), (String) field.get(null));
if (value != null) {
mSysInfoMap.put(field.getName(), value);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
/**
* 从系统属性中提取设备硬件和版本信息
*
* 迭代Build的字段key-value 此处的信息主要是为了在服务器端手机各种版本手机报错的原因
*
*/
private void collectBuildInfo() {
Field[] mFields = Build.class.getDeclaredFields();
for (Field field : mFields) {
try {
field.setAccessible(true);
mDeviceInfoMap.put(field.getName(), field.get("").toString());
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
/**
* 获取应用包参数信息
*/
private void collectPackageInfo() {
try {
PackageManager mPackageManager = mContext.getPackageManager();
PackageInfo mPackageInfo = mPackageManager.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
if (mPackageInfo != null) {
mPackageInfoMap.put("PackageName", TextUtils.isEmpty(mPackageInfo.packageName) ? "unknown" : mPackageInfo.packageName);
mPackageInfoMap.put("VersionName", TextUtils.isEmpty(mPackageInfo.versionName) ? "unknown" : mPackageInfo.versionName);
mPackageInfoMap.put("VersionCode", mPackageInfo.versionCode + "");
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
/**
* 将HashMap遍历转换成StringBuffer
*/
@NonNull
public static StringBuffer infoMapToStrBuffer(ConcurrentHashMap<String, String> infoMap) {
StringBuffer mStringBuffer = new StringBuffer();
for (Map.Entry<String, String> entry : infoMap.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
mStringBuffer.append(key + "=" + value + "\r\n");
}
return mStringBuffer;
}
private String addSlash(String pPath) {
if (!TextUtils.isEmpty(pPath)) {
if (!pPath.endsWith(File.separator)) {
pPath = pPath + File.separator;
}
}else {
pPath = File.separator;
}
return pPath;
}
private boolean deleteDirectory(String pDir){
File dirFile = new File(addSlash(pDir));
if (!dirFile.exists() || !dirFile.isDirectory()) {
return false;
}
boolean delFlag = true;
File[] files = dirFile.listFiles();
for (File file : files) {
if (file.isFile()) {
delFlag = deleteFile(file.getAbsolutePath());
if (!delFlag) {
break;
}
} else {
delFlag = deleteDirectory(file.getAbsolutePath());
if (!delFlag) {
break;
}
}
}
if (!delFlag) {
return false;
}
if (dirFile.delete()) {
return true;
} else {
return false;
}
}
private boolean deleteFile(String pFileName){
try {
File file = new File(pFileName);
if (file.isFile() && file.exists()) {
file.delete();
return true;
} else {
return false;
}
} catch (Exception e) {
return false;
}
}
/**
* 崩溃信息上传接口回调
*/
public interface CrashUploader {
void uploadSuccess();
void uploadError(String error);
}
}
在Application中使用CrashHandler:
@Override
public void onCreate() {
super.onCreate();
registerCrashHandler();
MLog.setShowLog(BuildConfig.LOG_DEBUG);
initUmeng();
//监听APP进入后台或切回前台
addAppForegroundListener();
}
private void registerCrashHandler(){
CrashHandler.getInstance().init(getApplicationContext(), new CrashHandler.CrashUploader() {
@Override
public void uploadSuccess() {
Toast.makeText(getContext(), "uploadSuccess",Toast.LENGTH_SHORT).show();
CrashHandler.getInstance().deleteCrashFile();
}
@Override
public void uploadError(String error) {
}
});
}
结果查看/sdrard/Android/data/com.aoaoyi.poker/files/crash/,里的文件内容如下:
附加
1. 需要在AndroidManifest.xml加入权限声明,Android6.0以上实现动态权限申请。
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
2. System.exit(0)和Process.killProcess(Process.myPid())
相同点
1、APP进程都会被直接杀掉
我们通过adb shell ps可以查看当前手机所有运行的进程状态,
在执行完这两种方式之后,APP进程都会消失。
2、生命周期都不会被调用
onPause()、onStop()和onDestory(),也包括onSaveInstanceState,这些生命周期方法都不会被调用。
3、App被杀死,然后都安排会立即重启进程,并重启之前状态对应的Activity、Service、ContentProvider等。
我们通过adb shell ps可以查看当前手机所有运行的进程状态,
在执行完这两种方式之后,APP进程都会消失。
2、生命周期都不会被调用
onPause()、onStop()和onDestory(),也包括onSaveInstanceState,这些生命周期方法都不会被调用。
3、App被杀死,然后都安排会立即重启进程,并重启之前状态对应的Activity、Service、ContentProvider等。
不同点
1、影响范围不同
System.exit(0)
只会影响当前的程序;
Process.killProcess(Process.myPid())
会杀掉所有PID一样的进程,比如那些拥有相同UID的应用,统统都会被杀掉。
2、方式不一样
System.exit(0)
是停止程序的虚拟机;
Process.killProcess(Process.myPid())
是通过PID去杀死进程。
System.exit(0)
只会影响当前的程序;
Process.killProcess(Process.myPid())
会杀掉所有PID一样的进程,比如那些拥有相同UID的应用,统统都会被杀掉。
2、方式不一样
System.exit(0)
是停止程序的虚拟机;
Process.killProcess(Process.myPid())
是通过PID去杀死进程。
3. System.exit(0)和System.exit(1)
看官方的方法注释,如下:
Use 0 to signal success to the calling process and 1 to signal failure.1
0:代表成功的信号(正常退出);
1:代表失败的信号(异常退出),常用于捕获到异常执行。
Use 0 to signal success to the calling process and 1 to signal failure.1
0:代表成功的信号(正常退出);
1:代表失败的信号(异常退出),常用于捕获到异常执行。
从上面可以看出,这两种方式都比较暴力,不推荐使用,尤其是现在的手机配置都很好的情况下。而且进程都被杀掉了,相应的Service也会被干掉.
真要用的话,也得注意:如果程序有多个Activity,最好在主Activity中的OnBackPressed()中使用,如果在其它Activity中使用的话,APP会直接退出,退出后会重启应用,这样对于用户体验不是很好。
真要用的话,也得注意:如果程序有多个Activity,最好在主Activity中的OnBackPressed()中使用,如果在其它Activity中使用的话,APP会直接退出,退出后会重启应用,这样对于用户体验不是很好。
4. 稍微接近完美的退出方案
Intent startMain = new Intent(Intent.ACTION_MAIN);
startMain.addCategory(Intent.CATEGORY_HOME);
startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(startMain);
System.exit(1);
先回到系统Home界面,在进行异常退出。
文章评论