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
或者其他的一些依賴注入工具,而是在它們所沒有涉及的領域做一個補全。
客官請移步 : 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
。
如果喜歡我的文章記得給我點個贊,拜託了,這對我真的很重要。