从显示 Tap 位置的原理窥探 Android Input 系统

Posted TechMerger

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从显示 Tap 位置的原理窥探 Android Input 系统相关的知识,希望对你有一定的参考价值。

原以为显示 Tap 位置是 ViewRootImpl 里依据 Touch 位置显示的 PopupWindow ,实际不是、而且要复杂得多。

开发者选项画面里的 “Show taps” 选项,开发者一定不陌生。开启之后,截屏或录屏里可以直观地展示点击过的位置,非常方便。

类似的选项还有显示 Touch 参数的 “Pointer location”,原理差不多。本次我们聚焦 “Show taps” 的功能,查阅 android 12 的源码,将开启和显示流程分析清楚。

借此也窥探一下 Android 最重要的 Input 系统。

1. Settings 写入设置

首先是 Settings App 提供的开发者选项画面响应点击,将 “Show taps” 选项对应的设置 Key SHOW_TOUCHES 的 ON 值通过 android.provder.Settings 接口写入到保存系统设置数据的 SettingsProvier 中。

// packages/apps/Settings/src/com/android/settings/development/ShowTapsPreferenceController.java
public class ShowTapsPreferenceController extends DeveloperOptionsPreferenceController ... 
    ...
    @Override
    public boolean onPreferenceChange(Preference preference, Object newValue) 
        final boolean isEnabled = (Boolean) newValue;
        Settings.System.putInt(mContext.getContentResolver(),
                Settings.System.SHOW_TOUCHES, isEnabled ? SETTING_VALUE_ON : SETTING_VALUE_OFF);
        return true;
    
    ...

2. IMS 监听和反映设置

负责管理输入的系统服务 InputManagerService 在启动之际,会注册监听 SHOW_TOUCHES Key 的观察者,在设置产生变化的时候调用 JNI 开始反映设置。

// frameworks/base/services/core/java/com/android/server/input/InputManagerService.java
public class InputManagerService extends IInputManager.Stub... 
    ...
    public void start() 
        registerShowTouchesSettingObserver();
        ...
    
    
    private void registerShowTouchesSettingObserver() 
        mContext.getContentResolver().registerContentObserver(
                Settings.System.getUriFor(Settings.System.SHOW_TOUCHES), true,
                new ContentObserver(mHandler) 
                    @Override
                    public void onChange(boolean selfChange) 
                        updateShowTouchesFromSettings();
                    
                , UserHandle.USER_ALL);
    
    
    private void updateShowTouchesFromSettings() 
        int setting = getShowTouchesSetting(0);
        nativeSetShowTouches(mPtr, setting != 0);
    
    ...

JNI 端的 NativeInputManager 持有 InputFlinger ,向其中负责读取事件的 InputReader 发出更新配置的请求,配置变更的 Type 为 CHANGE_SHOW_TOUCHES

在此之前需要先更新管理配置信息的 mLocked 结构体中的 showTouches 成员,InputFlinger 在刷新配置的时候需要验证。

// frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
static void nativeSetShowTouches(JNIEnv* /* env */,
        jclass /* clazz */, jlong ptr, jboolean enabled) 
    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);

    im->setShowTouches(enabled);


void NativeInputManager::setShowTouches(bool enabled) 
     // acquire lock
        ...
        mLocked.showTouches = enabled;
     // release lock

    mInputManager->getReader()->requestRefreshConfiguration(
            InputReaderConfiguration::CHANGE_SHOW_TOUCHES);

3. 通过 InputReader 请求刷新配置

InputReader 接收到配置变化的 Type 之后,会根据记录待刷新配置的变量 mConfigurationChangesToRefresh 判断当前是否已经在刷新过程中。

如果尚未处于刷新中,则标记需要 wake 事件源头 EventHub,之后会将该变化添加该变量到中,最后就是通知 EventHub 唤醒。

// frameworks/native/services/inputflinger/reader/InputReader.cpp
void InputReader::requestRefreshConfiguration(uint32_t changes) 
    std::scoped_lock _l(mLock);

    if (changes) 
        bool needWake = !mConfigurationChangesToRefresh;
        mConfigurationChangesToRefresh |= changes;

        if (needWake) 
            mEventHub->wake();
        
    

4. EventHub 唤醒 InputReader 线程

IMS 过来的刷新请求最终需要 InputReader 线程来处理。

可是 InputReader 线程处在从 EventHub 中读取事件和没有事件时便调用 epoll_wait 进入等待状态的循环当中。

