[譯][2.4K Start] 放棄 Dagger 擁抱 Koin

前言

做者這篇文章到目前爲止已經收到了 2.4k+ 的贊,衝上了 Medium 熱門,很是好的一篇文章,我也使用 Koin + kotlin + databinding 結合着 inline、reified 強大的特性封裝了基礎庫,包含了 DataBindingActivity、DataBindingFragment、DataBindingDialog、DataBindingListAdapter 等等, 正在陸續添加新的組件。html

經過這篇文章你將學習到如下內容,將在譯者思考部分會給出相應的答案前端

  • Dagger 和 Koin 優點劣勢對比?應該選擇 Dagger 仍是 Koin?
  • koin 語法特性?
  • Koin 爲何能夠作到無代碼生成、無反射?
  • Inline 修飾符作什麼用的?如何正確使用?帶來的性能損失?
    • 只是用 Inline 修飾符,爲何編譯器會給咱們一個警告?
    • 爲何編譯器建議 inline 修飾符須要和 lambda 表達式一塊兒使用呢?
    • 何時應該使用 inline 修飾符?
  • Reified 修飾符作什麼用?如何使用?
  • Koin 帶來性能損失的那些事?
  • Kotlin 用 5 行代碼實現快排算法?

這篇文章涉及不少重要的知識點,請耐心讀下去,我相信應該會給你們帶來不少不同的東西。java

譯文

當我正在反覆學習 Dagger 的時候,我碰見了 Koin,Koin 不只節省了個人時間,還提升了效率,將我從複雜 Dagger 給釋放出來了。android

這篇文章將會告訴你什麼是 Koin,與 Dagger 對比有那些優點,以及如何使用 Koin。git

是什麼 Koin

Koin 是爲 Kotlin 開發者提供的一個實用型輕量級依賴注入框架,採用純 Kotlin 語言編寫而成,僅使用功能解析,無代理、無代碼生成、無反射。github

Dagger vs Koin

爲了正確比較這兩種方式,咱們用 Dagger 和 Koin 去實現了一個項目,項目的架構都是 MVVM,其中用到了 retrofit 和 LiveData,包含了 1 個Activity、4 個 fragments、5 個 view models、1 個 repository 和 1 個 web service 接口, 這應該是一個小型項目的基礎架構了web

先來看一下 DI 包下的結構,左邊是 Dagger,右邊是 Koin算法

如你所見配置 Dagger 須要不少文件 而 Koin 只須要 2 個文件,例如 用 Dagger 注入 1 個 view models 就須要 3 個文件(真的須要用這麼多文件嗎?)編程

比較 Dagger 和 Koin 代碼行數

我使用 Statistic 工具來統計的,反覆對比了項目編譯前和編譯後,Dagger 和 Koin 生成的代碼行數,結果是很是吃驚的json

正如你看到的 Dagger 生成的代碼行比 Koin 多兩倍

Dagger 和 Koin 編譯時間怎麼樣呢

每次編譯以前我都會先 clean 而後纔會 rebuild,我獲得下面這個結果

Koin:
BUILD SUCCESSFUL in 17s
88 actionable tasks: 83 executed, 5 up-to-date

Dagger:
BUILD SUCCESSFUL in 18s
88 actionable tasks: 83 executed, 5 up-to-date
複製代碼

我認爲這個結果證實了,若是是在一個更大、更真實的項目中,這個代價是很是昂貴。

Dagger 和 Koin 使用上怎麼樣呢

若是你想在 MVVM 和 Android Support lib 中使用 Dagger 你必須這麼作。

首先在 module gradle 中 添加 Dagger 依賴。

kapt "com.google.dagger:dagger-compiler:$dagger_version"
kapt "com.google.dagger:dagger-android-processor:$dagger_version"
implementation "com.google.dagger:dagger:$dagger_version"
複製代碼

而後建立完 modules 和 components 文件以後, 須要在 Application 中 初始化 Dagger(或者其餘方式初始化 Dagger)。

Class MyApplication : Application(), HasActivityInjector { 
  @Inject
  lateinit var dispatchingAndroidInjector:    DispatchingAndroidInjector<Activity>
override fun activityInjector() = dispatchingAndroidInjector
fun initDagger() {
   DaggerAppComponent
      .builder()
      .application(this)
      .build()
      .inject(this)
  }
}
複製代碼

