亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

一文吃透Hilt自定義與跨壁壘

 更新時(shí)間:2023年04月10日 15:23:58   作者:leobert-lan  
這篇文章主要介紹了Hilt自定義與跨壁壘的實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

前言

本文隸屬于我歸納整理的Android知識體系的第四部分,屬于DI中Hilt的進(jìn)階內(nèi)容

如果您需要學(xué)習(xí)Hilt的基礎(chǔ)內(nèi)容,可以通過Android開發(fā)者官方提供的 MAD Skills 12-15篇 和 官方使用教程

文章按照以下內(nèi)容展開:

文中涉及的代碼和案例,均可以于 workshop 中獲得。

非常重要:經(jīng)過反復(fù)思考,我刪除了原先編寫的關(guān)于Hilt工作原理和生成代碼的部分。或許加上這部分,會(huì)讓部分讀者獲得更深刻地理解,但我擔(dān)心會(huì)讓更多的讀者陷入困境 而不敢使用。

正如同Google要?jiǎng)?chuàng)建Hilt一樣,他們希望開發(fā)者以更簡單的方式接入Dagger2,本篇文章也希望讀者朋友能夠先掌握如何使用,并結(jié)合場景選用最佳實(shí)踐方案。在此基礎(chǔ)上再行理解背后的設(shè)計(jì)原理。

跨越 IOC容器的壁壘

使用依賴注入(DI)時(shí),我們需要它對 實(shí)例 、依賴關(guān)系生命周期 進(jìn)行管理,因此DI框架會(huì)構(gòu)建一個(gè)容器,用于實(shí)現(xiàn)這些功能。這個(gè)容器我們慣稱為IOC容器。

在容器中,會(huì)按照我們制定的規(guī)則:

  • 創(chuàng)建實(shí)例
  • 訪問實(shí)例
  • 注入依賴
  • 管理生命周期

但容器外也有訪問容器內(nèi)部的需求,顯然這里存在一道虛擬的 邊界、壁壘。這種需求分為兩類:

  • 依賴注入客觀需要的入口
  • 系統(tǒng)中存在合理出現(xiàn)的、非DI框架管理的實(shí)例,但它不希望破壞其他實(shí)例對象的 生命周期 、作用域唯一性,即它的依賴希望交由DI框架管理

但請注意,IOC容器內(nèi)部也存在著 邊界、壁壘,這和它管理實(shí)例的機(jī)制有關(guān),在Hilt(包括Dagger)中,最大顆粒度的內(nèi)部壁壘是 Component。

即便從外部突破IOC容器的壁壘,也只能進(jìn)入某個(gè)特定的Component

使用EntryPoint跨越IOC容器壁壘

在Hilt中,我們可以很方便地

  • 使用接口定義 進(jìn)入點(diǎn)(EntryPoint),并使用 @EntryPoint 注解使其生效;
  • @InstallIn 注解指明訪問的Component;
  • 并利用 EntryPoints 完成訪問,突破容器壁壘

下面的代碼展示了如何定義:

UserComponent是自定義的Component,在下文中會(huì)詳細(xì)展開

@EntryPoint
@InstallIn(UserComponent::class)
interface UserEntryPoint {
    fun provideUserVO(): UserVO
}

下面的代碼展示了如何獲取進(jìn)入點(diǎn),注意,您需要先獲得對應(yīng)的Component實(shí)例。

對于Hilt內(nèi)建的Component,均有其獲取方法,而自定義的Component,需從外界發(fā)起生命周期控制,同樣會(huì)預(yù)留實(shí)例訪問路徑

fun manualGet(): UserEntryPoint {
    return EntryPoints.get(
        UserComponentManager.instance.generatedComponent(),
        UserEntryPoint::class.java
    )
}

當(dāng)獲取進(jìn)入點(diǎn)后,即可使用預(yù)定義的API,訪問容器內(nèi)的對象實(shí)例。

自定義Scope、Component

部分業(yè)務(wù)場景中,Hilt內(nèi)建的Scope和Component并不能完美支持,此時(shí)我們需要進(jìn)行自定義。

為了下文能夠更順利的展開,我們再花一定的筆墨對 ScopeComponent、Module 的含義進(jìn)行澄清。

Scope、Component、Module的真實(shí)含義

前文提到兩點(diǎn):

  • DI框架需要 創(chuàng)建實(shí)例、訪問實(shí)例、注入依賴管理生命周期
  • IOC容器內(nèi)部也存在著 邊界、壁壘,這和它管理實(shí)例的機(jī)制有關(guān),在Hilt(包括Dagger)中,最大顆粒度的內(nèi)部壁壘是 Component

