补:《Android面试题思考与解答》2021年1月刊(三)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 今年最后一篇,《Android面试题思考与解答21年1月刊》送给大家。

要实现可以拖动的View该怎么做?


还是接着刚才的btn例子,如果要修改btn的位置,使用updateViewLayout即可,然后在ontouch方法中传入移动的坐标即可。


btn.setOnTouchListener { v, event ->
            val index = event.findPointerIndex(0)
            when (event.action) {
                ACTION_MOVE -> {
                    windowParams.x = event.getRawX(index).toInt()
                    windowParams.y = event.getRawY(index).toInt()
                    windowManager.updateViewLayout(btn, windowParams)
                }
                else -> {
                }
            }
            false
        }


Window的添加、删除和更新过程。


Window的操作都是通过WindowManager来完成的,而WindowManager是一个接口,他的实现类是WindowManagerImpl,并且全部交给WindowManagerGlobal来处理。下面具体说下addView,updateViewLayout,和removeView。


1)addView


//WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        if (parentWindow != null) {
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        }
            ViewRootImpl root;
            View panelParentView = null;
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
            try {
                root.setView(view, wparams, panelParentView);
            } 
        }
    }


  • 这里可以看到,创建了一个ViewRootImpl实例,这样就说明了每个Window都对应着一个ViewRootImpl。
  • 然后通过add方法修改了WindowManagerGlobal中的一些参数,比如mViews—存储了所有Window所对应的View,mRoots——所有Window所对应的ViewRootImpl,mParams—所有Window对应的布局参数。
  • 最后调用了ViewRootImpl的setView方法,继续看看。


final IWindowSession mWindowSession;
mWindowSession = WindowManagerGlobal.getWindowSession();
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    //
    requestLayout();
    res = mWindowSession.addToDisplay(mWindow,);
}


setView方法主要完成了两件事,一是通过requestLayout方法完成异步刷新界面的请求,进行完整的view绘制流程。其次,会通过IWindowSession进行一次IPC调用,交给到WMS来实现Window的添加。


其中mWindowSession是一个Binder对象,相当于在客户端的代理类,对应的服务端的实现为Session,而Session就是运行在SystemServer进程中,具体就是处于WMS服务中,最终就会调用到这个Session的addToDisplay方法,从方法名就可以猜到这个方法就是具体添加Window到屏幕的逻辑,具体就不分析了,下次说到屏幕绘制的时候再细谈。


2)updateViewLayout


public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
//...
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
        view.setLayoutParams(wparams);
        synchronized (mLock) {
            int index = findViewLocked(view, true);
            ViewRootImpl root = mRoots.get(index);
            mParams.remove(index);
            mParams.add(index, wparams);
            root.setLayoutParams(wparams, false);
        }
    }


这里更新了WindowManager.LayoutParamsViewRootImpl.LayoutParams,然后在ViewRootImpl内部同样会重新对View进行绘制,最后通过IPC通信,调用到WMS的relayoutWindow完成更新。


3)removeView


public void removeView(View view, boolean immediate) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        synchronized (mLock) {
            int index = findViewLocked(view, true);
            View curView = mRoots.get(index).getView();
            removeViewLocked(index, immediate);
            if (curView == view) {
                return;
            }
            throw new IllegalStateException("Calling with view " + view
                    + " but the ViewAncestor is attached to " + curView);
        }
    }
    private void removeViewLocked(int index, boolean immediate) {
        ViewRootImpl root = mRoots.get(index);
        View view = root.getView();
        if (view != null) {
            InputMethodManager imm = view.getContext().getSystemService(InputMethodManager.class);
            if (imm != null) {
                imm.windowDismissed(mViews.get(index).getWindowToken());
            }
        }
        boolean deferred = root.die(immediate);
        if (view != null) {
            view.assignParent(null);
            if (deferred) {
                mDyingViews.add(view);
            }
        }
    }


该方法中,通过view找到mRoots中的对应索引,然后同样走到ViewRootImpl中进行View删除工作,通过die方法,最终走到dispatchDetachedFromWindow()方法中,主要做了以下几件事:


  • 回调onDetachedFromeWindow。
  • 垃圾回收相关操作;
  • 通过Session的remove()在WMS中删除Window;
  • 通过Choreographer移除监听器


Activity、PhoneWindow、DecorView、ViewRootImpl 的关系?


