Android Input架构

Android Input架构

Linux Input子系统简介

Android 是基于Linux 内核,Linux内核实现了一套input子系统,Input 子系统会在/dev/input/路径下创建我们硬件输入设备的节点,这些节点是以eventXX 来命名的,如event0,event1 等

/ # ls -l /dev/input/                                                
total 0
crw-rw---- 1 root input 13,  64 2015-01-01 08:00 event0
crw-rw---- 1 root input 13,  65 2015-01-01 08:00 event1
crw-rw---- 1 root input 13,  66 2015-01-01 08:00 event2
crw-rw---- 1 root input 13,  67 2015-01-01 08:00 event3
crw-rw---- 1 root input 13,  63 2015-01-01 08:00 mice
crw-rw---- 1 root input 13,  32 2015-01-01 08:00 mouse0

可以从/proc/bus/input/devices 中读出 eventXX等相关的硬件设备,

:/ # cat /proc/bus/input/devices

I: Bus=0019 Vendor=0001 Product=0001 Version=0000
N: Name="XXX Input Key Board"
P: Phys=
S: Sysfs=/devices/i2c-1/1-005b/input/input3
U: Uniq=
H: Handlers=kbd event3 
B: PROP=0
B: EV=3
B: KEY=40000800 1680 0 0 10000000

Android 读取事件信息就是从/dev/input/ 目录下的设备节点中读取出来的,比如我在串口下输入getevent,然后按下一个按键板上的按键,我这边实现的按键板驱动是i2c 扩展IO模拟的按键功能,可以看到串口的打印event3 相关信息如下

add device 4: /dev/input/event3
  name:     "XXX Input Key Board"

[  180.936582@0] D/[aw9523] : index:4 keycode:105 pre:0 cur:1
/dev/input/event3: 0001 0069 00000001
/dev/input/event3: 0000 0000 00000000
[  181.046977@0] D/[aw9523] : index:4 keycode:105 pre:1 cur:0
/dev/input/event3: 0001 0069 00000000
/dev/input/event3: 0000 0000 00000000

当然这里只是简单介绍下Linux input 子系统,真正的Linux Kernel子系统也是很庞大复杂的,有兴趣可以继续深入学习,这里不过多介绍了。

Android Framework Input 系统总体架构

Android 事件传递的流程,按键,触屏等事件是经由WindowManagerService
获取,并通过共享内存和管道的方式传递给ViewRoot,ViewRoot 再dispatch 给Application 的View。当有事件
从硬件设备输入时,system_server 端在检测到事件发生时,通过管道(pipe)通知ViewRoot 事件发生,此时
ViewRoot 再去的内存中读取这个事件信息。


Systemserver 创建了InputManagerService(inputManager)和WindowManagerService(WMS),并把
inputManager 传给了WMS,在构造函数中创建了InputManager 对象mInputManager,并把this 传进去,
InputManager 作为WMS 与Native 空间交互的中间类,WMS 通过InputManager 调用native 的函数,反过来
native 函数调用InputManager 的callback,间接调用WMS 内部类InputMonitor 的接口函数。

在JNI Native 端,主要通过NativeInputManager 来管理,在nativeInit()的时候创建。NativeInputManager
构造的时候创建一个eventHub 和一个本地InputManager,本地InputManager 构造的时候会创建一个
InputDispatcher 和一个InputReader 及对应的线程InputDispatcherThread 和InputReaderThread。

InputReader 构造函数的输入参数包括eventHub 和InputDispatcher,这样InputReaderThread 中就可以调
用eventHub 函数进行获取事件,做处理后,调用InputDispatcher 的接口把事件push 到队列中。
InputDispatcherThread 从队列中取出事件,并通过PIPE 传给客户端window。

Active 客户端创建RootView 的时候,调用WMS 的addWindow 来登记,addWindow 通过InputChannel 类
用来实现创建用于进程通信的PIPE(Java 层叫做channel),PIPE 有两个:一个是server,一个是client。

  1. Server channel 通过InputManager 注册到InputDispatcher
  2. Client channel 由参数返回给客户端,通过客户端进行注册。

