kotlin函数式接口SAM实际使用的坑

整理了自己在实际开发中关于kotlin的学习和思考:深入学习Kotlin,感兴趣的可以查看,后续会不断地更新。

函数式接口

函数式接口,英文名称(Functional (SAM) interfaces),全称是 Single Abstract Method (SAM) interface。在 Kotlin 中,仅具有一个抽象方法的接口称为函数式接口或单一抽象方法(SAM)接口。函数式接口可以有多个非抽象成员,但只能有一个抽象成员。比如下面的接口:

kotlin 复制代码
fun interface IServiceInterface {
   fun getService()
}

很多人看到这会觉得这个不是和普通的接口一样吗?是的,我开始的时候,也是这么认为,这不都一样的吗?但是你仔细看,就可以看到接口定义的时候,多了一个fun关键字。是的,函数式接口在定义的时候,多一个了fun关键字修饰,如果没有,那这个接口就是一个普通的接口。

作用

那么函数式接口有什么作用?其实最大的作用就是:可以做 ASM 转化,即把接口实现转为lambda实现

我们知道,在 kotlin 中,对于 Java 定义的SAM接口,会默认当作函数式接口。比如常用的设置Viewclick事件,我们正常可以类似 Java 的写法:

kotlin 复制代码
    view.setOnClickListener(object : OnClickListener {
        override fun onClick(v: View?) {
            TODO("Not yet implemented")
        }

    })

使用lambda实现的方式,如下面:

kotlin 复制代码
    view.setOnClickListener {
        TODO("Not yet implemented")
    }

可以看到,直接使用lambda的方式实现这个接口,写起非常的方便。

这是针对 Java 接口默认的操做。但是对于 kotlin 中定义的接口,默认是没有这种写法的,即使这个接口是一个SAM接口,比如下面这个接口:

kotlin 复制代码
interface IServiceInterface {
   fun getService()
}

他在实现的时候,就只能通过object创建匿名对象的方式实现调用

kotlin 复制代码
    setIServiceInterface(object : IServiceInterface {
        override fun getService() {
            print("定义的接口")
        }

    })

那如果要实现 lambda 的调用方式,就需要定义函数式接口,也就是:

kotlin 复制代码
fun interface IServiceInterface {
   fun getService()
}

这样的话,就能够实现

kotlin 复制代码
    setIServiceInterface{
        print("定义的接口")
    }

Android 源码中也定义了好多类似的接口,比如我们常用的Observer接口

kotlin 复制代码
fun interface Observer<T> {

    /**
     * Called when the data is changed is changed to [value].
     */
    fun onChanged(value: T)
}

实际使用中的坑

函数式接口在使用的时候确实很方便,但是使用不当或者不理解背后的原理,很可能会埋下坑,觉得逻辑很奇怪。

举个不太恰当的例子(仅仅是用来说明问题):

kotlin 复制代码
    // ViewModel请求数据,假设返回的值为:七郎
    viewModel.requestData()
    findViewById<ViewGroup>(R.id.tv1).apply {
         setOnClickListener {
            // 每次点击,就注册观察者,接收数据
            viewModel?.liveData?.observe(this@MainActivity) {
                // 打印获取到的数据,假设就是就是上面的:七郎
                Log.i("MainActivity", "return result is ${it}")
            }
        }
    }

上面的例子中,先是 ViewModel获取数据,然后每次点击文字控件tv1就注册一个观察者,监听数据回调。正常按照我的理解,每点击一次打印一次日志return result is 七郎。因为在点击之前已经加载了数据,按照 LiveData 的特性,后面注册的观察者,都会接收到一次上次的数据。但是实际上整个日志就打印了一次。

那这是为什么呢?我们反编译下 apk 的代码:

java 复制代码
    public static final void onCreate$lambda$1$lambda$0(MainActivity this$0, View it) {
        PictorialLiveData<String> liveData;
        TestViewModel testViewModel = this$0.viewModel;
        // 注释1
        liveData.observe(this$0, new MainActivity$sam$androidx_lifecycle_Observer$0(MainActivity$onCreate$1$1$1.INSTANCE));
    }

可以看到注释1的位置,调用liveDataobserve方法,其中的Observer接口是编译器自动生成的一个类MainActivity$sam$androidx_lifecycle_Observer$0,这个类实现了Observer接口,如下面:

java 复制代码
public final class MainActivity$sam$androidx_lifecycle_Observer$0 implements Observer, FunctionAdapter {
    private final /* synthetic */ Function1 function;

    public MainActivity$sam$androidx_lifecycle_Observer$0(Function1 function) {
        Intrinsics.checkNotNullParameter(function, "function");
        this.function = function;
    }
    ......
}

其中的Function1就是我们上面 kotlin 代码中的打印日志的业务逻辑实现,kotlin 会自动根据参数的个数编译成Function1,Function2....FunctionN,也就是上面的MainActivity$onCreate$1$1$1.INSTANCE变量,我们看下它的实现:

java 复制代码
public final class MainActivity$onCreate$1$1$1 extends Lambda implements Function1<String, Unit> {
    // !!!!!!!看到没有,这里是静态变量
    public static final MainActivity$onCreate$1$1$1 INSTANCE = new MainActivity$onCreate$1$1$1();
    ......

    public final void invoke2(String it) {
        Intrinsics.checkNotNullParameter(it, "it");
        Log.i("MainActivity", "return result is " + it);
    }
}

当你看到这个类的时候,估计你就明白为什么上面的日志只打印一次了,因为 kotlin 中每个函数式接口(如果没有引用外部的非静态变量或者对象,这里存在差异),对应 lambda 表达式编译成立一个静态变量,也就是不管你调用了 observe 多少次,他都是同一个对象

所以,在了解了背后的原理之后,我们在使用这种函数式接口的 lambda 方式时要非常注意,涉及到静态变量,稍微不小心,就有可能造成内存泄漏或者逻辑错误。

如果觉得对你有帮助,请点赞关注,或者关注我交流。也可以点击深入学习Kotlin看其他文章,希望一起学习进步,加油!

相关推荐
阿巴斯甜11 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker11 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952712 小时前
Andorid Google 登录接入文档
android
黄林晴14 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android