看完上面的流程,我们再来理理这四个小伙伴之间的关系:


  • PhoneWindow 其实是 Window 的唯一子类,是 Activity 和 View 交互系统的中间层,用来管理View的,并且在Window创建(添加)的时候就新建了ViewRootImpl实例。
  • DecorView 是整个 View 层级的最顶层,ViewRootImpl是DecorView 的parent,但是他并不是一个真正的 View,只是继承了ViewParent接口,用来掌管View的各种事件,包括requestLayout、invalidate、dispatchInputEvent 等等。


Window中的token是什么,有什么用?


token?又是个啥呢?刚才window操作过程中也没出现啊。


token其实大家应该工作中会发现一点踪迹,比如application的上下文去创建dialog的时候,就会报错:


unable to add window --token null


所以这个token跟window操作是有关系的,翻到刚才的addview方法中,还有个细节我们没说到,就是adjustLayoutParamsForSubWindow方法。


//Window.java
    void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
        if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
            //子Window
            if (wp.token == null) {
                View decor = peekDecorView();
                if (decor != null) {
                    wp.token = decor.getWindowToken();
                }
            }
        } else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW &&
                wp.type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
            //系统Window
        } else {
            //应用Window
            if (wp.token == null) {
                wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
            }
        }
    }


上述代码分别代表了三个Window的类型:


  • 子Window。需要从decorview中拿到token。
  • 系统Window。不需要token。
  • 应用Window。直接拿mAppToken,mAppToken是在setWindowManager方法中传进来的,也就是新建Window的时候就带进来了token。


然后在WMS中的addWindow方法会验证这个token,下次说到WMS的时候再看看。


所以这个token就是用来验证是否能够添加Window,可以理解为权限验证,其实也就是为了防止开发者乱用context创建window。


拥有token的context(比如Activity)就可以操作Window。没有token的上下文(比如Application)就不允许直接添加Window到屏幕(除了系统Window)。


Application中可以直接弹出Dialog吗?


这个问题其实跟上述问题相关:


  • 如果直接使用Application的上下文是不能创建Window的,而Dialog的Window等级属于子Window,必须依附与其他的父Window,所以必须传入Activity这种有window的上下文。
  • 那有没有其他办法可以在Application中弹出dialog呢?有,改成系统级Window:


//检查权限
if (!Settings.canDrawOverlays(this)) {
    val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
    intent.data = Uri.parse("package:$packageName")
    startActivityForResult(intent, 0)
}
dialog.window.setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG)
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>


  • 另外还有一种办法,在Application类中,可以通过registerActivityLifecycleCallbacks监听Activity生命周期,不过这种办法也是传入了Activity的context,只不过在Application类中完成这个工作。


关于事件分发,事件到底是先到DecorView还是先到Window的?


经过上述一系列问题,是不是对Window印象又深了点呢?最后再看一个问题,这个是wanandroid论坛上看到的(https://wanandroid.com/wenda/show/12119),


这里的window可以理解为PhoneWindow,其实这道题就是问事件分发在Activity、DecorView、PhoneWindow中的顺序。


当屏幕被触摸,首先会通过硬件产生触摸事件传入内核,然后走到FrameWork层(具体流程感兴趣的可以看看参考链接),最后经过一系列事件处理到达ViewRootImpl的processPointerEvent方法,接下来就是我们要分析的内容了:


//ViewRootImpl.java
 private int processPointerEvent(QueuedInputEvent q) {
            final MotionEvent event = (MotionEvent)q.mEvent;
            ...
            //mView分发Touch事件,mView就是DecorView
            boolean handled = mView.dispatchPointerEvent(event);
            ...
        }
//DecorView.java
    public final boolean dispatchPointerEvent(MotionEvent event) {
        if (event.isTouchEvent()) {
            //分发Touch事件
            return dispatchTouchEvent(event);
        } else {
            return dispatchGenericMotionEvent(event);
        }
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //cb其实就是对应的Activity
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }
//Activity.java
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
//PhoneWindow.java
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
//DecorView.java
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }


事件的分发流程就比较清楚了:


ViewRootImpl——>DecorView——>Activity——>PhoneWindow——>DecorView——>ViewGroup


(这其中就用到了getCallback参数,也就是之前addView中传入的callback,也就是Activity本身)