不難理解:

  • 實(shí)例之間,也會(huì)存在依賴關(guān)系;
  • DI框架需要管理內(nèi)部實(shí)例的生命周期;
  • 需要進(jìn)行依賴注入的客戶,本身也存在生命周期,它的依賴對象,應(yīng)該結(jié)合實(shí)際需求被合理控制生命周期,避免生命周期泄漏;

因此,出現(xiàn)了 范圍、作用域Scope 的概念,它包含兩個(gè)維度:實(shí)例的生命周期范圍;實(shí)例之間的訪問界限。

并且DI框架通過Component控制內(nèi)部對象的生命周期。

舉一個(gè)例子描述,以Activity為例,Activity需要進(jìn)行依賴注入,并且我們不希望Activity自身需要的依賴出現(xiàn)生命周期泄漏,于是按照Activity的生命周期特點(diǎn)定義了:

  • ActivityRetainedScoped ActivityRetainedComponent,不受reCreate 影響
  • ActivityScopedActivityComponent,橫豎屏切換等配置變化引起reCreate 開始新生命周期

并據(jù)此對 依賴對象實(shí)例 實(shí)施 生命周期訪問范圍 控制

可以記住以下三點(diǎn)結(jié)論:

  • Activity實(shí)例按照 預(yù)定Scope對應(yīng)的生命周期范圍 創(chuàng)建、管理Component,訪問Component中的實(shí)例;
  • Component內(nèi)的實(shí)例可以互相訪問,實(shí)例的生命周期和Component一致;
  • Activity實(shí)例(需要依賴注入的客戶)和 Component中的實(shí)例 可以訪問 父Component中的實(shí)例,父Component的生命周期完全包含子Component的生命周期

內(nèi)建的Scope、Component關(guān)系參考:

而Module指導(dǎo)DI框架 創(chuàng)建實(shí)例、選用實(shí)例進(jìn)行注入

值得注意的是,Hilt(以及Dagger)可以通過 @Inject 注解類構(gòu)造函數(shù)指導(dǎo) 創(chuàng)建實(shí)例,此方式創(chuàng)建的實(shí)例的生命周期跟隨宿主,與 通過Module方式 進(jìn)行對比,存在生命周期管理粒度上的差異。

自定義

至此,已不難理解:因?yàn)橛袑?shí)際的生命周期范圍管理需求,才會(huì)自定義。

為了方便行文以及編寫演示代碼,我們舉一個(gè)常見的例子:用戶登錄的生命周期。

一般的APP在設(shè)計(jì)中,用戶登錄后會(huì)持久化TOKEN,下次APP啟動(dòng)后驗(yàn)證TOKEN真實(shí)性和時(shí)效性,通過驗(yàn)證后用戶仍保持登錄狀態(tài),直到TOKEN超時(shí)、登出。當(dāng)APP退出時(shí),可以等效認(rèn)為用戶登錄生命周期結(jié)束。

顯然,用戶登錄的生命周期完全涵蓋在APP生命周期(Singleton Scope)中,但略小于APP生命周期;和Activity生命周期無明顯關(guān)聯(lián)。

定義Scope

import javax.inject.Scope
@Scope
annotation class UserScope

就是這么簡單。

定義Component

定義Component時(shí),需要指明父Component和對應(yīng)的Scope:

import dagger.hilt.DefineComponent
@DefineComponent(parent = SingletonComponent::class)
@UserScope
interface UserComponent {
}

Hilt需要以Builder構(gòu)建Component,不僅如此,一般構(gòu)建Component時(shí)存在初始信息,例如:ActivityComponent需要提供Activity實(shí)例。

通常設(shè)計(jì)中,用戶Component存在 用戶基本信息、TOKEN 等初始信息

data class User(val name: String, val token: String) {
}

此時(shí),我們可以在Builder中完成初始信息的注入:

import dagger.BindsInstance
import dagger.hilt.DefineComponent
@DefineComponent.Builder
interface Builder {
    fun feedUser(@BindsInstance user: User?): Builder
    fun build(): UserComponent
}

我們以 @BindsInstance 注解標(biāo)識需要注入的初始信息,注意合理控制其可空性,在后續(xù)的使用中,可空性需保持一致

注意:方法名并不重要,采用習(xí)慣性命名即可,我習(xí)慣于將向容器喂入?yún)?shù)的API添加feed前綴

當(dāng)我們通過Hilt獲得Builder實(shí)例時(shí),即可控制Component的創(chuàng)建(即生命周期開始)

使用Manager管理Component

