【Android 性能分析】延伸阅读:关于异常捕获

Android中的异常捕获
在程序开发中,“崩溃”是开发者最不愿意见到的场景——它会直接导致应用闪退,严重影响用户体验。
在Java和Android中,崩溃的本质是“未被捕获的异常”。
本文将从基础的try-catch语法讲起,详细介绍如何捕获各类常见异常(崩溃),并结合实际场景说明异常处理的最佳实践。
异常与崩溃的本质
在Java/Android中,异常(Exception) 是程序运行时出现的非预期错误(如空指针、数组越界等),而崩溃是“未被捕获的异常”导致的程序终止。
异常分为两大类:
- Checked Exception(受检异常):编译期强制要求处理的异常(如
IOException),不处理会编译报错。 - Unchecked Exception(非受检异常):编译期不强制处理,运行时才可能抛出的异常(如
NullPointerException),通常由逻辑错误导致,是崩溃的主要来源。
try-catch基础语法:异常捕获的核心
try-catch是Java中处理异常的核心语法,用于“尝试执行可能出错的代码,并在出错时捕获异常”。完整语法包括try、catch、finally三个部分,以及用于主动抛异常的throw和throws。
1. 基础结构
try {// 可能抛出异常的代码(如调用一个可能出错的方法)riskyOperation();
} catch (ExceptionType1 e) {// 捕获并处理 ExceptionType1 类型的异常handleException1(e);
} catch (ExceptionType2 e) {// 捕获并处理 ExceptionType2 类型的异常handleException2(e);
} finally {// 无论是否发生异常,都会执行的代码(通常用于释放资源)releaseResources();
}
try块:包裹可能抛出异常的代码。如果执行过程中抛出异常,会立即跳转到对应的catch块。catch块:指定要捕获的异常类型,只有当try块抛出的异常与catch声明的类型匹配时,才会执行该块。finally块:可选,用于执行“必须完成的操作”(如关闭文件、释放网络连接),无论try块是否抛出异常,它都会执行。
2. 主动抛出异常:throw与throws
-
throw:在方法内部主动抛出一个具体的异常对象(通常用于校验参数合法性)。public void setAge(int age) {if (age < 0 || age > 150) {// 主动抛出非法参数异常throw new IllegalArgumentException("年龄必须在0-150之间");}this.age = age; } -
throws:在方法声明处标注该方法可能抛出的异常类型(用于Checked Exception,告知调用者需处理)。// 声明方法可能抛出 IOException(Checked Exception) public void readFile(String path) throws IOException {FileReader reader = new FileReader(path);// ... }
常见异常类型与捕获示例
实际开发中,不同场景会抛出不同类型的异常。以下是Android开发中高频出现的异常及其捕获方式。
1. 空指针异常(NullPointerException)
场景:调用null对象的方法或访问其属性(如String s = null; s.length();)。
捕获示例:
String text = null;
try {int length = text.length(); // 此处会抛出 NullPointerExceptionLog.d("TAG", "文本长度:" + length);
} catch (NullPointerException e) {// 处理空指针:记录日志 + 容错(如使用默认值)Log.e("TAG", "文本为空,无法获取长度", e); // 打印异常堆栈,方便调试int length = 0; // 容错:默认长度为0
}
预防建议:调用前校验对象是否为null(if (text != null) { ... })。
2. 类型转换异常(ClassCastException)
场景:强制转换不兼容的类型(如Object obj = "hello"; Integer num = (Integer) obj;)。
捕获示例:
Object data = getSomeData(); // 假设返回的数据类型不确定
try {String str = (String) data; // 可能抛出 ClassCastExceptionLog.d("TAG", "转换后的数据:" + str);
} catch (ClassCastException e) {Log.e("TAG", "数据类型不匹配,无法转换为String", e);// 容错:使用默认值或提示错误String str = "未知数据";
}
预防建议:转换前用instanceof判断类型(if (data instanceof String) { ... })。
3. 数组越界异常(IndexOutOfBoundsException)
场景:访问数组/集合中不存在的索引(如int[] arr = new int[3]; int val = arr[5];)。
捕获示例:
int[] numbers = {1, 2, 3};
int index = 5;
try {int value = numbers[index]; // 抛出 ArrayIndexOutOfBoundsExceptionLog.d("TAG", "索引" + index + "的值:" + value);
} catch (ArrayIndexOutOfBoundsException e) {Log.e("TAG", "数组索引越界,索引:" + index + ",数组长度:" + numbers.length, e);// 容错:使用默认值int value = -1;
}
预防建议:访问前校验索引范围(if (index >= 0 && index < numbers.length) { ... })。
4. 非法参数异常(IllegalArgumentException)
场景:方法接收无效参数(如WindowManager设置宽高为0、传递负数给要求非负的参数)。
捕获示例(以WindowManager添加悬浮窗为例):
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
View floatingView = new View(this);
int width = 0; // 无效参数:宽高不能为0
int height = 0;WindowManager.LayoutParams params = new WindowManager.LayoutParams(width, height, Build.VERSION.SDK_INT >= 26 ? TYPE_APPLICATION_OVERLAY : TYPE_PHONE,FLAG_NOT_TOUCH_MODAL,PixelFormat.TRANSLUCENT
);try {windowManager.addView(floatingView, params); // 抛出 IllegalArgumentException
} catch (IllegalArgumentException e) {Log.e("TAG", "添加悬浮窗失败:参数不合法", e);// 容错:使用默认宽高重试params.width = 300;params.height = 300;windowManager.addView(floatingView, params);
}
预防建议:调用方法前校验参数合法性(如宽高>0、权限是否已获取)。
5. 资源未找到异常(ResourceNotFoundException)
场景:访问不存在的资源(如getResources().getDrawable(R.drawable.nonexistent))。
捕获示例:
try {Drawable drawable = getResources().getDrawable(R.drawable.logo); // 资源不存在时抛出异常imageView.setImageDrawable(drawable);
} catch (ResourceNotFoundException e) {Log.e("TAG", "资源未找到:logo", e);// 容错:使用默认图片imageView.setImageResource(R.drawable.default_logo);
}
6. 网络异常(IOException与子类)
场景:网络请求失败(如无网络、连接超时),IOException是Checked Exception,需显式处理。
捕获示例:
public void fetchData(String url) {try {URL requestUrl = new URL(url);HttpURLConnection connection = (HttpURLConnection) requestUrl.openConnection();connection.connect();// 读取网络数据...} catch (MalformedURLException e) { // URL格式错误(IOException子类)Log.e("TAG", "URL格式错误:" + url, e);} catch (IOException e) { // 网络连接/读取错误Log.e("TAG", "网络请求失败", e);// 提示用户检查网络Toast.makeText(this, "网络异常,请稍后重试", Toast.LENGTH_SHORT).show();}
}
7. 权限异常(SecurityException)
场景:访问需要权限但未获取的功能(如未申请CAMERA权限时调用相机)。
捕获示例:
try {// 尝试打开相机(需要CAMERA权限)Camera camera = Camera.open();
} catch (SecurityException e) {Log.e("TAG", "未获取相机权限", e);// 引导用户申请权限ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, 100);
}
总结
以下是Java(含Android)中常见异常类型的分类表格,按“标准Java异常”和“Android特有异常”划分,包含类型(受检/非受检)、父类、常见场景及说明:
| 异常类名 | 类型(Checked/Unchecked) | 父类 | 常见场景 | 说明 |
|---|---|---|---|---|
| 一、标准Java异常 | ||||
NullPointerException | Unchecked | RuntimeException | 调用null对象的方法/属性(如String s = null; s.length();) | 最常见的运行时异常,因对象未初始化导致 |
ClassCastException | Unchecked | RuntimeException | 强制转换不兼容类型(如Object obj = "str"; Integer i = (Integer) obj;) | 类型转换时类型不匹配 |
IndexOutOfBoundsException | Unchecked | RuntimeException | 访问数组/集合的无效索引(如List.get(10)但列表长度为5) | 子类包括ArrayIndexOutOfBoundsException(数组)、StringIndexOutOfBoundsException(字符串) |
IllegalArgumentException | Unchecked | RuntimeException | 方法接收无效参数(如传递负数给要求非负的参数、宽高设为0) | 通常由参数校验失败触发(如if (age < 0) throw new ...) |
IllegalStateException | Unchecked | RuntimeException | 对象状态不符合操作要求(如调用Thread.start()两次、未初始化就使用) | 强调“对象状态错误”而非参数错误 |
ArithmeticException | Unchecked | RuntimeException | 算术错误(如整数除以0:int a = 1 / 0;) | 仅针对运行时才能发现的算术问题 |
NumberFormatException | Unchecked | IllegalArgumentException | 字符串转数字失败(如Integer.parseInt("abc")) | IllegalArgumentException的子类,专门处理格式错误 |
IOException | Checked | Exception | 输入/输出错误(如文件未找到、网络中断、流关闭后读写) | 受检异常,强制处理(如文件操作、网络请求) |
FileNotFoundException | Checked | IOException | 访问不存在的文件(如new FileInputStream("nonexist.txt")) | IOException的子类,文件操作常见 |
SQLException | Checked | Exception | 数据库操作错误(如SQL语法错误、连接失败) | 数据库编程中常见,需显式处理 |
InterruptedException | Checked | Exception | 线程被中断(如Thread.sleep()时调用thread.interrupt()) | 多线程中需处理线程中断逻辑 |
CloneNotSupportedException | Checked | Exception | 调用clone()但类未实现Cloneable接口 | 克隆对象时的受检异常 |
| 二、Android特有异常 | ||||
RemoteException | Checked | Exception | 跨进程通信(IPC)失败(如AIDL调用时目标进程崩溃、Binder断开) | Android IPC核心异常,强制处理跨进程通信失败场景 |
ResourceNotFoundException | Checked | NotFoundException | 访问不存在的资源(如getResources().getDrawable(R.id.nonexist)) | 资源ID错误或资源未打包时抛出 |
ActivityNotFoundException | Checked | NotFoundException | 启动Activity但未在Manifest注册(如startActivity(new Intent(this, Xxx.class))) | 组件未注册或Intent匹配失败 |
SecurityException | Unchecked | RuntimeException | 访问需要权限但未获取的功能(如未申请CAMERA时调用相机) | 权限相关错误,Android 6.0+动态权限未授权时常见 |
NetworkOnMainThreadException | Unchecked | RuntimeException | 主线程执行网络请求(Android 3.0+禁止) | 强制要求网络操作在子线程执行 |
AndroidRuntimeException | Unchecked | RuntimeException | Android运行时错误(如Surface创建失败、Parcel数据错误) | 子类包括BadTokenException(窗口令牌无效)、ServiceNotFoundException(服务未找到)等 |
ParseException | Checked | Exception | 数据解析错误(如SimpleDateFormat.parse("2023/13/01")) | Android中日期、JSON等解析失败时常见(继承自Java但高频用于Android) |
try-catch高级用法
1. 多重catch块的顺序
当try块可能抛出多种异常时,catch块需按“子类在前,父类在后”的顺序排列,否则子类异常会被父类异常捕获,导致子类catch块失效。
错误示例:
try {// 可能抛出 NullPointerException 或 Exception
} catch (Exception e) { // 父类异常在前,会拦截所有子类异常Log.e("TAG", "捕获异常", e);
} catch (NullPointerException e) { // 此块永远不会执行Log.e("TAG", "捕获空指针", e);
}
正确示例:
try {// 可能抛出 NullPointerException 或 Exception
} catch (NullPointerException e) { // 子类在前Log.e("TAG", "捕获空指针", e);
} catch (Exception e) { // 父类在后Log.e("TAG", "捕获其他异常", e);
}
2. try-with-resources:自动释放资源
对于实现了AutoCloseable接口的资源(如FileInputStream、HttpClient),可使用try-with-resources语法,无需手动在finally中关闭资源(会自动调用close()方法)。
示例:
// 自动关闭 FileInputStream(无需手动调用 close())
try (FileInputStream fis = new FileInputStream("data.txt")) {byte[] buffer = new byte[1024];fis.read(buffer);
} catch (IOException e) {Log.e("TAG", "文件读取失败", e);
}
3. 异常链:包装原始异常
当需要将低层次异常转换为业务异常时,可通过initCause或构造方法包装原始异常,保留完整堆栈信息。
示例:
public void processData() throws BusinessException {try {// 数据库操作,可能抛出 SQLExceptiondb.query("SELECT * FROM users");} catch (SQLException e) {// 包装为业务异常,保留原始异常信息BusinessException be = new BusinessException("数据查询失败");be.initCause(e); // 关联原始异常throw be;}
}
五、异常处理的最佳实践
-
优先预防,而非捕获
异常捕获是“补救措施”,更优的方式是提前校验参数(如if (obj != null)),从源头避免异常。 -
不要捕获所有异常(尤其是
Error)
Error(如OutOfMemoryError、StackOverflowError)通常是系统级错误,无法通过捕获恢复,捕获它们会掩盖严重问题。// 错误示例:不要捕获所有异常 try {// ... } catch (Throwable t) { // Throwable 包含 Exception 和 Error// 可能掩盖致命错误 } -
避免空的catch块
空的catch块会导致异常被“吞噬”,难以定位问题。至少应记录日志:catch (NullPointerException e) {Log.e("TAG", "空指针异常", e); // 必须记录堆栈信息 } -
finally块中避免抛异常
如果finally块抛出异常,会覆盖try或catch块中的异常,导致原始异常丢失。 -
结合全局异常捕获
在Android中,可通过Thread.setDefaultUncaughtExceptionHandler设置全局异常处理器,捕获未被局部try-catch处理的异常(如崩溃前记录日志):Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {Log.e("全局异常", "未捕获的异常", throwable);// 可选:重启应用或跳转错误页面 });
总结
异常处理是保证应用稳定性的核心环节。try-catch语法为我们提供了捕获异常的工具,但真正的关键在于:理解不同异常的场景,优先预防,合理捕获,详细记录,妥善容错。
通过本文的介绍,希望你能掌握从基础语法到复杂场景的异常处理技巧,让应用在面对错误时更“健壮”,给用户更流畅的体验。

