Android安全检测报告--全局异常捕获 问题处理:
建议使用全局异常捕获UncaughtExceptionHandler类,当程序发生未知异常时,由该类来处理,并记录发送错误报告。
目录
2. 在 Application 类中初始化 CrashHandler
3. 在 AndroidManifest.xml 中指定 Application 类
2. 在 AndroidManifest.xml 中指定 Application 类
2. 在 AndroidManifest.xml 中指定 Application 类,并添加必要权限
在 Android 应用开发中,UncaughtExceptionHandler
类可用于捕获应用中未被捕获的异常,当程序发生未知异常时,会触发该处理器的处理逻辑。下面为你详细介绍如何使用 UncaughtExceptionHandler
进行全局异常捕获。
实现步骤
1. 创建自定义的全局异常捕获类
创建一个类实现 Thread.UncaughtExceptionHandler
接口,重写 uncaughtException
方法,在该方法中处理未捕获的异常。
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Environment;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class CrashHandler implements UncaughtExceptionHandler {
public static final String TAG = "CrashHandler";
// 系统默认的UncaughtException处理类
private Thread.UncaughtExceptionHandler mDefaultHandler;
// CrashHandler实例
private static CrashHandler INSTANCE = new CrashHandler();
// 程序的Context对象
app开发,使用全局异常捕获UncaughtExceptionHandler类,当程序发生未知异常时,由该类来处理,并记录发送错误报告,如何设计及完美实现
在 Android 应用开发中,使用 UncaughtExceptionHandler
类进行全局异常捕获并记录、发送错误报告是一个非常实用的功能。以下是一个详细的设计与实现步骤:
设计思路
- 创建自定义异常处理器:实现
Thread.UncaughtExceptionHandler
接口,重写uncaughtException
方法,在该方法中处理未捕获的异常。 - 记录错误信息:将异常信息、设备信息、应用版本信息等记录到本地文件中,方便后续分析。
- 发送错误报告:可以通过网络将错误报告发送到服务器,便于开发者及时了解应用的异常情况。
- 设置自定义异常处理器:在应用启动时,将自定义的异常处理器设置为默认的异常处理器。
代码实现
1. 创建自定义异常处理器类 CrashHandler
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Environment;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class CrashHandler implements UncaughtExceptionHandler {
public static final String TAG = "CrashHandler";
// 系统默认的 UncaughtException 处理类
private Thread.UncaughtExceptionHandler mDefaultHandler;
// CrashHandler 实例
private static CrashHandler INSTANCE = new CrashHandler();
// 程序的 Context 对象
private Context mContext;
// 用来存储设备信息和异常信息
private Map<String, String> infos = new HashMap<>();
// 用于格式化日期,作为日志文件名的一部分
private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
/**
* 保证只有一个 CrashHandler 实例
*/
private CrashHandler() {
}
/**
* 获取 CrashHandler 实例 ,单例模式
*/
public static CrashHandler getInstance() {
return INSTANCE;
}
/**
* 初始化
*
* @param context
*/
public void init(Context context) {
mContext = context;
// 获取系统默认的 UncaughtException 处理器
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
// 设置该 CrashHandler 为程序的默认处理器
Thread.setDefaultUncaughtExceptionHandler(this);
}
/**
* 当 UncaughtException 发生时会转入该函数来处理
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (!handleException(ex) && mDefaultHandler != null) {
// 如果用户没有处理则让系统默认的异常处理器来处理
mDefaultHandler.uncaughtException(thread, ex);
} else {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Log.e(TAG, "error : ", e);
}
// 退出程序
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(1);
}
}
/**
* 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成.
*
* @param ex
* @return true:如果处理了该异常信息;否则返回 false.
*/
private boolean handleException(Throwable ex) {
if (ex == null) {
return false;
}
// 使用 Toast 来显示异常信息
new Thread() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出.", Toast.LENGTH_LONG).show();
Looper.loop();
}
}.start();
// 收集设备参数信息
collectDeviceInfo(mContext);
// 保存日志文件
saveCrashInfo2File(ex);
// 发送错误报告到服务器
sendCrashReportToServer();
return true;
}
/**
* 收集设备参数信息
*
* @param ctx
*/
public void collectDeviceInfo(Context ctx) {
try {
PackageManager pm = ctx.getPackageManager();
PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES);
if (pi != null) {
String versionName = pi.versionName == null ? "null" : pi.versionName;
String versionCode = pi.versionCode + "";
infos.put("versionName", versionName);
infos.put("versionCode", versionCode);
}
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "an error occurred when collect package info", e);
}
Field[] fields = Build.class.getDeclaredFields();
for (Field field : fields) {
try {
field.setAccessible(true);
infos.put(field.getName(), field.get(null).toString());
} catch (Exception e) {
Log.e(TAG, "an error occurred when collect crash info", e);
}
}
}
/**
* 保存错误信息到文件中
*
* @param ex
* @return 返回文件名称, 便于将文件传送到服务器
*/
private String saveCrashInfo2File(Throwable ex) {
StringBuffer sb = new StringBuffer();
for (Map.Entry<String, String> entry : infos.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
sb.append(key + "=" + value + "\n");
}
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
ex.printStackTrace(printWriter);
Throwable cause = ex.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
printWriter.close();
String result = writer.toString();
sb.append(result);
try {
long timestamp = System.currentTimeMillis();
String time = formatter.format(new Date());
String fileName = "crash-" + time + "-" + timestamp + ".log";
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
String path = Environment.getExternalStorageDirectory() + "/crash/";
File dir = new File(path);
if (!dir.exists()) {
dir.mkdirs();
}
FileOutputStream fos = new FileOutputStream(path + fileName);
fos.write(sb.toString().getBytes());
fos.close();
}
return fileName;
} catch (Exception e) {
Log.e(TAG, "an error occurred while writing file...", e);
}
return null;
}
/**
* 发送错误报告到服务器
*/
private void sendCrashReportToServer() {
// 这里可以实现将错误报告发送到服务器的逻辑
// 例如使用 OkHttp 等网络库发送文件或错误信息
Log.d(TAG, "Sending crash report to server...");
}
}
2. 在 Application
类中初始化 CrashHandler
import android.app.Application;
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 初始化 CrashHandler
CrashHandler.getInstance().init(this);
}
}
3. 在 AndroidManifest.xml
中指定 Application
类
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<!-- 其他组件 -->
</application>
代码解释
-
CrashHandler
类:实现了Thread.UncaughtExceptionHandler
接口,用于处理未捕获的异常。init
方法:初始化CrashHandler
,并将其设置为默认的异常处理器。uncaughtException
方法:当未捕获的异常发生时,调用handleException
方法处理异常,如果处理失败则交给系统默认的异常处理器处理。handleException
方法:显示提示信息,收集设备信息,保存错误信息到文件中,并发送错误报告到服务器。collectDeviceInfo
方法:收集设备的参数信息,如应用版本号、设备型号等。saveCrashInfo2File
方法:将异常信息和设备信息保存到本地文件中。sendCrashReportToServer
方法:实现将错误报告发送到服务器的逻辑。
-
MyApplication
类:继承自Application
类,在onCreate
方法中初始化CrashHandler
。 -
AndroidManifest.xml
:指定MyApplication
为应用的Application
类。
注意事项
- 权限问题:保存错误日志文件到外部存储需要
WRITE_EXTERNAL_STORAGE
权限,需要在AndroidManifest.xml
中声明,并在运行时请求该权限。 - 网络请求:发送错误报告到服务器需要使用网络请求库,如 OkHttp,需要在项目中添加相应的依赖。
通过以上步骤,你可以实现一个完整的全局异常捕获并记录、发送错误报告的功能。
完美了吗?是否还有优化之处?
上述实现已经能够基本满足全局异常捕获、记录与发送错误报告的需求,但仍然存在一些可以优化的地方,下面从不同方面为你详细分析:
1. 日志文件管理
- 文件大小和数量限制:当前代码没有对日志文件的大小和数量进行限制,随着应用运行时间增长,可能会占用大量存储空间。可以添加逻辑定期清理旧的日志文件,或者当文件总大小达到一定阈值时进行清理。
private void manageLogFiles() {
String path = Environment.getExternalStorageDirectory() + "/crash/";
File dir = new File(path);
if (dir.exists()) {
File[] files = dir.listFiles();
if (files != null) {
long totalSize = 0;
for (File file : files) {
totalSize += file.length();
}
if (totalSize > MAX_LOG_SIZE) {
// 按照文件修改时间排序,删除旧的文件
Arrays.sort(files, Comparator.comparingLong(File::lastModified));
for (File file : files) {
if (totalSize > MAX_LOG_SIZE) {
totalSize -= file.length();
file.delete();
}
}
}
}
}
}
在 saveCrashInfo2File
方法中调用 manageLogFiles
方法。
- 日志文件压缩:可以将日志文件进行压缩,减少存储空间的占用。例如使用
ZipOutputStream
对日志文件进行压缩。
private void compressLogFile(String filePath) {
try {
File inputFile = new File(filePath);
FileOutputStream fos = new FileOutputStream(filePath + ".zip");
ZipOutputStream zipOut = new ZipOutputStream(fos);
FileInputStream fis = new FileInputStream(inputFile);
ZipEntry zipEntry = new ZipEntry(inputFile.getName());
zipOut.putNextEntry(zipEntry);
byte[] bytes = new byte[1024];
int length;
while ((length = fis.read(bytes)) >= 0) {
zipOut.write(bytes, 0, length);
}
zipOut.close();
fis.close();
fos.close();
inputFile.delete();
} catch (IOException e) {
e.printStackTrace();
}
}
2. 错误报告发送
- 重试机制:当前代码没有对错误报告发送失败的情况进行处理。可以添加重试机制,当发送失败时,在一定时间间隔后重试,重试次数达到上限后再放弃。
private static final int MAX_RETRY_TIMES = 3;
private static final int RETRY_INTERVAL = 5000; // 5 秒
private void sendCrashReportToServer(String filePath) {
int retryCount = 0;
while (retryCount < MAX_RETRY_TIMES) {
try {
// 发送错误报告的逻辑
boolean success = sendReport(filePath);
if (success) {
// 发送成功,删除本地日志文件
new File(filePath).delete();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
retryCount++;
try {
Thread.sleep(RETRY_INTERVAL);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private boolean sendReport(String filePath) {
// 实现具体的发送逻辑,返回发送结果
return false;
}
- 网络状态检查:在发送错误报告之前,先检查网络状态,只有在网络可用的情况下才进行发送,避免不必要的重试和资源浪费。
private boolean isNetworkAvailable(Context context) {
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
return activeNetworkInfo != null && activeNetworkInfo.isConnected();
}
在 sendCrashReportToServer
方法中调用 isNetworkAvailable
方法进行检查。
3. 异常处理
- 避免二次崩溃:在
handleException
方法中,要确保处理异常的代码不会再次抛出异常,否则可能会导致应用无限循环崩溃。可以在关键代码处添加try-catch
块。
private boolean handleException(Throwable ex) {
if (ex == null) {
return false;
}
try {
// 使用 Toast 来显示异常信息
new Thread() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出.", Toast.LENGTH_LONG).show();
Looper.loop();
}
}.start();
// 收集设备参数信息
collectDeviceInfo(mContext);
// 保存日志文件
String filePath = saveCrashInfo2File(ex);
if (filePath != null) {
// 发送错误报告到服务器
sendCrashReportToServer(filePath);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
4. 兼容性和性能优化
- 适配不同 Android 版本:在不同的 Android 版本中,文件存储权限和方式可能会有所不同。需要对不同版本进行适配,例如在 Android 10 及以上版本中,需要使用
Scoped Storage
来访问外部存储。 - 性能优化:在收集设备信息和保存日志文件时,要尽量减少对主线程