走两步比一比?Android依赖注入Hilt与Koin到底谁不行?

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Android依赖注入框架Hilt与Koin的全方位对比

有人看了我的框架有疑问了,怎么依赖注入都是用的Hilt啊,都看不懂懂啦,那么复杂。你看Koin框架的依赖注入更简单的啦。

由此展开Hilt与Koin的对比。看看性能,代码各方面有没有谁比谁差。

一. Hilt的简单使用

了解Hilt得先了解它的前身Dagger2。这玩意太复杂了,学习成本陡峭,在MVP框架的时代(2017年左右)还能勉强应付,对应MVVM框架中ViewModel完全的水土不服。甚至是Google的官方都是一堆问题。

痛定思痛,谷歌是铁了心让Android开发者使用依赖注入啊,这不就搞了个Android中使用Dagger2的场景化框架Hilt。专门为Android开发而生,简化了构建过程,使用注解生成代码。很方便的使用Hilt。完美的的适配MVVM/MVI框架。最近更新的版本也兼容到Compose了。

谷歌为了开发者煞费苦心了。如何使用看下简单的示例代码:(后期会出详细的代码教程)

新版依赖,部分用法和老版本不同:

  implementation 'com.google.dagger:hilt-android:2.38.1'
  kapt 'com.google.dagger:hilt-android-compiler:2.38.1'
复制代码

