第1章 依赖注入基础:理解控制反转与依赖注入

大家好,我是老张。干Android开发这些年,我见过不少因为耦合太紧导致的“改一行崩一片”的惨案。今天咱们就从最基础的依赖注入(DI)开始聊,你想想看,这玩意儿到底解决了什么问题。

1.1 什么是控制反转(IoC)

控制反转,英文Inversion of Control,简称 IoC。说白了,就是把“控制权”从你手里交出去。你写代码时,常常需要某个对象去干活,比如一个网络请求库、一个数据库操作类。传统方式是谁需要谁就自己new一个,控制权在自己手里。

核心思想: 将对象创建和依赖管理的控制权,从调用方转移到外部容器或框架。

我在早期做项目时,每写一个Activity就new一堆工具类,后来发现:想换一个底层实现?得把每个new的地方改一遍。那时我才意识到,把创建权交出去有多重要。

1.2 依赖注入(DI)是什么

依赖注入是IoC的一种具体实现。简单说就是:我不自己new,而是让别人把依赖“注射”进来。常见的注入方式有三种:构造器注入、字段注入(@Inject注解)、方法注入。

注入方式描述我推荐的场景
构造器注入通过构造函数传参核心依赖,不可变,首选
字段注入用@Inject等注解直接赋值Android框架类(如Activity)
方法注入通过setter或自定义方法传参可选依赖或初始化后配置
提示: 我个人习惯优先用构造器注入——因为它让依赖关系一目了然,不会出现空指针。

1.3 为什么需要依赖注入

你可能会问:“我直接new不好吗?干嘛非要搞个注入?”嗯,看看这几个痛点你就明白了。

  • 解耦: 调用方不依赖具体实现,只依赖接口。换实现时只改一处。
  • 可测试性: 单元测试时可以轻松传入Mock对象,不用启动整个App。
  • 生命周期管理: 容器能帮你管理单例、作用域(比如ActivityScope),防止内存泄漏。
  • 配置集中: 所有依赖的创建逻辑统一在一个地方,维护起来清爽。

我一同事之前接手一个老项目,里面到处都是静态工厂和单例。想加个单元测试?得先启动模拟器跑一遍完整流程……后来改成DI,测试日子好过多了。

1.4 手动依赖注入的局限

在没有Hilt这类框架之前,我们只能手动实现DI——自己写一个“容器”类,负责创建和提供依赖。比如这样:

class AppContainer {
    private val database = Room.databaseBuilder(...)
    val userRepository = UserRepository(database.userDao())
    val orderRepository = OrderRepository(database.orderDao())
}

// 在Activity里手动接收
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        val container = (application as MyApp).appContainer
        val repo = container.userRepository
        // ...
    }
}

这种手动DI有几个明显问题:

  1. 模板代码爆炸: 每个依赖都要手动注册、手动传参,项目一大多达几百个类,光写容器就累死人。
  2. 作用域管理困难: 想实现Activity级别的单例?你得自己维护Map,还要注意在Activity销毁时清除——稍不留神就内存泄漏。
  3. 耦合依旧存在: Activity仍然知道容器的存在(它强依赖AppContainer),并没有完全解耦。
  4. 重构风险高: 修改一个依赖的构造参数,所有手动创建的地方都得改一遍。
避坑指南: 我曾经在一个中大型项目里用手动DI,结果忘了在Fragment销毁时清理一个单例Map,导致整个Activity泄漏。后来查了好几天才定位到——那会儿真想抽自己。

1.5 从手动到自动:Hilt登场的前夜

手动DI的局限催生了自动DI框架。Dagger 2 先在Java/Kotlin领域兴起,但它配置繁琐、编译慢。Google后来基于Dagger推出了Hilt,专门为Android优化——自动生成组件、简化注解、集成ViewModel等。你看这张图就明白了DI的核心演进:

依赖注入核心概念结构 传统手动方式 类A(调用方) new 类B() 硬编码 类B(具体实现) ❌ 耦合紧 · 难测试 · 泄漏风险 ❌ 作用域难管理 依赖注入方式(Hilt) 类A(调用方) Hilt容器(自动管理) 接口 / 抽象 ✅ 松耦合 · Mock测试 · 生命周期安全 ✅ 作用域自动管理 → 演进

嗯,看完这张图你应该明白了:手动DI像你亲自买菜、切菜、炒菜;Hilt这样的自动DI则像找了一个专业厨师团队——你只管点菜(声明依赖),厨房帮你搞定一切,出错了还帮你排查。

个人心得: 我刚开始用Hilt时也有点抵触——觉得注解太多、黑箱操作。但用了一个项目后,发现手动DI的代码量减少了60%以上,内存泄漏也基本绝迹。真香定律没错。