不難想象,Component的管理基本為模板代碼,Hilt中提供了模板和接口類:

如果您想避免模板代碼編寫,可以定義擴(kuò)展模塊,使用APT、KCP、KSP生成

此處展示非線程安全的簡單使用Demo

@Singleton
class UserComponentManager @Inject constructor(
    private val builder: UserComponent.Builder
) : GeneratedComponentManager<UserComponent> {
    companion object {
        lateinit var instance: UserComponentManager
    }
    private var userComponent = builder
        .feedUser(null)
        .build()
    fun onLogin(user: User) {
        userComponent = builder.feedUser(user).build()
    }
    fun onLogout() {
        userComponent = builder.feedUser(null).build()
    }
    override fun generatedComponent(): UserComponent {
        return userComponent
    }
}

您也可以定義如下的線程安全的Manager,并使用 ComponentSupplier 提供實(shí)例

class CustomComponentManager(
    private val componentCreator: ComponentSupplier
) : GeneratedComponentManager<Any> {
    @Volatile
    private var component: Any? = null
    private val componentLock = Any()
    override fun generatedComponent(): Any {
        if (component == null) {
            synchronized(componentLock) {
                if (component == null) {
                    component = componentCreator.get()
                }
            }
        }
        return component!!
    }
}

您可以根據(jù)實(shí)際需求選擇最適宜的方法進(jìn)行管理,不再贅述。

在生命周期范圍更小的Component中使用

至此,我們已經(jīng)完成了自定義Scope、Component的主要工作,通過Manager即可控制生命周期。

如果想在生命周期范圍更小的Component中訪問 UserComponent中的對象實(shí)例,您需要謹(jǐn)記前文提到的三條結(jié)論。

該需求很合理,但下面的例子并不足夠典型

此時(shí),您需要通過一個(gè)合理的Component實(shí)現(xiàn)訪問,例如在Activity中需要注入相關(guān)實(shí)例時(shí)。 因?yàn)?ActivityRetainedComponentUserComponent 不存在父子關(guān)系,Scope沒有交集,所以 需要找到共同的父Component進(jìn)行幫助,并通過EntryPoint突破壁壘

前文中,我們將 UserComponentManager 劃入 SingletonComponent, 他是兩種的共同父Component,此時(shí)可以這樣處理:

@Module
@InstallIn(ActivityRetainedComponent::class)
object AppModule {
    @Provides
    fun provideUserVO(manager: UserComponentManager):UserVO {
        return UserEntryPoint.manualGet(manager.generatedComponent()).provideUserVO()
    }
}

解決獨(dú)立library的依賴初始化問題

此問題屬于常見案例,通過研究它的解決方案,我們可以更深刻地理解前文內(nèi)容,做到吃透。

當(dāng)處理主工程時(shí),沒有代碼隔離,我們可以很輕易的修改Application的代碼,因此很多問題難以暴露。

例如,我們可以在Application中通過注解標(biāo)明依賴 (滿足Singleton Scope前提) ,DI框架會(huì)幫助我們進(jìn)行注入,在注入后可以編寫邏輯代碼,將對象賦值給全局變量,便可以 "方便" 的使用。

為方便下文表述,我們稱之 "方案1"

顯然,這是有異味的代碼,雖然它有效且方便。

因此,我們選取一些場景來說明該做法的弊端:

  • 場景1:創(chuàng)建獨(dú)立Library,其中使用Hilt作為DI框架,Library中存在自定義Component,需要初始化管理入口
  • 場景2:項(xiàng)目采用了組件化,該Library按照渠道包需求,渠道包A集成、渠道包B不集成
  • 場景3:項(xiàng)目采用了Uni-App、React-Native等技術(shù),該Library中存在實(shí)例由反射方式創(chuàng)建、不受Hilt管理,無法借助Hilt自動(dòng)注入依賴

以上場景并不相互孤立

在場景1中,我們?nèi)匀豢梢酝ㄟ^ 方案1 完成需求,但在場景2中便不再可行。

常規(guī)的組件化、插件化,都會(huì)完成代碼隔離&使用抽象,因此無法在主工程的Application中使用目標(biāo)類。通過定制字節(jié)碼工具曲線救國,則屬實(shí)是大炮打蚊子、屎盆子鑲金邊

使用hilt的聚合能力解決問題

在 MAD Skills 系列文章的最后一篇中,簡單提及了Hilt的聚合能力,它至少包含以下兩個(gè)層面:

  • 即便一個(gè)已經(jīng)編譯為aar的庫,在被集成后,Hilt依舊能夠掃描該庫中Hilt相關(guān)的內(nèi)容,進(jìn)行依賴圖聚合
  • Hilt生成的代碼,依舊存在著注解,這些注解可以被注解處理器、字節(jié)碼工具識別、并進(jìn)一步處理??梢允荋ilt內(nèi)建的處理器或您自定義的擴(kuò)展處理器