但是这个流程确实有些奇怪,为什么绕来绕去的呢,光DecorView就走了两遍。


参考链接中的说法我还是比较认同的,主要原因就是解耦。


  • ViewRootImpl并不知道有Activity这种东西存在,它只是持有了DecorView。所以先传给了DecorView,而DecorView知道有AC,所以传给了AC。
  • Activity也不知道有DecorView,它只是持有PhoneWindow,所以这么一段调用链就形成了。


怎么理解Binder?


在java层面,其实Binder就是一个实现了IBinder接口的类。


真正跨进程的部分还是在客户端发起远程调用请求之后,系统底层封装好,交给服务端的时候。而这个系统底层封装,其实就是发生在Linux内核中。


而在内核中完成这个通信关键功能的还是Binder,这次不是Binder类了,而是Binder驱动


驱动你可以理解为一种硬件接口,可以帮助操作系统来控制硬件设备。


Binder驱动被添加运行到Linux内核空间,这样,两个不同进程就可以通过访问内核

空间来完成数据交换:把数据传给Binder驱动,然后处理好再交给对方进程,完成跨进程通信。


而刚才通过AIDL的例子我们可以知道,客户端在请求服务端通信的时候,并不是直接和服务端的某个对象联系,而是用到了服务端的一个代理对象,通过对这个代理对象操作,然后代理类会把方法对应的code、传输的序列化数据、需要返回的序列化数据交给底层,也就是Binder驱动。


然后Binder驱动把对应的数据交给服务器端,等结果计算好之后,再由Binder驱动把数据返回给客户端。


最后借用《Android开发艺术探索》书中的内容总结下,希望大家回味回味。


直观的说,Binder是一个类,实现了IBinder接口。
从IPC(进程间通信)角度来说,Binder是Android中一种跨进程通信方式。
还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder。
从Android FrameWork角度来说,Binder是ServiceManager连接各种Manager(ActivityManager,WindowManager等等)和响应ManagerService的桥梁。
从Android应用层来说,Binder是客户端和服务端进行通信的媒介。


怎么理解ServiceManager


ServiceManager其实是为了管理系统服务而设置的一种机制,每个服务注册在ServiceManager中,由ServiceManager统一管理,我们可以通过服务名在ServiceManager中查询对应的服务代理,从而完成调用系统服务的功能。所以ServiceManager有点类似于DNS,可以把服务名称和具体的服务记录在案,供客户端来查找。


在我们这个AIDL的案例中,能直接获取到服务端的Service,也就直接能获取到服务端的代理类IMsgManager,所以就无需通过ServiceManager这一层来寻找服务了。

而且ServiceManager本身也运行在一个单独的线程,所以它本身也是一个服务端,客户端其实是先通过跨进程获取到ServiceManager的代理对象,然后通过ServiceManager代理对象再去找到对应的服务。


ServiceManager就像我们刚才AIDL中的Service一样,是可以直接找到的,他的句柄永远是0,是一个“众所周知”的句柄,所以每个APP程序都可以通过binder机制在自己的进程空间中创建一个ServiceManager代理对象。


所以通过ServiceManager查找系统服务并调用方法的过程是进行了两次跨进程通信。


APP进程——>ServiceManager进程——>系统服务进程(比如AactivityManagerService)


17.png


网络通信的过程,以及中间用了什么协议


这个问题我之前专门做了一个动画,大家可以翻到上一篇文章看看:

网络数据原来是这么传输的(结合动画解析)


再简单总结下:


客户端:


  • 1、在浏览器输入网址
  • 2、浏览器解析网址,并生成http请求消息
  • 3、浏览器调用系统解析器,发送消息到DNS服务器查询域名对应的ip
  • 4、拿到ip后,和请求消息一起交给操作系统协议栈的TCP模块
  • 5、将数据分成一个个数据包,并加上TCP报头形成TCP数据包
  • 6、TCP报头包括发送方端口号、接收方端口号、数据包的序号、ACK号
  • 7、然后将TCP消息交给IP模块。
  • 8、IP模块会添加IP头部MAC头部
  • 9、IP头部包括IP地址,为IP模块使用,MAC头部包括MAC地址,为数据链路层使用。
  • 10、IP模块会把整个消息包交给网络硬件,也就是数据链路层,比如以太网,WIFI等
  • 11、然后网卡会将这些包转换成电信号或者在光信号,通过网线或者光纤发送出去,再由路由器等转发设备送达接收方。


