1. 什么是依赖注入?
说到依赖注入 (DI), 就不得不提控制反转 (IoC), 这两个词总是成对出现.
首先先给出结论. 控制反转是一种软件设计思想, 它被设计出来用于降低代码之间的耦合, 而依赖注入是用来实现控制反转最常见的手段.
那么什么是控制反转? 这得先从它的反面说起, 也就是 "正转" 说起, 所谓的 "正转" 也就是我们在程序中手动的去创建依赖对象 (也就是 new), 而控制反转则是把创建依赖对象的权利交给了框架或者说是 IoC 容器.
看下面的代码, 我们的 MainActivity 中依赖了三个对象, 分别是 Request,Bean 和 AppHolder
- public class MainActivity extends AppCompatActivity
- {
- private static final String TAG = "MainActivity";
- private Request request;
- private Bean bean;
- private AppHolder holder;
- @Override
- protected void onCreate(Bundle savedInstanceState)
- {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- request = new Request.Builder();
- bean = new Bean();
- holder = new AppHodler(this);
- //TODO 使用 request,bean 和 holder
- }
- }
我们当然可以手动 new 调用类的构造函数给这三个对象赋值, 也就是所谓的 "正转".
乍一看这是没有问题的, 但这是因为我们现在只有这一个 Activity, 也只有三个对象需要依赖, 并且这三个依赖并没有互相依赖. 但是, 如果这是一个实际的项目的话, 怎么可能只有一个 Activity 呢? 而且就算是一个 Activity 也不可能仅仅依赖三个对象.
那么问题来了, 如果这是一个实际的项目, 如果这些依赖的对象还有互相依赖, 如果这些类的构造函数发生了改变, 如果逻辑实现的子类发生了变更, 会发生什么?
Boom! 难道要把每一个依赖这些改变的类的 Java 文件中的 new 都修改一遍吗? 这也太蠢了吧!
此时依赖注入闪亮登场, 它有助于我们解除这种耦合.
使用依赖注入最大的好处就是你不需要知道一个对象是怎么来的了, 你只管使用它, 这可以让你的代码更加整洁.
并且如果后来它的构造函数或者是具体实现类发生了改变, 那都与你现在所写的代码无关, 它们的改变不会迫害你去更新现有的代码.
而在传统的软件开发过程中, 我们通常要在一些控制器中去主动依赖一些对象, 如果这些对象的依赖方式在未来频繁地发生改变, 那我们的程序是无法经受住考验的.
这就是所谓控制反转, 它将获得依赖对象的方式反转了.
2. 常见的依赖注入框架
在服务器后端, 一般使用 Spring 框架进行依赖注入.
在 Android 上, 一般使用 Dagger 系列进行依赖注入.
3. 实现自己的依赖注入框架
有些同学可能知道 Dagger 实现了 Java 的依赖注入标准 (JSR-330), 这个标准使用的有些注解确实让人有点摸不着头脑, 而且 Dagger 使用的门槛也较高, 估计应该有不少人看了许多《Dagger 完全入门》之类的文章, 然而到最后还是没搞懂 Dagger 到底是怎么一回事.
所以我就想, 能不能搞一个稍微亲民一点的依赖注入框架让我直接先能用上. 我不是大神, 所以它不一定要实现 JSR-330, 也不一定使用注解处理器来追求极致的效率, 但它必须要好理解, 里面的概念必须是常见的.
在参考了服务器上 Spring 框架的依赖注入后, 我决定使用 xml 作为依赖注入的配置文件, 本来想上 GitHub 看看有没有现成的轮子可以让我 "抄抄" 之类的, 谁知道逛了一圈下来之后才发现 Android 开发者除了 Dagger 和 Dagger2 根本没得选, 这更加坚定了我造轮子的信心.
使用 xml 是有优势的, xml 是最常见的配置文件, 它能更明确的表达依赖关系. 所以就有了 Liteproj 这个库与 Dagger 不同, Liteproj 不使用 Java 来描述对象间的依赖关系, 而是像 Spring 一样使用 xml.
Liteproj 目前的实现中也没有使用注解处理器而是使用了反射, 因为 Liteproj 追求的并非是极致的性能, 而是便于理解和上手以及轻量化和易用性, 它的诞生并不是为了取代 Dagger2 或者其他的一些依赖注入工具, 而是在它们所没有涉及的领域做一个补全.
客官请移步 : GitHub->Liteproj https://github.com/LukeXeon/Liteproj
4.xml 解析
既然选择了 xml, 那么就要需要解决解析 xml 的问题.
经过考虑之后最终选择了 https://github.com/dom4j/dom4j 作为 xml 解析依赖库. 其实 Android 本身自带了 xml 的解析器, 而且它的效率也不错, 那我为什么还要使用 dom4j 呢, 那当然是因为它好用啊. Android 自带的 xml 解析器是基于事件驱动的, 而 dom4j 提供了面向对象的 xml 操作接口, 我觉得这会给我的编码带来极大的便利, 可以降低开发难度.
比如 dom4j 中的 Document->Element->Attribute 等抽象, 非常好地描述了 xml 的结构, 你甚至无需看它的文档就能简单上手, 这可比 XmlPullParser 中定义的一堆常量和事件好理解多了.
而且 dom4j 也是老牌的 xml 解析库, 大名鼎鼎的 hibernate 也使用它来解析 xml 配置文件.
解析 xml, 首先要解决 assets 文件夹下的 xml 文件解析问题, 这个还算比较好处理, 使用 AssetManager 获取 Java 标准流, 然后把他交给 dom4j 解析就可以了.
但是想要解析 res/xml 文件夹下的 xml 就比较麻烦了, 熟悉安卓的人应该都知道, 打包后的 APK,res 文件夹下除了 raw 文件夹会原样保留, 其他文件夹里的内容都会被编译压缩, 为了解析 res/xml 下的 xml, 我依赖 https://github.com/xgouchet/AXML 这个库编写了一个 Axml 到 dom4j 的转换层, 这样一来解析结果就可以共用一套依赖图生成方案.
由此 Liteproj 现在支持解析 assets,res/raw,res/xml 三个位置的 xml 文件, 使用 @Using 注解在你需要注入的组件中标注你要使用那些 xml
- @Retention(RUNTIME)
- @Target({TYPE})
- public @interface Using
- {
- @XmlRes
- @RawRes
- int[] value();//res/xml 或 res/raw 文件夹下的 xml
- String[] assets() default {};//assets 文件夹下的 xml
- }
- // 使用 @Using 注解
- @Using({R.xml.all_test,R.xml.test2,R.raw.test2,assets = {"test3.xml"}})
- public class MainActivity extends AppCompatActivity
- {
- //TODO
- }
5. 对象构造适配
Java 是一门灵活的程序设计语言, 由此诞生了多种对象构造方式. 如传统的使用构造函数构造对象, 又或者是工厂模式, Builder 模式, JavaBean 模式等. Liteproj 必须从一开始就兼容这些现有方案, 否则就是开倒车了.
在 Liteproj 中你需要为你的依赖关系在 xml 中编写一些配置.
第一行是惯例的 <?xml version="1.0" encoding="utf-8"?>, 第二行是最外层是 dependency 标签, 这个标签必须要指定一个 owner 的属性来指定此依赖配置文件所兼容的类型, 下面的 xml 中我指定了 Android.App.Application 作为此 xml 所兼容的类型, 那么所有从这个类型派生的类型都可以使用这个配置文件 (其他类型在满足一定条件时也可以使用, 见下文标题 "生命周期和对象所有权")
- <?xml version="1.0" encoding="utf-8"?>
- <dependency owner="android.app.Application">
- </dependency>
使用 new 生成对象
首先从最原始的对象生成方式开始, 下面的代码将会使用 new 来构造对象.
在配置文件中, 你可以使用 var 标签声明一个依赖, 并用 name 属性指定它在上下文中的唯一名字, 使用 type 属性指定它的类型, 使用 provider 属性指定它的提供模式, 有两种模式可以选择, singleton 和 factory,singleton 保证每次返回的对象都是相同的, 而 factory 则是每次都会重新创建一个新的对象, factory 还是默认的行为, 你可以不写 provider 属性, 那么它默认就是 factory 的.
然后 var 标签中包裹的 new 标签表明此依赖使用构造函数创建, 使用 arg 标签填入构造函数的参数并用 ref 属性引用一个上文中已经存在的另一个已经声明的 var 的 name.
这里我引用了一个特殊的 name->owner, 这个依赖不是你使用 var 声明的, 而是默认导入的, 也就是我们的 Android.App.Application 实例, 除此之外还有另外一个特殊的 var, 那就是 null, 它永远提供 Java 中的 null 值.
Liteproj 会按照 arg 标签 ref 所引用的类型的顺序自动去查找类的 public 构造函数. 不过 Liteproj 的对象生成是惰性的, 这意味这只有你真正使用到该对象它才会被创建, 在 xml 中配置的其实是依赖关系.
- //xml 配置文件
- <?xml version="1.0" encoding="utf-8"?>
- <dependency owner="android.app.Application">
- <var
- name="holder"
- provider="singleton"
- type="org.kexie.android.liteproj.sample.AppHolderTest">
- <new>
- <arg ref="owner"/>
- <!-- 可以有多个 arg-->
- <!-- 如 < arg ref="otherRef"/>-->
- </new>
- </var>
- </dependency>
- //java bean
- public class AppHolderTest
- {
- final Context context;
- public AppHolderTest(Context context)
- {
- this.context = context;
- }
- @Override
- public String toString()
- {
- return super.toString() + context;
- }
- }
使用 Builder 模式
Liteproj 也支持使用 Builder 模式创建对象, 这在 xml 配置中都很直观.
使用 builder 标签指定此依赖使用 Builder 模式生成, 指定 builder 的 type 为 okhttp3.Request$Builder, 使用 action 标签指定最后是调用 build 方法生成所需要的对象 (当然这也是默认行为, 你可以不写出 action 属性), 并使用 arg 标签给 builder 赋值, 不过要注意, 这里的 arg 标签是有 name 的, 它将会映射到 Builder 对象的方法调用上去给 Builder 赋值.
- <var
- name="request"
- type="okhttp3.Request"
- provider="singleton">
- <builder
- action="build"
- type="okhttp3.Request$Builder">
- <arg name="url" ref="url"/>
- </builder>
- </var>
使用工厂模式
下面的代码模拟了工厂模式的使用场景.
使用 factory 标签表明此依赖使用工厂函数生成, 使用 type 属性标明工厂类, 并使用 action 标明需要调用的工厂函数.
你可能注意到了下面出现了一个新的属性 val, 它是用来引用字面值的, 之前的 ref 只能引用标注名字的 var 但是无法引用字面值, 所以我加入了一个新的属性 val, 它可以在 arg 标签中使用, 与 ref 属性不能同时出现, 如果 val 以一个 @开头, 那么它的内容就是 @后面的的字符串, 否则他会被转换成数字或布尔值.
- <var
- name="bean"
- type="org.kexie.android.liteproj.sample.Bean"
- provider="factory">
- <factory
- action="test"
- type="org.kexie.android.liteproj.sample.Factory">
- <arg val="@asdasdd"/>
- </factory>
- </var>
- // 一个简单的工厂类, 包含一个工厂方法 test
- public class Factory
- {
- public static Bean test(String text)
- {
- Log.d("test",text);
- return new Bean();
- }
- }
- public class Bean
- {
- public float field;
- public String string;
- Object object;
- public void setObject(Object object)
- {
- this.object = object;
- }
- @Override
- public String toString()
- {
- return super.toString() + "\n" + field + "\n" + object + "\n" + string;
- }
- }
使用 JavaBean
代码还是上面的代码, 只不过这次加了点东西, factory,builder,new 定义了对象的构造方式, 我们还可以用 field 和 property 标签在对象生成后为对象赋值, 通过 name 属性指定要赋值给哪个字段或属性, property 所指定的 name 应该是一个方法, 它的命名应该符合 Java 的 setter 标准, 比如 name="abc", 对应 void setAbc(YourType) 方法
- <var
- name="bean"
- type="org.kexie.android.liteproj.sample.Bean"
- provider="factory">
- <factory
- action="test"
- type="org.kexie.android.liteproj.sample.Factory">
- <arg val="@asdasdd"/>
- </factory>
- <field
- name="field"
- val="100"/>
- <field
- name="string"
- val="@adadadad"/>
- <property
- name="object"
- ref="owner"/>
- </var>
将 val 转换为 var
我知道每次重复写字面值很蠢, 所以提供了 val 转换为 var 的方法, 让字面值可以像 var 一样被 ref 使用
<var name="url" val="@http://www.hao123.com"/>
完整的 xml
最后在这里提一点无论是 factory 还是 builder 都不允许返回 null 值, 默认导入的 null 只是为了兼容某些特殊情况而设计的, factory 和 builder 返回 null 是没有意义的.
- <?xml version="1.0" encoding="utf-8"?>
- <dependency owner="android.app.Application">
- <var name="url" val="@http://www.hao123.com"/>
- <var
- name="request"
- type="okhttp3.Request"
- provider="singleton">
- <builder
- type="okhttp3.Request$Builder">
- <arg name="url" ref="url"/>
- </builder>
- </var>
- <var
- name="bean"
- type="org.kexie.android.liteproj.sample.Bean"
- provider="factory">
- <factory
- action="test"
- type="org.kexie.android.liteproj.sample.Factory">
- <arg val="@asdasdd"/>
- </factory>
- <field
- name="field"
- val="100"/>
- <field
- name="string"
- val="@adadadad"/>
- <property
- name="object"
- ref="owner"/>
- </var>
- <var
- name="holder"
- type="org.kexie.android.liteproj.sample.AppHolderTest">
- <new>
- <arg ref="owner"/>
- </new>
- </var>
- </dependency>
6. 生命周期和对象所有权
如果说 Android 开发中影响范围最广泛的概念是什么, 我想那一定就是生命周期了.
因为你会发现几乎什么东西都能跟生命周期扯上关系, 在组件创建的时候订阅或请求数据, 并一定要记得在组件销毁的时候取消订阅和清理数据, 要不然你就等着内存泄漏和迷之报错吧.
还有一个和生命周期有关联的词, 那就是对象所有权.
如果 Activity 或者 Service 引用了 Application 的资源, 这很合理, 因为 Application 的生命周期比 Activity 要长, 不必担心内存泄漏, 但如果 Application 引用了 Activity 的资源, 这就有点不合理了, 因为 Activity 可能随时被杀掉, 而 Application 的生命周期又比 Activity 长, 这就容易造成本该在 Activity 中释放的资源一直被 Application 持有, 进而造成内存泄漏, 所以 Application 不应该有 Activity 或者 Service 上资源的对象所有权.
所以 Liteproj 从一开始就设计成和组件的生命周期绑定在一起, 并制定了合理的对象所有权.
Liteproj 支持对 5 组件进行依赖注入:
Application, 无特殊要求, 会在 attachBaseContext 之后与 onCreate 之前执行依赖注入
Activity, 至少是 FragmentActivity(AppCompatActivity 继承了 FragmentActivity)
Service, 需要继承 Liteproj 的
org.kexie.Android.liteproj.LiteService
Fragment, 继承 appcompat 的 Fragment 即可
ViewModel, 需要继承 Liteproj 的
org.kexie.Android.liteproj.LiteViewModel
可以看到 Liteproj 的倾入性还是很低的, 除了 Service 和 ViewModel 需要强制继承基类, 其他组件的基本上都无需代码改动.
图是用 ProcessOn https://www.processon.com 画的:
Service 和 Activity 可以使用 Application 的 xml 配置文件, 因为 Application 的生命周期比 Service 和 Activity 都长, 同理 Fragment 可以使用 Activity 的 xml 配置文件, 而 ViewModel 由于不能违背 MVVM 的设计原则 (ViewModel 不应该知道他是与哪一个 View 进行交互的), 所以除了自己能使用自己的 xml 配置文件之外只允许它使用 Application 的 xml 配置文件.
在 Liteproj 中各种组件的依赖都由 DependencyManager 进行管理, 可以通过 DependencyManager.from(owner) 获得该实例的 DependencyManager.
可以通过 DependencyManager#get(String name) 主动获取 xml 中定义的依赖, 也可以使用隐式装配 (下面马上介绍).
当一个依赖的名字在本组件的 DependencyManager 找不到的时候, DependencyManager 就会把请求转发到上层的 DependencyManager 中, 比如在 Activity 中找不到某个依赖时, 就跑到 Application 上去找 (但前提是你的 Activity 的 @Using 注解中引用了 Application 的依赖配置文件).
DependencyManager 与组件的生命周期绑定, 在组件生命周期结束时, 会释放自己占有的所有资源.
7. 隐式装配
在继续对比 Dagger 和 Spring 两者依赖注入的行为中, 我发现 Spring 有一个 Dagger 没有的优点, 那就是在依赖注入中的一个设计原则, 即一个对象不应该知道自己的依赖是何时, 怎样被注入的.
为了实现这个功能, 我编写了一个 ContentProvider 作为框架的初始化器 (仿照 Android Jetpack Lifecycle 包的做法),ContentProvider 可以在 Application 的 attachBaseContext 之后与 onCreate 之前对框架进行初始化, 并对 Application 进行依赖注入, 自此, Liteproj 终于大功告成.
现在, 你只需要使用 @Reference 注解, 然后填入名字就可以就可以给自己的组件进行依赖注入了,@Reference 注解与 xml 中的 ref 作用基本一致, 但是你将 value 留空的时候, 它可以使用属性名或字段名进行自动装配.
- @Retention(RUNTIME)
- @Target({FIELD, METHOD})
- public @interface Reference
- {
- String value() default "";
- }
就好比这样 (所有代码都来自 GitHub 的 Demo 中):
- @Using({R.xml.all_test})
- public class MainActivity extends AppCompatActivity
- {
- private static final String TAG = "MainActivity";
- @Reference("request")
- Request request;
- @Reference("bean")
- Bean bean;
- @Reference("holder")
- AppHolderTest holderTest;
- @Override
- protected void onCreate(Bundle savedInstanceState)
- {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- Logger.d(request + "\n" + bean + "\n" + holderTest.context);
- }
- }
直接运行你的 App, 就可以看到这些对象居然都被自动设置好了, 对的, 不需要自定义的 Application 类, 也不需要你去调用奇怪的 init 方法再传入一个 Context 实例.
与 JSR-330 相比, Liteproj 只有 @Using 和 @Reference 这两个注解, 这样是不是简单多了?
8. 发布到 jitpack.io
一切代码都编写完成后最后一步当然就是把它发布到在线的 maven 仓库了, 这里我选择了 https://www.jitpack.io , 因为它实在是太方便了有木有, 它与 GitHub 高度集成, 发布一个自己的类库甚至都不需要你登录账号.
在根项目的 build.gradle 中添加
- buildscript {
- repositories {
- google()
- jcenter()
- }
- dependencies {
- classpath 'com.android.tools.build:gradle:3.2.0'
- // ↓↓↓↓↓↓↓↓↓↓↓↓ 加这行! 加这行! ↓↓↓↓↓↓↓↓↓↓↓↓
- classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
- // NOTE: Do not place your application dependencies here; they belong
- // in the individual module build.gradle files
- }
- }
然后继续在你要发布的模块的 build.gradle 的头部添加
- apply plugin: 'com.android.library'
- //↓↓↓↓↓↓↓↓↓↓↓↓ 加这行! 加这行! ↓↓↓↓↓↓↓↓↓↓↓↓
- apply plugin: 'com.github.dcendents.android-maven'
- //↓↓↓↓↓↓↓↓↓↓↓↓ 加这行! 加这行! 并且 group 改成你想要的 ↓↓↓↓↓↓↓↓↓↓↓↓
- group='org.kexie.android'
然后 Look up
在 log 中查看编译 log, 点击 get it 即可开始在 jitpack 上编译你的项目
如果成功
- allprojects {
- repositories {
- ...
- maven { url 'https://www.jitpack.io' }
- }
- }
- dependencies {
- implementation 'com.github.LukeXeon:Liteproj:+'
- }
你就可以用 gradle 远程依赖了.
如果失败, 你就得注意一下 classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'这个插件了, 不同的 gradle 版本有对应不同的插件版本, 笔者的 gradle 是 4.10.1, 具体版本对应可以在这里查看.
9.Liteproj 的缺点
我每次写文章, 我总会在写便了 xxx 的好处后的倒数第二个标题总结 xxx 的缺点, 当然我也不会放过我自己写的库.(我认真起来连我自己都盘, 盘我!)
如你所见 Liteproj 还是一个很年轻的依赖注入框架, 如果你要将它用到商业项目中, 可能需要辛苦你测试一下它有没有一些坑之类的 (逃...... 不过好在咱是开源的对吧, 代码其实也就 1-2k 也不多).
其次, Liteproj 没有使用注解处理器来在编译时处理注解, 而是依赖纯反射, 而且它还需要解析 xml, 虽然只会解析一次, 之后 xml 文件中的依赖信息就会转换为内存中的数据结构, 下次再使用这个 xml 配置文件就是直接使用内存中已经加载好的数据了, 且在 xml 解析时也使用了多线程来进行优化, 尽最大的可能减少了主线程的等待时间, 但这依然可能会带来一些微小的效率问题.
10. 结语
写这篇文章时, Liteproj 基本上已经稳定, 欢迎到我的 GitHub 去 star 或 fork, 如果你在使用的过程中发现了问题, 可以给我 issue, 或者直接给我发一个 pull request.
如果喜欢我的文章记得给我点个赞, 拜托了, 这对我真的很重要.