Android开发Viewbinding委托实例详解

目录
  • 背景
  • 从Crash到有意思的源码
    • 有趣的代码
    • 另外一些有意思的地方
  • 结尾

背景

前一阵子我们在使用viewbinding的委托的时候碰到了点crash问题,然后发现了一个比较有意思的解决方案,就和大家展开聊聊。

另外一点就是我后面打算将kotlin extensions这个插件统一移除掉。

估计大家应该对Viewbinding的委托应该都有一定的了解,好几个大佬分享过类似的文章,但是大佬们的代码貌似也有一阵子都没有维护了,所以我找到了一个外国大佬写的仓库,其实应该算是一个相对来说比较稳定的库了,而且也一直处于一个持续更新迭代的状态。

仓库地址 ViewBindingPropertyDelegate

从Crash到有意思的源码

委托模式是软件设计模式中的一项基本技巧。在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。

Kotlin 直接支持委托模式,更加优雅,简洁。Kotlin 通过关键字 by 实现委托。

上述是kotlin对于委托的释义,Viewbinding委托就是把生成Viewbinding实例的过程交给委托类去完成,然后让使用方可以忽略掉其中的细节,是一种非常好玩的模式了。

但是由于Viewbinding的特殊性,它其实就会和当前的lifecycle绑定在一起。因为我们要在销毁的情况下把实例重置为空。否则当我们页面重新生成的情况下,就会出现view并不是当前的页面的困扰。

作者在定义的时候就将Viewbinding委托获取的实例定义为了非空,这里我和我的同事其实是存在一些分歧的,我认为非空其实挺合理的,但是对方并不认为。

恰巧这种空非空的问题,在实际的使用中就出现了很多不可预期的crash问题。比如说在一个异步操作中获取viewbinding实例然后进行赋值操作,就会出现空指针异常。另外由于使用的是lifecycle的页面销毁方法,如果我们复写了销毁方法之后在设置这个值,也会出现崩溃问题。

上述问题我在几个我之前参考的库中其实都发现了对应的问题。我参考了Binding,还有之前彭旭说的那个也有类似的情况。

另外在fragment中,其实问题尤其的明显。因为我们很多时候使用的fragment相关的LifecycleOwner是fragment本身,但是Android官方其实推荐我们使用的是fragment内部的view相关的LifecycleOwner。因为fragment相比较于activity,存在的问题就是多了几个生命周期,比如createView,和onDestroyView。其中出现最多问题的也就是onDestroyView和onDestroy。

有趣的代码

接下来我们看下这个作者是如何解决这些奇奇怪怪的问题的哦。

private class FragmentViewBindingProperty<in F : Fragment, out T : ViewBinding>(
    private val viewNeedInitialization: Boolean,
    viewBinder: (F) -> T,
    onViewDestroyed: (T) -> Unit,
) : LifecycleViewBindingProperty<F, T>(viewBinder, onViewDestroyed) {
    private var fragmentLifecycleCallbacks: FragmentManager.FragmentLifecycleCallbacks? = null
    private var fragmentManager: Reference<FragmentManager>? = null
    // 赋值操作
    override fun getValue(thisRef: F, property: KProperty<*>): T {
        val viewBinding = super.getValue(thisRef, property)
        registerFragmentLifecycleCallbacksIfNeeded(thisRef)
        return viewBinding
    }
    private fun registerFragmentLifecycleCallbacksIfNeeded(fragment: Fragment) {
        if (fragmentLifecycleCallbacks != null) return
        val fragmentManager = fragment.parentFragmentManager.also { fm ->
            this.fragmentManager = WeakReference(fm)
        }
        fragmentLifecycleCallbacks = ClearOnDestroy(fragment).also { callbacks ->
            fragmentManager.registerFragmentLifecycleCallbacks(callbacks, false)
        }
    }
    override fun isViewInitialized(thisRef: F): Boolean {
        if (!viewNeedInitialization) return true
        if (thisRef !is DialogFragment) {
            return thisRef.view != null
        } else {
            return super.isViewInitialized(thisRef)
        }
    }
    override fun clear() {
        super.clear()
        fragmentManager?.get()?.let { fragmentManager ->
            fragmentLifecycleCallbacks?.let(fragmentManager::unregisterFragmentLifecycleCallbacks)
        }
        fragmentManager = null
        fragmentLifecycleCallbacks = null
    }
    override fun getLifecycleOwner(thisRef: F): LifecycleOwner {
        try {
            return thisRef.viewLifecycleOwner
        } catch (ignored: IllegalStateException) {
            error("Fragment doesn't have view associated with it or the view has been destroyed")
        }
    }
    // 有意思的代码
    private inner class ClearOnDestroy(
        fragment: Fragment
    ) : FragmentManager.FragmentLifecycleCallbacks() {
        private var fragment: Reference<Fragment> = WeakReference(fragment)
        override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
            // Fix for destroying view for case with issue of navigation
            if (fragment.get() === f) {
                postClear()
            }
        }
    }
}

