4. 作用域错误引发的空指针崩溃
谈到依赖注入,作用域是个绕不开的坎儿。我最早接手一个组件化项目时,心想不就是加几个注解嘛,结果线上崩溃率直接飙升。调查下来,八成都是作用域没配好,导致空指针。今天我就把最典型的几种踩坑场景掰开揉碎讲清楚。
4.1 作用域到底是什么?
说白了,作用域就是告诉 DI 框架:“这个对象该活多久”。
- @Singleton —— 整个应用进程只有一个实例。进程在,它就在。
- @ActivityScoped ——Activity 生命周期内唯一。Activity 销毁它就跟着没了。
- @FragmentScoped —— 跟 Fragment 的生命周期绑定。
- @ViewModelScoped —— 配合 ViewModel 使用,挡住配置变更。
你想想看,如果我把一个 @ActivityScoped 的对象注入了 @Singleton 的类里,会发生什么?Activity 一关,那个对象就被释放了,但单例还在引用着它。这时候再调用它——砰,空指针。
4.2 崩溃复现:Singleton 持有 ActivityScoped 对象
来看一段我亲自修过的代码:
// 错误写法
@Singleton
public class GlobalManager {
@Inject
UserSession userSession; // 假设 UserSession 是 @ActivityScoped
}
// Activity 里
@ActivityScoped
public class UserSession {
public String getToken() { return ; }
}
这个 GlobalManager 是单例,UserSession 绑定 Activity 生命周期。当 Activity 被 finish 后,userSession 变为 null。下次任何地方调用 GlobalManager.getUserSession(),直接空指针。
正确的做法是:不要让短生命周期的对象被长生命周期的对象直接持有。 要么改用 @Reusable,要么通过 Provider<UserSession> 懒加载并在调用时判断 null。
Lazy 或 Provider 拿,并做非空校验。
4.3 另一个坑:ActivityScoped 组件里注入 Singleton 子组件
这种情况更隐蔽。我们经常把网络层、数据库层做成单例,然后在 Activity 组件里直接依赖它们。看起来没问题——单例不会死。但如果你不小心在子组件里又创建了一个新的单例实例,Dagger 会报编译错误。可一旦你用 @Subcomponent 并且没加 @Singleton 限制,就可能在运行时拿到不同的实例。
举个例子:
// AppComponent 是 @Singleton
@Singleton
@Component(modules = {NetworkModule.class})
public interface AppComponent {
void inject(App app);
}
// ActivityComponent 是 @ActivityScoped
@ActivityScoped
@Subcomponent(modules = {ActivityModule.class})
public interface ActivityComponent {
void inject(MainActivity activity);
}
// 错误:ActivityModule 里提供了 @Singleton 的 OkHttpClient
@Module
public class ActivityModule {
@Provides
@Singleton
OkHttpClient provideOkHttp() { // 这里 @Singleton 与本组件 @ActivityScoped 冲突
return new OkHttpClient();
}
}
Dagger 会在编译时报错,因为 @ActivityScoped 的子组件不能提供 @Singleton 作用域的对象。但如果你在 ActivityModule 里忘记加 @Singleton,OkHttpClient 就会在每个 Activity 里重新创建,浪费资源不说,还可能因为连接池不一致导致莫名崩溃。
4.4 作用域正确配置表
| 场景 | 推荐作用域 | 不推荐作用域 | 典型错误后果 |
|---|---|---|---|
| 全局网络库 / 数据库 | @Singleton | @ActivityScoped | Activity 销毁后资源未释放,内存泄漏 |
| Activity 级别的 ViewModel 依赖 | @ActivityScoped | @Singleton | 单例持有 Activity 引用,导致泄漏 |
| Fragment 级别的 UI 状态 | @FragmentScoped | @ActivityScoped | 多个 Fragment 复用同一状态,数据混乱 |
| 短时任务(如一次网络请求) | @Reusable 或无作用域 | @Singleton | 全局对象膨胀,不必要 |
4.5 作用域层级与依赖关系图
下面这张图帮你看清楚不同作用域的生命周期范围,以及严禁的依赖方向。
4.6 避坑指南
- 我曾经遇到过一个线上崩溃:
@ActivityScoped的 Repository 里注入了@Singleton的缓存管理器。本来没问题,直到某次 Activity 被回收后,Repository 也被回收,但缓存管理器还在 old 引用里持有它——后来用@Reusable解决了。 - 建议在每个 Component 的
@Component或@Subcomponent注解里明确声明作用域,编译阶段就能发现大部分冲突。 - 使用 Dagger 的
@BindsInstance绑定 Activity 实例时,务必确保该 Activity 组件的作用域与@ActivityScoped一致。 - 千万别在
@Module的@Provides方法上瞎加作用域。比如你在AppModule里给一个工具类加了@ActivityScoped,Dagger 会报错,因为AppModule属于@Singleton组件。
Provider 或 Lazy 并在使用时判空。
好了,作用域这块说复杂也复杂,说简单也简单——记住生命周期谁长谁短,箭头方向别画反了,大部分崩溃就能提前规避。下个案例我们聊聊多重绑定冲突导致的诡异行为,那才叫一个头大。