全部的 Activity 繼承 BaseActivity,咱們須要實現 HasSupportFragmentInjector 和 inject DispatchingAndroidInjector。

對於 view models,咱們須要在 BaseFragment 中注入 ViewModelFactory,並實現 Injectable。

但這並非所有。還有更多的事情要作。

對於每個 ViewModel、Fragment 和 Activity 咱們須要告訴 DI 如何注入它們,正如你所見咱們有 ActivityModule、FragmentModule、和 ViewModelModule。

咱們來看一下下面的代碼

@Module
abstract class ActivityModule {
    @ContributesAndroidInjector(modules = [FragmentModule::class])
    abstract fun contributeMainActivity(): MainActivity
   
    //Add your other activities here
}
複製代碼

Fragments 以下所示:

@Module
abstract class FragmentModule {
    @ContributesAndroidInjector
    abstract fun contributeLoginFragment(): LoginFragment

    @ContributesAndroidInjector
    abstract fun contributeRegisterFragment(): RegisterFragment

    @ContributesAndroidInjector
    abstract fun contributeStartPageFragment(): StartPageFragment
}
複製代碼

ViewModels 以下因此:

@Module
abstract class ViewModelModule {

    @Binds
    abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory

    @Binds
    @IntoMap
    @ViewModelKey(loginViewModel::class)
    abstract fun bindLoginFragmentViewModel(loginViewModel: loginViewModel): ViewModel

    @Binds
    @IntoMap
    @ViewModelKey(StartPageViewModel::class)
    abstract fun bindStartPageViewModel(startPageViewModel:  StartPageViewModel): ViewModel
    ......
}
複製代碼

因此你必須在 DI modules 中添加這些 Fragments、Activities 和 ViewModels。

那麼在 Koin 中如何作

你須要在 module gradle 中添加 Koin 依賴

implementation "org.koin:koin-android-viewmodel:$koin_version"
複製代碼

而後咱們須要建立 module 文件,稍後我告訴你怎麼作,實際上咱們並不須要像 Dagger 那麼多文件。

Dagger 還有其餘問題

學習 Dagger 成本是很高的,若是有人加入你的項目或者團隊,他/她不得不花不少時間學習 Dagger,我使用 Dagger 兩年了,到如今還不是很瞭解,每次我開始學習 Andorid 新技術的時候,我不得不去搜索和學習如何用 Dagger 實現新技術。

來看一下 Koin 代碼

首先咱們須要添加 Koin 依賴,以下所示:

implementation "org.koin:koin-android-viewmodel:$koin_version"
複製代碼

咱們使用的是 koin-android-viewmodel 庫,由於咱們但願在 MVVM 中使用它,固然還有其餘的依賴庫。

添加完依賴以後,咱們來實現第一個 module 文件,像 Dagger 同樣,能夠在一個單獨的文件中實現每一個模塊,可是因爲代碼簡單,我決定在一個文件中實現全部模塊,你也能夠把它們分開。

首先咱們須要瞭解一下 koin 語法特性

  • get(): 解析 Koin 模塊中的實例,調用 get() 函數解析所請求組件須要的實例,這個 get() 函數一般用於構造函數中,注入構造函數值
  • factory:聲明這是一個工廠組件,每次請求都爲您提供一個新實例
  • single:採用單例設計模式
  • name:用於命名定義,當您但願具備不一樣類型的同一個類的多個實例時,須要使用它

咱們沒有建立具備多個註釋和多個組件的許多文件,而是爲 DI 注入每一個類的時候,提供一個簡單、可讀的文件。

瞭解完 koin 語法特性以後,咱們來解釋下面代碼什麼意思

private val retrofit: Retrofit = createNetworkClient()
複製代碼

createNetworkClient 方法建立 Retrofit 實例,設置 baseUrl,添加 ConverterFactory 和 Interceptor

private val generalApi: GeneralApi =  retrofit.create(GeneralApi::class.java)
private val authApi: AuthApi = retrofit.create(AuthApi::class.java)
複製代碼

AuthApi 和 GeneralApi 是 retrofit 接口

val viewModelModule = module {
    viewModel { LoginFragmentViewModel(get()) }
    viewModel { StartPageViewModel() }    
}
複製代碼

在 module 文件中聲明爲 viewModel, Koin 將會向 ViewModelFactory 提供 viewModel,將其綁定到當前組件。

