一、依赖注入基础概念与组件化项目中的依赖管理挑战

说实话,很多同学刚接触依赖注入时,都会被「控制反转」「DI容器」这些术语绕晕。

我个人习惯用一个生活例子来理解:你开了一家咖啡店。依赖就是「制作咖啡需要咖啡豆和牛奶」——如果你每天自己跑去进货、自己烘焙、自己挤牛奶,那叫「主动依赖」,耦合度极高。依赖注入就是让外卖小哥把原料送到店里,你只管做咖啡。

👉 核心思想:依赖的创建与获取,从类内部转移到外部。

1.1 到底什么是依赖注入?

依赖注入(Dependency Injection,简称DI)其实就三个字:给我用。

传统的写法,你在一个类里 new 另一个对象:

public class CoffeeMaker {
    private Grinder grinder = new Grinder(); // 硬编码依赖
    private WaterPump pump = new WaterPump();
    
    public Coffee make() {
        // ...
    }
}

问题在哪?Grinder 和 WaterPump 被写死了。想换一个高级研磨机?得改代码。

依赖注入的写法:

public class CoffeeMaker {
    private final Grinder grinder;
    private final WaterPump pump;

    // 依赖通过构造函数注入
    public CoffeeMaker(Grinder grinder, WaterPump pump) {
        this.grinder = grinder;
        this.pump = pump;
    }
}

嗯,其实就是把对象的创建权交出去。谁交?通常交给一个「DI容器」来统一管理。

注入的三种常见方式:

  • 构造函数注入 —— 我最推荐,依赖明确,不可变,便于测试
  • 属性/字段注入 —— 用 @Inject 直接标记字段,但依赖不透明,容易出空指针
  • 方法/Setter注入 —— 灵活性高,但可能忘记调用

我在项目中遇到过一个坑:有同事全用字段注入,结果单元测试时 Mock 没注入进去,跑起来直接 NullPointerException。从那以后,我要求团队优先用构造函数注入。

1.2 为什么组件化项目需要DI?

你想想看,一个组件化项目,模块通常有几十个甚至上百个。比如电商App里,有 :user、:order、:payment、:product 等组件。

没有DI时,组件间互相 new 依赖,就会变成一团乱麻:

  • 循环依赖:A组件 new 了B,B又 new 了A,编译报错
  • 重复创建:每个页面都 new 一个网络请求实例,资源浪费
  • 难以替换:想给测试环境注入Mock数据?得改多处代码

依赖注入帮你解决了这些问题。每个组件只声明「我需要什么」,不关心「谁给我」。DI容器负责把依赖组装好,在合适的时机注入。

1.3 组件化中的依赖管理挑战

讲几个我实际遇到的痛点,大家感受一下:

挑战 场景描述 后果
版本冲突 两个组件依赖同一个库的不同版本 编译失败或运行时 ClassNotFoundException
作用域混乱 LoginActivity 里的 UserManager 是单例,但另一个组件也创建了新的实例 数据不一致,用户信息乱窜
组件间通信耦合 组件A直接调用组件B的静态方法获取数据 拆不掉、改不动,违背组件化初衷
生命周期错乱 Fragment 销毁后,注入的对象还在持有 Activity 引用 内存泄漏或崩溃:Attempt to invoke virtual method on a null reference

当年我参与过一个项目,购物车组件和支付组件各自引入了不同版本的 OkHttp,编译时没报错,运行到某个页面直接因为 Gson 解析不兼容挂掉。排查了整整一个下午……后来我们用统一依赖版本管理 + 依赖注入来控制。

⚠️ 避坑指南: 曾经有个团队,在组件化初期图省事,让每个组件自己维护一个全局的 ServiceLocator。结果上线后出现大量「xxx is not bound」的运行时异常。因为组件 A 初始化顺序比组件 B 晚,B 调用 A 提供的服务时空指针了。

解决方案:要么用框架管理初始化顺序(如 Dagger 的 @Module + @Provides),要么用懒加载 + 条件检查。

1.4 本章知识体系概览

下面这张图,帮你把依赖注入的核心概念和组件化中的挑战串起来:

组件化项目依赖注入 —— 核心知识体系 依赖注入基础 概念:控制反转 + 依赖注入 三种注入方式:构造/字段/方法 DI容器:组装/管理/提供 组件化项目 多模块/组件:依赖关系复杂 生命周期管理:组件独立运行 版本/仓库/依赖冲突 崩渍场景(本课程核心) 空指针:注入为 null 类转换异常:版本/作用域冲突 初始化顺序错误导致的崩渍 核心关系图 依赖注入解决组件化中「依赖如何组织与提供」的问题;但管理不当会引发一系列运行时崩渍。 第1章:概念与挑战 第2-10章:常见崩渍复现 第11-20章:解决方案 第21-30章:最佳实践

学习路径小贴士:

  • 先搞懂 DI 的三个基本形式(构造/字段/方法),理解「谁控制谁」
  • 再了解组件化项目中依赖管理的典型难题(版本、作用域、生命周期)
  • 后续章节我们会逐一复现崩溃现场,手把手带你排查、修复

1.5 一个简单的Dagger示例

我们团队在组件化项目中常用 Dagger 或 Hilt。这里给个最简例子感受下:

// 定义依赖
public class UserRepository {
    // ...
}

// 在组件中声明
@Module
public class AppModule {
    @Provides
    @Singleton
    public UserRepository provideUserRepository() {
        return new UserRepository();
    }
}

// 注入到Activity
@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {
    @Inject
    UserRepository repository; // 别担心,Hilt会在构造时注入
}

看到没?你只管写 @Inject,框架帮你 new 好对象,且默认单例。如果多个组件需要同一个 UserRepository,不会各自创建——这避免了数据不一致的问题。

⚠️ 特别提醒: 不要在组件间共享不设防的单例。我曾经见过一个项目,把 Retrofit 实例做成全局单例,但每个组件往里面加了不同的拦截器……最后请求头混乱,排错排到崩溃。依赖注入不是万能药,它解决的是「谁来创建」的问题,而「如何设计依赖」还是得你自己负责。

小结

这一章我们聊了:

  • 依赖注入的本质——把对象的创建和绑定从类内部移到外部
  • 组件化项目中常见的四大依赖管理挑战:版本冲突、作用域混乱、通信耦合、生命周期错乱
  • 梳理了课程的知识体系