写点什么

[译][2(1),android 开发计算器源码

用户头像
Android架构
关注
发布于: 刚刚

abstract fun bindLoginFragmentViewModel(loginViewModel: loginViewModel): ViewModel


@Binds@IntoMap@ViewModelKey(StartPageV


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


iewModel::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 17s88 actionable tasks: 83 executed, 5 up-to-date


Dagger:BUILD SUCCESSFUL in 18s88 actionable tasks: 83 executed, 5 up-to-date


  • 学习成本巨大,如果使用了 Dagger 朋友,应该和作者的感觉是一样的,Dagger 学习的成本是非常高的,如果项目中引入了 Dagger 意味着团队每个人都要学习 Dagger,无疑这个成本是巨大的,而且使用起来非常的复杂


注意:作者在 Application 中调用 startKoin 方法初始化 Koin 的模块列表,是 Koin 1X 的方式,Koin 团队在 2x 的时候做了很多改动(下面会介绍),初始化 Koin 的模块列有所改动,代码如下所示:


startKoin {// Use Koin Android LoggerandroidLogger()// declare Android contextandroidContext(this@MainApplication)// declare modules to usemodules(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 {// 创建 Moduleval 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 团队也发现确实存在这个问题,到底是怎么回事呢?


他们在 BeanRegistry 类中维护了一个列表,用来存储了 BeanDefinition,然后使用 Kotlin 的 filter 函数找出对应的 BeanDefinition,所以找出一个 Definition 时间复杂度是 O(n),如果平均有 M 层 Dependency,那么时间复杂度会变成 O(m*n)。


Koin 团队的解决方案是用了 HashMap,使用空间换取时间,查找一个 Definition 时间复杂度变成了 O(1),优化之后的结果如下:



Koin 2x 不仅在性能优化上有很大的提升,也拓展了很多新的特性,例如 FragmentFactory 能够依赖注入到 Fragments 中就像 ViewModels 一样,还有自动拆箱等等,在后面的文章会详细的分析一下。

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
[译][2(1),android开发计算器源码