这样服务端(InputDispatcher)就可以和客户端(Phone window)进行通信了。

WMS 会告知InputManager 当前活动的窗口,通过调用InputDispatcher 的setInputWindow 函数把窗口信
息传递给InputDispatcher 线程对象。这样InputDispatcher 就可以判断可以接收消息的窗口,有目的的进行PIPE
通信。

Inputmanagerservice 初始化

在Android 的开机过程中,系统中的服务很多都是由SystemServer 中启动的。

InputManager 类是整个android 的input 的上层代码最重要的类,就是通过这个类繁衍出了整个复杂的在
InputManger 的构造函数中,调用了JNI 的nativeInit 函数;在JNI 的代码中,又构造了一个重要的
NativeInputManager 类,这是个C++的本地类。

NativeInputManager 的构造函数中 ,new 了两个类,EventHubInputManager类。

  1. EventHub 就是Input 子系统的HAL 层了,负责将linux 的所有的input 设备打开并负责轮询读取他们的上报的数据。

  2. InputManager 类主要是负责管理input Event,由InputReader 从EventHub 读取事
    件,然后交给InputDispatcher 进行分发。 在InputManager 中的initialize 的初始化了两个线程。一个是inputReaderThread,负责从EventHub 中读取事件,另外一个是InputDispatcherThread 线程,主要负责分发读取的事件去处理。


在java 代码中用了nativeStart(),然后JNI 中又调用了NativeInputManager 的start 方法。这个方法就是在
前面InputManager 中的构造函数initialize 中的两个线程运行起来。
先看Input Dispatcher 线程运行的情况,然后就是InputReader 线程。InputDispatcher 线程调用了Dispatcher 的dispatchOnce 的方法。同样的InputReader 线程也会调用Reader 的threadLoop 的方法。

Inputchannel 创建过程

在ViewRoot 和WMS(WindowManagerService)建立起连接之前首先会创建一个InputChannel 对象,同样的
WMS 端也会创建一个InputChannel 对象,不过WMS 的创建过程是在ViewRoot 调用addToDisplay ()方法时调
用的。InputChannel 的构造不做任何操作,所以在ViewRoot 中创建InputChannel 时尚未初始化,它的初始化
过程是在调用WMS 方法addToDisplay()时,会调用到addWindow(),ViewRoot 将mInputChannel 作为参数传
递给WMS,由WMS addWindow()统一初始化。

Android7.1 代码中

frameworks/base/services/core/java/com/android/server/wm/WindowState.java

    void openInputChannel(InputChannel outInputChannel) {
        if (mInputChannel != null) {
            throw new IllegalStateException("Window already has an input channel.");
        }
        String name = makeInputChannelName();
        InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
        mInputChannel = inputChannels[0];
        mClientChannel = inputChannels[1];
        mInputWindowHandle.inputChannel = inputChannels[0];
        if (outInputChannel != null) {
            mClientChannel.transferTo(outInputChannel);
            mClientChannel.dispose();
            mClientChannel = null;
        } else {
            // If the window died visible, we setup a dummy input channel, so that taps
            // can still detected by input monitor channel, and we can relaunch the app.
            // Create dummy event receiver that simply reports all events as handled.
            mDeadWindowEventReceiver = new DeadWindowEventReceiver(mClientChannel);
        }
        mService.mInputManager.registerInputChannel(mInputChannel, mInputWindowHandle);
    }

outInputChannel 为ViewRoot 传递来的InputChannel 对象,上述代码主要的工作其实就是创建一对
InputChannel,这一对InputChannel 中实现了一组全双工管道。

在创建InputChannel 对的同时,会申请共享内存,并向2 个InputChannel 对象中各自保存一个共享内存的文件描述符。

InputChannel 创建完成后,会将其中一个的native InputChannel 赋值给outInputChannel,也就是对ViewRoot 端InputChannel 对象的初始化,这样随着ViewRoot 和WMS 两端的InputChannel 对象的创建,事件传输系统的管道通信也就建立了起来。