服务器端:


  • 1、数据包到达服务器的数据链路层,比如以太网,然后会将其转换为数据包(数字信号)交给IP模块
  • 2、IP模块会将MAC头部和IP头部后面的内容,也就是TCP数据包发送给TCP模块。
  • 3、TCP模块会解析TCP头信息,然后和客户端沟通表示收到这个数据包了。
  • 4、TCP模块在收到消息的所有数据包之后,就会封装好消息,生成相应报文发给应用层,也就是HTTP层。
  • 5、HTTP层收到消息,比如是HTML数据,就会解析这个HTML数据,最终绘制到浏览器页面上。


TCP连接过程,三次握手和四次挥手,为什么?


连接阶段(三次握手):


  • 创建套接字Socket,服务器会在启动的时候就创建好,客户端是在需要访问服务器的时候创建套接字
  • 然后发起连接操作,其实就是Socket的connect方法
  • 这时候客户端会生成一个TCP数据包。这个数据包的TCP头部有几个重要信息:SYN、ACK、Seq、Ack


SYN,同步序列编号,是TCP/IP建立连接时使用的握手信号,如果这个值为1就代表是连接消息。ACK,确认标志,如果这个值为1就代表是确认消息。Seq,数据包序号,是发送数据的一个顺序编号。Ack Number,确认数字号,是接收数据的一个顺序编号。


  • 所以客户端就生成了这样一个数据包,其中头部信息的控制位SYN设置为1,代表连接。SEQ设置一个随机数,代表初始序号,比如100。
  • 然后服务器端收到这个消息,知道了客户端是要来连接的(SYN=1),知道了传输数据的初始序号(SEQ=100)
  • 服务器端也要生成一个数据包发送给客户端,这个数据包的TCP头部会包含:表示我也要连接你的SYN(SYN=1),我已经收到了你的上个数据包的确认号ACK=1(Ack=Seq+1=101),以及服务器端随机生成的一个序号Seq(比如Seq=200)
  • 最后客户端收到这个消息后,表示客户端到服务器的连接是无误了,然后再发送一个数据包表示也确认收到了服务器发来的数据包,这个数据包的头部就主要就是一个ACK=1(Ack=Seq+1=201)
  • 至此,连接成功,三次握手结束,后面数据就会正常传输,并且每次都要带上TCP头部中的Seq和Ack


这里有个问题是关于为什么需要三次握手


最主要的原因就是需要通信双方都确认自己的消息被准确传达过去了。


A发送消息给B,B回一条消息表示我收到了,这个过程就保证了A的通信能力。B发送消息给A,A回一条消息表示我收到了,这个过程就保证了B的通信能力。


也就是四条消息能保证双方的消息发送都是正常的,其中B回消息和B发消息,可以融合为一次消息,所以就有了三次握手


16.png


数据传输阶段:


数据传输阶段有个改变就是Ack确认号不再是Seq+1了,而是Seq+数据长度。例如:


  • A发送给B的数据包(Seq=100,长度=1000字节)
  • B回给A的数据包(Ack=100+1000=1100)


这就是一次数据传输的头部信息,Ack代表下个数据包应该从哪个字节开始所以等于上个数据包的Seq+长度,Seq就等于上个数据包的Ack。


当然,TCP通信是双向的,所以实际数据每个消息都会有Seq和Ack


  • A发送给B的数据包(Ack=200,Seq=100,长度=1000字节)
  • B回给A的数据包(Ack=100+1000=1100,Seq=上一个数据包的Ack=200,长度=500字节)
  • A发送给B数据包(Seq=1100,Ack=200+500=700)


15.png


断开阶段(四次挥手):


和连接阶段一样,TCP头部也有一个专门用作关闭连接的值叫做FIN。


  • 客户端准备关闭连接,会发送一个TCP数据包,头部信息中包括(FIN=1代表要断开连接)
  • 服务器端收到消息,回复一个数据包给客户端,头部信息中包括Ack确认号。但是此时服务器端的正常业务可能没有完成,还要处理下数据,收个尾。
  • 客户端收到消息。
  • 服务器继续处理数据。
  • 服务器处理数据完毕,准备关闭连接,会发送一个TCP数据包给客户端,头部信息中包括(FIN=1代表要断开连接)
  • 客户端端收到消息,回复一个数据包给服务器端,头部信息中包括Ack确认号
  • 服务器收到消息,到此服务器端完成连接关闭工作。
  • 客户端经过一段时间(2MSL),自动进入关闭状态,到此客户端完成连接关闭工作。


MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。


这里有个问题是关于为什么需要四次挥手?


A发送断开消息给B,B回一条消息表示我收到了,这个过程就保证了A断开成功。B发送断开消息给A,A回一条消息表示我收到了,这个过程就保证了B断开成功。


其实和连接阶段的区别就在于,这里的B的确认消息和断开消息不能融合。因为A要断开的时候,B可能还有数据要处理要发送,所以要等正常业务处理完,在发送断开消息。


14.png


相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
目录
相关文章
|
5月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
180 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
|
5月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin中常见作用域函数
**Kotlin作用域函数概览**: `let`, `run`, `with`, `apply`, `also`. `let`安全调用并返回结果; `run`在上下文中执行代码并返回结果; `with`执行代码块,返回结果; `apply`配置对象后返回自身; `also`附加操作后返回自身
62 8
|
5月前
|
Android开发 开发者
Android经典面试题之SurfaceView和TextureView有什么区别?
分享了`SurfaceView`和`TextureView`在Android中的角色。`SurfaceView`适于视频/游戏,独立窗口低延迟,但变换受限;`TextureView`支持复杂变换,视图层级中渲染,适合动画/视频特效,但性能略低。两者在性能、变换、使用和层级上有差异,开发者需按需选择。
104 1
|
5月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
69 6
|
5月前
|
SQL Java Unix
Android经典面试题之Java中获取时间戳的方式有哪些?有什么区别?
在Java中获取时间戳有多种方式,包括`System.currentTimeMillis()`(毫秒级,适用于日志和计时)、`System.nanoTime()`(纳秒级,高精度计时)、`Instant.now().toEpochMilli()`(毫秒级,ISO-8601标准)和`Instant.now().getEpochSecond()`(秒级)。`Timestamp.valueOf(LocalDateTime.now()).getTime()`适用于数据库操作。选择方法取决于精度、用途和时间起点的需求。
74 3
|
5月前
|
XML Android开发 数据格式
Android面试题之DialogFragment中隐藏导航栏
在Android中展示全屏`DialogFragment`并隐藏状态栏和导航栏,可通过设置系统UI标志实现。 记得在布局文件中添加内容,并使用`show()`方法显示`DialogFragment`。
75 2
|
5月前
|
Android开发
Android面试题之View的invalidate方法和postInvalidate方法有什么区别
本文探讨了Android自定义View中`invalidate()`和`postInvalidate()`的区别。`invalidate()`在UI线程中刷新View,而`postInvalidate()`用于非UI线程,通过消息机制切换到UI线程执行`invalidate()`。源码分析显示,`postInvalidate()`最终调用`ViewRootImpl`的`dispatchInvalidateDelayed`,通过Handler发送消息到UI线程执行刷新。
64 1
|
5月前
|
消息中间件 调度 Android开发
Android经典面试题之View的post方法和Handler的post方法有什么区别?
本文对比了Android开发中`View.post`与`Handler.post`的使用。`View.post`将任务加入视图关联的消息队列,在视图布局后执行,适合视图操作。`Handler.post`更通用,可调度至特定Handler的线程,不仅限于视图任务。选择方法取决于具体需求和上下文。
63 0
|
5月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式有哪些用法
Kotlin的Lambda表达式是匿名函数的简洁形式,常用于集合操作和高阶函数。基本语法是`{参数 -&gt; 表达式}`。例如,`{a, b -&gt; a + b}`是一个加法lambda。它们可在`map`、`filter`等函数中使用,也可作为参数传递。单参数时可使用`it`关键字,如`list.map { it * 2 }`。类型推断简化了类型声明。
32 0
|
5月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
**Kotlin中的匿名函数与Lambda表达式概述:** 匿名函数(`fun`关键字,明确返回类型,支持非局部返回)适合复杂逻辑,而Lambda(简洁语法,类型推断)常用于内联操作和高阶函数参数。两者在语法、返回类型和使用场景上有所区别,但都提供无名函数的能力。
38 0