正如你所見,在 LoginFragmentViewModel 構造函數中有調用了 get() 方法,get() 會解析一個 LoginFragmentViewModel 須要的參數,而後傳遞給 LoginFragmentViewModel,這個參數就是 AuthRepo。

最後在 Application onCreate 方法中添加以下代碼

startKoin(this, listOf(repositoryModule, networkModule, viewModelModule))
複製代碼

這裏只是調用 startKoin 方法,傳入一個上下文和一個但願用來初始化 Koin 的模塊列表。

如今使用 ViewModel 比使用純 ViewModel 更容易,在 Fragment 和 Activity 視圖中添加下面的代碼

private val startPageViewModel: StartPageViewModel by viewModel()
複製代碼

經過這段代碼,koin 爲您建立了一個 StartPageViewModel 對象,如今你能夠在 Fragment 和 Activity 中使用 view model

譯者思考

做者總共從如下 4 個方面對比了 Dagger 和 Kotlin:

  • 文件數量:基於 mvvm 架構,分別使用了 Dagger 和 koltin 做爲依賴注入框架,初始化 Dagger 時至少須要 9 個文件,而 koltin 只須要 2 個文件,Dagger 文件數量遠超過 koltin
  • 代碼行數:做者使用了 Statistic 工具,反覆對比了項目編譯前和編譯後,Dagger 和 Koin 生成的代碼行數,以下圖所示

  • 反覆的對比了 Dagger 和 Koin 編譯時間,結果以下所示 koin 比 Dagger 快
Koin:
BUILD SUCCESSFUL in 17s
88 actionable tasks: 83 executed, 5 up-to-date

Dagger:
BUILD SUCCESSFUL in 18s
88 actionable tasks: 83 executed, 5 up-to-date
複製代碼
  • 學習成本巨大,若是使用了 Dagger 朋友,應該和做者的感受是同樣的,Dagger 學習的成本是很是高的,若是項目中引入了 Dagger 意味着團隊每一個人都要學習 Dagger,無疑這個成本是巨大的,並且使用起來很是的複雜

注意:做者在 Application 中調用 startKoin 方法初始化 Koin 的模塊列表,是 Koin 1X 的方式,Koin 團隊在 2x 的時候作了不少改動(下面會介紹),初始化 Koin 的模塊列有所改動,代碼以下所示:

startKoin {
    // Use Koin Android Logger
    androidLogger()
    // declare Android context
    androidContext(this@MainApplication)
    // declare modules to use
    modules(module1, module2 ...)
}
複製代碼

Koin 爲何能夠作到無代碼生成、無反射

Koin 做爲一個輕量級依賴注入框架,爲何能夠作到無代碼生成、無反射?由於 kotlin 強大的語法糖(例如 Inline、Reified 等等)和函數式編程,咱們先來看一段代碼。
koin-projects/koin-core/src/main/kotlin/org/koin/dsl/Module.kt

案例一

//  typealias 是用來爲已經存在的類型從新定義名字的
typealias ModuleDeclaration = Module.() -> Unit

fun module(createdAtStart: Boolean = false, override: Boolean = false, moduleDeclaration: ModuleDeclaration): Module {
    // 建立 Module
    val module = Module(createdAtStart, override)
    // 執行匿擴展函數
    moduleDeclaration(module)
    return module
}

// 如何使用
val mModule: Module = module {
   single { ... }
   factory { ... }
}
複製代碼

Module 是一個 lambda 表達式,才能夠在 「{}」 裏面自由定義 single 和 factory,會等到你須要的時候纔會執行。

案例二

inline fun <reified T : ViewModel> Module.viewModel(
    qualifier: Qualifier? = null,
    override: Boolean = false,
    noinline definition: Definition<T>
): BeanDefinition<T> {
    val beanDefinition = factory(qualifier, override, definition)
    beanDefinition.setIsViewModel()
    return beanDefinition
}
複製代碼

內聯函數支持具體化的類型參數,使用 reified 修飾符來限定類型參數,以在函數內部訪問它了,因爲函數是內聯的,不須要反射,經過上面兩個案例,說明了爲何 Koin 能夠作到無代碼生成、無反射。建議你們都去看看 Koin 的源碼,可以從中學到不少技巧,後面我會花好幾篇文章分析 Koin 源碼。

Inline 修飾符帶來的性能損失

