从显示 Tap 位置的原理窥探 Android Input 系统
Posted TechMerger
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从显示 Tap 位置的原理窥探 Android Input 系统相关的知识,希望对你有一定的参考价值。
原以为显示 Tap 位置是
ViewRootImpl
里依据 Touch 位置显示的PopupWindow
,实际不是、而且要复杂得多。
开发者选项画面里的 “Show taps” 选项,开发者一定不陌生。开启之后,截屏或录屏里可以直观地展示点击过的位置,非常方便。
![](https://image.cha138.com/20221204/a85228d0e1384ded9226552a95c7f890.jpg)
类似的选项还有显示 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
相关的参数,并执行两个重要步骤:
loadResourcesLocked()
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
中。
![](https://image.cha138.com/20221204/2ecdee94d0df42a9b1d556fb47c9f2d8.jpg)
而 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 系统的主要内容,如果未能解决你的问题,请参考以下文章