從零開始的Android新專案(4):Dagger2 篇

markzhai發表於2016-04-21

本系列:

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的誕生就是源於開發者們對Dagger1半靜態化半執行時的不滿(尤其是在服務端的大型應用上),想要改造成完整的靜態依賴圖生成,完全的程式碼生成式依賴注入解決方案。在權衡了什麼對Android更適合,以及對大型應用來說什麼更有意義(往往有可怕數量的注入)兩者後,Dagger2誕生了。

初衷

Dagger2的初衷就是裝逼,啊,不對,是通過依賴注入讓你少些很多公式化程式碼,更容易測試,降低耦合,建立可複用可互換的模組。你可以在Debug包,測試執行包以及release包優雅注入三種不同的實現。

依賴注入

說到依賴注入,或許很多以前做過JavaEE的朋友會想到Spring(SSH在我本科期間折磨得我欲生欲死,最後Spring MVC拯救了我)。

我們看個簡單的比較圖,左邊是沒有依賴注入的實現方式,右邊是手動的依賴注入:
Without DI and with Maunl DI

我們想要一個咖啡機來做一杯咖啡,沒有依賴注入的話,我們就需要在咖啡機裡自己去new泵(pump)和加熱器(heater),而手動依賴注入的實現則將依賴作為引數,然後傳入,而不是自己去顯示建立。在沒有依賴注入的時候,我們喪失了靈活性,因為一切依賴是在內部建立的,所以我們根本沒有辦法去替換依賴例項,比如想把電加熱器換成火爐或者核加熱器,看一看下圖,是不是更清晰了:
Without DI and with Maunl DI

為什麼我們需要DI庫

但問題在於,在大型應用中,把這些依賴全都分離,然後自己去建立的話,會是一個很大的工作量——毫無營養的公式化程式碼,一堆Factory類。不僅僅是工作量的問題,這些依賴可能還有順序的問題,A依賴B,B依賴C,B依賴D,如此一來C、D就必須在A、B的後面,手動去做這些工作簡直是一個噩夢 =。=(哈哈,是不是想到了appliation初始化那些依賴)。Google的工程師碰到的問題就是在Android上有3000行這樣的程式碼,而在伺服器上的大型程式則是100000行。

你會想自己維護這樣的程式碼嗎?

Why Dagger2

先來看看如果用Spring實現上面提到的咖啡機依賴,我們需要做什麼:
DI with Spring
不錯,就是xml,當然,我們也不需要去關心順序了,Spring會幫我們解決前後順序的依賴問題。

但仔細想想,你會想去自己寫這樣的xml程式碼嗎?layout.xml已經寫得我很煩了。而且Spring是在執行時驗證配置和依賴圖的,你不會想在外網執行的app裡讓使用者發現你的依賴注入出了問題的(比如bean名字打錯了)。再加上xml和Java程式碼分離,很難追蹤應用流。

Guice雖然較Spring進了一步,幹掉了xml,通過Java宣告依賴注入比起Spring好找多了,但其跟蹤和報錯(執行時的圖驗證)實在令人抓狂,而且在不同環境注入不同例項的配置也挺噁心的(if else各種判斷),感興趣的可以去看看,專案就在GitHub上,Android版本的叫RoboGuice。

而Dagger2和Dagger1的差別在上節已經提到了,更專注於開發者的體驗,從半靜態變為完全靜態,從Map式的API變成申明式API(@Module),生成的程式碼更優雅,更高的效能(跟手寫一樣),更簡單的debug跟蹤,所有的報錯也都是在編譯時發生的。

Dagger2使用了JSR 330的依賴注入API,其實就是Provider了:

Dagger2基於Component註解:

除了上面提到的各種好處,不得不提的是也有對應問題:喪失了動態性,在之後的實踐中我會舉個例子描述一下,但相對於那些好處來說,我覺得是可接受的。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基於編譯時的靜態依賴圖構建還能避免執行時再出現一些坑,比如迴圈依賴,編譯的時候就會報錯,而不會在執行時死迴圈。

生動點來說的話。有一場派對:

Android開發A說,有妹子我才來。
美女前端B說,有帥哥設計師,我才來。
iOS開發C說,有Android開發,我才來。
帥哥設計師說,只有禮拜天我才有空。

Scope

Dagger2的Scope,除了Singleton(root),其他都是自定義的,無論你給它命名PerActivity、PerFragment,其實都只是一個命名而已,真正起作用的是inject的位置,以及dependency。

Scope起的更多是一個限制作用,比如不同層級的Component需要有不同的scope,注入PerActivity scope的component後activity就不能通過@Inject去獲得SingleTon的例項,需要從application去暴露介面獲得(getAppliationComponent獲得component例項然後訪問,比如全域性的navigator)。

