Input源码解读——从"Show tabs"开始

本文基于Android T版本源码,梳理当用户在开发者选项中开启Show tabs功能后显示第点按操作的视觉反馈的原理,来进一步了解Android Input系统

Settings 写入设置

首先是设置应用(Settings)提供的开发者选项画面响应点击,将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;
}
...
}

InputManagerService监听设置

负责管理输入的系统服务InputManagerService在启动之际,会监听设置中的 SHOW_TOUCHES字段的变化,在设置产生变化的时候调用native侧的代码进行处理。

// 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);
mNative.setShowTouches(setting != 0);
}
... // frameworks/base/services/core/java/com/android/server/input/NativeInputManagerService.java
public interface NativeInputManagerService {
...
void setShowTouches(boolean enabled);
...
}
// frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
class NativeInputManager : public virtual RefBase, ...{
...
void setShowTouches(bool enabled);
...
} void NativeInputManager::setShowTouches(bool enabled) {
{ // acquire lock
AutoMutex _l(mLock); if (mLocked.showTouches == enabled) {
return;
} ALOGI("Setting show touches feature to %s.", enabled ? "enabled" : "disabled");
mLocked.showTouches = enabled;
} // release lock mInputManager->getReader().requestRefreshConfiguration(
InputReaderConfiguration::CHANGE_SHOW_TOUCHES);
}

这里的mInputManagerInputManagerInterface对象实例,InputManagerInputManagerInterface和子类,所以通过mInputManager可以连接NativeInputManagerInputReader

这里向负责读取事件的InputReader发出更新配置的请求,配置变更的TypeCHANGE_SHOW_TOUCHES

通过 InputReader 请求刷新配置

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

如果尚未处于刷新中,则更新mConfigurationChangesToRefresh的值,并唤醒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();
}
}
}

EventHub 唤醒 InputReader 线程

InputManagerService过来的刷新请求最终需要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);
...
}
...
}

InputReader线程刷新配置

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

其首先将检查是否存在待刷新的配置变化changes,存在的话调用refreshConfigurationLockedInputDevice去重新适配变化。

// frameworks/native/services/inputflinger/reader/InputReader.cpp
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进一步处理之前需要先获取配置的变化放入mConfig中。

// frameworks/native/services/inputflinger/reader/InputReader.cpp
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配置变化

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

// 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, display dimensions, orientation and
// scaling factors.
configureInputDevice(when, &resetNeeded);
}
...
}

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

创建和初始化 PointerController

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

// frameworks/native/services/inputflinger/reader/mapper/TouchInputMapper.cpp
void TouchInputMapper::configureInputDevice(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.pointerCaptureRequest.enable)) {
if (mPointerController == nullptr) {
mPointerController = getContext()->getPointerController(getDeviceId());
}
if (mConfig.pointerCaptureRequest.enable) {
mPointerController->fade(PointerControllerInterface::Transition::IMMEDIATE);
}
} else {
mPointerController.reset();
}
...
}

这里调用InputReaderContext#getPointerControllerInputReader::ContextImplInputReaderContext的子类,所以会回调到InputReader开启PointerController的创建和初始化。

// frameworks/native/services/inputflinger/reader/InputReader.cpp
std::shared_ptr<PointerControllerInterface> InputReader::ContextImpl::getPointerController(
int32_t deviceId) {
// lock is already held by the input loop
return mReader->getPointerControllerLocked(deviceId);
} 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;
}

这里调用InputReaderPolicyInterface#obtainPointerController,而NativeInputManagerInputReaderPolicyInterface的子类。

// frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
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

调用PointerControllersetDisplayViewport传入显示用的DisplayViewPort

// frameworks/native/services/inputflinger/reader/InputReader.cpp
void InputReader::updatePointerDisplayLocked() {
...
std::optional<DisplayViewport> viewport =
mConfig.getDisplayViewportById(mConfig.defaultPointerDisplayId);
if (!viewport) {
...
viewport = mConfig.getDisplayViewportById(ADISPLAY_ID_DEFAULT);
}
...
controller->setDisplayViewport(*viewport);
}

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

// frameworks/base/libs/input/PointerController.cpp
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 相关资源

// frameworks/base/libs/input/MouseCursorController.cpp
void MouseCursorController::loadResourcesLocked(bool getAdditionalMouseResources) REQUIRES(mLock) {
...
policy->loadPointerResources(&mResources, mLocked.viewport.displayId);
policy->loadPointerIcon(&mLocked.pointerIcon, mLocked.viewport.displayId);
...
}

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

getSystemIcon则是负责的函数,其将读取系统资源里名为PointerStyle,并读取指针对应的资源 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中。

SpriteIconupdatePointerLocked阶段会被存放到SpriteController中,等待显示的调度。

// frameworks/base/libs/input/MouseCursorController.cpp
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();
}

显示Tap

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

// frameworks/native/services/inputflinger/reader/InputReader.cpp
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的一些参数。

