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. Can not set int field xxx to java.lang.Long 错误

    Can not set int field xxx to java.lang.Long 错误 这个错误其实是因为Java程序和MySQL表中字段的属性匹配不一致 我的报错是Can not set ja ...

  2. ES6 学习笔记(八)基本类型Symbol

    1.前言 大家都知道,在ES5的时候JavaScript的基本类型有Number.String.Boolean.undefined.object.Null共6种,在es6中,新增了Symbol类型,用 ...

  3. Pycharm系列---QT配置

    PYSIDE2 添加外部工具 file---settings External Tools,点击左上角的 加号+ designer 位置: envs\QT6\Lib\site-packages\PyS ...

  4. Nginx的概述和配置

    一.Nginx概述 1.1Nginx的特点 (1)一款高性能.轻量级web服务 稳定性高 系统资源消耗低高 对HTTP并发连接的处理能力 (2)单台物理服务器可支持30000~50000个并发请求 1 ...

  5. Vue3 企业级优雅实战 - 组件库框架 - 3 搭建组件库开发环境

    前文已经初始化了 workspace-root,从本文开始就需要依次搭建组件库.example.文档.cli.本文内容是搭建 组件库的开发环境. 1 packages 目录 前面在项目根目录下创建了 ...

  6. [排序算法] 树形选择排序 (C++)

    树形选择排序解释 树形选择排序 又称为锦标赛排序,其实理解起来很简单. 数组的 n 个元素就好像在进行锦标赛一样,每一轮小比赛每两个一组决出胜负(比较谁更小). 再将每一轮的胜者每两个一组进行小比赛, ...

  7. JAVA-面向对象之对象拷贝

    Java 中的数据类型分为基本数据类型和引用数据类型.对于这两种数据类型,在进行赋值操作.用作方法参数或返回值时,会有值传递和引用(地址)传递的差别. Map对象 测试01-等号赋值: @Test p ...

  8. 关于更改 lightdm 主题的方法

    lightdm 是一个显示管理器,它支持很多种 greeter lightdm 的主要配置文件都在 /etc/lightdm 目录下面 各个文件的作用: 配置文件名称 作用 图形化界面配置工具 lig ...

  9. volatile关键字在并发中有哪些作用?

    作者:小牛呼噜噜 | https://xiaoniuhululu.com 计算机内功.JAVA源码.职业成长.项目实战.面试相关资料等更多精彩文章在公众号「小牛呼噜噜」 前言 读过笔者之前的一篇文章J ...

  10. PGL图学习之项目实践(UniMP算法实现论文节点分类、新冠疫苗项目实战,助力疫情)[系列九]

    原项目链接:https://aistudio.baidu.com/aistudio/projectdetail/5100049?contributionType=1 1.图学习技术与应用 图是一个复杂 ...