您好,登錄后才能下訂單哦!
本篇內容介紹了“Hilt自定義與跨壁壘的方法是什么”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
使用依賴注入(DI)時,我們需要它對 實例
、依賴關系
、 生命周期
進行管理,因此DI框架會構建一個容器,用于實現這些功能。這個容器我們慣稱為IOC容器。
在容器中,會按照我們制定的規則:
創建實例
訪問實例
注入依賴
管理生命周期
但容器外也有訪問容器內部的需求,顯然這里存在一道虛擬的 邊界、壁壘
。這種需求分為兩類:
依賴注入客觀需要的入口
系統中存在合理出現的、非DI框架管理的實例,但它不希望破壞其他實例對象的 生命周期
、作用域唯一性
,即它的依賴希望交由DI框架管理
但請注意,IOC容器內部也存在著 邊界、壁壘
,這和它管理實例的機制有關,在Hilt(包括Dagger)中,最大顆粒度的內部壁壘是 Component
。
即便從外部突破IOC容器的壁壘,也只能進入某個特定的Component
在Hilt中,我們可以很方便地
使用接口定義 進入點(EntryPoint),并使用 @EntryPoint
注解使其生效;
用 @InstallIn
注解指明訪問的Component;
并利用 EntryPoints
完成訪問,突破容器壁壘
下面的代碼展示了如何定義:
UserComponent是自定義的Component,在下文中會詳細展開
@EntryPoint @InstallIn(UserComponent::class) interface UserEntryPoint { fun provideUserVO(): UserVO }
下面的代碼展示了如何獲取進入點,注意,您需要先獲得對應的Component實例。
對于Hilt內建的Component,均有其獲取方法,而自定義的Component,需從外界發起生命周期控制,同樣會預留實例訪問路徑
fun manualGet(): UserEntryPoint { return EntryPoints.get( UserComponentManager.instance.generatedComponent(), UserEntryPoint::class.java ) }
當獲取進入點后,即可使用預定義的API,訪問容器內的對象實例。
部分業務場景中,Hilt內建的Scope和Component并不能完美支持,此時我們需要進行自定義。
為了下文能夠更順利的展開,我們再花一定的筆墨對 Scope
、Component
、Module
的含義進行澄清。
前文提到兩點:
DI框架需要 創建實例
、訪問實例
、注入依賴
、管理生命周期
IOC容器內部也存在著 邊界、壁壘
,這和它管理實例的機制有關,在Hilt(包括Dagger)中,最大顆粒度的內部壁壘是 Component
。
不難理解:
實例之間,也會存在依賴關系;
DI框架需要管理內部實例的生命周期;
需要進行依賴注入的客戶,本身也存在生命周期,它的依賴對象,應該結合實際需求被合理控制生命周期,避免生命周期泄漏;
因此,出現了 范圍、作用域
即 Scope
的概念,它包含兩個維度:實例的生命周期范圍;實例之間的訪問界限。
并且DI框架通過Component控制內部對象的生命周期。
舉一個例子描述,以Activity為例,Activity需要進行依賴注入,并且我們不希望Activity自身需要的依賴出現生命周期泄漏,于是按照Activity的生命周期特點定義了:
ActivityRetainedScoped
ActivityRetainedComponent
,不受reCreate 影響
ActivityScoped
、 ActivityComponent
,橫豎屏切換等配置變化引起reCreate 開始新生命周期
并據此對 依賴對象實例
實施 生命周期
和 訪問范圍
控制
可以記住以下三點結論:
Activity實例按照 預定Scope對應的生命周期范圍 創建、管理Component,訪問Component中的實例;
Component內的實例可以互相訪問,實例的生命周期和Component一致;
Activity實例(需要依賴注入的客戶)和 Component中的實例 可以訪問 父Component
中的實例,父Component的生命周期完全包含子Component的生命周期
內建的Scope、Component關系參考:
而Module指導DI框架 創建實例
、選用實例進行注入
值得注意的是,Hilt(以及Dagger)可以通過 @Inject
注解類構造函數指導 創建實例
,此方式創建的實例的生命周期跟隨宿主,與 通過Module方式
進行對比,存在生命周期管理粒度上的差異。
至此,已不難理解:因為有實際的生命周期范圍管理需求,才會自定義。
為了方便行文以及編寫演示代碼,我們舉一個常見的例子:用戶登錄的生命周期。
一般的APP在設計中,用戶登錄后會持久化TOKEN,下次APP啟動后驗證TOKEN真實性和時效性,通過驗證后用戶仍保持登錄狀態,直到TOKEN超時、登出。當APP退出時,可以等效認為用戶登錄生命周期結束。
顯然,用戶登錄的生命周期完全涵蓋在APP生命周期(Singleton Scope)中,但略小于APP生命周期;和Activity生命周期無明顯關聯。
import javax.inject.Scope @Scope annotation class UserScope
就是這么簡單。
定義Component時,需要指明父Component和對應的Scope:
import dagger.hilt.DefineComponent @DefineComponent(parent = SingletonComponent::class) @UserScope interface UserComponent { }
Hilt需要以Builder構建Component,不僅如此,一般構建Component時存在初始信息,例如:ActivityComponent需要提供Activity實例。
通常設計中,用戶Component存在 用戶基本信息、TOKEN
等初始信息
data class User(val name: String, val token: String) { }
此時,我們可以在Builder中完成初始信息的注入:
import dagger.BindsInstance import dagger.hilt.DefineComponent @DefineComponent.Builder interface Builder { fun feedUser(@BindsInstance user: User?): Builder fun build(): UserComponent }
我們以 @BindsInstance
注解標識需要注入的初始信息,注意合理控制其可空性,在后續的使用中,可空性需保持一致
注意:方法名并不重要,采用習慣性命名即可,我習慣于將向容器喂入參數的API添加feed前綴
當我們通過Hilt獲得Builder實例時,即可控制Component的創建(即生命周期開始)
不難想象,Component的管理基本為模板代碼,Hilt中提供了模板和接口類:
如果您想避免模板代碼編寫,可以定義擴展模塊,使用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
提供實例
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!! } }
您可以根據實際需求選擇最適宜的方法進行管理,不再贅述。
至此,我們已經完成了自定義Scope、Component的主要工作,通過Manager即可控制生命周期。
如果想在生命周期范圍更小的Component中訪問 UserComponent中的對象實例,您需要謹記前文提到的三條結論。
該需求很合理,但下面的例子并不足夠典型
此時,您需要通過一個合理的Component實現訪問,例如在Activity中需要注入相關實例時。 因為 ActivityRetainedComponent
和 UserComponent
不存在父子關系,Scope沒有交集,所以 需要找到共同的父Component進行幫助,并通過EntryPoint突破壁壘 :
前文中,我們將 UserComponentManager
劃入 SingletonComponent
, 他是兩種的共同父Component,此時可以這樣處理:
@Module @InstallIn(ActivityRetainedComponent::class) object AppModule { @Provides fun provideUserVO(manager: UserComponentManager):UserVO { return UserEntryPoint.manualGet(manager.generatedComponent()).provideUserVO() } }
此問題屬于常見案例,通過研究它的解決方案,我們可以更深刻地理解前文內容,做到吃透。
當處理主工程時,沒有代碼隔離,我們可以很輕易的修改Application的代碼,因此很多問題難以暴露。
例如,我們可以在Application中通過注解標明依賴 (滿足Singleton Scope前提) ,DI框架會幫助我們進行注入,在注入后可以編寫邏輯代碼,將對象賦值給全局變量,便可以 "方便" 的使用。
為方便下文表述,我們稱之 "方案1"
顯然,這是有異味的代碼,雖然它有效且方便。
因此,我們選取一些場景來說明該做法的弊端:
場景1:創建獨立Library,其中使用Hilt作為DI框架,Library中存在自定義Component,需要初始化管理入口
場景2:項目采用了組件化,該Library按照渠道包需求,渠道包A集成、渠道包B不集成
場景3:項目采用了Uni-App、React-Native等技術,該Library中存在實例由反射方式創建、不受Hilt管理,無法借助Hilt自動注入依賴
以上場景并不相互孤立
在場景1中,我們仍然可以通過 方案1
完成需求,但在場景2中便不再可行。
常規的組件化、插件化,都會完成代碼隔離&使用抽象,因此無法在主工程的Application中使用目標類。通過定制字節碼工具曲線救國,則屬實是大炮打蚊子、屎盆子鑲金邊
在 MAD Skills 系列文章的最后一篇中,簡單提及了Hilt的聚合能力,它至少包含以下兩個層面:
即便一個已經編譯為aar的庫,在被集成后,Hilt依舊能夠掃描該庫中Hilt相關的內容,進行依賴圖聚合
Hilt生成的代碼,依舊存在著注解,這些注解可以被注解處理器、字節碼工具識別、并進一步處理。可以是Hilt內建的處理器或您自定義的擴展處理器
依據第一個層面,我們可以制定一個約定:
子Library按照抽象接口提供Library初始化實例,主工程的Application通過DI框架獲取后進行初始化
我們將其稱為方案2
例如,在Library中定義如下初始化類:
class LibInitializer @Inject constructor( private val userComponentManager: UserComponentManager ) : Function1<Application, Any> { override fun invoke(app: Application): Any { UserComponentManager.instance = userComponentManager return Unit } }
不難發現,他是方案1的變種,將依賴獲取從Application中挪到了LibInitializer中
并約定綁定實例&集合注入, 依舊在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的需求。
但仔細思考一下,這種做法太 "強硬" 了,不僅要求主工程的Application進行配合,而且需要小心的處理初始化代碼的分配。
在場景3中,這些技術均有相適應的插件初始化入口;組件化插件化項目中,也具有類似的設計。隨集成方式的不同,很可能造成 初始化邏輯遺漏或者重復 。
注意:重復初始化可能造成潛在的Scope泄漏,滋生bug。
前文中,我們已經討論了使用EntryPoint突破IOC容器的壁壘,也體驗了Hilt的聚合能力。而 SingletonComponent
作為內建Component,同樣可以使用EntryPoint突破容器壁壘。
如果您對Hilt的源碼或其設計有一定程度的了解,應當清楚:
內建Component均有對應的ComponentHolder,而SingletonComponent對應的Holder即為Application。
通過 Holder實例和 EntryPointAccessors
可以獲得定義的 EntryPoint接口
為 SingletonComponent
自定義EntryPoint后,即可擺脫Hilt自定注入的傳遞鏈而通過邏輯編碼獲取實例。
@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即可突破壁壘訪問容器內部實例,Hilt不再約束Library的初始化方式。
至此,您可以在原先的Library初始化模塊中,按需自由的添加邏輯!
注意:Builder由Hilt生成實現,無法干預其生命周期,故每次調用時生成新的實例,從一般的編碼需求,獲取Manager實例即可。您可以在WorkShop項目中獲得驗證
在場景3中,我們繼續進行衍生:
Library作為動態插件,并不直接集成,而是通過插件化技術,動態集成啟用功能。又該如何處理呢?
在MAD Skills系列文章的第四篇中,簡單提及了Hilt的擴展能力。考慮到篇幅以及AAB(Dynamic Feature)、插件化的背景,我們將在下一篇文章中對該問題展開解決方案的討論。
“Hilt自定義與跨壁壘的方法是什么”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。