// frameworks/native/services/inputflinger/reader/mapper/TouchInputMapper.cpp
void TouchInputMapper::updateTouchSpots() {
...
mPointerController->setPresentation(PointerControllerInterface::Presentation::SPOT);
mPointerController->fade(PointerControllerInterface::Transition::GRADUAL); mPointerController->setButtonState(mCurrentRawState.buttonState);
setTouchSpots(mCurrentCookedState.cookedPointerData.pointerCoords,
mCurrentCookedState.cookedPointerData.idToIndex,
mCurrentCookedState.cookedPointerData.touchingIdBits, mViewport.displayId);
}

其中比较关键的setTouchSpots是显示Taps的关键步骤,准备 x、y 坐标和压力值。

在 Reader 而不是 Dispatch、更不是 ViewRootImpl 的时候处理的原因在于:Read 到事件即显示可以更早地响,同时不用占用 App 进程。

// frameworks/native/services/inputflinger/reader/mapper/TouchInputMapper.cpp
void TouchInputMapper::setTouchSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex,
BitSet32 spotIdBits, int32_t displayId) {
std::array<PointerCoords, MAX_POINTERS> outSpotCoords{}; for (BitSet32 idBits(spotIdBits); !idBits.isEmpty();) {
const uint32_t index = spotIdToIndex[idBits.clearFirstMarkedBit()];
float x = spotCoords[index].getX();
float y = spotCoords[index].getY();
float pressure = spotCoords[index].getAxisValue(AMOTION_EVENT_AXIS_PRESSURE);
...
} mPointerController->setSpots(outSpotCoords.data(), spotIdToIndex, spotIdBits, displayId);
}

其后PointerController会通过TouchSpotController创建Spot实例向其发送updateSprite请求。最后回调 SpriteController调用setIcon处理。

// frameworks/base/libs/input/TouchSpotController.cpp
void TouchSpotController::Spot::updateSprite(const SpriteIcon* icon, float x, float y,
int32_t displayId) {
sprite->setLayer(Sprite::BASE_LAYER_SPOT + id);
...
if (icon != mLastIcon) {
mLastIcon = icon;
if (icon) {
sprite->setIcon(*icon);
sprite->setVisible(true);
} else {
sprite->setVisible(false);
}
}
}
// frameworks/base/libs/input/SpriteController.cpp
void SpriteController::SpriteImpl::setIcon(const SpriteIcon& icon) {
AutoMutex _l(mController->mLock);
...
invalidateLocked(dirty);
} void SpriteController::SpriteImpl::invalidateLocked(uint32_t dirty) {
...
if (!wasDirty) {
mController->invalidateSpriteLocked(this);
}
} void SpriteController::invalidateSpriteLocked(const sp<SpriteImpl>& sprite) {
bool wasEmpty = mLocked.invalidatedSprites.isEmpty();
mLocked.invalidatedSprites.push(sprite);
if (wasEmpty) {
if (mLocked.transactionNestingCount != 0) {
mLocked.deferredSpriteUpdate = true;
} else {
mLooper->sendMessage(mHandler, Message(MSG_UPDATE_SPRITES));
}
}
}

MSG_UPDATE_SPRITES经过 Handler 回调doUpdateSprites,将取出封装在SpriteUpdate中的SpriteIcon并执行 draw。

// frameworks/base/libs/input/SpriteController.cpp
void SpriteController::doUpdateSprites() {
...
for (size_t i = 0; i < numSprites; i++) {
SpriteUpdate& update = updates.editItemAt(i); if ((update.state.dirty & DIRTY_BITMAP) && update.state.surfaceDrawn) {
update.state.surfaceDrawn = false;
update.surfaceChanged = surfaceChanged = true;
} if (update.state.surfaceControl != NULL && !update.state.surfaceDrawn
&& update.state.wantSurfaceVisible()) {
sp<Surface> surface = update.state.surfaceControl->getSurface();
if (update.state.icon.draw(surface)) {
update.state.surfaceDrawn = true;
update.surfaceChanged = surfaceChanged = true;
}
}
}
...
updates.clear();
}

最后,SpriteIcon将取出Bitmap描画到SurfaceCanvas上去。

// frameworks/base/libs/input/SpriteIcon.cpp
bool SpriteIcon::draw(sp<Surface> surface) const {
...
graphics::Paint paint;
paint.setBlendMode(ABLEND_MODE_SRC); graphics::Canvas canvas(outBuffer, (int32_t)surface->getBuffersDataSpace());
canvas.drawBitmap(bitmap, 0, 0, &paint);
...
status = surface->unlockAndPost();
if (status) {
ALOGE("Error %d unlocking and posting sprite surface after drawing.", status);
}
return !status;
}

总体流程

通过一个框图简单回顾一下整个流程。

可以看到,简简单单的 Show taps 功能,从设置、配置、刷新再到显示,经历了多个进程、多个模块的协力。

涉及的Input核心逻辑框图