當然,另一方面則是可讀性和方便理解,通過scope的不同很容易能辨明2個例項的作用域的區別。

依賴圖例子

Simple Graph

如上是一個我現在使用的Dagger2的依賴圖的簡化版子集。

ApplicationComponent作為root,拆分出了3個module

  • ApplicationModule(application context,lbs服務,全域性設定等)
  • ApiModule(Retrofit那堆Api在這裡)
  • RepositoryModule(各種repository)。
    這裡為了妥協內聚和簡潔所以保持了這三個module。你不會想看到自己的di package下有一大堆module類,或者某個module裡面摻雜著上百個例項注入的。

UserComponent用在使用者主頁、登入註冊,以及好友列表頁。所以你能看到UserModule(使用者系統以及那些UseCase)以及需要的贊Module、相簿Module。

TagComponent是標籤系統,有自己的標籤Module以及贊Module(module重用),用在了標籤搜尋、熱門標籤等頁面。

是不是很好理解?位於上層的component是看不到下層的,而下層則可以使用上層的,但不能引用同一層相鄰component內的例項。

如果你的應用是強登入態的,則更可以只把UserComponent放在第二層,Module建構函式傳入uid(PerUser scope,沒有uid則為遊客態,供deeplink之類使用),而所有需要登入態的則都放在第三層。

一個簡單的應用就是這樣了,而Component繼承,SubComponent(共享的放在上層父類),不同component的module複用(一樣可以生成例項繫結,只是沒法共享component中暴露的介面罷了)這些則是不同場景下的策略,如果有必要我會再開一篇講講這些深入的使用。

具體應用和心得體會

  • No Proguard rules need。因為0反射,所以完全不需要去配置proguard規則。

  • 因為需要靜態地去inject,如果一些引數需要執行時通過使用者行為去獲得,就只能使用set去設定注入例項的引數(因為我們的injection通常在最早,比如onCreate就需要執行)。這就是上文提到過的,因為完全靜態而喪失了一定的動態性。

  • Singleton是執行緒安全的,請放心,如果實在懷疑,可以去檢查生成的原始碼,筆者已經檢查過了…

  • 粒度的問題,如果基於頁面去劃分的話,老實說筆者覺得實在太細太麻煩,建議稍微粗一點,按照大功能去分,完全可以通過拆分module或者SubComponent的形式去解決複用的問題,而不用拆分出一大堆component,module只要足夠內聚就可以,而不需要拆分到某個頁面使用的那些。

  • fragment的問題,因為其詭異的生命週期,所以建議在實在需要fragment的時候,讓activity去建立component,fragment通過介面(比如HasComponent)去獲得component(一個activity只能inject一個component哦)。

  • 舉一個我遇到的例子來說說方便的地方,有一個UseCase叫做SearchTag,原先只需要TagRepository,ThreadExecutor,PostThreadExecutor三個引數。現在需求改變了,需要在發起請求前先進行定位,然後把位置資訊也作為請求的引數。我們只需要簡單地在建構函式增加一個LbsRepository,然後在buildUseCaseObservable通過RxJava組合一下,這樣既避免了底層repository的耦合,又對上層遮蔽了複雜性。

  • 再講講之前提到的依賴吧,我們有很多同級的例項,以Singleton為例,比如有一個要提供給第三方sdk的Provider依賴了某個Repository,直接在建構函式里加上那個Repository,然後加上@Inject,完全不需要關心前後順序了,省不省心?還可以隨時在單元測試的包注入一個不需要物理環境的模擬repository。想想以前你怎麼做,或者在呼叫這個的初始化前init依賴的例項,或者在初始化裡去使用依賴類的getInstance(),是不是太土鱉?

  • 強烈推薦你在自己的專案裡使用上,初期可能懷著裝逼的心情覺得有點麻煩,熟練後你會發現簡直太方便了,根本離不開(其實是我的親身經歷 哈哈)。

總結

本篇講了講Dagger2,主要還是在安利為什麼要用Dagger2,以及一些正確的使用姿勢,因為時間原因來不及寫個demo來說說具體實現,歡迎大家提出意見和建議。
有空的話我最近會在GitHub上寫一下demo,你如果有興趣可以follow一下等等更新:markzhai(希望在4月能完成,哈哈…)。

下集預告

怎麼用Retrofit、Realm和RxJava搭建data層。

參考文獻

擴充套件閱讀

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式

從零開始的Android新專案(4):Dagger2 篇 從零開始的Android新專案(4):Dagger2 篇

相關文章