android java异常 笔记

18 阅读6分钟

常见异常:

  • 空指针异常(NullPointerException)
  • 非法状态异常(IllegalStateException)
  • 索引越界异常(IndexOutOfBoundsException)
  • 不合法的参数异常(IllegalArgumentException)
  • 内存溢出错误(OutOfMemoryError)

自定义异常处理器:

    //设置全局捕获的异常处理器
    Thread.setDefaultUncaughtExceptionHandler(new MyCrashHandler())
   public class MyApplication extends Application {
   
   @Override
   public void onCreate() {
       super.onCreate();
       setupGlobalExceptionHandler();
   }
   
   private void setupGlobalExceptionHandler() {
       // 1. 获取系统默认的处理器(备用)
       final Thread.UncaughtExceptionHandler defaultHandler = 
               Thread.getDefaultUncaughtExceptionHandler();
       
       // 2. 设置自定义的全局处理器
       Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
           @Override
           public void uncaughtException(Thread thread, Throwable ex) {
               // 3. 收集崩溃信息
               String crashInfo = collectCrashInfo(thread, ex);
               
               // 4. 执行紧急操作(在子线程中进行耗时操作)
               new Thread(() -> {
                   // 4.1 保存崩溃信息到本地文件
                   saveCrashToLocal(crashInfo);
                   
                   // 4.2 可选:尝试上传到服务器(网络操作需谨慎)
                   // uploadCrashToServer(crashInfo);
                   
                   // 5. 所有操作完成后,交给系统默认处理(结束进程)
                   // 也可以在此处直接杀死进程:android.os.Process.killProcess(android.os.Process.myPid());
                   defaultHandler.uncaughtException(thread, ex);
               }).start();
               
               // 6. 为文件保存等操作争取一点时间(不推荐在主线程等待太久)
               try {
                   Thread.sleep(2000); // 等待2秒
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       });
   }
   
   private String collectCrashInfo(Thread thread, Throwable ex) {
       StringBuilder sb = new StringBuilder();
       // 收集时间
       sb.append("Crash Time: ").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date())).append("\n\n");
       
       // 收集设备信息
       sb.append("Device Info:\n");
       sb.append("Model: ").append(Build.MODEL).append("\n");
       sb.append("Android SDK: ").append(Build.VERSION.SDK_INT).append("\n");
       sb.append("App Version: ").append(BuildConfig.VERSION_NAME).append("\n\n");
       
       // 收集异常堆栈
       sb.append("Stack Trace:\n");
       StringWriter sw = new StringWriter();
       PrintWriter pw = new PrintWriter(sw);
       ex.printStackTrace(pw);
       sb.append(sw.toString());
       
       return sb.toString();
   }
   
   private void saveCrashToLocal(String crashInfo) {
       // 将崩溃信息保存到应用私有目录
       String fileName = "crash_" + System.currentTimeMillis() + ".log";
       File crashFile = new File(getExternalFilesDir("crashes"), fileName);
       
       try (FileOutputStream fos = new FileOutputStream(crashFile)) {
           fos.write(crashInfo.getBytes());
       } catch (IOException e) {
           e.printStackTrace();
       }
   }
}

设计一个异常兜底方案,需要在应用崩溃前执行清理操作并引导用户。这个方案的核心思路是:通过全局异常处理器捕获崩溃 → 在新进程中执行兜底操作 → 提供用户友好的恢复界面

下图展示了完整的兜底方案架构和工作流程:

flowchart TD
    A[应用发生未捕获异常] --> B[全局异常处理器被触发]
    
    B --> C{决策: 执行兜底策略}
    
    C --> D[启动独立进程的<br>兜底Activity]
    C --> E[在子线程中执行<br>紧急清理操作]
    
    D --> F[显示用户友好的崩溃恢复界面]
    E --> G[选择性清理<br>缓存/临时文件]
    
    F --> H{用户选择操作}
    G --> I[记录清理日志]
    
    H --> J[重启应用]
    H --> K[检查更新]
    H --> L[提交反馈]
    H --> M[退出应用]
    
    J --> N[结束原进程<br>启动新进程]
    K --> O[跳转应用商店]
    
    subgraph P [独立进程环境]
        D
        F
        H
        J
        K
        L
        M
    end

🛠️ 核心实现方案

1. 增强全局异常处理器

在Application中设置异常处理器,执行紧急清理并启动兜底Activity:

public class SafetyApplication extends Application {
    
    @Override
    public void onCreate() {
        super.onCreate();
        setupSafetyNet();
    }
    