Input源码解读——从"Show tabs"开始的更多相关文章

  1. SDWebImage源码解读 之 NSData+ImageContentType

    第一篇 前言 从今天开始,我将开启一段源码解读的旅途了.在这里先暂时不透露具体解读的源码到底是哪些?因为也可能随着解读的进行会更改计划.但能够肯定的是,这一系列之中肯定会有Swift版本的代码. 说说 ...

  2. SDWebImage源码解读_之SDWebImageDecoder

    第四篇 前言 首先,我们要弄明白一个问题? 为什么要对UIImage进行解码呢?难道不能直接使用吗? 其实不解码也是可以使用的,假如说我们通过imageNamed:来加载image,系统默认会在主线程 ...

  3. AFNetworking 3.0 源码解读(四)之 AFURLResponseSerialization

    本篇是AFNetworking 3.0 源码解读的第四篇了. AFNetworking 3.0 源码解读(一)之 AFNetworkReachabilityManager AFNetworking 3 ...

  4. 第二十五课:jQuery.event.trigger的源码解读

    本课主要来讲解jQuery.event.trigger的源码解读. trigger = function(event, data, elem, onlyHandlers){ if(elem & ...

  5. [Hadoop源码解读](六)MapReduce篇之MapTask类

    MapTask类继承于Task类,它最主要的方法就是run(),用来执行这个Map任务. run()首先设置一个TaskReporter并启动,然后调用JobConf的getUseNewAPI()判断 ...

  6. Normalize.css 介绍与源码解读

    开始 Normalize.css 是一个可定制的 CSS 文件,使浏览器呈现的所有元素,更一致和符合现代标准;是在现代浏览器环境下对于CSS reset的替代. 它正是针对只需要统一的元素样式.该项目 ...

  7. HttpClient 4.3连接池参数配置及源码解读

    目前所在公司使用HttpClient 4.3.3版本发送Rest请求,调用接口.最近出现了调用查询接口服务慢的生产问题,在排查整个调用链可能存在的问题时(从客户端发起Http请求->ESB-&g ...

  8. Alamofire源码解读系列(二)之错误处理(AFError)

    本篇主要讲解Alamofire中错误的处理机制 前言 在开发中,往往最容易被忽略的内容就是对错误的处理.有经验的开发者,能够对自己写的每行代码负责,而且非常清楚自己写的代码在什么时候会出现异常,这样就 ...

  9. Alamofire源码解读系列(十一)之多表单(MultipartFormData)

    本篇讲解跟上传数据相关的多表单 前言 我相信应该有不少的开发者不明白多表单是怎么一回事,然而事实上,多表单确实很简单.试想一下,如果有多个不同类型的文件(png/txt/mp3/pdf等等)需要上传给 ...

  10. Apache Beam WordCount编程实战及源码解读

    概述:Apache Beam WordCount编程实战及源码解读,并通过intellij IDEA和terminal两种方式调试运行WordCount程序,Apache Beam对大数据的批处理和流 ...

随机推荐

  1. 构造器与this、package、import关键字的使用

    构造器一.构造器的作用: 1.创建对象 2.初始化对象的信息 二.说明: 1.如果没有显式的定义类的构造器的话,则系统默认提供一个空参的构造器 2.定义构造器的格式:权限修饰符 类名(形参列表){} ...

  2. echarts在Vue项目中的实际运用效果图

    文章目录 1.在后台系统首页中.可以根据需求制作相应的图表 2.在Vue中使用echarts的详细过程参照这个链接 1.在后台系统首页中.可以根据需求制作相应的图表 2.在Vue中使用echarts的 ...

  3. 关于引用JS和CSS文件刷新浏览器缓存问题,部署服务器后客户端样式不刷新

    问题描述 对样式的css文件进行了修改,部署到服务器后访问发现页面展示不正常,但是刷新之后就会展示正常. 问题分析 研究之后发现可能的原因有 css文件过大,加载缓慢 本地缓存问题,虽然服务器修改了c ...

  4. 你给文字描述,AI艺术作画,精美无比!附源码,快来试试!

    作者:韩信子@ShowMeAI 深度学习实战系列:https://www.showmeai.tech/tutorials/42 TensorFlow 实战系列:https://www.showmeai ...

  5. 编译安装PHP7.4

    1.下载PHP源码包 wget https://www.php.net/distributions/php-7.4.30.tar.gz 2.解压缩 tar xf php-7.4.30.tar.gz - ...

  6. 关于Position Encoding 的理解

    encoding Sinusoidal Position Encoding \[\begin{aligned} P E_{(p o s, 2 i)} &=\sin \left(\frac{p ...

  7. 安装nvm 和 yarn

    安装nvm curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash 执行上面的命令 如果出现问题 ...

  8. JUC学习笔记——共享模型之管程

    JUC学习笔记--共享模型之管程 在本系列内容中我们会对JUC做一个系统的学习,本片将会介绍JUC的管程部分 我们会分为以下几部分进行介绍: 共享问题 共享问题解决方案 线程安全分析 Monitor ...

  9. WebKit策略:<foreignObject>可用于绘制svg中的html标签,但与<use>搭配不生效

    在<svg>里面可以利用<foreignObject>绘制html标签,原本是我在iconfont采用Font class方式引入svg的无奈之举. 起初的设计是所有icon先 ...

  10. 2022春每日一题:Day 7

    题目:Fire 先预处理出每个F蔓延的时间,再bfs走迷宫. 代码: #include <cstdio> #include <cstdlib> #include <cst ...