创建InputChannel pair 的过程就是在native 创建了socketpair,即两个sockets[2],sockets[0]对应到
inputchannel[0], sockets[1]对应到inputchannel[1],根据socketpair 的原理,往sockets[0]写的数据从sockets[1]读出来,从sockets[0]读到的是sockets[1]写入的数据,所以通信的两端分别持有0 和1 就可以全双工通信的了。sockets[0]和sockets[1]分别被保存到InputChannel 的mFd。

这样WMS 持有inputChannels[0],Viewroot 持有inputChannels[1]。WMS 从channel 0 发送按键信息给
viewroot,同时从channel 0 接收viewroot 按键处理完成的信息;Viewroot 从channel 1 接收按键信息,同时从
channel 1 发送按键处理完成的信息。

Inputchannel 注册过程

一个管道通信只是对应一个Activity 的事件处理,也就是当前系统中有多少个Activity 就会有多少个全双
工管道,那么系统需要一个管理者来管理以及调度每一个管道通信,因此我们在创建完InputChannel 对象后,
需要将其注册到这个管理者中去。

Viewroot 注册channel 1 来接收按键

ViewRoot 端InputChannel 对象在向NativeInputQueue 注册时,需要注册2 个参数:

  1. 将InputChannel[1]对象对应的Native InputChannel 传递给NativeInputQueue;
  2. 还有一个很重要的参数需要传递给NativeInputQueue,那就是当前Application 的主进程的
    MessageQueue。在注册过程中,android 会将InputChannel 对象中保存的管道的文件描述符交给MessageQueue 的native looper 去监听,同时向native looper 指示一个回调函数,一旦有事件发生,native looper 就会检测到管道上的数据,同时会去调用指示的回调函数。这个回调函数为NativeInputEventReceiver::handleEvent.

底层的一个代码注册逻辑如下:

frameworks/base/core/jni/android_view_InputEventReceiver.cpp

static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak,
        jobject inputChannelObj, jobject messageQueueObj) {
    sp<InputChannel> inputChannel = android_view_InputChannel_getInputChannel(env,
            inputChannelObj);
    if (inputChannel == NULL) {
        jniThrowRuntimeException(env, "InputChannel is not initialized.");
        return 0;
    }

    sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);
    if (messageQueue == NULL) {
        jniThrowRuntimeException(env, "MessageQueue is not initialized.");
        return 0;
    }

    sp<NativeInputEventReceiver> receiver = new NativeInputEventReceiver(env,
            receiverWeak, inputChannel, messageQueue);
    status_t status = receiver->initialize();  // 1. 这里看初始化函数
    if (status) {
        String8 message;
        message.appendFormat("Failed to initialize input event receiver.  status=%d", status);
        jniThrowRuntimeException(env, message.string());
        return 0;
    }

    receiver->incStrong(gInputEventReceiverClassInfo.clazz); // retain a reference for the object
    return reinterpret_cast<jlong>(receiver.get());
}
status_t NativeInputEventReceiver::initialize() {
    setFdEvents(ALOOPER_EVENT_INPUT); // 2. 调用添加loop轮询接口,类型是ALOOPER_EVENT_INPUT
    return OK;
}

void NativeInputEventReceiver::setFdEvents(int events) {
    if (mFdEvents != events) {
        mFdEvents = events;
        int fd = mInputConsumer.getChannel()->getFd();
        if (events) { 
            // 3. 将管道的fd 添加到进程的message queue中轮询
            // 把channel 的fd 注册到looper,this 就是 NativeInputEventReceiver
            // 即callback,callback 对应的handleEvent
            mMessageQueue->getLooper()->addFd(fd, 0, events, this, NULL);
        } else {
            mMessageQueue->getLooper()->removeFd(fd);
        }
    }
}
WMS 注册channel 0 的过程

WMS 端的对linux Input 系统的检测和ViewRoot 对管道接收端的检测机制不同,前面分析过了,
ViewRoot 端很好的复用了Application 主线程的Looper 轮询机制来实现对事件响应的实时性,