从上述代码上我们可以看出来,其中获取的LifecycleOwner就是我上文说的viewLifecycleOwner。这个就其实已经非常精彩了。

另外我们可以看下他在内部定义了ClearOnDestroy这个类,然后当onFragmentDestroyed触发的时候调用postClear方法。而这个方法就是解决当我们在Destroyed中还执行了ViewBinding内的对象的操作的空指针问题。

经典面试题的真实使用场景,Handler.post执行。很多人觉得Handler相关的面试题都是八股文,这次我们就通过这个真是场景来给大家说说这个有意思的问题。

首先从onFragmentDestroyed方法会执行在Fragment本身的onDestroyView之前,原来我们会在这个方法下执行引用清空的操作。然后当onDestroyView执行的时候就会出现空指针异常了。那么Lifecycle有没有提供一个在onDestroyView之后的方法呢?我们是不是可以考虑自己造一个呢?面试中,我们知道所有生命周期方法都是有主线程Handler来负责调度的,这也就是说活我么可以把生命周期方法认为就是一个Message,当onFragmentDestroyed执行的时候,onDestroyView也已经被添加到主线程的MessageQueue中,这个时候我们在post一个runnable,那么他的排序规则上来说,就必然在onDestroyView之后了。

另外一些有意思的地方

这个库另外一个优点就是他同时支持反射和非反射的写法。同时也支持了Activity,Fragment,View,FragmentDialog,ViewHolder等等。反射写法是基于非反射写法的,所以也保证了底层库的一致性。

    //非反射写法
    private val viewBinding by viewBinding(ViewProfileBinding::bind)
    //反射写法
    private val viewBinding: ItemProfileBinding by viewBinding()

同时他的反射相关的混淆配置文件也非常有意思。

allowoptimization 指定对象可能会被优化,即使他们被keep选项保留。所指定对象可能会被改变(优化步骤),但可能不会被混淆或者删除。该修饰符只对实现异常要求有用。

-keep,allowoptimization class * implements androidx.viewbinding.ViewBinding {
    public static *** bind(android.view.View);
    public static *** inflate(...);
}

它只会keep实现了ViewBinding的类的bind和inflate方法。因为ViewBinding会将所有的xml转化成一个类实例,如果不删除掉没有实际被调用的类的情况下就会导致dex包变大,大家对于包体积优化都是有追求的吗。然后用了-keep,allowoptimization,这样在混淆的代码优化过程中就可以删除掉没有被调用的ViewBinding类了。

结尾

本次内卷到此结束。但是又是一个老生常谈的话题,一个开源库还是要持续的进行迭代和解决问题才能持续变好,而不是一次性的工作。拥抱变化的代码世界,解决一些奇奇怪怪的问题,都是挺好玩的,更多关于Android开发Viewbinding委托的资料请关注我们其它相关文章!

时间: 2022-06-21

Android Studio 3.6中新的视图绑定工具ViewBinding 用法详解

前言 我们在Android开发的过程中总是需要获取XML布局中的ViewId,以便给其赋值进行显示,早期我们只能使用 findViewById 这个API,会导致很多的模版代码出现.2013年左右Android界大神 Jake Wharton开源了Butter Knife框架,通过Bind("viewid")方式方便开发者获取ViewId.近两年由于谷歌对Kotlin的支持,我们开始使用 Android Kotlin extensions. 在文件中导入布局文件直接引用viewId.无

Android Studio3.6新特性之视图绑定ViewBinding使用指南

View Binding是一项功能,使您可以更轻松地编写与视图交互的代码.在模块中启用视图绑定后,它将为该模块中存在的每个XML布局文件生成一个绑定类.绑定类的实例包含对在相应布局中具有ID的所有视图的直接引用. 正文 Android Studio 3.6 Canary 11 及更高版本中推出了ViewBinding功能,ViewBinding将逐步替换掉findViewById,还等什么,抓紧时间学习吧! 谷歌官方文档的ViewBinding Demo是用Kotlin语言写的,看起来比较生疏,

Android使用ViewBinding的详细步骤(Kotlin简易版)

ViewBinding 是什么 2020年的3月份 巨佬 JakeWharton 开源的 butterknife 被官宣 停止维护,在github 上 说明 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

Android ViewBinding的使用详解

目录 一.什么是view binding 设置说明 二.基本用法 Activity中使用 Fragment 中使用 Adapter 中使用 三.了解源码实现 BaseActivity BaseFragment BaseAdapter 四.其他 最近Android Studio 升级后 butterknife 有一个警告: Resource IDs will be non-final in Android Gradle Plugin version 5.0, avoid using them as

Android分包MultiDex策略详解