Inline (內聯函數) 的做用:提高運行效率,調用被 inline 修飾符的函數,會把裏面的代碼放到我調用的地方。

若是閱讀過 Koin 源碼的朋友,應該會發現 inline 都是和 lambda 表達式和 reified 修飾符配套在一塊兒使用的,若是隻使用 inline 修飾符標記函數會怎麼樣?

只使用 inline 修飾符會有性能問題,在這篇文章 Consider inline modifier for higher-order functions 也分析了只使用 inline 修飾符爲何會帶來性能問題,而且 Android Studio 也會給一個大大大的警告。

編譯器建議咱們在含有 lambda 表達式做爲形參的函數中使用內聯,既然 Inline 修飾符能夠提高運行效率,爲何編譯器會給咱們一個警告? 爲何編譯器建議 inline 修飾符須要和 lambda 表達式一塊兒使用呢?

1. 既然 Inline 修飾符能夠提高運行效率,爲何編譯器會給咱們一個警告?

剛纔咱們說過調用被 inline 修飾符的函數,會把裏面的代碼放到我調用的地方,來看一下下面這段代碼。

inline fun twoPrintTwo() {
    print(2)
    print(2)
}

inline fun twoTwoPrintTwo() {
    twoPrintTwo()
    twoPrintTwo()
}

inline fun twoTwoTwoPrintTwo() {
    twoTwoPrintTwo()
    twoTwoPrintTwo()
}

fun twoTwoTwoTwoPrintTwo() {
    twoTwoTwoPrintTwo()
    twoTwoTwoPrintTwo()
}
複製代碼

執行完最後一個方法 twoTwoTwoTwoPrintTwo,反編譯出來的結果是很是使人吃驚的,結果以下所示:

public static final void twoTwoTwoTwoPrintTwo() {
   byte var1 = 2;
   System.out.print(var1);
   var1 = 2;
   System.out.print(var1);
   var1 = 2;
   System.out.print(var1);
   var1 = 2;
   System.out.print(var1);
   var1 = 2;
   System.out.print(var1);
   var1 = 2;
   System.out.print(var1);
   var1 = 2;
   System.out.print(var1);
   var1 = 2;
   System.out.print(var1);
   var1 = 2;
   System.out.print(var1);
   var1 = 2;
   System.out.print(var1);
   var1 = 2;
   System.out.print(var1);
   var1 = 2;
   System.out.print(var1);
   var1 = 2;
   System.out.print(var1);
   var1 = 2;
   System.out.print(var1);
   var1 = 2;
   System.out.print(var1);
   var1 = 2;
   System.out.print();
}
複製代碼

這顯示了使用 Inline 修飾符的主要問題,當咱們過分使用它們時,代碼會快速增加。這就是爲何 IntelliJ 在咱們使用它的時候會給出警告。

2. 爲何編譯器建議 inline 修飾符須要和 lambda 表達式一塊兒使用呢?

由於 JVM 是不支持 lambda 表達式的,非內聯函數中的 Lambda 表達式會被編譯爲匿名類,這對性能開銷是很是巨大的,並且它們的建立和使用都較慢,當咱們使用 inline 修飾符時,咱們根本不須要建立任何其餘類,來看一下下面代碼。

fun main(args: Array<String>) {
    var a = 0
    // 帶 inline 的 Lambda 表達式
    repeat(100_000_000) {
        a += 1
    }
    var b = 0
    
    // 不帶 inline 的 Lambda 表達式
    noinlineRepeat(100_000_000) {
        b += 1
    }
}
複製代碼

編譯結果以下:

// Java 代碼
public static final void main(@NotNull String[] args) {
   int a = 0;
   // 帶 inline 的 Lambda 表達式, 會把裏面的代碼放到我調用的地方
   int times$iv = 100000000;
   int var3 = 0;

   for(int var4 = times$iv; var3 < var4; ++var3) {
      ++a;
   }

   // 不帶 inline 的 Lambda 表達式,會被編譯爲匿名類
   final IntRef b = new IntRef();
   b.element = 0;
   noinlineRepeat(100000000, (Function1)(new Function1() {
      public Object invoke(Object var1) {
         ++b.element;
         return Unit.INSTANCE;
      }
   }));
}
複製代碼

那麼咱們應該在何時使用 inline 修飾符呢?