    private void setupSafetyNet() {
        final Thread.UncaughtExceptionHandler defaultHandler = 
                Thread.getDefaultUncaughtExceptionHandler();
        
        Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> {
            // 1. 收集崩溃信息
            String crashInfo = collectCrashInfo(ex);
            saveCrashLog(crashInfo);
            
            // 2. 根据异常类型执行不同的清理策略
            executeSafetyCleaning(ex);
            
            // 3. 在新进程中启动兜底Activity
            Intent intent = new Intent(getApplicationContext(), SafetyNetActivity.class);
            intent.putExtra("crash_info", crashInfo);
            intent.putExtra("exception_type", ex.getClass().getSimpleName());
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
            
            // 4. 设置新进程启动标记
            intent.putExtra("from_safety_net", true);
            
            // 5. 尝试启动,如果失败则交给系统处理
            try {
                startActivity(intent);
            } catch (Exception e) {
                if (defaultHandler != null) {
                    defaultHandler.uncaughtException(thread, ex);
                    return;
                }
            }
            
            // 6. 给新Activity启动时间后结束进程
            try {
                Thread.sleep(1500);
            } catch (InterruptedException ignored) {}
            
            android.os.Process.killProcess(android.os.Process.myPid());
            System.exit(1);
        });
    }
    
    private void executeSafetyCleaning(Throwable ex) {
        new Thread(() -> {
            try {
                // A. 内存相关异常:清理缓存
                if (ex instanceof OutOfMemoryError || 
                    ex instanceof MemoryError) {
                    clearAppCache();
                }
                
                // B. 存储相关异常:清理临时文件
                if (ex instanceof IOException || 
                    ex.getMessage() != null && 
                    ex.getMessage().contains("storage")) {
                    clearTempFiles();
                }
                
                // C. 网络相关异常:清理网络缓存
                if (ex instanceof SocketTimeoutException ||
                    ex instanceof IOException && 
                    ex.getMessage() != null &&
                    (ex.getMessage().contains("Network") || 
                     ex.getMessage().contains("socket"))) {
                    clearNetworkCache();
                }
                
                // D. 总是执行的轻量级清理
                clearPartialCache();
                
            } catch (Exception e) {
                // 清理过程中的异常要静默处理
            }
        }).start();
    }
}

2. 实现兜底恢复Activity

在独立进程中运行的恢复界面,提供用户操作选项:

<!-- AndroidManifest.xml 配置 -->
<activity
    android:name=".SafetyNetActivity"
    android:process=":safety_net"
    android:theme="@style/Theme.AppCompat.Light.NoActionBar"
    android:excludeFromRecents="true"
    android:noHistory="true"
    android:taskAffinity=".safety_net"
    android:exported="false" />
public class SafetyNetActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_safety_net);
        
        // 获取崩溃信息
        String crashType = getIntent().getStringExtra("exception_type");
        String crashInfo = getIntent().getStringExtra("crash_info");
        
        setupUI(crashType);
        analyzeAndSuggest(crashType, crashInfo);
    }
    
    private void setupUI(String crashType) {
        TextView title = findViewById(R.id.tv_title);
        TextView suggestion = findViewById(R.id.tv_suggestion);
        Button btnRestart = findViewById(R.id.btn_restart);
        Button btnUpgrade = findViewById(R.id.btn_upgrade);
        Button btnFeedback = findViewById(R.id.btn_feedback);
        Button btnExit = findViewById(R.id.btn_exit);
        
        // 根据崩溃类型显示不同提示
        String[] suggestions = getSuggestionsForCrash(crashType);
        title.setText(suggestions[0]);
        suggestion.setText(suggestions[1]);
        
        btnRestart.setOnClickListener(v -> restartApp());
        btnUpgrade.setOnClickListener(v -> checkAndUpgrade());
        btnFeedback.setOnClickListener(v -> submitFeedback());
        btnExit.setOnClickListener(v -> exitApp());
    }
    
    private String[] getSuggestionsForCrash(String crashType) {
        Map<String, String[]> suggestionMap = new HashMap<>();
        suggestionMap.put("OutOfMemoryError", new String[]{
            "内存不足",
            "已自动清理缓存,建议重启应用。如果问题持续,请检查设备存储空间。"
        });
        suggestionMap.put("NullPointerException", new String[]{
            "程序异常",
            "应用遇到了意外错误,重启可能解决问题。"
        });
        // ... 其他异常类型的提示
        
        return suggestionMap.getOrDefault(crashType, new String[]{
            "应用程序遇到问题",
            "抱歉给您带来不便,请尝试重启应用。"
        });
    }
    
    private void restartApp() {
        // 重启应用
        Intent intent = new Intent(this, MainActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        startActivity(intent);
        finish();
        
        // 结束当前进程
        android.os.Process.killProcess(android.os.Process.myPid());
        System.exit(0);
    }
    
    private void checkAndUpgrade() {
        // 检查更新逻辑
        if (shouldForceUpgrade()) {
            showForceUpgradeDialog();
        } else {
            // 引导到应用商店
            try {
                Uri uri = Uri.parse("market://details?id=" + getPackageName());
                Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                startActivity(intent);
            } catch (Exception e) {
                // 跳转失败,显示网页版应用商店
                Uri uri = Uri.parse("https://play.google.com/store/apps/details?id=" + getPackageName());
                Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                startActivity(intent);
            }
        }
    }
}