InputManager 启动了2 个进程来管理事件发生与传递,
InputReaderThread 和InputDispatcherThread,InputReaderThread 进程负责轮询事件发
生; InputDispatcherThread 负责dispatch 事件。为什么需要2 个进程来管理,用一个会出现什么问题?很明
显,如果用一个话,在轮询input 系统event 的时间间隔会变长,有可能丢失事件。

InputDispatcher 使用了native looper 来轮询检查管道通信,这个管道通信表示InputQueue 是否消化完成dispatch 过去的事件。注意的是这个native looper 并不是WMS线程的,而是线程InputDispatcher 自定定义的,因此所有的轮询过程,需要InputDispatcher 主动去调用。

WMS 注册Inputchannel 0 来读取viewroot 发往channel 1 的事件处理完成信息,收到完成信息后就可以执
行下一条事件,不然会出现ANR,注册过程如下:


Inputchannel 收发事件过程

两端的inputchannel 都注册好后,就可以进行事件发送和接收了。

  1. inputdispatcher 往channel 0 写入事件;
  2. viewroot 从channel 1 检测到事件,进行处理;
  3. viewroot 处理完按键后,往channel 1 写入完成信息;
  4. inputdispatcher 从channel 0 检测到完成信息,继续发送下一条事件 。

inputdispatcher 往channel 0 写入事件


1611394420633.png

调试问题处理

这里记录一下一个之前遇到的问题。

在Android中客制化单手模式,即将显示的区域修改3/4后,发现触控的坐标不对

问题现象:3/4 屏幕内容触控位置不对,比如点击左上角XtYt处,实际控制到的地方是XsYs

说明映射关系是在的,只是映射关系不对

如下,红框的触摸范围内响应到到是蓝色3/4 屏的,而红框外的坐标也有映射出来,不过映射出来的坐标并没有地方可以响应到,看这个log

01-01 20:09:42.821  1845  1894 I InputDispatcher: Dropping event because there is no touchable window at (1921, 1053)

因为之前是做了Layer的长宽修改,所以实际触摸框映射到Android的坐标是变大了

从1920,1080 -》 1920+640,1080+360

为了解决这个问题也比较简单,直接将inputreader 计算的坐标加上个偏移量就好了

void TouchInputMapper::cookPointerData()
    static char aSingHand[PROPERTY_VALUE_MAX];
    property_get("persist.sys.single_hand_model",aSingHand, "false");
    if(strcmp(aSingHand, "on") == 0){
        x += mViewport.logicalLeft;  // this mViewport.logicalLeft can dump from input
        y += mViewport.logicalTop;  // this mViewport.logicalTop can dump from input
    }

调试主要使用了getevent 和dumpsys input

getevent 命令可以查看存在的输入设备信息及输入事件信息

关于framework 和native的input 信息可以使用 dumpsys input

inputmanager 加载jni

顺便提一下system service 等服务load jni的过程

frameworks/base/services/Android.mk

include $(CLEAR_VARS)

LOCAL_SRC_FILES :=
LOCAL_SHARED_LIBRARIES :=

# include all the jni subdirs to collect their sources
include $(wildcard $(LOCAL_PATH)/*/jni/Android.mk)

LOCAL_CFLAGS += -DEGL_EGLEXT_PROTOTYPES -DGL_GLEXT_PROTOTYPES

LOCAL_MODULE:= libandroid_servers

include $(BUILD_SHARED_LIBRARY)

这里使用了Makefile中 wildcard 关键字通配符,引用了 include($(LOCAL_PATH)/*/jni/Android.mk) 所有的Android.mk文件后编译了 libandroid_servers.so

以 frameworks/base/services/core/jni/Android.mk 为例

