Android技術棧(三)依賴注入技術的探討與實現
說到依賴注入( 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
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
就比較麻煩了,熟悉安卓的人應該都知道,打包後的 APK
, res
資料夾下除了 raw
資料夾會原樣保留,其他資料夾裡的內容都會被編譯壓縮,為了解析 res/xml
下的 xml
,我依賴 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畫的:

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
倉庫了,這裡我選擇了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
。
如果喜歡我的文章記得給我點個贊,拜託了,這對我真的很重要。