InputManagerService: 一个 Android 系统服务,分为 Java 层和 Native 层两部分,Java 层负责与 WMS 通信,而 Native 层则是 InputReader 和 InputDispatcher 两个输入系统关键组件的运行容器;
EventHub: 直接访问所有的设备节点,通过一个名为 getEvents() 的函数将所有输入系统相关的待处理的底层事件返回给使用者,包括原始输入事件,设备节点的增删等;
InputReader: IMS 中的关键组件之一,它运行在一个独立的线程中,负责管理输入设备的列表和配置,以及进行输入事件的加工处理,它通过其线程循环不断地通过 getEvents() 函数从 EventHub 中将事件取出并进行处理,对于设备节点的增删事件,它会更新输入设备列表与配置;对于原始输入事件,InputReader对其进行翻译,组装,封装为包含更多信息,更多可读性的输入事件,然后交给InputDispatcher进行派发;
InputReaderPolicy: 为 InputReader 的事件加工处理提供一些策略配置
InputDispatcher: 是 IMS 中的另一个关键组件,运行于一个独立的线程中,InputDispatcher 中保管来自 WMS 的所有窗口的信息,收到 InputReader 的输入事件后,会在其保管的窗口中寻找合适的窗口,并将事件派发给此窗口;
InputDispatcherPolicy: 为 InputDispatcher 的派发过程提供策略控制,例如 HOME 键被 InputDispatcherPolicy 截取到 PhoneWindowManager 中处理,并阻止窗口收到 HOME 键按下的事件;
WindowManagerService: 它并不是输入系统的一员,新建窗口时,WMS 为新窗口和 IMS 创建了事件传递所用的通道,会将窗口的可点击区域,焦点窗口等信息实时更新到 IMS 的 InputDispatcher 中,使得 InputDispatcher 可以正确将事件派发到指定窗口;
ViewRootImpl: 对某些窗口,如壁纸窗口,SurfaceView 的窗口来说,窗口就是输入事件派发的终点,而对其他的如Activity,对话框等使用了 Android 控件系统的窗口来说,输入事件的终点是控件;
1. 创建新的IMS对象
SystemServer->ServerThread.run()
—>com_android_server_input_InputManagerService.cpp —>nativeInit()
创建了 NativeInputManager 对象,该对象实现了 InputReaderPolicyInterface 与 InputDispatcherPolicyInterface 接口,创建 EventHub 和 InputManager,InputManager 创建了 InputReader 与 InputDispatcher 以及 InputReaderThread 与 InputDispatcherThread
2.调用IMS对象的start函数完成启动
InputReader.cpp —>InputReaderThread::threadLoop()
分三步:
1.首先从 EventHub 抽取未处理的时间列表,一类是从设备节点读取的原始输入事件,另一类则是输入设备可用性变化事件,简称设备事件;
2.processEventsLocked 对事件进行处理,对于设备事件,此函数对根据设备的可用性加载或移除设备对应的配置信息,对原始输入事件,则在进行转译,封装与加工后将结果暂存到 mQueuedListener;
3.所有事件处理完毕后,调用 mQueuedListener.flush 将所有暂存的输入事件一次性地交给InputDispatcher。
1.设备节点监听的建立
通过 INotify 与 Epoll 机制建立起对设备节点增删事件以及可读状态的监听
EventHub.cpp:EventHub
2.getEvents
使用 Epoll 的核心是 mPendingEventItems 数组,它是一个事件池,getEvents 包含了原始输入事件读取,输入设备加载/卸载等 *** 作。
3.输入设备管理
每个输入设备在 dev/input/ 下有一个设备节点,设备节点包含输入设备的所有信息,EventHub 负责在设备节点可用时加载并维护这些信息,并在设备节点被删除时将其移除,名为 Device 的私有结构体保存。
4.原始输入事件的监听与读取
当设备的原始输入事件到来之时,getevents 函数将会获得一个 Epoll 事件,然后根据 Epoll 事件读取文件描述符的原始输入事件,将其填充到 RawEvents 结构体并放入 buffer 中被调用者取走。
1.原始输入事件的加工
InputReader:processEventsLocked
—>inputReader:processEventsForDeviceLocked
—> InputReader:InputDevice::process
InputDevice 描述一个输入设备,是一个存储输入设备信息的类
InputMapper 是 InputReader 中实际进行原始输入事件加工的场所
2.InputDevice 与 InputMapper
inputDevice 创建 InputReader:addDeviceLocked
—> InputReader:createDeviceLocked
3.keyboard 类型事件的加工处理
(1)keyboardInputMapper 配置(屏幕旋转状态)
(2)键盘扫描码与虚拟键值
扫描码是硬件实现,虚拟键值是 *** 作系统实现
(3)扫描码到虚拟键值的映射
InputReader —>KeyboardInputMapper:process
(4)按键事件的加工处理
InputReader —>keyboardInputMapper:processKey
4.Touch类型事件的加工处理
(1)Touch 类型事件的信息与原始事件的组织方式
(2)TouchInputMapper 的体系
(3)MultiTouchInputMapper 的配置
MultiTouchInputMapper 的 configureRawPointerAxes 获取来自设备节点的各项触控信息,同时构建传感器的物理坐标系
TouchInputMapper 的 configureSurface 获取来自 DisplayViewPort 的屏幕方向以及屏幕坐标系的信息,并计算物理坐标系到屏幕坐标系的差异信息
(4)点击事件的信息收集
InputReader->MultiTouchInputMapper:process
->MultiTouchMotionAccumulator:process
(5)点击事件信息的整合,变换与高级事件的生成
1.将事件注入派发队列
InputDispatcher 实现了 InputListenerInterface,并在 InputReader 循环的最后,QueuedInputListener 调用此接口将InputReader 产生的事件以及 NotifyXXXArgs 结构体的形式提交给 InputDispatcher
—> InputDispatcher:notifyMotionLocked
—> InputDispatcher:enqueueInboundEventLocked
2.派发线程的线程循环
InputDispatcher:dispatchOnce
派发线程的一次循环包括以下三项:
进行一次事件派发,事件的派发工作仅当命令队列中没有命令时才会进行,派发工作会设置nextWakeupTime指明随后休眠时间长短
执行命令列表中的命令
陷入休眠状态
3.派发工作的整体流程
dispatchOnceInnerLocked函数体现派发过程
InputDispatcher:dispatchOnceInnerLocked
4.事件被丢弃的原因
.....
5.Motion事件目标窗口的确定
InputDispatcher:dispatchMotionLocked
三项工作:
对于被丢弃的事件,返回 true
为事件寻找合适的窗口,窗口分为普通窗口和监听窗口,普通通过按点和焦点查找,监听窗口则无条件监听所有输入事件
如果成功地找到可以接收事件的目标窗口,通过 dispatchEventLocked 完成实际的派发工作
6.向窗口发送事件
InputDispatcher:dispatchEventLocked
1.将事件注入派发队列
2.额外的派发策略查询
InputDispatcher:dispatchOnceInnerLocked
3.重复按键事件
InputDispatcher:dispatchOnceInnerLocked
—>InputDispatcher:dispatchKeyLocked 开启与关闭重复按键模拟
—> InputDispatcher:synthesizeKeyRepeatLocked 重复按键的生成
4.按键事件派发总结
按键事件通过 notifyKey 函数进入 InputDispatcher,在注入派发队列前,使用 DispatcherPolicy 的interceptKeyBeforeQueueing 函数询问后续的派发策略 policyFlag
按键事件在正式派发给窗口前,进行一次额外的派发策略查询,查询的结果保存在 keyEntry:interceptKeyResult,结果觉得事件是正常派发,稍后派发还是丢弃
当按键按下到按键抬起之间的时间里,dispatchOnceInnerLocked 和 dispatchKeyLocked 会协同工作完成对重复按键事件的模拟
按键事件的派发目标仅通过焦点方式进行查找
InputDispatcher 运行于 system_server 进程,窗口运行于其它的应用进程中
InputChannel 的本质是一对 SocketPair,SocketPair 用来实现在本机内进行进程间的通信
InputTransport —>InputChannel:openInputChannelPair
WinodwManagerService:addwindow
WMS 添加窗口时,会创建一对 InputChannel,其中一个保存在 WindowState 中,并注册给 IMS,它是服务端,另一个则通过传出参数 outInputChannel 交给调用者,是客户端
1.服务端连接的建立
addwindow 函数中,有以下三项工作:
通过 WindowState.setInputChannel 函数保存服务端的 InputChannel
通过 IMS.registerInputChannel 将 InputChannel 注册到 IMS
通过 InputMonitor.updateInputWindowsLw 将所有窗口的信息更新到 IMS
2.窗口端连接的建立
当窗口端通过 addwindow 函数获取 InputChannel,便会使用它创建一个 InputEventReceiver 对象,可以接收来自InputChannel 的输入事件,触发 onInputEvent 回调
InputEventRecevier 如何工作?将 InputChannel 的可读事件注册到 Looper,然后在事件到来时从 InputChannel 中读取 InputMessage,并翻译成 InputEvent,然后回调 InputEventReceiver 的 onInputEvent
3.InputDispatcher与窗口的连接
派发循环是指 InputDispatcher 不断地派发队列取出事件,寻找合适的窗口并进行发送的过程,是 InputDispatcher 线程的主要工作
事件发送循环是 InputDispatcher 通过 Connection 对象将事件发送给窗口,并接受其反馈的过程
InputDispatcher —>dispatchEventLocked:dispatchEventLocked 根据 InputTarget 中的 InputChannel 找到对应的Connection
—> InputDispatcher:prepareDispatcCycleLocked
—> InputDispatcher:enqueueDispatchEntriesLocked
—> InputDispatcher:startDispatchCycleLocked
输入事件被 InputPublisher 以 InputMessage 的形式写入 InputChannel,然后将事件转存到 waitQueue 中等待窗口的反馈
当 InputPublisher 将事件以 InputMessage 的形式写入 InputChannel 中,窗口端的 Looper 会因此而被唤醒,并执行NativeInputEventReceiver 的 handleEvent 调用 consumeEvent
—>android_view_InputEventReceiver —> NativeInputEventReceiver:consumeEvent 读取一个 InputEvent,生成 java 层的 InputEvent 对象,最后通过 JNI 回调
—> InputEventReceier:dispatchInputEvent
—> inputEventReceiver:finishInputEvent
—> android_view_InputEventReceiver —>NativeInputEventReceiver:finishInputEvent 触发服务端 InputChannel 回调
—> InputDispatcher:handleReceiveCallback
—> InputDispatcher:doDispatchCycleFinishedLockedInterruptible
对于输入事件反馈的处理主要有两个方面
将事件从 Connection 的 waitQueue 队列中删除,这个删除动作标志着此事件的派发流程完成
最后调用 startDispatchCycleLocked 继续尝试发送队列中的下一个事件
—> InputDispatcher:findFocusedWindowTargetsLocked
1.窗口可以接收事件的条件
—> InputDispatcher:isWindowReadyForMoreInputLocked 判断窗口是否可以接收事件:InputPublisher 是否被阻塞以及 Connection 两个队列的状态
2.重试派发与ANR的引发
—> InputDispatcher:handleTargetsNotReadyLocked
Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,这也意味着程序的大多数 *** 作都必须在16ms内完成。如果无法完成,则发生丢帧,上一帧画面被重复显示,造成卡顿的视觉。
从整个视图渲染流程看:
Surfaceflinger由init启动的独立进程,提供合成视图的系统服务。如果Surfaceflinger挂掉,会重启zygote。
在Surfaceflinger的init方法中,实例化了HWComposer和两个EventThread。
HWComposer :负责输出硬件产生或软件模拟的Vsync信号。
EventThread :负责分发vsync到Choreographer和SurfaceFlinger。其中mEventThread对应Choreographer;而mSFEventThread:对应SurfaceFlinger。
VSYNC信号主要的两个订阅者:SurfaceFlinger 和 Choreographer。
SurfaceFlinger :接收信号执行合成Layer流程。
Choreographer :接收信号来控制同步处理输入(Input)、动画(Animation)、绘制(Draw)三个UI *** 作。
Choreographer通知应用层绘制、SurfaceFlinger负责合成视图、两者之前加上了一定的offset,这样能保证两者步调一致。
在这个过程中,CPU负责把视图加工为多边形和纹理。GPU负责把多边形和纹理做栅格化处理,成为送显的像素数据。
1 视图层面的问题
包括layout层级太深View太多、View太复杂、重复绘制、ListView没优化、动画设计不合理等等。
这是遇到卡顿问题首先需要排查的,部分问题可以通过开发阶段的coding规范来避免的。
1)layout层级太深View太多:可以通过Lint来检测,优化:通过合理容器的使用,优先减少层级,其次减少View数目,能重用的尽量重用。
2)View太复杂:如果是自定义View,那还是从视图太深、View太多两个层面来考虑优化。如果是成熟的View:比如WebView、VideoView这种重量级的View,尽量复用和管理好生命周期。
3)重复绘制:通过Settings中打开GPU过度绘制 &GPU呈现模式可以了解当前视图层级关系,当然这部分与前面两点也是分不开的,最基本的要注意移除xml中非必须背景。
4)ListView优化,这部分主要是convertView的复用,能减少View的创建;ViewHolder的使用,减少View的find和赋值,加快加载速度;分页加载:控制一次加载的数据量,这样加载速度会快,内存压力也相对小。
5)动画:合理设计动画,能不用帧动画尽量不用,因为图片比较占内存,尤其是数量多的时候。另外针对属性动画,同一个view的一系列动画,可以使用Keyframe+PropertyValuesHolder组合方式达到只使用一个ObjectAnimator,多个view的动画用AnimatorSet进行动画组合和排序。
2 消息相关耗时
我们都知道,耗时 *** 作放到子线程做,通过handle返回主线程更新UI。但是消息本身也是会耗时的,主要分两方面:1)消息本身执行耗时, 2)消息执行被delay。消息本身执行耗时那就是主线程耗时,消息执行被delay,在messageQueue中,由于之前的Message太多或者执行时间过长,导致当前需更新UI的 *** 作得不到及时处理,尤其是16.6ms硬性标准下,一旦delay必然丢帧。
3 主线程耗时
这部分我要说的并不是在主线程做耗时 *** 作了,而是站在CPU调度的角度来看耗时问题,也就是说,比如主线程有500ms的耗时,要么Running了多久,是否存在Sleeping和Uninterruptible sleep等状态,这段时间内CPU被抢占了压根就没腾出功夫来执行你这 *** 作。如果有现场的话,通过抓systrace能比较明显看出来。
4 Input事件本身耗时
在Android整个Input体系中有三个重要的成员:Eventhub,InputReader,InputDispatcher。它们分别担负着各自不同的职责,Eventhub负责监听/dev/input产生Input事件,InputReader负责从Eventhub读取事件,并将读取的事件发给InputDispatcher,InputDispatcher则根据实际的需要具体分发给当前手机获得焦点实际的Window,最终交给ActivityThread通过消息来处理。
系统角度:
InputDispatcher分发事件给Window这个过程是跨进程通信,获取对应window本身可能存在耗时。
应用角度:
客户端接收事件的消息本身又可能存在耗时和delay的情况,这又回到消息耗时的范畴了。
5 持锁耗时
这属于业务逻辑层面的问题,最简单的就是主线程死锁,亦或是主线程在等锁,然后当前锁被其他线程持有在做耗时 *** 作等等。
6 频繁GC
我们知道,执行GC *** 作的时候,所有线程的任何 *** 作都会需要暂停,等待GC *** 作完成之后,其他 *** 作才能够继续运行。通常来说,单个的GC并不会占用太多时间,但是大量不停的GC *** 作则会显著占用帧间隔时间(16ms)。如果在帧间隔时间里面做了过多的GC *** 作,那么自然其他类似计算,渲染等 *** 作的可用时间就变得少了。
导致GC频繁执行有两个原因:
1)内存抖动,在memory monitor里能很明显看出来,短时间内创建大量对象然后又迅速被释放。
比如:在一个方法里for循环拼接String。会产生大量废弃的String对象,短时间内又会被回收,所以容易造成抖动,可以用StringBuilder/StringBuffer来替代,它们实现是动态数组,初始长度128,不够用了通过arraycopy来增加长度。对象统一管理,不会短时间内造成短时间内大量创建和销毁的问题,同时append与+相比更安全。
2)瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,也会触发GC。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。这个 *** 作有可能会影响到帧率,并使得用户感知到性能问题。
1 内存原因
在系统内存非常低的情况下,常规经验是:MemAvailable 低于MemTotal 1/10的情况下,容易出现内存引起的卡顿,原因无非就是在内存低的情况下内核在分配内存时,很难从物理内存(伙伴系统)直接拿到合适大小的页面,此时会触发回收 *** 作,如内存整理(compact)、回收匿名页(swap)、回收文件页(dirty=回写,clean=丢弃)等 *** 作。这些回收 *** 作较慢,因此耗时。这个过程主要体现在新启一个应用,zygote fork进程申请内存的时候。
2 系统服务持锁耗时
应用binder call请求系统服务,一般来说,系统服务如AMS、WMS对应的方法,一上来先不管三七二十一,就是一把大锁,很多情况下,特定的 *** 作会造成持锁耗时的情况,具体问题具体分析。
3 CPU调度问题
这类情况不太多见,但是也是存在的。在某个绘制周期中,CPU被抢占,无法及时开始绘制 *** 作。这分几部分来看,首先是不是被某个进程抢占的,比如dex2oat。或者看这段时间CPU使用率非常高,但是可能是大核跑满了,但是小核相对比较闲,这属于系统调度有问题等等。
例如:dex2oat发生的时候,占用所有有CPU(默认策略是有多少个核,就启动多少个线程),会将原文件中的dex文件抽出来,逐个指令的判断,然后进行翻译,并生成大量的中间内容,这些在memory当中是保存不下的,所以采用了swap机制, memory越少,越容易发生交换,所以还可能引起IO上的瓶颈。
可以设置系统属性:dalvik.vm.bg-dex2oat-threads 和 dalvik.vm.dex2oat-threads ,这两个系统属性是分别设置在前后台执行dex2oat限制的线程数,对应8核CPU来说,比如设置前后台分别为4,这样dex2oat执行时间会变长,但是卡顿会被缓解。
当然还有一种情况是,当手机温度过高,导致CPU降频,也会出现系统卡顿。
本文只是对卡顿分析提供一点不成熟的小思路。随着学习的深入,我会持续更新。
文中牵涉到的布局和重复绘制相关的内容可以参考我的文章: 布局优化
文中牵扯到的相关性能优化工具可以参考我的系列文章: 性能优化工具篇总结
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)