引子
2022 年 3 月辞职,没多久上海爆发疫情,蜗居在家准备面试。在经历 1 个月的闭关和 40+ 场 Android 面试后,拿到一些 offer。
总体上说,有如下几种面试题型:
- 基础知识
- 算法题
- 项目经历
- 场景题
场景题,即“就业务场景给出解决方案”,考察运用知识解决问题的能力。这类题取决于临场应变、长期积累、运气。
项目经历题取决于对工作内容的总结提炼、拔高升华、运气:
- 争取到什么样的资源
- 安排了怎么样的分工
- 搭建了什么样的架构
- 运用了什么模式
- 做了什么样的取舍
- 采用了什么策略
- 做了什么样的优化
- 解决了什么问题
力争把默默无闻的“拧螺丝”说成惊天动地的“造火箭”。(这是一门技术活)
但也不可避免地会发生“有些人觉得这是高大上的火箭,有些人觉得不过是矮小下的零件”。面试就好比相亲,甲之蜜糖乙之砒霜是常有的事。除非你优秀到解决了某个业界的难题。
算法题取决于刷题,运气,相较于前两类题,算法题可“突击”的成分就更多了。只要刷题足够多,胜算就足够大。大量刷,反复刷。
基础知识题是所有题型中最能“突击”的,它取决于对“考纲”的整理复习、归纳总结、背诵、运气。Android 的知识体系是庞杂的,对于有限的个人精力来说,考纲是无穷大的。
这不是一篇面经,把面试题公布是不讲武德的。但可以分享整个复习稿,它是我按照自己划定的考纲整理出的全部答案,由于篇幅太长,决定把全部内容分成两篇分享给大家,这一篇的内容是 Android 和 Java & Kotlin。(必有遗漏,欢迎补充~)
整个复习稿分为如下几大部分:
- Android
- Java & Kotlin
- 设计模式 & 架构
- 多线程
- 网络
- OkHttp & Retrofit
- Glide
Android
绘制流程
- 画多大?(测量measure)
- 画在哪?(定位layout)
- 画什么?(绘制draw)
- 测量、定位、绘制都是从View树的根结点开始自顶向下进行地,即都是由父控件驱动子控件进行地。父控件的测量在子控件件测量之后,但父控件的定位和绘制都在子控件之前。
- 父控件测量过程中ViewGroup.onMeasure(),会遍历所有子控件并驱动它们测量自己View.measure()。父控件还会将父控件的布局模式与子控件布局参数相结合形成一个MeasureSpec对象传递给子控件以指导其测量自己(3*3的表格,如果孩子是wrapcontent,根据measureChildWithMargin,则孩子的模式是AtMost)。View.setMeasuredDimension()是测量过程的终点,它表示View大小有了确定值。
- 第一次加载onMeasure()至少调用两次,最多调用5次(都是有ViewRootImpl调用performMeasure()),会进行多次测量尝试,总是希望以更小的窗口大小进行绘制,如果不行则扩大
- 通过 MEASURED_DIMENSION_SET 强制指定需要为 measuredDimension赋值,否则抛异常
- 父控件在完成自己定位之后,会调用ViewGroup.onLayout()遍历所有子控件并驱动它们定位自己View.layout()。子控件总是相对于父控件左上角定位。View.setFrame()是定位过程的终点,它表示视图矩形区域以及相对于父控件的位置已经确定。
- 控件按照绘制背景,绘制自身,绘制孩子的顺序进行。重写onDraw()定义绘制自身的逻辑,父控件在完成绘制自身之后,会调用ViewGroup.dispatchDraw()遍历所有子控件并驱动他们绘制自己View.draw()
- 为什么只能在主线程绘制界面:因为重绘指令会有子视图一层层传递给父视图,最终传递到ViewRootImpl,它在每次触发View树遍历时都会调用ViewRootImpl.checkThread()
MeasureSpec
MeasureSpec用于在View测量过程中描述尺寸,它是一个包含了布局模式和布局尺寸的int值(32位),其中最高的2位代表布局模式,后30位代表布局尺寸。它包含三种布局模式分别是
- UNSPECIFIED:父亲没有规定你多大
- EXACTLY:父亲给你设定了一个死尺寸
- AT_MOST:父亲规定了你的最大尺寸
同步消息屏障
- ViewRootImpl 将遍历view树包装成一个Runnable并抛到Choreographer, 在抛之前会向主线程消息队列中抛同步屏障
- 同步屏障也是一个Message,只不过 target 等于null
- 取下一条message的算法中,若遇到同步屏障,则会越过同步消息,向后遍历找第一条异步消息找到则返回(Choreographer抛的异步消息),若没有找到则会执行epoll挂起
- 当执行到遍历View树的 runnable时,ViewRootImpl会移除同步屏障
Choreographer
- 将和ui相关的任务与vsync同步的一个类。
- 每个任务被抽象成CallbackRecord,同类任务按时间先后顺序组成一条任务链CallbackQueue。四条任务链存放在mCallbackQueues[]数组结构中
- 触摸事件,动画,View树遍历都会被抛到编舞者,并被包装成CallbackRecord并存入链式数组结构,当Choreographer收到一个Vsync就会依次从输入,动画,绘制这些链中取出任务执行
- 当vsync到来时,会向主线程抛异步消息(执行doFrame)并带上消息生成时间,当异步消息被执行时,从任务链上摘取所有以前的任务,并按时间先后顺序逐个执行。
关于 Choreographer 的详细分析可以点击读源码长知识 | Android卡顿真的是因为”掉帧“?
绘制模型
- 1.软件绘制 2.硬件加速绘制
- 软件绘制就是调用ViewRootImpl持有的 surface.lockCanvas(),获取和SurfaceFlinger共享的匿名内存,并往里面填充像素数据。这些操作发生在主线程。
- 硬件加速和软件绘制的分歧点是在ViewRootImpl中,如果开启了硬件加速则调用mHardwareRenderer.draw,否则drawSoftware。
- 硬件加速绘制分为两个步骤
- 在主线程构建DrawOp树:从根视图触发,自顶向下地为每一个视图构建DrawOp。(使用DisplayListCanvas缓存DrawOp,最终存储到RenderNode)
- 向RenderThread发一个DrawFrameTask,唤醒它进行渲染
- 进行DrawOp合并
- 调用gpu命令进行绘制,gpu向匿名共享内存写内容
- 最终将填充好的raw Buffer提交给SurfaceFlinger合成显示。
- RenderThread 是单例,每个进程只有一个。
- cpu 不擅长图形计算,硬件加速即是把这些计算交由 GPU 实现(图形计算转成 gpu指令),由此加入了RenderNode(相当于View),和 DisplayList(相当于view的绘制内容)。当重绘请求发起时,只更新 displyList
Android 应用冷启动流程
冷启动就是在 Launcher 进程中开启另一个引用 Activity 的过程。这是一个 Launcher 进程和 AMS,应用进程和 AMS,WMS 双向通信的过程:
- Launcher 进程和 AMS 说:“我要启动Activity1”
- AMS创建出 Activity1 对应的 ActivityRecord 以及 TaskRecord,通知 Launcher 进程执行 onPause()
- Launcher 执行 onPause(),并告知 AMS
- 启动一个 starting window,AMS 请求 zygote 进程 fork一个新进程
- 在新进程中,构建ActivityThread,并调用main(),在其中开启主线程消息循环。
- AMS 开始回调Activity1的各种生命周期方法。
- 当执行到 Activity.onAttch()时,PhoneWindow 被构建。
- 当执行到 Activity.onCreate()时,setContentView()会被委托给 PhoneWindow,并在其中构建DecorView,再根据主题解析系统预定义文件,作为 DecorView 的孩子,布局文件中肯定有一个 id 为 content 的容器控件,他将成为 setContentView 的父亲。
- 当执行到 Activity.onResume()时,DecorView 先被设置为 invisible,然后将其添加到窗口,此过程中会构建 ViewRootImpl 对象,它是 app 进行和 WMS 双向通信的纽带。ViewRootImpl.requestLayout()会被调用,以触发View树自顶向下的绘制。
- View 树遍历,会被包装成一个任务抛给 Choreographer。在此之前 ViewRootImpl 会向主线程消息队列抛一个同步消息屏障。以达到优先遍历异步消息的效果。
- Choreographer 将任务暂存在链式数组结构中,然后注册监听下一个 vsync 信号。
- 待下一个 vsync 信号到来之时,Choreographer 会从链上摘取所有比当前时间更早的任务,并将他们包装成一个异步消息抛到主线程执行。
- 异步消息的执行,即是从顶层视图开始,自顶向下,逐个视图进行 measure,layout,draw的过程。
- ViewRootImpl 持有一个 surface,它是原始图形缓冲区的一个句柄,原始图形缓冲区是一块存放像素数据的内存地址,这块内存地址由app进程和SurfaceFlinger共享。当 app进程执行完上述步骤时,就意味着像素数据已经填入该块内存,于是 app 通知 SurfaceFlinger 像素数据已经就绪,可以进行合成并渲染到屏幕了。
- 当 DecorView 完成渲染后,就会被设置为 visible,界面展示出来。
Surface
- 它是原始图像缓冲区的一个句柄。即raw buffer的内存地址,raw buffer是保存像素数据的内存区域,通过Surface的canvas 可以将图像数据写入这个缓冲区
- Surface类是使用一种称为双缓冲的技术来渲染
- 这种双缓冲技术需要两个图形缓冲区GraphicBuffer,其中一个称为前端缓冲区frontBuffer,另外一个称为后端缓冲区backBuffer。前端缓冲区是正在渲染的图形缓冲区,而后端缓冲区是接下来要渲染的图形缓冲区,当vsync到来时,交换前后缓冲区的指针
- 部分刷新是通过前端缓冲区拷贝像素到后端缓冲区,并且合并脏区以缩小它。
- 每个ViewRootImpl都持有一个Surface。
SurfaceFlinger
- SurfaceFlinger 是由 init 进程启动的运行在底层的一个系统进程,它的主要职责是合成和渲染多个Surface,并向目标进程发送垂直同步信号 VSync,并在 vsync 产生时合成帧到frame buffer
- SurfaceFlinger持有BufferQueue消费者指针,用于从BufferQueue中取出图形数据进行合成后送到显示器
- View.draw()绘制的数据是如何流入SurfaceFlinger进行合成的?
- Surface.lockCanvas()从BufferQueue中取出图形缓冲区并锁定
- View.draw()将内容绘制到Canvas中的Bitmap,就是往图形缓冲区填充数据
- Surface.unlockCanvasAndPost()解锁缓冲区并将其入队BufferQueue,然后通知SurfaceFlinger进行合成,在下一个vsync到来时进行合成(app直接喝surfaceFlinger通信)
- 应用进程通过Anonymous Shared Memory将数据传输给SurfaceFlinger,因为Binder通信数据量太小
Surfaceview
- 一个嵌入View树的独立绘制表面,他位于宿主Window的下方,通过在宿主canvas上绘制透明区域来显示自己
- 虽然它在界面上隶属于view hierarchy,但在WMS及SurfaceFlinger中都是和宿主窗口分离的,它拥有独立的绘制表面,绘制表面在app进程中表现为Surface对象,在系统进程中表现为在WMS中拥有独立的WindowState,在SurfaceFlinger中拥有独立的Layer,而普通view和其窗口拥有同一个绘制表面
- 因为它拥有独立与宿主窗口的绘制表面,所以它独立于主线程的刷新机制。
- 它的背景是属于宿主窗口的绘制表面,所以如果背景不透明则会盖住它的绘制内容
TextureView
- SurfaceView 拥有独立的绘制表面,而TextureView和View树共享绘制表面
- TextureView通过观察BufferQueue中新的SurfaceTexture到来,然后调用invalidate触发View树重绘,如果有View叠加在TextureView上面,它们的脏区有交集,则会触发不必要的重绘,所以他的刷新操作比SurfaceView更重
- TextureView 持有 SurfaceTexture,它是一个GPU纹理
- SurfaceView 有双缓冲机制,绘制更加流畅
- TextureView 在5.0之前在主线程绘制,5.0之后在RenderThread绘制。
界面卡顿
- 刷新率是屏幕每秒钟刷新次数,即每秒钟去buffer中拿帧数据的次数, 帧率是GPU每秒准备帧的速度,即gpu每秒向buffer写数据的速度,
- 卡顿是因为掉帧,掉帧是因为写buffer的速度慢于取buffer的速度。对于60HZ的屏幕,每隔16.6ms显示设备就会去buffer中取下一帧的内容,没有取到就掉帧了
- 当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次的循环,此时有一段时间空隙,称为VerticalBlanking Interval(VBI), Vsync就是 VBI 发生时产生的垂直脉冲,这是最好的交换双缓冲的时间点,交换双缓冲是交换内存地址,瞬间就完成了
- 一帧的显示要经历如下步骤:cpu计算画多大,画在哪里,画什么,然后gpu渲染计算结果存到buffer,显示器每隔一段时间从buffer取帧。若没有取到帧,只能继续显示上一帧。
- VSYNC=Vertical Synchronization垂直同步,它就是为了保证CPU、GPU生成帧的速度和display刷新的速度保持一致
- VSYNC信号到来意味着,交换双缓冲的内存地址,font buffer 和 back buffer 互换,这个瞬间 back font就供下一帧使用, project butter(4.1)以后, Vsync一到就 GPU和cpu就开始渲染下一帧的数据
- 双缓冲机制:用两个buffer存储帧内容,屏幕始终去font buffer取显示内容,GPU始终向back buffer存放准备好的下一帧,每个Vsync发生吃就是他们交换之时,若下一帧没有准备好,则就错过一次交换,发生掉帧
- 三缓冲机制:为了防止耗时的绘制任务浪费一个vsync周期,用三个buffer存储帧。当前显示buffer a中内容,正在渲染的下一帧存放在buffer b中,当VSYNC触发时,buffer b中内容还没有渲染好,此时buffer a不能清除,因为下一帧需要继续显示buffer a,如果没有第三个buffer,cpu和gpu要白白等待到下一个VSYNC才有可能可以继续渲染后序帧
- View的重绘请求最少要等待两个vsync 才能显示到屏幕上:重绘请求首先会包装成一个runnable,存放在Choreographer中,下一个Vsync到来之时,就是它执行之时,执行完毕,形成的帧数据会放在back buffer中,等到下一个Vsync到来时才会显示到屏幕上
requestLayout() vs invalidate()
- 其实两个函数都会自底向上传递到顶层视图ViewRootImpl中
- requestLayout()会添加两个标记位PFLAG_FORCE_LAYOUT,PFLAG_INVALIDATED,而invalidate()只会添加PFLAG_INVALIDATED(所以不会测量和布局)
- invalidate(),会检查主线程消息队列中是否已经有遍历view树任务,通过ViewRootImpl.mWillDrawSoon是否为true,若有则不再抛
- invalidate表示当前控件需要重绘,会标记PFLAG_INVALIDATED,重绘请求会逐级上传到根视图(但只有这个view会被重绘,因为其他的父类没有PFLAG_INVALIDATED,并且携带脏区域.初始脏区是发起view的上下左右,逐级向上传递后每次都会换到父亲的坐标系(平移 left,top)。
- View.measure()和View.layout()会先检查是否有PFLAG_FORCE_LAYOUT标记,如果有则进行测量和定位
- View.requestLayout()中设置了PFLAG_FORCE_LAYOUT标记,所以测量,布局,有可能触发onDraw是因为在在layout过程中发现上下左右和之前不一样,那就会触发一次invalidate,所以触发了onDraw。
- postInvalidate 向主线程发送了一个INVALIDATE的消息
Binder
- Linux内存空间 = 内核空间(操作系统+驱动)+用户空间(应用程序)。为了保证内核安全,它们是隔离的。内核空间可访问所有内存空间,而用户空间不能访问内核空间。
- 用户程序只能通过系统调用陷入内核态,从而访问内核空间。系统调用主要通过 copy_to_user() 和 copy_from_user() 实现,copy_to_user() 用于将数据从内核空间拷贝到用户空间,copy_from_user() 将数据从用户空间拷贝到内核空间。
- Android 将 Binder driver 挂载为动态内存(LKM:Loadable Kernel Module),通过它以 mmap 方式将内核空间与接收方用户空间进行内存映射(用户空间一块虚拟内存地址和内核空间虚拟内存地址指向同一块物理地址),这样就只需发送方将数据拷贝到内核就等价于拷贝到了接收方的用户空间。
- Binder 通信优点:1. 安全性好:为发送方添加UID/PID身份信息。2. 性能更佳:传输过程只要一次数据拷贝,而Socket、管道等传统IPC手段都至少需要两次数据拷贝。
- 通过Binder实现的跨进程通信是c/s模式的,客户端通过远程服务的本地代理像服务端发请求,服务端处理请求后将结果返回给客户端
- binder通信的大小限制是1mb-8kb(Binder transaction buffer),这是mmap内存映射的大小限制(单个进程),其中异步传输oneway传输限制是同步的一半(1mb-8kb)/2,内核允许Binder传输的最大限制是4M(mmap的最大空间4mb)
- 在生成的stub中有一个asInterface():它用于将服务端的IBinder对象转换成服务接口,这种转化是区分进程的,如果客户端和服务端处于同一个进程中,此方法返回的是服务端Stub对象本身,否则新建一个远程服务的本地代理
- aidl 中的 oneway 表示异步调用,发起RPC之前不会构建parcel reply
- aidl 中的 in 表示客户端向服务端传送值并不关心值的变化,out表示服务端向客户端返回值
进程通信方式(IPC)
- Messenger:不支持RPC(Remote Procedure Call) ,低并发串行通信,并发高可能等待时间长。
- 它是除了aidl之外的创建Remote bound service的方法
- 它可以实现客户端和服务器的双向串行通信(来信和回信)
- 服务器和客户端本质上是通过拿到对方的Handler像对方发送消息。但Handler不能跨进程传递,所以在外面包了一层Messenger,它继承与IBinder。服务端和客户端分别将定义在自己进程中的Messenger传递给对方,通过Messenger相互发送消息,其实是Messenger中的Handler发送的,服务端将自己的Messenger通过onServiceConnected()返回,客户端通过将Messenger放在Message.replyTo字段发送给服务器
- AIDL:支持RPC 一对多并发通信
- 存在多线程问题:跨进程通信是不同进程之间线程的通信,如果有多个客户端发起请求,则服务端binder线程池就会有多个线程响应。即服务端接口存在多线程并发安全问题。
- RemoteCallbackList用于管理跨进程回调:其内部有一个map结果保存回调,键是IBinder对象,值是回调,服务端持有的接口必须是RemoteCallbackList类型的
- 远程服务运行在binder线程池,客户端发出请求后被挂起,如果服务耗时长,客户端可能会产生anr,所以需要新启线程请求服务
- AIDL 进程通信流程:
- 1.通过IInterface定义服务后会自动生成stub和proxy
- 2.服务器通过实现stub来实现服务逻辑并将其以IBinder形式返回给客户端
- 3.客户端拿到IBinder后生成一个本地代理对象(通过asInterface()),通过代理对象请求服务,代理会构建两个Parcel对象,一个data用来传递参数,另一个reply用来存储服务端处理的结果,并调用BinderProxy的transact()发起RPC(Remote Procedure Call),同时将当前进程挂起。所以如果远程调用很费时,不能在UI线程中请求服务
- 4.这个请求通过Binder驱动传递到远程的Stub.onTransact()调用了服务真正的实现
- 5.返回结果:将返回值写入reply parcel并返回
- 文件:通过读写同一个文件实现数据传递。(复杂对象需要序列化),并发读可能发生数据不是最新的情况,并发写可以破坏数据,适用于对同步要求低的场景
- ContentProvider:一对多进程的数据共享,支持增删改查
- Bundle:仅限于跨进程的四大组件间传递数据,且只能传递Bundle支持的数据类型
Bundle
- 使用ArrayMap存储结构,省内存,查询速度稍慢,因为是二分查找,适用于小数据量
- 使用Parcelable接口实现序列化,而hashmap使用serializable
Parcel
- 将各种类型的数据或对象的引用进行序列化和反序列化,经过mmap直接写入内核空间。另一个进程可以直接读取这个内核空间(因为做了mmap,不需要另一次copy_to_user())
- 使用复用池(是一个Parcel数组),获取Parcel对象
- parcel 存取数据顺序需要保持一致,因为parcel在一块连续的内存地址,通过首地址+偏移量实现存取
持久化方式
- SharedPreference
- 以xml文件形式存储在磁盘
- 读写速度慢,需要解析xml文件
- 文明存储,安全性差
- 是线程安全的,但不是进程安全的
- 调用写操作后会先写入内存中的map结构,调用commit或者apply才会执行写文件操作
- 可能造成 anr:
- Activity或者service onstop的时候,sp会等待写任务结束,如果任务迟迟没有结束则会造成anr
- getSharedPreference会开启线程读文件,调用getString会调用wait()等待读文件结束,如果文件大,则会造成主线程阻塞。
- sp一共有三个锁:写文件锁,读内存map的锁,写editor中map的锁
- SQLite
- 文件
- DataStore
- 基于Flow,提供了挂起方法而不是阻塞方法。
- 基于事物更新数据
- 支持protocol buffer
- 不支持部分刷新
Service
- 分类
- started service
- 生命周期和启动他的组件无关,必须显示调用stopservice()才能停止
- bound service
- 生命周期和启动他的组件绑定,组件都销毁了 他也销毁
- Local Bound Service :为自己应用程序的组件提供服务
- Remote Bound Service:为其他应用的组件提供服务
- 1.aidl
- 2.Messenger
- bound service如何和绑定组件生命周期联动:在绑定的时候会将serviceConnection保存在LoadedApk的ArrayMap结构中,当Activity finish的时候,会遍历这个结构逐个解绑
- service默认是后台进程
- Service和Activity通信
- 通过 Intent 传入startService
- 发送广播,或者本地广播
- bound service,通过方法调用通信
IntentService
- 他是Service和消息机制的结合,它适用于后台串行处理一连串任务,任务执行完毕后会自销毁。
- 它启动时会创建HandlerThread和Handler,并将HandlerThread的Looper和Handler绑定,每次调用startService()时,通过handler发送消息到新线程执行
- 但是它没有将处理结果返回到主线程,需要自己实现(可以通过本地广播)
广播
- 是一种用观察者模式实现的异步通信
- 有静态注册和动态注册两种方式,静态注册生命周期比动态注册长(在应用安装后就处于监听状态)
- 静态注册广播接收器时只要定义了intent-filter 则android:exported属性为true 表示该广播接收器是跨进程的
- 静态注册广播容易被攻击:其他App可能会针对性的发出与当前App intent-filter相匹配的广播,由此导致当前App不断接收到广播并处理;
- 静态注册广播容易被劫持:其他App可以注册与当前App一致的intent-filter用于接收广播,获取广播具体信息。
- 通过manifest注册的广播是静态广播
- 静态广播是常驻广播,常驻广播在应用退出后依然可以收到
- 动态注册的不是常驻广播,它的生命周期痛注册组件一致
- 动态注册要等到启动他的组件启动时时才注册
- 动态广播优先级比静态高
- 广播接收者BroadcastReceiver通过Binder机制向AMS进行注册,广播发送者通过binder机制向AMS发送广播,广播的Intent和Receiver会被包装在BroadcastRecord中,多个BroadcastRecord组成队列
- onReceive()回调执行时间超过10s 会发生anr,如果有耗时操作需要使用IntentService处理,不建议新建线程
- 分为有序广播和无序广播
- 无序广播:所有广播接收器接受广播的先后顺序不确定,广播接收者无法阻止广播发送给其他接收者。
- 有序广播:广播接收器收到广播的顺序按预先定义的优先级从高到低排列 接收完了如果没有丢弃,就下传给下一个次高优先级别的广播接收器进行处理,依次类推,直到最后
- 自定义Intent并通过sendOrderBroadcast()发送
- 可以通过在intent-filter中设置android:priority属性来设置receiver的优先级,优先级相同的receiver其执行顺序不确定,如果BroadcastReceiver是代码中注册的话,且其intent-filter拥有相同android:priority属性的话,先注册的将先收到广播
- 使用setResult系列函数来结果传给下一个BroadcastReceiver
- getResult系列函数来取得上个BroadcastReceiver返回的结果
- abort系列函数来让系统丢弃该广播,使用该广播不再传送到别的BroadcastReceiver
- 还可以分为本地广播和跨进程广播
- 本地广播仅限于在应用内发送广播,向LocalBroadCastManager注册的接收器都存放在本地内存中,跨进程广播都注册到system_server进程
消息机制
- Android 主线程存在一个无限循环,该循环在不停地从消息队列中取消息。消息是按时间先后顺序插入到链表结构的消息队列中,最旧的消息在队头,最新的消息在队尾。
- Looper通过无限循环从消息队列中取消息并分发给其对应的Handler,并回收消息
- Handler 是用于串行通信的工具类。
- 消息池:链式结构,静态,所有消息共用,取消息时从头部取,消息分发完毕后头部插入
- Android消息机制共有三种消息处理方式,它们是互斥的,优先级从高到低分别是1. Runnable.run() 2. Handler.callback 3. 重载Handler.handleMessage()
- 若消息队列中消息分发完毕,则调用natviePollOnce()阻塞当前线程并释放cpu资源,当有新消息插入时或者超时时间到时线程被唤醒
- idleHandler 是在消息队列空闲时会被执行的逻辑,每拿取一次消息有且仅有一次机会执行.通过queueIdle()返回true表示每次取消息时都会执行,否则执行一次就会被移出
- 同步消息屏障是一种特殊的同步消息,他的target为null, 在 MessageQueue.next()中遇到该消息,则会遍历消息队列优先执行所有异步消息,若遍历到队列尾部还是没有异步消息,则阻塞会调用epoll,直到异步消息到来或者同步屏障被移出
- 使用epoll实现消息队列的阻塞和唤醒,Message.next()是个无限循环,若当前无消息可处理会阻塞在nativePollOnce(),若有延迟消息,则设置超时,没有消息时主线程休眠不会占用cpu
- epoll是一个IO事件通知机制 监听多个文件描述符上的事件 epoll 通过使用红黑树搜索被监控的文件描述符(内核维护的文件打开表的索引值)
- epoll最终是在epoll_wait上阻塞
- nativeWake() 唤醒是通过往管道中写入数据,epoll监听写入事件,epoll_wait()就返回了
触摸事件
- 触摸事件的传递是从根视图自顶向下“递”的过程,触摸事件的消费是自下而上“归”的过程。
- 触摸事件由ViewRootImpl通过ViewRootHandler接收到,然后存取一个链式队列,再逐个分发给Activity接收到触摸事件后,会传递给PhoneWindow,再传递给DecorView,由DecorView调用ViewGroup.dispatchTouchEvent()自顶向下分发ACTION_DOWN触摸事件。
- ACTION_DOWN事件通过ViewGroup.dispatchTouchEvent()从DecorView经过若干个ViewGroup层层传递下去,最终到达View。View.dispatchTouchEvent()被调用。
- View.dispatchTouchEvent()是传递事件的终点,消费事件的起点。它会调用onTouchEvent()或OnTouchListener.onTouch()来消费事件。
- 每个层次都可以通过在onTouchEvent()或OnTouchListener.onTouch()返回true,来告诉自己的父控件触摸事件被消费。只有当下层控件不消费触摸事件时,其父控件才有机会自己消费。
- ACTION_MOVE和ACTION_UP会沿着刚才ACTION_DOWN的传递路径,传递给消费了ACTION_DOWN的控件,如果该控件没有声明消费这些后序事件,则它们也像ACTION_DOWN一样会向上回溯让其父控件消费。
- 父控件可以通过在onInterceptTouchEvent()返回true来拦截事件向其孩子传递。如果在孩子已经消费了ACTION_DOWN事情后才进行拦截,父控件会发送ACTION_CANCEL给孩子。
滑动冲突
父子都可以消费滑动事件时会发生滑动冲突:
- 父控件主动:父控件只拦截自己滑动方向上的事件(通过在onInterceptTouchEvent中返回true实现),其余事件不拦截继续传递给子控件
- 子控件主动:子控件要求父控件进行拦截或者不拦截(通过getParent().requestDisallowInterceptTouchEvent(true)实现)
ArrayMap & HashMap
- 存储结构:arrayMap用一个数组存储key的哈希值,用另一个数组存储key和value(挨着i和i+1),而HashMap用一个Entry结构包裹key,value,所以HashMap更加占用空间。
- 访问方式:arrayMap通过二分查找key数组,时间复杂度是o(log2n),HashMap通过散列定位方式,时间复杂度是o(n),
- ArrayMap 删除键值对时候会进行数组平移以压缩数组
- ArrayMap 插入键值对时可能发生数组整体平移以腾出插入位置
SparseArray & HashMap
- SparseArray用于存放键值对,键是int,值是Object。
- SparseArray用两个长度相等的数组分别存储键和值,同一个键值对所在两个数组中的索引相等。
- SparseArray比HashMap访问速度更慢,因为二分查找速度慢于散列定位。
- SparseArray比HashMap更节省空间,因为不需要创建额外的Entry存放键值对。
- SparseArray中存放键的数组是递增序列。
- SparseArray删除元素时并不会真正删除,而是标记为待删除元素,在合适的时候会将后面的元素往前挪覆盖掉待删除元素。待删除元素在没有被覆盖前有可能被复用。