使用 inline 修飾符時最多見的場景就是把函數做爲另外一個函數的參數時(高階函數),例如 filter、map、joinToString 或者一些獨立的函數 repeat。

若是沒有函數類型做爲參數,也沒有 reified 實化類型參數時,不該該使用 inline 修飾符了。

從分析 Koin 源碼,inline 應該 lambda 表達式或者 reified 修飾符配合在一塊兒使用的,另外 Android Studio 愈來愈智能了,若是在不正確的地方使用,會有一個大大大的警告。

Reified 修飾符,具體化的類型參數

reified (具體化的類型參數):使用 reified 修飾符來限定類型參數,結合着 inline 修飾符具體化的類型參數,能夠直接在函數內部訪問它。

我想分享兩個使用 Reified 修飾符很常見的例子 reified-type-parameters,使用 Java 是不可能實現的。

案例一:

inline fun <reified T> Gson.fromJson(json: String) = 
        fromJson(json, T::class.java) 

// 使用
val user: User = Gson().fromJson(json)
val user = Gson().fromJson<User>(json)
複製代碼

案例二:

inline fun <reified T: Activity> Context.startActivity(vararg params: Pair<String, Any?>) =
        AnkoInternals.internalStartActivity(this, T::class.java, params)
複製代碼

Koin 帶來性能損失的那些事

思考了好久需不須要寫這部份內容,由於在 Koin 2x 的版本的時候已經修復了,這是官方的連接 News from the trenches — What’s next for Koin?,後來想一想仍是寫寫吧,做爲本身的一個學習筆記。

這個源於有我的開了一個 Issue(Bad performance in some Android devices) 如今已經被關閉了,他指出了當 Dependency 數量愈來愈多的時候,Koin 效能會愈來愈差,並且還作了一個對好比下圖所示:

若是使用過 Koin 1x 的朋友應該會感受到,引入 Koin 1x 冷啓動時間邊長了,並且在有大量依賴的時候,查找的時間會有點長,後來 Koin 團隊也發現確實存在這個問題,究竟是怎麼回事呢?

由於他們用了 HashSet 存儲了 BeanDefinition,而後在搜索的時候查找對應的 BeanDefinition,找出一個 BeanDefinition 時間複雜度是 O(n),若是平均有 M 層 Dependency,那麼時間複雜度會變成 O(m*n)。

Koin 團隊的解決方案是用了 HashMap,使用空間換取時間,查找一個 Definition 時間複雜度變成了 O(1),優化以後的結果以下:

Koin 2x 不只在性能優化上有很大的提高,也拓展了不少新的特性,例如 FragmentFactory 可以依賴注入到 Fragments 中就像 ViewModels 同樣,還有自動拆箱等等,在後面的文章會詳細的分析一下。

Kotlin 用 5 行代碼實現快排算法

我想分享一個快速排序算法,這是一個很酷的函數編程的例子 share cool examples,當我看到這段代碼的時候驚呆了,竟然還能夠這麼寫。

fun <T : Comparable<T>> List<T>.quickSort(): List<T> = 
    if(size < 2) this
    else {
        val pivot = first()
        val (smaller, greater) = drop(1).partition { it <= pivot}
        smaller.quickSort() + pivot + greater.quickSort()
    }
    
// 使用 [2,5,1] -> [1,2,5]
listOf(2,5,1).quickSort() // [1,2,5]
複製代碼

最後分享一個譯者本身擼的導航網站

譯者基於 Material Design 的響應式框架,擼了一個 "爲互聯網人而設計 國內國外名站導航" ,收集了國內外熱門網址,涵括新聞、體育、生活、娛樂、設計、產品、運營、前端開發、Android開發等等導航網站,若是你有什麼好的建議,也能夠留言,點擊前去瀏覽 若是對你有幫助,請幫我點個贊,感謝

ps: 網站中的地址若是有原做者不但願展現的,能夠留言告訴我,我會馬上刪除

國際資訊網址大全

Android 網址大全

參考文獻

結語

致力於分享一系列 Android 系統源碼、逆向分析、算法、翻譯相關的文章,目前正在翻譯一系列歐美精選文章,不只僅是翻譯,更重要的是翻譯背後對每篇文章思考,若是你喜歡這片文章,請幫我點個贊,感謝,期待與你一塊兒成長。

文章列表

Android 10 源碼系列

Android 應用系列

工具系列

逆向系列

相關文章
相關標籤/搜索