依據(jù)第一個(gè)層面,我們可以制定一個(gè)約定:

子Library按照抽象接口提供Library初始化實(shí)例,主工程的Application通過DI框架獲取后進(jìn)行初始化

我們將其稱為方案2

例如,在Library中定義如下初始化類:

class LibInitializer @Inject constructor(
    private val userComponentManager: UserComponentManager
) : Function1<Application, Any> {
    override fun invoke(app: Application): Any {
        UserComponentManager.instance = userComponentManager
        return Unit
    }
}

不難發(fā)現(xiàn),他是方案1的變種,將依賴獲取從Application中挪到了LibInitializer中

并約定綁定實(shí)例&集合注入, 依舊在Library中編碼 :

@InstallIn(SingletonComponent::class)
@Module
abstract class AppModuleBinds {
    @Binds
    @IntoSet
    abstract fun provideLibInitializer(bind: LibInitializer): Function1<Application, Any>
}

在主工程的Application中:

@HiltAndroidApp
class App : Application() {
    @Inject
    lateinit var initializers: Set<@JvmSuppressWildcards Function1<Application, Any>>
    override fun onCreate() {
        super.onCreate()
        initializers.forEach {
            it(this)
        }
    }
}

如此即可滿足場景1、場景2的需求。

但仔細(xì)思考一下,這種做法太 "強(qiáng)硬" 了,不僅要求主工程的Application進(jìn)行配合,而且需要小心的處理初始化代碼的分配。

在場景3中,這些技術(shù)均有相適應(yīng)的插件初始化入口;組件化插件化項(xiàng)目中,也具有類似的設(shè)計(jì)。隨集成方式的不同,很可能造成 初始化邏輯遺漏或者重復(fù)

注意:重復(fù)初始化可能造成潛在的Scope泄漏,滋生bug。

聚合能力+EntryPoint

前文中,我們已經(jīng)討論了使用EntryPoint突破IOC容器的壁壘,也體驗(yàn)了Hilt的聚合能力。而 SingletonComponent 作為內(nèi)建Component,同樣可以使用EntryPoint突破容器壁壘。

如果您對Hilt的源碼或其設(shè)計(jì)有一定程度的了解,應(yīng)當(dāng)清楚:

內(nèi)建Component均有對應(yīng)的ComponentHolder,而SingletonComponent對應(yīng)的Holder即為Application。

通過 Holder實(shí)例和 EntryPointAccessors 可以獲得定義的 EntryPoint接口

SingletonComponent 自定義EntryPoint后,即可擺脫Hilt自定注入的傳遞鏈而通過邏輯編碼獲取實(shí)例。

@EntryPoint
@InstallIn(SingletonComponent::class)
interface UserComponentEntryPoint {
    companion object {
        fun manualGet(context: Context): UserComponentEntryPoint {
            return EntryPointAccessors.fromApplication(
                context, UserComponentEntryPoint::class.java
            )
        }
    }
    fun provideBuilder(): UserComponent.Builder
    fun provideManager():UserComponentManager
}

通過這一方式,我們只需要獲得Context即可突破壁壘訪問容器內(nèi)部實(shí)例,Hilt不再約束Library的初始化方式。

至此,您可以在原先的Library初始化模塊中,按需自由的添加邏輯!

注意:Builder由Hilt生成實(shí)現(xiàn),無法干預(yù)其生命周期,故每次調(diào)用時(shí)生成新的實(shí)例,從一般的編碼需求,獲取Manager實(shí)例即可。您可以在WorkShop項(xiàng)目中獲得驗(yàn)證

問題衍生

在場景3中,我們繼續(xù)進(jìn)行衍生:

Library作為動(dòng)態(tài)插件,并不直接集成,而是通過插件化技術(shù),動(dòng)態(tài)集成啟用功能。又該如何處理呢?

在MAD Skills系列文章的第四篇中,簡單提及了Hilt的擴(kuò)展能力??紤]到篇幅以及AAB(Dynamic Feature)、插件化的背景,我們將在下一篇文章中對該問題展開解決方案的討論。

先做到正確使用,再逐步理解原理。我會(huì)在后續(xù)系列文章中,同讀者酣暢淋漓的討論Hilt的工作原理和實(shí)現(xiàn)原理。

更多關(guān)于Hilt自定義跨壁壘的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論