Android技術棧(三)依賴注入技術的探討與實現

曦瞳發表於2019-03-22

Android技術棧(三)依賴注入技術的探討與實現

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

Android技術棧(三)依賴注入技術的探討與實現

所以我就想,能不能搞一個稍微親民一點的依賴注入框架讓我直接先能用上.我不是大神,所以它不一定要實現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畫的:

Android技術棧(三)依賴注入技術的探討與實現

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高度整合,釋出一個自己的類庫甚至都不需要你登入賬號.

Android技術棧(三)依賴注入技術的探討與實現

在根專案的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

Android技術棧(三)依賴注入技術的探討與實現
log中檢視編譯log,點選get it即可開始在jitpack上編譯你的專案

Android技術棧(三)依賴注入技術的探討與實現
如果成功

	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

如果喜歡我的文章記得給我點個贊,拜託了,這對我真的很重要。

相關文章