美文网首页Android开发经验谈Android技术知识程序员
Android技术栈(三)依赖注入技术的探讨与实现

Android技术栈(三)依赖注入技术的探讨与实现

作者: 06fd4cf1f427 | 来源:发表于2019-04-11 15:00 被阅读125次

    1.什么是依赖注入?

    说到依赖注入(DI),就不得不提控制反转(IoC),这两个词总是成对出现.

    首先先给出结论。控制反转是一种软件设计思想,它被设计出来用于降低代码之间的耦合,而依赖注入是用来实现控制反转最常见的手段。

    那么什么是控制反转?这得先从它的反面说起,也就是"正转"说起,所谓的"正转"也就是我们在程序中手动的去创建依赖对象(也就是new),而控制反转则是把创建依赖对象的权利交给了框架或者说是IoC容器.

    看下面的代码,我们的MainActivity中依赖了三个对象,分别是Request,BeanAppHolder

    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到底是怎么一回事.

    image.png

    所以我就想,能不能搞一个稍微亲民一点的依赖注入框架让我直接先能用上.我不是大神,所以它不一定要实现JSR-330,也不一定使用注解处理器来追求极致的效率,但它必须要好理解,里面的概念必须是常见的.

    在参考了服务器上Spring框架的依赖注入后,我决定使用xml作为依赖注入的配置文件,本来想上Github看看有没有现成的轮子可以让我"抄抄"之类的,谁知道逛了一圈下来之后才发现Android开发者除了DaggerDagger2根本没得选,这更加坚定了我造轮子的信心.

    使用xml是有优势的,xml是最常见的配置文件,它能更明确的表达依赖关系。所以就有了Liteproj这个库与Dagger不同,Liteproj不使用Java来描述对象间的依赖关系,而是像Spring一样使用xml.

    Liteproj目前的实现中也没有使用注解处理器而是使用了反射,因为Liteproj追求的并非是极致的性能,而是便于理解和上手以及轻量化和易用性,它的诞生并不是为了取代Dagger2或者其他的一些依赖注入工具,而是在它们所没有涉及的领域做一个补全。

    客官请移步 : Liteproj

    4.xml解析

    既然选择了xml,那么就要需要解决解析xml的问题.

    经过考虑之后最终选择了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就比较麻烦了,熟悉安卓的人应该都知道,打包后的APKres文件夹下除了raw文件夹会原样保留,其他文件夹里的内容都会被编译压缩,为了解析res/xml下的xml,我依赖AXML这个库编写了一个Axmldom4j的转换层,这样一来解析结果就可以共用一套依赖图生成方案。

    由此Liteproj现在支持解析assetsres/rawres/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属性指定它的提供模式,有两种模式可以选择,singletonfactory,singleton保证每次返回的对象都是相同的,而factory则是每次都会重新创建一个新的对象,factory还是默认的行为,你可以不写provider属性,那么它默认就是factory的.

    然后var标签中包裹的new标签表明此依赖使用构造函数创建,使用arg标签填入构造函数的参数并用ref属性引用一个上文中已经存在的另一个已经声明的varname.

    这里我引用了一个特殊的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模式生成,指定buildertypeokhttp3.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定义了对象的构造方式,我们还可以用fieldproperty标签在对象生成后为对象赋值,通过name属性指定要赋值给哪个字段或属性,property所指定的name应该是一个方法,它的命名应该符合Javasetter标准,比如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只是为了兼容某些特殊情况而设计的,factorybuilder返回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,需要继承Liteprojorg.kexie.android.liteproj.LiteService
    • Fragment,继承appcompatFragment即可
    • ViewModel,需要继承Liteprojorg.kexie.android.liteproj.LiteViewModel

    可以看到Liteproj的倾入性还是很低的,除了ServiceViewModel需要强制继承基类,其他组件的基本上都无需代码改动.

    图是用ProcessOn画的:

    image.png

    ServiceActivity可以使用Applicationxml配置文件,因为Application的生命周期比ServiceActivity都长,同理Fragment可以使用Activityxml配置文件,而ViewModel由于不能违背MVVM的设计原则(ViewModel不应该知道他是与哪一个View进行交互的),所以除了自己能使用自己的xml配置文件之外只允许它使用Applicationxml配置文件.

    Liteproj中各种组件的依赖都由DependencyManager进行管理,可以通过DependencyManager.from(owner)获得该实例的DependencyManager.

    可以通过DependencyManager#get(String name)主动获取xml中定义的依赖,也可以使用隐式装配(下面马上介绍).

    当一个依赖的名字在本组件的DependencyManager找不到的时候,DependencyManager就会把请求转发到上层的DependencyManager中,比如在Activity中找不到某个依赖时,就跑到Application上去找(但前提是你的Activity@Using注解中引用了Application的依赖配置文件).

    DependencyManager与组件的生命周期绑定,在组件生命周期结束时,会释放自己占有的所有资源.

    7.隐式装配

    在继续对比DaggerSpring两者依赖注入的行为中,我发现Spring有一个Dagger没有的优点,那就是在依赖注入中的一个设计原则,即一个对象不应该知道自己的依赖是何时怎样被注入的。

    为了实现这个功能,我编写了一个ContentProvider作为框架的初始化器(仿照Android Jetpack Lifecycle包的做法),ContentProvider可以在ApplicationattachBaseContext之后与onCreate之前对框架进行初始化,并对Application进行依赖注入,自此,Liteproj终于大功告成.

    现在,你只需要使用@Reference注解,然后填入名字就可以就可以给自己的组件进行依赖注入了,@Reference注解与xml中的ref作用基本一致,但是你将value留空的时候,它可以使用属性名或字段名进行自动装配.

    @Retention(RUNTIME)
    @Target({FIELD, METHOD})
    public @interface Reference
    {
        String value() default "";
    }
    

    就好比这样(所有代码都来自GithubDemo中):

    @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仓库了,这里我选择了jitpack.io,因为它实在是太方便了有木有,它与Github高度集成,发布一个自己的类库甚至都不需要你登录账号.

    image.png

    在根项目的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

    image.png

    log中查看编译log,点击get it即可开始在jitpack上编译你的项目

    image.png

    如果成功

        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版本有对应不同的插件版本,笔者的gradle4.10.1,具体版本对应可以在这里查看.

    9.Liteproj的缺点

    我每次写文章,我总会在写便了xxx的好处后的倒数第二个标题总结xxx的缺点,当然我也不会放过我自己写的库。(我认真起来连我自己都盘,盘我!)

    如你所见Liteproj还是一个很年轻的依赖注入框架,如果你要将它用到商业项目中,可能需要辛苦你测试一下它有没有一些坑之类的(逃......不过好在咱是开源的对吧,代码其实也就1-2k也不多)。

    其次,Liteproj没有使用注解处理器来在编译时处理注解,而是依赖纯反射,而且它还需要解析xml,虽然只会解析一次,之后xml文件中的依赖信息就会转换为内存中的数据结构,下次再使用这个xml配置文件就是直接使用内存中已经加载好的数据了,且在xml解析时也使用了多线程来进行优化,尽最大的可能减少了主线程的等待时间,但这依然可能会带来一些微小的效率问题。

    10.结语

    写这篇文章时,Liteproj基本上已经稳定,欢迎到我的githubstarfork,如果你在使用的过程中发现了问题,可以给我issue,或者直接给我发一个pull request

    【附】相关架构及资料
    Android高级技术大纲

    资料及源码领取

    点赞+加群免费获取 Android IOC架构设计

    领取获取往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术

    相关文章

      网友评论

        本文标题:Android技术栈(三)依赖注入技术的探讨与实现

        本文链接:https://www.haomeiwen.com/subject/hamtwqtx.html