Android安全检测报告--全局异常捕获 问题处理

Android安全检测报告--全局异常捕获 问题处理:

建议使用全局异常捕获UncaughtExceptionHandler类,当程序发生未知异常时,由该类来处理,并记录发送错误报告。

目录

实现步骤

1. 创建自定义的全局异常捕获类

设计思路

代码实现

1. 创建自定义异常处理器类 CrashHandler

2. 在 Application 类中初始化 CrashHandler

3. 在 AndroidManifest.xml 中指定 Application 类

代码解释

注意事项

完美了吗?是否还有优化之处?

1. 日志文件管理

2. 错误报告发送

3. 异常处理

4. 兼容性和性能优化

5. 完整代码及详细注释

使用方法

1. 创建自定义 Application 类

2. 在 AndroidManifest.xml 中指定 Application 类

代码解释

注意事项

若app真的发生异常了会不会导致闪退问题?

可能导致闪退的情况

1. 异常处理代码本身出错

2. 日志文件存储权限问题

3. 内存耗尽

4. 异常过于严重

避免闪退的措施

1. 完善异常处理逻辑

2. 权限检查与请求

3. 内存优化

4. 合理的异常处理策略

优化思路

优化后的代码示例

代码解释

以下是重新添加详细注释及完善处理逻辑后的代码

使用方法

1. 创建自定义 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 类进行全局异常捕获并记录、发送错误报告是一个非常实用的功能。以下是一个详细的设计与实现步骤:

设计思路

  1. 创建自定义异常处理器:实现 Thread.UncaughtExceptionHandler 接口,重写 uncaughtException 方法,在该方法中处理未捕获的异常。
  2. 记录错误信息:将异常信息、设备信息、应用版本信息等记录到本地文件中,方便后续分析。
  3. 发送错误报告:可以通过网络将错误报告发送到服务器,便于开发者及时了解应用的异常情况。
  4. 设置自定义异常处理器:在应用启动时,将自定义的异常处理器设置为默认的异常处理器。

代码实现

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>

代码解释

  1. CrashHandler 类:实现了 Thread.UncaughtExceptionHandler 接口,用于处理未捕获的异常。

    • init 方法:初始化 CrashHandler,并将其设置为默认的异常处理器。
    • uncaughtException 方法:当未捕获的异常发生时,调用 handleException 方法处理异常,如果处理失败则交给系统默认的异常处理器处理。
    • handleException 方法:显示提示信息,收集设备信息,保存错误信息到文件中,并发送错误报告到服务器。
    • collectDeviceInfo 方法:收集设备的参数信息,如应用版本号、设备型号等。
    • saveCrashInfo2File 方法:将异常信息和设备信息保存到本地文件中。
    • sendCrashReportToServer 方法:实现将错误报告发送到服务器的逻辑。
  2. MyApplication 类:继承自 Application 类,在 onCreate 方法中初始化 CrashHandler

  3. 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 来访问外部存储。
  • 性能优化:在收集设备信息和保存日志文件时,要尽量减少对主线程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值