所以为了让其即刻处理配置变化,需要 EventHub 的手动唤醒。

// frameworks/native/services/inputflinger/reader/EventHub.cpp
void EventHub::wake() 
    ALOGV("wake() called");

    ssize_t nWrite;
    do 
        nWrite = write(mWakeWritePipeFd, "W", 1);
     while (nWrite == -1 && errno == EINTR);

    if (nWrite != 1 && errno != EAGAIN) 
        ALOGW("Could not write wake signal: %s", strerror(errno));
    


size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) 
    ...
    for (;;) 
        ...
        int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);
        ...
    
    ...

5. InputReader 线程刷新配置

EventHub 唤醒后处于等待状态的 getEvents() 会结束,之后 InputReader 线程会进入下次循环即 loopOnce()

其首先将检查是否存在待刷新的配置变化 changes,存在的话调用 refreshConfigurationLocked()InputDevice 去重新配置这项变化。

void InputReader::loopOnce() 
    ...
    std::vector<InputDeviceInfo> inputDevices;
     // acquire lock
        ...
        uint32_t changes = mConfigurationChangesToRefresh;
        if (changes) 
            mConfigurationChangesToRefresh = 0;
            timeoutMillis = 0;
            refreshConfigurationLocked(changes);
         else if (mNextTimeout != LLONG_MAX) 
            nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
            timeoutMillis = toMillisecondTimeoutDelay(now, mNextTimeout);
        
     // release lock

    size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);
    ...

需要留意,refreshConfigurationLocked() 在调用 InputDevice 进一步处理之前需要先从 JNI 获取配置(getReaderConfiguration())的变化放入 mConfig 中。

void InputReader::refreshConfigurationLocked(uint32_t changes) 
    mPolicy->getReaderConfiguration(&mConfig);
    ...

    if (changes & InputReaderConfiguration::CHANGE_MUST_REOPEN) 
        mEventHub->requestReopenDevices();
     else 
        for (auto& devicePair : mDevices) 
            std::shared_ptr<InputDevice>& device = devicePair.second;
            device->configure(now, &mConfig, changes);
        
    
    ...

InputDevice 配置变化

InputDevice 的 configure() 需要处理很多配置变化,比如键盘布局、麦克风等。对于 Show taps 的变化关注调用 InputMapper 的 congfigure() 即可。

// frameworks/native/services/inputflinger/reader/InputDevice.cpp
void InputDevice::configure(nsecs_t when, const InputReaderConfiguration* config,
                            uint32_t changes) 
    ...
    if (!isIgnored()) 
        ...
        for_each_mapper([this, when, config, changes](InputMapper& mapper) 
            mapper.configure(when, config, changes);
            mSources |= mapper.getSources();
        );
        ...
    

TouchInputMapper 进一步处理

众多输入事件的物理数据需要对应的 InputMapper 来转化为上层能识别的事件类型。比如识别键盘输入的 KeyboardInputMapper、识别震动的 VibratorInputMapper 等等。

现在的触摸屏都支持多点触控,所以是 MultiTouchInputMapper 来处理的。可 MultiTouchInputMapper 没有复写 configure(),而是沿用由父类 TouchInputMapper的共通处理。

// frameworks/native/services/inputflinger/reader/mapper/TouchInputMapper.cpp
void TouchInputMapper::configure(nsecs_t when, const InputReaderConfiguration* config,
                                 uint32_t changes) 
    ...
    bool resetNeeded = false;
    if (!changes ||
        (changes &
         (InputReaderConfiguration::CHANGE_DISPLAY_INFO |
          InputReaderConfiguration::CHANGE_POINTER_CAPTURE |
          InputReaderConfiguration::CHANGE_POINTER_GESTURE_ENABLEMENT |
          InputReaderConfiguration::CHANGE_SHOW_TOUCHES |
          InputReaderConfiguration::CHANGE_EXTERNAL_STYLUS_PRESENCE))) 
        // Configure device sources, surface dimensions, orientation and
        // scaling factors.
        configureSurface(when, &resetNeeded);
    
    ...

TouchInputMapper 会依据 changes 的类型进行对应处理,对于 SHOW_TOUCHES 的变化需要调用 configureSurface() 进一步处理。

6. 创建和初始化 PointerController

configureSurface() 进行多个参数的测量和配置,其中和 “Show taps” 相关的是 PointerController 的创建,该类是 Mouse、Taps、Pointer location 等系统 Touch 显示的专用类。

