最近反思了一下近期的工做,突然就出现了一个想法,将平时干的好玩的事情,整理成一个系列,和你们分享一下,想来想去也没想好这个系列叫啥,索性就叫好玩系列得了。java
文中涉及的代码均在此处能够找到android
若是你的项目中使用了ButterKnife或者Kotlin-Android-Extention(KAE)插件,近半年你必定关注过以下信息:git
Attention: This tool is now deprecated. Please switch to view binding. Existing versions will continue to work, obviously, but only critical bug fixes for integration with AGP will be considered. Feature development and general bug fixes have stopped. -- ButterKnifegithub
Resource IDs will be non-final in Android Gradle Plugin version 5.0, avoid using them in switch case statements Inspection info:Avoid the usage of resource IDs where constant expressions are required. A future version of the Android Gradle Plugin will generate R classes with non-constant IDs in order to improve the performance of incremental compilation.算法
Issue id: NonConstantResourceId -- lintexpress
The 'kotlin-android-extensions' Gradle plugin is deprecated. Please use this migration guide (goo.gle/kotlin-andr…) to start working with View Binding (developer.android.com/topic/libra…) and the 'kotlin-parcelize' plugin.安全
是的,这两个在Android中使用面很广的内容被标记为废弃了。markdown
对于ButterKnife,被废弃的缘由是:从AGP-5.0版本开始,R类生成的值再也不是常量app
对于KAE,问题以下:ide
按照官方或者社区的推荐,替代方案仍是回归到findViewById or ViewBinding or DataBinding.
将来可能替代XML描述布局文件的技术:Compose尚未真正到来,并且一时半会也不可能把原先的内容所有迁移到Compose实现,因此咱们仍是要老老实实回归到上面的三个方案。
有些同窗知识面广一点,立马想到了psi,经过分析代码文件的psi树,实现代码转换,直接搞一个插件来处理ButterKnife的迁移问题。
固然,这篇文章并不许备去讲psi,虽然这是一个挺好玩的东西。下次有时间会专门写一个好玩的psi
由于AGP生成的R类资源值再也不是常量,不管是library仍是application,那么要继续再思考一个问题:library的R类资源也不是常量,原先ButterKnife是怎么处理的? 咱们知道,Butterknife有运行时反射用法,也有编译期使用apt预生成代码的用法。bk提供了gradle插件,用于copy原始R类内容,生成R2类,R2复刻了R的内容,但均为常量。由于注解中的内容,是须要在编译期肯定,它被要求为常量,而且在编译时被优化。但咱们知道,经过字节码技术,能够修改不少东西,不管是一个常量的值,仍是索性连类都给换了。 一旦这个值被修改,注解中的信息便为谬误。但由于R2的存在,咱们能够经过常量值反向获取到常量的名字,从而去使用R类。
显然不是,由于findviewbyid还没用被革命性改变,bk中全部的核心代码仍是有用的 若是你使用的apt方式,那么就有意思了,对于一个特定的target,bk生成的绑定代码彻底是没有“废弃”风险的,咱们彻底能够拷贝其中的逻技,或者直接对生成类实行“拿来主义” 最终,咱们只须要扔掉bk的gradle插件,注解和apt处理器,岁月静好。 若是你使用的是运行时反射方案,我不排斥运行时反射,虽然他会多耗一些时间,若是你不介意耗费更多的时间,彻底能够改造bk的注解和逻辑,虽然它很好玩,但这并非一个值得推荐的作法。
没错,仍是经过findviewbyid,它被废弃并非犯了什么大错,只是不在适应潮流,且有各类各样的小毛病。 咱们以Fragment为例子,看一下编译器为咱们植入的代码:
public android.view.View _$_findCachedViewById(int var1) {
if (this._$_findViewCache == null) {
this._$_findViewCache = new HashMap();
}
android.view.View var2 = (android.view.View)this._$_findViewCache.get(var1);
if (var2 == null) {
android.view.View var10000 = this.getView();
if (var10000 == null) {
return null;
}
var2 = var10000.findViewById(var1);
this._$_findViewCache.put(var1, var2);
}
return var2;
}
public void _$_clearFindViewByIdCache() {
if (this._$_findViewCache != null) {
this._$_findViewCache.clear();
}
}
复制代码
以及:
// $FF: synthetic method
public void onDestroyView() {
super.onDestroyView();
this._$_clearFindViewByIdCache();
}
复制代码
//源码
vMallAccountTitleBar.setTitle("个人钱包")
//反编译结果
((BarStyle4)this._$_findCachedViewById(id.vMallAccountTitleBar))
.setTitle((CharSequence)"个人钱包");
复制代码
能够很轻易的发现,具备多种场景下潜在的npe风险。本质上仍是在使用findViewByID机制
首先仍是要粗略提一下databinding和viewbinding。记忆中databinding技术先于viewbinding,是Google提供的声明式UI解决方案,这里必需要岔开一句:什么是声明式UI?
这里我用SQL举个例子类比, select * from t where 't.id' = 1, 这就是声明式,声明一个符合规则的定则,让对应的系统执行,获得目标结果。相应的,对立面就是命令式,命令式须要准确的指出每一步操做的具体指令,以完成一个特定的算法。
肤浅的总结,声明式是底层实现了一类行为的抽象,其核心的算法或者控制段均被封装,只须要控制输入,便可获得输出。而命令式则彻底须要自行实现。
理解了这一点,咱们就会意识到,databinding自己不该该对外暴露这些view,只是这么干的话,项目迁移成本就会变大,因此仍是选择了开放,这也就有了后来的viewbinding。
言归正传,原先用bk,咱们须要一个根view做为起始点,以实现视图绑定,基本是找到Activity#setContentView(Int id)后activity的decorview,或者是viewholder#getRoot(),或者是开发者inflate获得的一个view等等。
不难判断,若是完全的修改代码,从基类出发应该是没什么方案。只能进行一件枯燥乏味的事情
延伸:属性委托指的是一个类的某个属性值不是在类中直接进行定义,而是将其托付给一个代理类,从而实现对该类的属性统一管理。 属性委托语法格式:
val/var <属性名>: <类型> by <表达式>
先定一个小目标,咱们会将注解形式变成相似如下代码的形式:
val tvHello1 by bindView<TextView>(R.id.dialog)
val tvHello by bindView<TextView>(viewProvider, R.id.hello, lifecycle) {
bindClick { changeText() }
}
val tvHellos by bindViews<TextView>(
viewProvider,
arrayListOf(R.id.hello1, R.id.hello2),
lifecycle
) {
this.forEach {
it.bindClick { tv ->
Toast.makeText(tv.context, it.text.toString(), Toast.LENGTH_SHORT).show()
}
}
}
复制代码
那么咱们须要先定义一个属性代理类,并实现操做符,以bindView为例,
咱们先缓一缓,定义一个基类,接受属性持有者的生命周期,以实现其生命周期走到特定节点时释放依赖。
abstract class LifeCycledBindingDelegate<F,T>(lifecycle: Lifecycle): ReadOnlyProperty<F,T> {
protected var property: T? = null
init {
lifecycle.onDestroyOnce { destroy() }
}
protected open fun destroy() {
property = null
}
}
internal class OnDestroyObserver(var lifecycle: Lifecycle?, val destroyed: () -> Unit) :
LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
val lifecycleState = source.lifecycle.currentState
if (lifecycleState == Lifecycle.State.DESTROYED) {
destroyed()
lifecycle?.apply {
removeObserver(this@OnDestroyObserver)
lifecycle = null
}
}
}
}
fun Lifecycle.onDestroyOnce(destroyed: () -> Unit) {
addObserver(OnDestroyObserver(this, destroyed))
}
复制代码
这时候咱们来处理findViewById的核心部分
class BindView<T:View>(
private val targetClazz: Class<T>,
private val rootViewProvider: ViewProvider,
@IdRes val resId: Int,
lifecycle: Lifecycle,
private var onBind: (T.() -> Unit)?
):LifeCycledBindingDelegate<Any,T>(lifecycle) {
override fun getValue(thisRef: Any, property: KProperty<*>): T {
return this.property ?: let {
val rootView = rootViewProvider.provide()
val v = rootView.findViewById<T>(resId)
?: throw IllegalStateException(
"could not findViewById by id $resId," +
" given name: ${rootView.context.resources.getResourceEntryName(resId)}"
)
return v.apply {
this@BindView.property = this
onBind?.invoke(this)
onBind = null
}
}
}
}
复制代码
咱们须要几样东西以支持:
View#<T extends View> T findViewById(@IdRes int id)
对应了目标类,根View提供者,目标view的id,属性持有者的生命周期和初次属性初始化后的附加逻辑
至于BindViews,咱们如法炮制便可。
这时候会发现,这样使用太累了,对于Activity、Fragment、ViewHolder等常见的类而言,虽然他们提供根视图等内容的方式有所差异,但这种行为基本是能够抽象的。
以ComponentActivity为例,咱们只须要定义扩展函数:
inline fun <reified T : View> ComponentActivity.bindView(@LayoutRes resId: Int) =
BindView<T>(
targetClazz = T::class.java,
rootViewProvider = object : ViewProvider {
override fun provide(): View {
return this@bindView.window.decorView
}
},
resId = resId,
lifecycle = this.lifecycle,
onBind = null
)
复制代码
就能够比较方便的使用,剩下来的Fragment、ViewHolder之类的东西,讲起来太啰嗦了,都是如法炮制。
再定义一个大而全的:
inline fun <reified T : View> Any.bindView(
rootViewProvider: ViewProvider,
@LayoutRes resId: Int,
lifecycle: Lifecycle,
noinline onBind: (T.() -> Unit)?
) =
BindView<T>(
targetClazz = T::class.java,
rootViewProvider = rootViewProvider,
resId = resId,
lifecycle = lifecycle,
onBind = onBind
)
复制代码
实际项目中想怎么用彻底看实际就好了。
固然是有价值的,一个大项目中,尤为是进行了模块化拆分,不一样模块使用不一样的技术是很正常的,DataBinding和ViewBinding并存的状况必定会发生,虽然我并无真正遇到过同时使用的,而且并不清楚同时使用会不会有bug
由于笔者项目中没有使用ViewBinding,咱们就粗暴的只实现DataBinding了,其实都是获取Binding类实例而已,机制是一致的,ViewBinding能够如法炮制
得益于咱们上面定义的基类,咱们能够直接干一个处理DataBinding的子类了
class BindDataBinding<T : ViewDataBinding>(
private val targetClazz: Class<T>,
private val inflaterProvider: LayoutInflaterProvider,
@LayoutRes val resId: Int,
lifecycle: Lifecycle,
private var onBind: (T.() -> Unit)?
) : LifeCycledBindingDelegate<Any, T>(lifecycle) {
override fun getValue(thisRef: Any, property: KProperty<*>): T {
return this.property ?: let {
val layoutInflater = inflaterProvider.provide()
val bind = DataBindingUtil.bind<T>(layoutInflater.inflate(resId, null))
?: throw IllegalStateException(
"could not create binding ${targetClazz.name} by id $resId," +
" given name: ${layoutInflater.context.resources.getResourceEntryName(resId)}"
)
return bind.apply {
this@BindDataBinding.property = this
onBind?.invoke(this)
onBind = null
}
}
}
}
复制代码
依葫芦画瓢,咱们直接搞定inflate方式获取Binding。
仔细一想,这还不够,原本咱们将布局改成DataBinding模板,有多种方案设置视图,使用属性代理,有一个目的是:让设置视图和获得Binding实例之间减小限制。
再干一个:
class FindDataBinding<T : ViewDataBinding>(
private val targetClazz: Class<T>,
private val viewProvider: ViewProvider,
lifecycle: Lifecycle,
private var onBind: (T.() -> Unit)?
) : LifeCycledBindingDelegate<Any, T>(lifecycle) {
override fun getValue(thisRef: Any, property: KProperty<*>): T {
return this.property ?: let {
val view = viewProvider.provide()
val bind = DataBindingUtil.bind<T>(view)
?: throw IllegalStateException(
"could not find binding ${targetClazz.name}"
)
return bind.apply {
this@FindDataBinding.property = this
onBind?.invoke(this)
onBind = null
}
}
}
}
复制代码
咱们又能够经过bind的方式,从一个View发现其binding了。寻找binding和设置视图的前后,就能够灵活选择了。
加上一些扩展方法后,咱们就能够开心的使用了:
class MainActivity2 : AppCompatActivity() ,ViewProvider{
val binding by dataBinding<ActivityMainBinding>(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding.hello.text = "fragment"
binding.hello.bindClick {
}
}
override fun provide(): View {
return window.decorView.findViewById(R.id.ll_root)
}
}
复制代码
正如开篇提到的,好玩系列其出发点必定是好玩,它极可能是对一个问题展开的一次脑暴和尝试,不必定是一个真正成熟的特定问题通用解法。
这一篇,咱们从Butterknife的废弃和KAE的废弃开始思考,回顾了二者的实现原理和被废弃的缘由,再到寻找迁移方案,并进行了实践。抛开还未涉及到的PSI,基本能够画上一个阶段性句号了。
再次贴上代码连接: UIBinding,若是本文中的内容对你有一丝丝的帮助,但愿能够获得点赞支持。
补充:2021-1-25 再补充一段内容重点。