1.分包背景 这里首先介绍下MultiDex的产生背景. 当Android系统安装一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt.DexOpt的执行过程是在第一次加载Dex文件的时候执行的.这个过程会生成一个ODEX文件,即Optimised Dex.执行ODex的效率会比直接执行Dex文件的效率要高很多. 但是在早期的Android系统中,DexOpt有一个问题,DexOpt会把每一个类的方法id检索起来,存在一个链表结构里面.但是这个链表的长度是用一

Android系统对话框使用详解(最详细)

在实际应用开发中,用到系统对话框中的情况几乎是没有的.按开发流程来说,UI工程师都会给出每一个弹窗的样式,故而在实际开发中都是自定义弹窗的. 即使用到的地方不多,但是我们也是需要了解并且能熟练的运用它,下面为大家奉上各种系统对话框的实现. 目录 一.系统对话框的几种类型与实现 在项目的实际开发中,用到的系统对话框几乎是没有的.原因大概包含以下几点: 样式过于单一,不能满足大部分实际项目中的需求. 对话框的样式会根据手机系统版本的不同而变化.不能达到统一的样式. 能实现的功能过于简单. 在这里先附

Android HandlerThread使用方法详解

Android HandlerThread使用方法详解 HandlerThread 继承自Thread,内部封装了Looper. 首先Handler和HandlerThread的主要区别是:Handler与Activity在同一个线程中,HandlerThread与Activity不在同一个线程,而是别外新的线程中(Handler中不能做耗时的操作). 用法: import android.app.Activity; import android.os.Bundle; import androi

Android中menu使用详解

Menu(菜单)是Android中一定会使用的模块,每个Android项目都会用到Menu来给用户起到选择和导航的作用,提升用户体验,下面通过本文给大家分享android 中menu使用,需要的朋友一起看看吧 很多activity界面中都存在一个菜单栏,就是点击右上角的一个按钮的时候会出现一个下拉列表差不多的东西,这个功能的实现其实只需要下面的两步,每一个activity都可以拥有自己独一无二的menu,具体的格式可以自己进行定义,详细的创建步骤如下 ①在res下的menu中创建file_men

Android xml解析实例详解

Android  xml解析实例详解 实现效果图: XmlActivity package com.Android.xiong.gridlayoutTest; import android.app.Activity; import android.content.res.XmlResourceParser; import android.graphics.Color; import android.graphics.Typeface; import android.os.Bundle; impo

Android AOP注解Annotation详解(一)

Android 注解Annotation 相关文章: Android AOP注解Annotation详解(一) Android AOP之注解处理解释器详解(二) Android AOP 注解详解及简单使用实例(三) Android AOP 等在Android上应用越来越广泛,例如框架ButterKnife,Dagger2,EventBus3等等,这里我自己总结了一个学习路程. - Java的注解Annotation - 注解处理解析器APT(Annotation Processing Tool)

Android Tab 控件详解及实例

Android Tab 控件详解及实例 在桌面应用中Tab控件使用得非常普遍,那么我们经常在Android中也见到以Tab进行布局的客户端.那么Android中的Tab是如何使用的呢? 1.Activity package com.wicresoft.activity; import com.wicresoft.myandroid.R; import android.app.TabActivity; import android.os.Bundle; import android.util.Lo

Android canvas drawBitmap方法详解及实例

 Android canvas drawBitmap方法详解及实例 之前自己在自定义view,用到canvas.drawBitmap(Bitmap, SrcRect, DesRect, Paint)的时候,对其中的第2和3个参数的含义含糊不清.看源码函数也没理解,然后看了一些其他的博客加上自己的理解,整理如下.首先,我们看一张图片,今天就要绘制这张图片. 然后将图片用红色的线条分成4个部分,如下: 我们自定义一个View,代码如下: public class PoterDuffLoadingVi

Android init.rc文件详解及简单实例

Android init.rc文件详解 本文主要来自$ANDROID_SOURCE/system/init/readme.txt的翻译. 1 简述 Android init.rc文件由系统第一个启动的init程序解析,此文件由语句组成,主要包含了四种类型的语句:Action,Commands,Services,Options.在init.rc文件中一条语句通常是占据一行.单词之间是通过空格符来相隔的.如果需要在单词内使用空格,那么得使用转义字符"\",如果在一行的末尾有一个反斜杠,那么

Android getevent用法实例详解

 Android getevent用法实例详解 最近在测试设备按键的常用命令,感觉这些命令都有的,但就是不知道怎么查找. 翻阅了几篇博文,才发现有一个getevent,就是指这样的命令. 首先需要说明的是getevent命令后面可以带上具体的input设备,列如getevent /dev/iput/event0,这样可以过滤掉一些不用显示的input的设备. 我在之前的使用中,还是有些找不到点子,也是一步一步使用起来的. 首先看-p 选项, -p选项用于输出input设备相关的一些信息,列如,