3. 实现智能清理策略

public class SafetyCleaner {
    
    public static void clearAppCache(Context context) {
        try {
            // 1. 清理内部缓存
            File cacheDir = context.getCacheDir();
            if (cacheDir != null && cacheDir.isDirectory()) {
                deleteDir(cacheDir);
            }
            
            // 2. 清理外部缓存
            File externalCacheDir = context.getExternalCacheDir();
            if (externalCacheDir != null && externalCacheDir.isDirectory()) {
                deleteDir(externalCacheDir);
            }
            
            // 3. 清理WebView缓存
            clearWebViewCache(context);
            
            // 4. 清理图片加载框架缓存
            clearImageLoaderCache(context);
            
        } catch (Exception e) {
            // 静默处理清理异常
        }
    }
    
    public static void clearPartialCache() {
        // 只清理过期或过大的缓存文件
        File cacheDir = context.getCacheDir();
        if (cacheDir.exists() && cacheDir.isDirectory()) {
            File[] files = cacheDir.listFiles();
            if (files != null) {
                long now = System.currentTimeMillis();
                for (File file : files) {
                    // 删除7天前的缓存文件
                    if (now - file.lastModified() > 7 * 24 * 60 * 60 * 1000L) {
                        file.delete();
                    }
                    // 删除超过10MB的大文件
                    if (file.length() > 10 * 1024 * 1024) {
                        file.delete();
                    }
                }
            }
        }
    }
    
    private static void clearWebViewCache(Context context) {
        // 清理WebView缓存
        try {
            context.deleteDatabase("webview.db");
            context.deleteDatabase("webviewCache.db");
        } catch (Exception e) {
            // 忽略异常
        }
    }
}

📊 兜底策略配置表

异常类型触发条件清理操作用户提示自动操作
内存不足OutOfMemoryError清除所有缓存、临时文件"内存不足,已清理缓存"自动重启
存储异常IOException (含storage关键字)清除临时文件、日志文件"存储空间可能不足"引导清理存储
网络超时SocketTimeoutException清除网络缓存"网络连接不稳定"重试或离线模式
空指针NullPointerException轻量级清理"程序异常,建议重启"
版本不兼容NoSuchMethodError"检测到版本兼容问题,请更新"强制升级提示

⚠️ 重要注意事项

  1. 进程隔离:兜底Activity必须在独立进程(android:process=":safety_net")中运行,确保即使主进程崩溃也能正常显示。

  2. 权限处理:Android 11+ 对文件访问有限制,清理缓存时应使用 Context.getCacheDir() 等API,而非直接访问绝对路径。

  3. 数据安全

    // 避免清理用户重要数据
    public static void safeClean(Context context) {
        // 不清理以下目录:
        // - getFilesDir() 用户文件
        // - getDatabasePath() 数据库
        // - SharedPreferences
        // - 外部私有目录
    }
    
  4. 性能影响:清理操作应在子线程执行,且设置超时时间:

    ExecutorService executor = Executors.newSingleThreadExecutor();
    Future<?> future = executor.submit(() -> executeSafetyCleaning(ex));
    try {
        future.get(3, TimeUnit.SECONDS); // 3秒超时
    } catch (TimeoutException e) {
        future.cancel(true); // 超时取消清理
    }
    
  5. 避免循环崩溃:记录崩溃频率,防止频繁重启:

    public boolean shouldRestartApp() {
        SharedPreferences sp = getSharedPreferences("crash_stats", MODE_PRIVATE);
        long lastCrashTime = sp.getLong("last_crash", 0);
        int crashCount = sp.getInt("crash_count", 0);
        
        long currentTime = System.currentTimeMillis();
        if (currentTime - lastCrashTime < 60000) { // 1分钟内
            crashCount++;
        } else {
            crashCount = 1;
        }
        
        // 1分钟内崩溃3次,不再自动重启
        if (crashCount >= 3) {
            return false;
        }
        
        sp.edit().putLong("last_crash", currentTime)
                  .putInt("crash_count", crashCount)
                  .apply();
        return true;
    }
    

🎯 最佳实践建议

  1. 分级策略

    • 一级崩溃(UI相关):仅提示,不清理
    • 二级崩溃(资源相关):轻度清理
    • 三级崩溃(系统相关):重度清理+引导升级
  2. 用户控制:提供设置选项让用户决定是否启用自动清理:

    if (Settings.isAutoCleanEnabled()) {
        executeSafetyCleaning(ex);
    }
    
  3. 反馈机制:收集用户选择的处理方式,优化兜底策略。