第3章:Android生命周期与空指针陷阱
生命周期和空指针,这俩就像一对冤家。说白了,大多数字段访问空指针崩溃,根源就在生命周期没对齐。我刚入行那会儿,觉得生命周期背得滚瓜烂熟,结果线上还是崩了一堆。后来才发现,代码写的和实际跑的,完全是两回事。
为什么会这样?因为Activity和Fragment的生命周期是异步的。你的异步回调、Handler消息、动画监听,都可能在你不知道的时候,引用了已经被销毁的View或Context。嗯,这一章我们就来彻底扒一扒这些坑。
3.1 最常见的场景:异步回调来了,Activity走了
你想想看,你的网络请求在onCreate里发了出去。但用户手快,页面没加载完就返回了。这时候Activity进入了onDestroy。然后网络回调回来了——它尝试去更新一个已经被系统回收的TextView。结果呢?NullPointerException,直接闪退。
我在项目中排查过一个类似问题。用户反馈说“打开个人资料页,快速返回,有一定概率闪退”。日志定位到是在一个Retrofit回调里操作了一个ImageView。当时那个回调是放在匿名内部类里的,持有的是Activity的弱引用?不,是完全引用。这就导致了典型的生命周期空指针。
典型案例代码 (有问题的写法)
public class ProfileActivity extends AppCompatActivity {
private ImageView avatarIv;
private TextView nameTv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_profile);
avatarIv = findViewById(R.id.avatar);
nameTv = findViewById(R.id.name);
// 发起网络请求,回调直接操作 View
UserApi.getUserInfo(new Callback<User>() {
@Override
public void onSuccess(User user) {
// 可能此时Activity已经销毁,avatarIv为null
avatarIv.setImageUrl(user.avatar);
nameTv.setText(user.name);
}
});
}
}
避坑指南:我曾经接手过一个项目,全项目都是这样写的。没有一个异步回调加了生命周期判断。上线第一个月,崩溃率直接干到0.3%。全是这个套路。这种代码必须用Lifecycle-aware组件或弱引用+空判断重构。
3.2 Fragment的分离问题
Fragment的坑比Activity更深。为什么?因为它多了一个onDetach()方法。而且Fragment的View是在onCreateView()里创建的,在onDestroyView()里被销毁。但Fragment的实例还在。
也就是说,Fragment的成员变量View对象在onDestroyView之后就已经是null了。但你的异步回调如果持有Fragment的引用,依然可以调用到那些成员变量。结果就是:Fragment没死,View死了。
我建议在Fragment中使用getView()或isAdded()来双重校验。
稳妥写法:
@Override
public void onSuccess(User user) {
// Fragment是否还在活动状态
if (!isAdded() || getActivity() == null || getView() == null) {
return;
}
// 安全操作View
avatarIv.setImageUrl(user.avatar);
nameTv.setText(user.name);
}
3.3 Activity重建与状态保存
还有一类情况跟屏幕旋转有关。手机转一下,Activity就重建了。如果你的异步回调里持有的是旧的Activity实例,而新的Activity已经创建好,那回调里的View引用就是悬空的。
说白了,别直接持有Activity的强引用传到异步回调里。我个人的习惯是:在Fragment或Activity内部,维护一个LifecycleOwner的观察,然后配合LiveData或Flow来消费结果。这样生命周期自动管理,根本不用手动判断。
| 场景 | 原因 | 推荐解决方案 |
|---|---|---|
| 异步回调晚于 onDestroy | View 已被回收,引用为 null | 使用 LifecycleScope 或 LiveData |
| Fragment View 分离 | onDestroyView 后 View 为 null | isAdded() + getView() 双重判断 |
| Activity 重建 | 旧实例的 View 失效 | 不使用强引用持有 Activity |
| Handler 延迟消息 | 消息到达时页面已关闭 | removeCallbacks 或在 onStop 中清理 |
3.4 如何系统化避免生命周期空指针
解决这类问题,其实就三板斧。第一斧:使用生命周期感知组件。比如ViewModel配合LiveData,或者Kotlin的lifecycleScope。让框架帮你自动处理订阅和取消。
第二斧:主动取消异步操作。在onStop或onDestroy中,主动取消网络请求、移除Handler消息、注销广播监听。别把清理工作交给系统,系统只是帮你回收内存,不保证回调不乱跑。
第三斧:任何时候访问View之前,做空安全判断。这听着很笨,但效果立竿见影。尤其对于团队协作项目,你永远不知道别人什么时候在onCreate里调了个异步方法。保底写法就是:
if (textView != null) {
textView.setText("...");
}
最佳实践总结:
- Activity/Fragment + 异步回调 -> 用
LifecycleOwner绑定生命周期 - Fragment 内操作 View -> 校验
isAdded()和getView() - Handler ->
removeCallbacksAndMessages(null)在onStop - ViewModel -> 使用
viewModelScope自动管理协程 - Kotlin -> 利用
?.let{}优雅地处理空安全
嗯,这一章的核心其实就一句话:别让异步回调的生命周期长过UI组件的生命周期。你只要记住了这个,生命周期相关的空指针能避免80%以上。