需要留意的是创建之前需要验证从 JNI 里取得的 mConfig.showTouches 变量。

void TouchInputMapper::configureSurface(nsecs_t when, bool* outResetNeeded) 
    ...
    // Create pointer controller if needed, and keep it around if Pointer Capture is enabled to
    // preserve the cursor position.
    if (mDeviceMode == DeviceMode::POINTER ||
        (mDeviceMode == DeviceMode::DIRECT && mConfig.showTouches) ||
        (mParameters.deviceType == Parameters::DeviceType::POINTER && mConfig.pointerCapture)) 
        if (mPointerController == nullptr) 
            mPointerController = getContext()->getPointerController(getDeviceId());
        
        if (mConfig.pointerCapture) 
            mPointerController->fade(PointerControllerInterface::Transition::IMMEDIATE);
        
     else 
        mPointerController.reset();
    
    ...

getPointerController() 会回调到 InputReader 开启 PointerController 的创建和初始化。

std::shared_ptr<PointerControllerInterface> InputReader::getPointerControllerLocked(
        int32_t deviceId) 
    std::shared_ptr<PointerControllerInterface> controller = mPointerController.lock();
    if (controller == nullptr) 
        controller = mPolicy->obtainPointerController(deviceId);
        mPointerController = controller;
        updatePointerDisplayLocked();
    
    return controller;

用于创建的 obtainPointerController() 的实现在 JNI 里,调用 PointerController 的静态方法 create() 开始构建实例。

std::shared_ptr<PointerControllerInterface> NativeInputManager::obtainPointerController(
        int32_t /* deviceId */) 
    ...
    std::shared_ptr<PointerController> controller = mLocked.pointerController.lock();
    if (controller == nullptr) 
        ensureSpriteControllerLocked();

        controller = PointerController::create(this, mLooper, mLocked.spriteController);
        mLocked.pointerController = controller;
        updateInactivityTimeoutLocked();
    

    return controller;

PointerController 构建的同时需要构建持有的 MouseCursorController

// frameworks/base/libs/input/PointerController.cpp
std::shared_ptr<PointerController> PointerController::create( ... ) 
    std::shared_ptr<PointerController> controller = std::shared_ptr<PointerController>(
            new PointerController(policy, looper, spriteController));
    ...
    return controller;


PointerController::PointerController( ... )
      : mContext(policy, looper, spriteController, *this), mCursorController(mContext) 
    std::scoped_lock lock(mLock);
    mLocked.presentation = Presentation::SPOT;

obtainPointerController() 执行完之后调用 updatePointerDisplayLocked() 执行 PointerController 的初始化。

初始化 PointerController

调用 PointerController 的 setDisplayViewport() 传入显示用的 DisplayViewPort

void InputReader::updatePointerDisplayLocked() 
    ...
    std::optional<DisplayViewport> viewport =
            mConfig.getDisplayViewportById(mConfig.defaultPointerDisplayId);
    if (!viewport) 
        ...
        viewport = mConfig.getDisplayViewportById(ADISPLAY_ID_DEFAULT);
    
    ...
    controller->setDisplayViewport(*viewport);

setDisplayViewport() 需要持有的 MouseCursorController 进一步初始化。

void PointerController::setDisplayViewport(const DisplayViewport& viewport) 
    ...
    mCursorController.setDisplayViewport(viewport, getAdditionalMouseResources);

MouseCursorController 需要获取 Display 相关的参数,并执行两个重要步骤:

  1. loadResourcesLocked()
  2. updatePointerLocked()
// frameworks/base/libs/input/MouseCursorController.cpp
void MouseCursorController::setDisplayViewport(const DisplayViewport& viewport,
                                               bool getAdditionalMouseResources) 
    ...
    // Reset cursor position to center if size or display changed.
    if (oldViewport.displayId != viewport.displayId || oldDisplayWidth != newDisplayWidth ||
        oldDisplayHeight != newDisplayHeight) 
        float minX, minY, maxX, maxY;
        if (getBoundsLocked(&minX, &minY, &maxX, &maxY)) 
            mLocked.pointerX = (minX + maxX) * 0.5f;
            mLocked.pointerY = (minY + maxY) * 0.5f;
            // Reload icon resources for density may be changed.
            loadResourcesLocked(getAdditionalMouseResources);
        ...
        
     else if (oldViewport.orientation != viewport.orientation) 
        ...
    

    updatePointerLocked();

加载 Pointer 相关资源