根目录的build.gradle定义Hilt插件

    dependencies {
        classpath 'com.android.tools.build:gradle:7.0.3'

        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

        //路由
        classpath 'com.alibaba:arouter-register:1.0.2'

        //Hilt插件
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1'
复制代码

运行模块需要导入插件(没办法APT代码生成就是这个流程)

apply plugin: 'dagger.hilt.android.plugin'
复制代码

Application标注入口(Hilt中Android场景化的入口标识)

@HiltAndroidApp
class MyApplication : BaseApplication() {
 }
复制代码

开始写注入模块,全局的单例

@Module
@InstallIn(SingletonComponent::class)
class ApplicationModule {
    
    @Provides
    fun provideMyApplication(application: Application): MyApplication {
        return application as MyApplication
    }

    //全局的Gson
    @Provides
    @Singleton
    fun provideGson(): Gson {
        return GsonFactory.getSingletonGson()
    }

}
复制代码

Activity级别的注入模块

@Module
@InstallIn(ActivityComponent::class)
class AuthDIModel {

    //Activity级别同一个实例
    @Provides
    fun providePencil(): Pencil {
        return Pencil()
    }

    @Provides
    fun provideBook(pencil: Pencil): Book {
        return Book(pencil)
    }

}
复制代码

ViewModel的注入

@HiltViewModel
class UserLoginViewModel @Inject constructor(
    private val mRepository: DemoRepository,
    private val mSchoolRepository: SchoolRepository,
    private val savedStateHandle: SavedStateHandle
) : BaseViewModel() {
    ...
}

@Singleton
class DemoRepository @Inject constructor() : BaseRepository()

@Singleton
class SchoolRepository @Inject constructor() : BaseRepository()
复制代码

Activity的使用:

@AndroidEntryPoint
class UserLoginActivity : YYBaseVDBActivity<UserLoginViewModel, ActivityUserLoginBinding>() {

    @Inject
    lateinit var userServer: UserServer

    @Inject
    lateinit var mBook: Book

    @Inject
    lateinit var mGson: Gson

      fun test(){
        //测试DI注入
        userServer.testUser()
        mBook.sayBook()
    }

    ...
}
复制代码

到此一个对象注入的流程就完结了,可以看到,对象的注入都需要加注解@Inject,有特殊需求的需要加入@Module,在其中配置指定的规则,如果没有特殊需求,可以直接简单的构造方法注入@Inject。

例如ViewModel的注入,就是会自动找到标记了@Inject的Repository。

再比如,普通的类没有特殊需求就可以直接在构造方法中标记注入

@Singleton
class UserDao @Inject constructor() {

    fun printUser(): String {
        return this.toString()
    }
}

@Singleton
class UserServer @Inject constructor(private val userDao: UserDao) {

    fun testUser() {
        YYLogUtils.w(userDao.printUser())
        toast(userDao.printUser())
    }

    fun getDaoContent(): String {
        return userDao.printUser()
    }

}
复制代码

当然了Hilt还有很多高级的用法,自定义的Scope,接口如何注入等等,后面会专门开一期讲解。这期主要是对比性能。

二. Koin的简单使用

Koin是一款轻量级的依赖注入框架,只使用Kotlin的函数解析特性,没有代理,没有代码生成,没有反射。

简单的集成确实是很简单,官网说5分钟,我觉得都多了。

    api "io.insert-koin:koin-android:3.2.0"
复制代码

初始化Koin框架,加载依赖注入配置表

class MyApplication : BaseApplication() {

    override fun onCreate() {
        startTime = System.currentTimeMillis()
        super.onCreate()


        startKoin {
            androidLogger()
            androidContext(this@MyApplication)
            androidFileProperties()
            modules(listOf(appModule, authModule))
        }

    }
复制代码

我在不同的组件中定义了不同的di文件:

val appModule = module {

    single<Gson> { GsonFactory.getSingletonGson() }

}
复制代码

另一个组件

val authModule = module {

    //单例
    single { DemoRepository() }
    single { SchoolRepository() }

    //ViewModel
    viewModel { UserLoginViewModel(get(), get()) }

    //每次生成实例
    factory { Pencil() }
    factory { Book(get()) }
    factory { UserDao() }
    factory { UserServer(get()) } 
}
复制代码

然后就能直接使用啦。相比上面确实是简单好多。

class UserLoginActivity : YYBaseVDBActivity<UserLoginViewModel, ActivityUserLoginBinding>() {

    val myViewModel: UserLoginViewModel by viewModel()

    val userServer: UserServer by inject()
    val mBook: Book by inject()
    val mGson: Gson by inject()

    fun test(){
        //测试DI注入
        userServer.testUser()
        mBook.sayBook()
    }
}
复制代码

三. 各方面比一比

为了公平起见,两者的代码都是我同样的Demo复制出来的,只是一个是Hilt一个是Koin。

3.1 代码生成

这个不用比,肯定是Hilt多了。不过是APT注解生成器生成的,开发者也无感知

3.2 内存占用

这里我对比了三组数据,自己也只截了几个图。

默认不带依赖注入Activity占用内存 第一组106.3 第二组 107.5 第三组 110.7 平均108.17

Hilt依赖注入的Activity占用内存 第一组95.8 第二组 93.6 第三组 94.3 平均94.57

Koin依赖注入的Activity占用内 第一组106.5 第二组 103.4 第三组 105.6 平均105.17

结论:内存占用结果 Hilt > Koin > null

3.3 启动时间

听说Koin启动慢,读取配置表耗时? 真的假的?这里记录的是App启动到对象全部注入进去的全部时间。

W/Message: ┃ 启动耗时:2132
复制代码

Hilt依赖注入的启动时间 第一组2132 第二组 2085 第三组 2110 平均 2109

Koin依赖注入的启动时间 第一组2184 第二组 2168 第三组 2174 平均 2175

为了严谨,我不是使用的空项目,注入的也不是一个对象,差距不是很明显,可能随着项目越来越大,依赖注入表的越来越大,Koin会表现明显一点。

结论:Hilt > Koin

3.3 Apk大小也看看

这个无法测试很多组数据,因为大小都是固定的。

结论 Koin > Hilt 。是的我也没想到,Hilt的安装包居然会小一点。

四. 优缺点总结

两个框架都是可以用,也都流行,都在维护,都支持Compose了。在开发上都支持组件化和MVVM等流行框架。

两者在性能上差距不是很大。Koin的入门门口比较低,Hilt虽说已经是简化版的 Dagger2 了,但是还是蛮复杂的。两者都可以用。

个人感觉两者最大的区别就是AS对它们的支持。跳转都是支持的 但是报错的情况大不相同。比如Hilt报错

编译期间就会告诉你,我哪里没有添加注解,需要怎么改动,这一点在依赖注入框架中非常的重要。

而Koin的保存,大家看看

java.lang.RuntimeException: Unable to get provider androidx.startup.InitializationProvider: androidx.startup.StartupException: java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/lifecycle/DefaultLifecycleObserver;
        at android.app.ActivityThread.installProvider(ActivityThread.java:8215)
        at android.app.ActivityThread.installContentProviders(ActivityThread.java:7746)
        at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7566)
        at android.app.ActivityThread.access$1500(ActivityThread.java:301)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2177)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:246)
        at android.app.ActivityThread.main(ActivityThread.java:8653)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)
     Caused by: androidx.startup.StartupException: java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/lifecycle/DefaultLifecycleObserver;
复制代码
Already existing definition or try to override an existing one
复制代码

这都是啥。根本与依赖注入Module之类的毫不相干。这怎么排查错误嘛!其实我只是重复定义了依赖对象而已。

当然可能是我太菜了依赖注入还会出现报错的问题。所以我选择了Hilt这样的依赖注入方式。因为以前学过 Dagger2 所以觉得Hilt不是很复杂。所以框架选项也是选择的Hilt。后期可能会讲下Hilt的实战,到底哪种情况下该如何注入。

最后感谢大家看到这里,大家分析好利弊之后自行选择即可。

到处完结!

猜你喜欢

转载自juejin.im/post/7099757089081262117