Dagger - 匕首,顧名思義绵咱,比ButterKnife這把黃油刀鋒利得多碘饼。Square為什么這么有自信地給它取了這個名字,Google又為什么會拿去做了Dagger2呢(不都有Guice和基于其做的RoboGuice了么)悲伶?希望本文能講清楚為什么要用Dagger2艾恼,又如何用好Dagger2。
本文會從Dagger2的起源開始麸锉,途徑其初衷蒂萎、使用場景、依賴圖淮椰,最后介紹一下我在項目中的具體應用和心得體會五慈。
Origin
Dagger2,起源于Square的Dagger主穗,是一個完全在編譯期間進行的依賴注入框架泻拦,完全去除了反射。
關于Dagger2的最初想法忽媒,來自于2013年12月的Proposal: Dagger 2.0争拐,Jake大神在issue里面也有回復哦,而idea的來源者Gregory Kick的GitHub個人主頁也沒多少follower晦雨,自己也沒幾個項目架曹,主要都在貢獻其他的repository,可見海外重復造輪子的風氣比我們這兒好多了闹瞧。
扯遠了绑雄,Dagger2的誕生就是源于開發(fā)者們對Dagger1半靜態(tài)化半運行時的不滿(尤其是在服務端的大型應用上),想要改造成完整的靜態(tài)依賴圖生成奥邮,完全的代碼生成式依賴注入解決方案万牺。在權衡了什么對Android更適合,以及對大型應用來說什么更有意義(往往有可怕數量的注入)兩者后洽腺,Dagger2誕生了脚粟。
初衷
Dagger2的初衷就是裝逼,啊蘸朋,不對核无,是通過依賴注入讓你少些很多公式化代碼,更容易測試藕坯,降低耦合团南,創(chuàng)建可復用可互換的模塊噪沙。你可以在Debug包,測試運行包以及release包優(yōu)雅注入三種不同的實現已慢。
依賴注入
說到依賴注入曲聂,或許很多以前做過JavaEE的朋友會想到Spring(SSH在我本科期間折磨得我欲生欲死霹购,最后Spring MVC拯救了我)佑惠。
我們看個簡單的比較圖,左邊是沒有依賴注入的實現方式齐疙,右邊是手動的依賴注入:
我們想要一個咖啡機來做一杯咖啡膜楷,沒有依賴注入的話,我們就需要在咖啡機里自己去new泵(pump)和加熱器(heater)贞奋,而手動依賴注入的實現則將依賴作為參數赌厅,然后傳入,而不是自己去顯示創(chuàng)建轿塔。在沒有依賴注入的時候特愿,我們喪失了靈活性,因為一切依賴是在內部創(chuàng)建的勾缭,所以我們根本沒有辦法去替換依賴實例揍障,比如想把電加熱器換成火爐或者核加熱器,看一看下圖俩由,是不是更清晰了:
為什么我們需要DI庫
但問題在于毒嫡,在大型應用中,把這些依賴全都分離幻梯,然后自己去創(chuàng)建的話兜畸,會是一個很大的工作量——毫無營養(yǎng)的公式化代碼,一堆Factory類碘梢。不僅僅是工作量的問題咬摇,這些依賴可能還有順序的問題,A依賴B煞躬,B依賴C菲嘴,B依賴D,如此一來C汰翠、D就必須在A龄坪、B的后面,手動去做這些工作簡直是一個噩夢 =复唤。=(哈哈健田,是不是想到了appliation初始化那些依賴)。Google的工程師碰到的問題就是在Android上有3000行這樣的代碼佛纫,而在服務器上的大型程序則是100000行妓局。
你會想自己維護這樣的代碼嗎总放?
Why Dagger2
先來看看如果用Spring實現上面提到的咖啡機依賴,我們需要做什么:
不錯好爬,就是xml局雄,當然,我們也不需要去關心順序了存炮,Spring會幫我們解決前后順序的依賴問題炬搭。
但仔細想想,你會想去自己寫這樣的xml代碼嗎穆桂?layout.xml已經寫得我很煩了宫盔。而且Spring是在運行時驗證配置和依賴圖的,你不會想在外網運行的app里讓用戶發(fā)現你的依賴注入出了問題的(比如bean名字打錯了)享完。再加上xml和Java代碼分離灼芭,很難追蹤應用流。
Guice雖然較Spring進了一步般又,干掉了xml彼绷,通過Java聲明依賴注入比起Spring好找多了,但其跟蹤和報錯(運行時的圖驗證)實在令人抓狂茴迁,而且在不同環(huán)境注入不同實例的配置也挺惡心的(if else各種判斷)寄悯,感興趣的可以去看看,項目就在GitHub上笋熬,Android版本的叫RoboGuice热某。
而Dagger2和Dagger1的差別在上節(jié)已經提到了,更專注于開發(fā)者的體驗胳螟,從半靜態(tài)變?yōu)橥耆o態(tài)昔馋,從Map式的API變成申明式API(@Module),生成的代碼更優(yōu)雅糖耸,更高的性能(跟手寫一樣)秘遏,更簡單的debug跟蹤,所有的報錯也都是在編譯時發(fā)生的嘉竟。
Dagger2使用了JSR 330的依賴注入API邦危,其實就是Provider了:
public interface Provider<T> {
T get();
}
// Usage:
Provider<T> coffeeMakerProvider = ...;
CoffeeMaker coffeeMaker = coffeeMakerProvider.get();
Dagger2基于Component注解:
@Component(modules = DripCoffeModule.class)
interface CoffeeMakerComponet {
CoffeeMaker getCoffeeMaker();
}
// 會生成這樣的代碼,Dagger_CoffeeMakerComponent里面就是一堆Provider舍扰,
// 或者是單例倦蚪,或者是通過DripCoffeeModule申明new的方式,開發(fā)者不必關心依賴順序
CoffeeMakerComponent component = Dagger_CoffeeMakerComponent.create();
CoffeeMaker coffeeMaker = component.getCoffeeMaker();
除了上面提到的各種好處边苹,不得不提的是也有對應問題:喪失了動態(tài)性陵且,在之后的實踐中我會舉個例子描述一下,但相對于那些好處來說个束,我覺得是可接受的慕购。Everything has a Price to Pay聊疲。啊,對了沪悲,還有另一點获洲,沒法自動升級,從Dagger1到Dagger2殿如,當然如果你的app是沒有歷史負擔的(本系列的前提)贡珊,那這不算問題。
如果對性能感興趣的話握截,可以去看看Comparing the Performance of Dependency Injection Libraries飞崖,RoboGuice:Dagger1:Dagger2差不多是50:2:1的一個性能差距烂叔。
如果你用了Dagger2谨胞,而你的服務端還在用Spring,你可以自豪地說蒜鸡,我們比你們領先5年胯努。而Google的服務端確實已經用了Dagger2。
使用場景
上面也曾經提到了逢防,因為手動去維護那些依賴關系叶沛、范圍很麻煩,就連單例我都懶得寫忘朝,何況是各種Factory類灰署,老在那synchroized煩不煩。而如果不去寫那些Factory局嘁,直接new溉箕,則會導致后期維護困難,比如增加了一個參數悦昵,為了保證兼容性肴茄,就只能留著原來的構造函數(習慣好一點的標一下deprecated),再新增一個構造函數但指。
Dagger2解決了這些問題寡痰,幫助我們管理實例,并進行解耦棋凳。new只需要寫在一個地方拦坠,getInstance也再也不用寫了。而需要使用實例的地方剩岳,只需要簡簡單單地來一個@inject贞滨,而不需要關心是如何注入的。Dagger2會在編譯時通過apt生成代碼進行注入卢肃。
想想你所有可能在多個地方使用的類實例依賴疲迂,比如lbs服務才顿,比如你的cache,比如用戶設置尤蒿,比起getInstance郑气,比起new,比起自己用注釋去注明必須維持這種先后關系(說到此處腰池,想到上個東家的android app初始化時候尾组,必須保持正確順序不然立馬crash,singleton還必須只能init一次的糟糕代碼)示弓,為什么不用dagger來做管理讳侨?Without any performance overhead。
Dagger2基于編譯時的靜態(tài)依賴圖構建還能避免運行時再出現一些坑奏属,比如循環(huán)依賴跨跨,編譯的時候就會報錯,而不會在運行時死循環(huán)囱皿。
生動點來說的話勇婴。有一場派對:
Android開發(fā)A說,有妹子我才來嘱腥。
美女前端B說耕渴,有帥哥設計師,我才來齿兔。
iOS開發(fā)C說橱脸,有Android開發(fā),我才來分苇。
帥哥設計師說添诉,只有禮拜天我才有空。
class AndroidDeveloper extends PartyMember {
public AndroidDeveloper(PartyMember female) throws NotMeizhiSayBB;
}
public class FrontEndDeveloper extends PartyMember {
public FrontEndDeveloper(Designer designer) throws NotHandsomeBoySayBB;
}
class IOSDeveloper extends PartyMember {
public IOSDeveloper(AndroidDeveloper dev);
}
class Designer extends PartyMember {
public Designer(Date date) throw CannotComeException;
}
class PartyMember {
private int mSex = 0; // 1 for male, 2 for female.
public void setSex(int sex);
}
// 手動DI组砚,要自己想怎么設計順序吻商,還不能輕易改動
Designer designer = new Designer("禮拜天");
FrontEndDeveloper dev1 = new FrontEndDeveloper(designer);
dev1.setSex(2);
AndroidDeveloper dev2 = new AndroidDeveloper(dev1);
IOSDeveloper dev3 = new IOSDeveloper(dev2);
// With Dagger2
@Inject
Designer designer;
@Inject
FrontEndDeveloper dev1;
@Inject
AndroidDeveloper dev2;
@Inject
IOSDeveloper dev3;
// 不使用DI太可怕了...自己想象一下會是什么樣吧
...我懶
Scope
Dagger2的Scope糟红,除了Singleton(root)艾帐,其他都是自定義的,無論你給它命名PerActivity盆偿、PerFragment柒爸,其實都只是一個命名而已,真正起作用的是inject的位置事扭,以及dependency捎稚。
Scope起的更多是一個限制作用,比如不同層級的Component需要有不同的scope,注入PerActivity scope的component后activity就不能通過@Inject去獲得SingleTon的實例今野,需要從application去暴露接口獲得(getAppliationComponent獲得component實例然后訪問葡公,比如全局的navigator)。
當然条霜,另一方面則是可讀性和方便理解催什,通過scope的不同很容易能辨明2個實例的作用域的區(qū)別。
依賴圖例子
如上是一個我現在使用的Dagger2的依賴圖的簡化版子集宰睡。
ApplicationComponent作為root蒲凶,拆分出了3個module
- ApplicationModule(application context,lbs服務拆内,全局設置等)
- ApiModule(Retrofit那堆Api在這里)
- RepositoryModule(各種repository)旋圆。
這里為了妥協(xié)內聚和簡潔所以保持了這三個module。你不會想看到自己的di package下有一大堆module類麸恍,或者某個module里面摻雜著上百個實例注入的灵巧。
UserComponent用在用戶主頁、登錄注冊或南,以及好友列表頁孩等。所以你能看到UserModule(用戶系統(tǒng)以及那些UseCase)以及需要的贊Module艾君、相冊Module采够。
TagComponent是標簽系統(tǒng),有自己的標簽Module以及贊Module(module重用)冰垄,用在了標簽搜索蹬癌、熱門標簽等頁面。
是不是很好理解虹茶?位于上層的component是看不到下層的逝薪,而下層則可以使用上層的,但不能引用同一層相鄰component內的實例蝴罪。
如果你的應用是強登錄態(tài)的董济,則更可以只把UserComponent放在第二層,Module構造函數傳入uid(PerUser scope要门,沒有uid則為游客態(tài)虏肾,供deeplink之類使用),而所有需要登錄態(tài)的則都放在第三層欢搜。
一個簡單的應用就是這樣了封豪,而Component繼承,SubComponent(共享的放在上層父類)炒瘟,不同component的module復用(一樣可以生成實例綁定吹埠,只是沒法共享component中暴露的接口罷了)這些則是不同場景下的策略,如果有必要我會再開一篇講講這些深入的使用。
具體應用和心得體會
No Proguard rules need缘琅。因為0反射粘都,所以完全不需要去配置proguard規(guī)則。
因為需要靜態(tài)地去inject刷袍,如果一些參數需要運行時通過用戶行為去獲得驯杜,就只能使用set去設置注入實例的參數(因為我們的injection通常在最早,比如onCreate就需要執(zhí)行)做个。這就是上文提到過的鸽心,因為完全靜態(tài)而喪失了一定的動態(tài)性。
Singleton是線程安全的居暖,請放心顽频,如果實在懷疑,可以去檢查生成的源碼太闺,筆者已經檢查過了...
粒度的問題糯景,如果基于頁面去劃分的話,老實說筆者覺得實在太細太麻煩省骂,建議稍微粗一點蟀淮,按照大功能去分,完全可以通過拆分module或者SubComponent的形式去解決復用的問題钞澳,而不用拆分出一大堆component怠惶,module只要足夠內聚就可以,而不需要拆分到某個頁面使用的那些轧粟。
fragment的問題策治,因為其詭異的生命周期,所以建議在實在需要fragment的時候兰吟,讓activity去創(chuàng)建component通惫,fragment通過接口(比如HasComponent)去獲得component(一個activity只能inject一個component哦)。
舉一個我遇到的例子來說說方便的地方混蔼,有一個UseCase叫做SearchTag履腋,原先只需要TagRepository,ThreadExecutor惭嚣,PostThreadExecutor三個參數∽窈現在需求改變了,需要在發(fā)起請求前先進行定位料按,然后把位置信息也作為請求的參數奄侠。我們只需要簡單地在構造函數增加一個LbsRepository,然后在buildUseCaseObservable通過RxJava組合一下载矿,這樣既避免了底層repository的耦合垄潮,又對上層屏蔽了復雜性烹卒。
再講講之前提到的依賴吧,我們有很多同級的實例弯洗,以Singleton為例旅急,比如有一個要提供給第三方sdk的Provider依賴了某個Repository,直接在構造函數里加上那個Repository牡整,然后加上@Inject藐吮,完全不需要關心前后順序了,省不省心逃贝?還可以隨時在單元測試的包注入一個不需要物理環(huán)境的模擬repository谣辞。想想以前你怎么做,或者在調用這個的初始化前init依賴的實例沐扳,或者在初始化里去使用依賴類的getInstance()泥从,是不是太土鱉?
強烈推薦你在自己的項目里使用上沪摄,初期可能懷著裝逼的心情覺得有點麻煩躯嫉,熟練后你會發(fā)現簡直太方便了,根本離不開(其實是我的親身經歷 哈哈)杨拐。
總結
本篇講了講Dagger2祈餐,主要還是在安利為什么要用Dagger2,以及一些正確的使用姿勢哄陶,因為時間原因來不及寫個demo來說說具體實現帆阳,歡迎大家提出意見和建議。
有空的話我最近會在GitHub上寫一下demo奕筐,你如果有興趣可以follow一下等等更新: markzhai(希望在4月能完成舱痘,哈哈...)。
下集預告
怎么用Retrofit离赫、Realm和RxJava搭建data層。
參考文獻
- DAGGER 2 - A New Type of dependency injection: https://youtu.be/oK_XtfXPkqw
- Dagger 2 Official Site: http://google.github.io/dagger/
- Dagger 2 Design Doc: http://goo.gl/mW474Z
原文鏈接:http://blog.zhaiyifan.cn/2016/03/27/android-new-project-from-0-p4/