void MouseCursorController::loadResourcesLocked(bool getAdditionalMouseResources) REQUIRES(mLock) 
    ...
    policy->loadPointerResources(&mResources, mLocked.viewport.displayId);
    policy->loadPointerIcon(&mLocked.pointerIcon, mLocked.viewport.displayId);
    ...

省略诸多细节,loadPointerResources() 将通过 IMS 的 JNI 端以及 PointerIcon 的 JNI 端创建 PointerIcon 实例,并读取显示 Taps 的资源。

getSystemIcon() 则是负责的函数,其将读取系统资源里名为 Pointer 的 Style,并读取 Taps 对应的资源 ID。

// frameworks/base/core/java/android/view/PointerIcon.java
    public static PointerIcon getSystemIcon(@NonNull Context context, int type) 
        ...
        int typeIndex = getSystemIconTypeIndex(type);
        if (typeIndex == 0) 
            typeIndex = getSystemIconTypeIndex(TYPE_DEFAULT);
        

        int defStyle = sUseLargeIcons ?
                com.android.internal.R.style.LargePointer : com.android.internal.R.style.Pointer;
        TypedArray a = context.obtainStyledAttributes(null,
                com.android.internal.R.styleable.Pointer,
                0, defStyle);
        int resourceId = a.getResourceId(typeIndex, -1);
        ...
        icon = new PointerIcon(type);
        if ((resourceId & 0xff000000) == 0x01000000) 
            icon.mSystemIconResourceId = resourceId;
         else 
            icon.loadResource(context, context.getResources(), resourceId);
        
        systemIcons.append(type, icon);
        return icon;
    

    private static int getSystemIconTypeIndex(int type) 
        switch (type) 
            ...
            case TYPE_SPOT_TOUCH:
                return com.android.internal.R.styleable.Pointer_pointerIconSpotTouch;
            ...
            default:
                return 0;
        
    

资源 ID 为 pointer_spot_touch_icon。

<!-- frameworks/base/core/res/res/drawable/pointer_spot_touch_icon.xml -->
<?xml version="1.0" encoding="utf-8"?>
<pointer-icon xmlns:android="http://schemas.android.com/apk/res/android"
    android:bitmap="@drawable/pointer_spot_touch"
    android:hotSpotX="16dp"
    android:hotSpotY="16dp" />

其指向的图片就是如下熟悉的 Spot png:pointer_spot_touch.png。之后的 loadPointerIcon 阶段会将该图片解析成 Bitmap 并被管理在 SpriteIcon 中。

而 SpriteIcon 在 updatePointerLocked() 阶段会被存放到 SpriteController 中,等待显示的调度。

void MouseCursorController::updatePointerLocked() REQUIRES(mLock) 
    if (!mLocked.viewport.isValid()) 
        return;
    
    sp<SpriteController> spriteController = mContext.getSpriteController();
    spriteController->openTransaction();

    ...
    if (mLocked.updatePointerIcon) 
        if (mLocked.requestedPointerType == mContext.getPolicy()->getDefaultPointerIconId()) 
            mLocked.pointerSprite->setIcon(mLocked.pointerIcon);
        ...
        
        mLocked.updatePointerIcon = false;
    

    spriteController->closeTransaction();

7. 显示 tap

点击的时候 EventHub#getEvents() 会产生事件,InputReader#loopOnce() 会调用 processEventsLocked() 处理事件。

void InputReader::loopOnce() 
    ...
    size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);

     // acquire lock
        ...
        if (count) 
            processEventsLocked(mEventBuffer, count);
        
        ....
     // release lock
    ...

之后调用 InputMapper 开始加工事件,并在 TouchInputMapper#cookAndDispatch() 的时候调用 updateTouchSpots() 更新 PointerController 的一些参数。

void TouchInputMapper::updateTouchSpots() 
    ...
    mPointerController->setPresentation(PointerControllerInterface::Presentation::SPOT);
    mPointerController->fade(PointerControllerInterface::Transition::GRADUAL);

    mPointerController->setButtonState(mCurrentRawState.以上是关于从显示 Tap 位置的原理窥探 Android Input 系统的主要内容,如果未能解决你的问题,请参考以下文章

从内存管理原理,窥探OS内存管理机制

从内存管理原理,窥探OS内存管理机制

Xamarin 从 Image Tap 显示 iOS UIDatePicker

Android Tap 高亮果冻豆

TUN/TAP设备浅析(一) -- 原理浅析

LocationManager-定位方式原理解析