LOCAL_SRC_FILES += \
    $(LOCAL_REL_DIR)/com_android_server_AlarmManagerService.cpp \
    $(LOCAL_REL_DIR)/com_android_server_am_BatteryStatsService.cpp \
    $(LOCAL_REL_DIR)/com_android_server_am_ActivityManagerService.cpp \
    $(LOCAL_REL_DIR)/com_android_server_AssetAtlasService.cpp \
    $(LOCAL_REL_DIR)/com_android_server_connectivity_Vpn.cpp \
    $(LOCAL_REL_DIR)/com_android_server_ConsumerIrService.cpp \
    $(LOCAL_REL_DIR)/com_android_server_HardwarePropertiesManagerService.cpp \
    $(LOCAL_REL_DIR)/com_android_server_hdmi_HdmiCecController.cpp \
    $(LOCAL_REL_DIR)/com_android_server_input_InputApplicationHandle.cpp \
    $(LOCAL_REL_DIR)/com_android_server_input_InputManagerService.cpp \
    $(LOCAL_REL_DIR)/com_android_server_input_InputWindowHandle.cpp \
    $(LOCAL_REL_DIR)/com_android_server_lights_LightsService.cpp \
    $(LOCAL_REL_DIR)/com_android_server_location_GnssLocationProvider.cpp \
    $(LOCAL_REL_DIR)/com_android_server_location_FlpHardwareProvider.cpp \
    $(LOCAL_REL_DIR)/com_android_server_power_PowerManagerService.cpp \
    $(LOCAL_REL_DIR)/com_android_server_SerialService.cpp \
    $(LOCAL_REL_DIR)/com_android_server_SystemServer.cpp \
    $(LOCAL_REL_DIR)/com_android_server_tv_TvUinputBridge.cpp \
    $(LOCAL_REL_DIR)/com_android_server_tv_TvInputHal.cpp \
    $(LOCAL_REL_DIR)/com_android_server_vr_VrManagerService.cpp \
    $(LOCAL_REL_DIR)/com_android_server_UsbDeviceManager.cpp \
    $(LOCAL_REL_DIR)/com_android_server_UsbMidiDevice.cpp \
    $(LOCAL_REL_DIR)/com_android_server_UsbHostManager.cpp \
    $(LOCAL_REL_DIR)/com_android_server_VibratorService.cpp \
    $(LOCAL_REL_DIR)/com_android_server_PersistentDataBlockService.cpp \
    $(LOCAL_REL_DIR)/onload.cpp

我们直接看最后的onload.cpp

extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
{
    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        ALOGE("GetEnv failed!");
        return result;
    }
    ALOG_ASSERT(env, "Could not retrieve the env!");

    register_android_server_ActivityManagerService(env);
    register_android_server_PowerManagerService(env);
    register_android_server_SerialService(env);
    // ……
}

可以看到一旦加载了android_server 的jni动态库,就注册各个service 的jni 方法,比如 activemanagerservice、powermanagerservice、inputmanagerservice 等

以inputmanagerservice 为例

./core/jni/com_android_server_input_InputManagerService.cpp

int register_android_server_InputManager(JNIEnv* env) {
    int res = jniRegisterNativeMethods(env, "com/android/server/input/InputManagerService",
            gInputManagerMethods, NELEM(gInputManagerMethods));
    (void) res;  // Faked use when LOG_NDEBUG.
    LOG_FATAL_IF(res < 0, "Unable to register native methods.");

    // Callbacks

    jclass clazz;
     FIND_CLASS(clazz, "com/android/server/input/InputManagerService");
    // ..
}

这里就注册了 com/android/server/input/InputManagerService 调用的jni方法

那么回到最开始的 libandroid_servers 是怎么被加载的呢?

在system_server跑起来的时候就会去加载

./java/com/android/server/SystemServer.java

public final class SystemServer {
    /**
     * The main entry point from zygote.
     */
    public static void main(String[] args) {
        new SystemServer().run();
}

private void run() {
        // ...
            // Initialize native services.
            System.loadLibrary("android_servers");
    }
}

可以看到system server 一加载就可以跑起来了

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,200评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,526评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,321评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,601评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,446评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,345评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,753评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,405评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,712评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,743评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,529评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,369评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,770评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,026评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,301评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,732评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,927评论 2 336

推荐阅读更多精彩内容