frameworks 之 触摸事件ANR流程

frameworks 之 触摸事件ANR流程

  • 1. 进入AnrTracker
  • 2. 判断是否ANR
  • 3. 正常响应

经常在应用开发中,会出现ANR 无响应的弹框,这是因为事件没在规定事件移除wq队列导致触发ANR。
涉及到的类如下

  • frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
  • frameworks/native/services/inputflinger/dispatcher/AnrTracker.cpp
  • frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
  • frameworks/base/services/core/java/com/android/server/input/InputManagerService.java
  • frameworks/base/services/java/com/android/server/SystemServer.java
  • frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
  • frameworks/base/services/core/java/com/android/server/wm/InputManagerCallback.java
  • frameworks/base/services/core/java/com/android/server/wm/AnrController.java
  • frameworks/base/core/java/android/app/ActivityManagerInternal.java
  • frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
  • frameworks/base/services/core/java/com/android/server/am/AnrHelper.java
  • frameworks/base/services/core/java/com/android/server/am/ProcessErrorStateRecord.java

1. 进入AnrTracker

触摸事件分发文章 在进入移除oq,放数据进入wq 队列时候 ,最后对数据进行 timeoutTime 属性的标记。并将数据对应的时间和connection 放进去 mAnrTracker

void InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,const sp<Connection>& connection) {...// 记录派发时间,为后面anr计时dispatchEntry->deliveryTime = currentTime;const std::chrono::nanoseconds timeout =getDispatchingTimeoutLocked(connection->inputChannel->getConnectionToken());// 超时时间dispatchEntry->timeoutTime = currentTime + timeout.count();...
// 将oq事件移除 添加到wqconnection->outboundQueue.erase(std::remove(connection->outboundQueue.begin(),connection->outboundQueue.end(),dispatchEntry));traceOutboundQueueLength(*connection);connection->waitQueue.push_back(dispatchEntry);// 插入数据进入 mAnrTracker 根据if (connection->responsive) {mAnrTracker.insert(dispatchEntry->timeoutTime,connection->inputChannel->getConnectionToken());}traceWaitQueueLength(*connection);
}

AnrTracker 的 insert 方法将参数 构造为 pair<nsecs_t /timeoutTime/, sp /connectionToken/> 放进去 multiset 数组

std::multiset<std::pair<nsecs_t /*timeoutTime*/, sp<IBinder> /*connectionToken*/>> mAnrTimeouts;void AnrTracker::insert(nsecs_t timeoutTime, sp<IBinder> token) {mAnrTimeouts.insert(std::make_pair(timeoutTime, std::move(token)));
}

2. 判断是否ANR

将对应的数据放进去 AnrTracker 里面后, 每次分发的线程都会唤醒,此时进入了 dispatchOnce 方法,该方法 processAnrsLocked 会解析是否触发 anr 是的话触发 anr。并返回下次唤醒的时间。

void InputDispatcher::dispatchOnce() {const nsecs_t nextAnrCheck = processAnrsLocked();nextWakeupTime = std::min(nextWakeupTime, nextAnrCheck);
}

processAnrsLocked 方法会对里面的第一次的对象 超时时间做判断,取最小的时间,如果小于当前时间则触发 anr

 // 检查是否有anr事件有则引发anr,没有返回下次唤醒的时间// 检查是否有anr事件有则引发anr,没有返回下次唤醒的时间
nsecs_t InputDispatcher::processAnrsLocked() {const nsecs_t currentTime = now();nsecs_t nextAnrCheck = LONG_LONG_MAX;// Check if we are waiting for a focused window to appear. Raise ANR if waited too long// 该判断用于 key 时间的判断if (mNoFocusedWindowTimeoutTime.has_value() && mAwaitedFocusedApplication != nullptr) {if (currentTime >= *mNoFocusedWindowTimeoutTime) {processNoFocusedWindowAnrLocked();mAwaitedFocusedApplication.reset();mNoFocusedWindowTimeoutTime = std::nullopt;return LONG_LONG_MIN;} else {// Keep waiting. We will drop the event when mNoFocusedWindowTimeoutTime comes.nextAnrCheck = *mNoFocusedWindowTimeoutTime;}}// Check if any connection ANRs are due// 检查首个超时时间和下一次 anr 检查时间,默认是 LONG_LONG_MAX 取最小那个// 如果大于当前时间,则返回下次唤醒时间// 否则则进入anr触发nextAnrCheck = std::min(nextAnrCheck, mAnrTracker.firstTimeout());if (currentTime < nextAnrCheck) { // most likely scenarioreturn nextAnrCheck;          // everything is normal. Let's check again at nextAnrCheck}// If we reached here, we have an unresponsive connection.// 通过 token 获取对应的 connection,不为空 则sp<Connection> connection = getConnectionLocked(mAnrTracker.firstToken());if (connection == nullptr) {ALOGE("Could not find connection for entry %" PRId64, mAnrTracker.firstTimeout());return nextAnrCheck;}connection->responsive = false;// Stop waking up for this unresponsive connection// 删除存在一样的 connection 数据mAnrTracker.eraseToken(connection->inputChannel->getConnectionToken());// 进入anr触发onAnrLocked(connection);return LONG_LONG_MIN;
}

进入 anr 后,判断对应的 wq 队列是否为空,为空则不触发。主要通过 updateLastAnrStateLocked 保存当前的一些状态。 processConnectionUnresponsiveLocked 进入触发 anr 留存。

// 触发anr
void InputDispatcher::onAnrLocked(const sp<Connection>& connection) {if (connection == nullptr) {LOG_ALWAYS_FATAL("Caller must check for nullness");}// Since we are allowing the policy to extend the timeout, maybe the waitQueue// is already healthy again. Don't raise ANR in this situationif (connection->waitQueue.empty()) {ALOGI("Not raising ANR because the connection %s has recovered",connection->inputChannel->getName().c_str());return;}/*** The "oldestEntry" is the entry that was first sent to the application. That entry, however,* may not be the one that caused the timeout to occur. One possibility is that window timeout* has changed. This could cause newer entries to time out before the already dispatched* entries. In that situation, the newest entries caused ANR. But in all likelihood, the app* processes the events linearly. So providing information about the oldest entry seems to be* most useful.*/DispatchEntry* oldestEntry = *connection->waitQueue.begin();const nsecs_t currentWait = now() - oldestEntry->deliveryTime;std::string reason =android::base::StringPrintf("%s is not responding. Waited %" PRId64 "ms for %s",connection->inputChannel->getName().c_str(),ns2ms(currentWait),oldestEntry->eventEntry->getDescription().c_str());sp<IBinder> connectionToken = connection->inputChannel->getConnectionToken();updateLastAnrStateLocked(getWindowHandleLocked(connectionToken), reason);processConnectionUnresponsiveLocked(*connection, std::move(reason));// Stop waking up for events on this connection, it is already unresponsivecancelEventsForAnrLocked(connection);
}
void InputDispatcher::updateLastAnrStateLocked(const sp<WindowInfoHandle>& window,const std::string& reason) {const std::string windowLabel = getApplicationWindowLabel(nullptr, window);updateLastAnrStateLocked(windowLabel, reason);
}void InputDispatcher::updateLastAnrStateLocked(const InputApplicationHandle& application,const std::string& reason) {const std::string windowLabel = getApplicationWindowLabel(&application, nullptr);updateLastAnrStateLocked(windowLabel, reason);
}void InputDispatcher::updateLastAnrStateLocked(const std::string& windowLabel,const std::string& reason) {// Capture a record of the InputDispatcher state at the time of the ANR.time_t t = time(nullptr);struct tm tm;localtime_r(&t, &tm);char timestr[64];strftime(timestr, sizeof(timestr), "%F %T", &tm);mLastAnrState.clear();mLastAnrState += INDENT "ANR:\n";mLastAnrState += StringPrintf(INDENT2 "Time: %s\n", timestr);mLastAnrState += StringPrintf(INDENT2 "Reason: %s\n", reason.c_str());mLastAnrState += StringPrintf(INDENT2 "Window: %s\n", windowLabel.c_str());dumpDispatchStateLocked(mLastAnrState);
}

processConnectionUnresponsiveLocked 会判断是否模拟器,正常的window后,则会执行命令 sendWindowUnresponsiveCommandLocked 命令。也是将 对应的函数放进命令数组中。

void InputDispatcher::processConnectionUnresponsiveLocked(const Connection& connection,std::string reason) {const sp<IBinder>& connectionToken = connection.inputChannel->getConnectionToken();if (connection.monitor) {ALOGW("Monitor %s is unresponsive: %s", connection.inputChannel->getName().c_str(),reason.c_str());std::optional<int32_t> pid = findMonitorPidByTokenLocked(connectionToken);if (!pid.has_value()) {ALOGE("Could not find unresponsive monitor for connection %s",connection.inputChannel->getName().c_str());return;}sendMonitorUnresponsiveCommandLocked(pid.value(), std::move(reason));return;}// If not a monitor, must be a windowALOGW("Window %s is unresponsive: %s", connection.inputChannel->getName().c_str(),reason.c_str());sendWindowUnresponsiveCommandLocked(connectionToken, std::move(reason));
}

放进去数组

void InputDispatcher::sendWindowUnresponsiveCommandLocked(sp<IBinder> connectionToken,std::string reason) {std::unique_ptr<CommandEntry> windowUnresponsiveCommand = std::make_unique<CommandEntry>(&InputDispatcher::doNotifyWindowUnresponsiveLockedInterruptible);windowUnresponsiveCommand->connectionToken = std::move(connectionToken);windowUnresponsiveCommand->reason = std::move(reason);postCommandLocked(std::move(windowUnresponsiveCommand));
}

进入 方法后,之前文章也知道,最终调用 com_android_server_input_InputManagerServicenotifyWindowUnresponsive 方法。

// 这里比较特殊,先解锁在执行对应的方法,在进行加锁
void InputDispatcher::doNotifyWindowUnresponsiveLockedInterruptible(CommandEntry* commandEntry) {mLock.unlock();mPolicy->notifyWindowUnresponsive(commandEntry->connectionToken, commandEntry->reason);mLock.lock();
}

最终又会通过 jni 进入 inputMangerService java 类的 notifyWindowUnresponsive

void NativeInputManager::notifyWindowUnresponsive(const sp<IBinder>& token,const std::string& reason) {
#if DEBUG_INPUT_DISPATCHER_POLICYALOGD("notifyWindowUnresponsive");
#endifATRACE_CALL();JNIEnv* env = jniEnv();ScopedLocalFrame localFrame(env);jobject tokenObj = javaObjectForIBinder(env, token);ScopedLocalRef<jstring> reasonObj(env, env->NewStringUTF(reason.c_str()));env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifyWindowUnresponsive, tokenObj,reasonObj.get());checkAndClearExceptionFromCallback(env, "notifyWindowUnresponsive");
}

进入 InputManagerService 的 notifyWindowUnresponsive 方法后又会调用 mWindowManagerCallbacks的方法,
mWindowManagerCallbacks 是从 InputManagerService 初始化后 通过 setWindowManagerCallbacks时候,传入的变量。(在SystemServer中)。可以看到 该callBack调用的是 wm对应的变量

private void notifyWindowUnresponsive(IBinder token, String reason) {mWindowManagerCallbacks.notifyWindowUnresponsive(token, reason);}
// frameworks/base/services/java/com/android/server/SystemServer.java
t.traceBegin("StartInputManager");inputManager.setWindowManagerCallbacks(wm.getInputManagerCallback());// 通过start 启动事件监听循环inputManager.start();t.traceEnd();

WindowManagerService. 对应的 mInputManagerCallback 对象为 InputManagerCallback。所以查看 InputManagerCallback 对应实现的方法 又调用了 AnrController 下的方法

	@Overridepublic void notifyWindowUnresponsive(@NonNull IBinder token, String reason) {mService.mAnrController.notifyWindowUnresponsive(token, reason);}

notifyWindowUnresponsive 方法主要 对 saveANRState 方法进行保存 ,最后在调用 inputDispatchingTimedOut 方法进行窗口提醒。mAmInternal 为 ActivityManagerInternal。其实现为 AMS的 内部类 LocalService。

	void notifyWindowUnresponsive(IBinder inputToken, String reason) {preDumpIfLockTooSlow();final int pid;final boolean aboveSystem;final ActivityRecord activity;synchronized (mService.mGlobalLock) {InputTarget target = mService.getInputTargetFromToken(inputToken);if (target == null) {Slog.e(TAG_WM, "Unknown token, dropping notifyConnectionUnresponsive request");return;}WindowState windowState = target.getWindowState();pid = target.getPid();// Blame the activity if the input token belongs to the window. If the target is// embedded, then we will blame the pid instead.activity = (windowState.mInputChannelToken == inputToken)? windowState.mActivityRecord : null;Slog.i(TAG_WM, "ANR in " + target + ". Reason:" + reason);aboveSystem = isWindowAboveSystem(windowState);dumpAnrStateLocked(activity, windowState, reason);}if (activity != null) {activity.inputDispatchingTimedOut(reason, pid);} else {mService.mAmInternal.inputDispatchingTimedOut(pid, aboveSystem, reason);}}

查看对应的 inputDispatchingTimedOut 方法,又调用了 ActivityManagerService里面的inputDispatchingTimedOut 方法,该方法里面主要调用了 inputDispatchingTimedOut进一步处理。

		@Overridepublic long inputDispatchingTimedOut(int pid, boolean aboveSystem, String reason) {return ActivityManagerService.this.inputDispatchingTimedOut(pid, aboveSystem, reason);}
	long inputDispatchingTimedOut(int pid, final boolean aboveSystem, String reason) {if (checkCallingPermission(FILTER_EVENTS) != PackageManager.PERMISSION_GRANTED) {throw new SecurityException("Requires permission " + FILTER_EVENTS);}ProcessRecord proc;synchronized (mPidsSelfLocked) {proc = mPidsSelfLocked.get(pid);}final long timeoutMillis = proc != null ? proc.getInputDispatchingTimeoutMillis() :DEFAULT_DISPATCHING_TIMEOUT_MILLIS;if (inputDispatchingTimedOut(proc, null, null, null, null, aboveSystem, reason)) {return 0;}return timeoutMillis;}

inputDispatchingTimedOut 又调用了 AnrHelper 类的 appNotResponding 方法

boolean inputDispatchingTimedOut(ProcessRecord proc, String activityShortComponentName,ApplicationInfo aInfo, String parentShortComponentName,WindowProcessController parentProcess, boolean aboveSystem, String reason) {if (checkCallingPermission(FILTER_EVENTS) != PackageManager.PERMISSION_GRANTED) {throw new SecurityException("Requires permission " + FILTER_EVENTS);}final String annotation;if (reason == null) {annotation = "Input dispatching timed out";} else {annotation = "Input dispatching timed out (" + reason + ")";}if (proc != null) {synchronized (this) {if (proc.isDebugging()) {return false;}if (proc.getActiveInstrumentation() != null) {Bundle info = new Bundle();info.putString("shortMsg", "keyDispatchingTimedOut");info.putString("longMsg", annotation);finishInstrumentationLocked(proc, Activity.RESULT_CANCELED, info);return true;}}mAnrHelper.appNotResponding(proc, activityShortComponentName, aInfo,parentShortComponentName, parentProcess, aboveSystem, annotation);}return true;}

appNotResponding 方法 构造对应的 AnrRecord 对象,并启动线程任务 startAnrConsumerIfNeeded

	void appNotResponding(ProcessRecord anrProcess, String activityShortComponentName,ApplicationInfo aInfo, String parentShortComponentName,WindowProcessController parentProcess, boolean aboveSystem, String annotation) {synchronized (mAnrRecords) {mAnrRecords.add(new AnrRecord(anrProcess, activityShortComponentName, aInfo,parentShortComponentName, parentProcess, aboveSystem, annotation));}startAnrConsumerIfNeeded();}

查看 AnrConsumerThread 的run 方法。通过 循环next函数 遍历 mAnrRecords 数组是否为空,不为空返回第一个,并计算时间 调用对应 ,调用对应的 AnrRecord 的 appNotResponding 方法。

private class AnrConsumerThread extends Thread {AnrConsumerThread() {super("AnrConsumer");}private AnrRecord next() {synchronized (mAnrRecords) {return mAnrRecords.isEmpty() ? null : mAnrRecords.remove(0);}}@Overridepublic void run() {AnrRecord r;while ((r = next()) != null) {scheduleBinderHeavyHitterAutoSamplerIfNecessary();final long startTime = SystemClock.uptimeMillis();// If there are many ANR at the same time, the latency may be larger. If the latency// is too large, the stack trace might not be meaningful.final long reportLatency = startTime - r.mTimestamp;final boolean onlyDumpSelf = reportLatency > EXPIRED_REPORT_TIME_MS;r.appNotResponding(onlyDumpSelf);final long endTime = SystemClock.uptimeMillis();Slog.d(TAG, "Completed ANR of " + r.mApp.processName + " in "+ (endTime - startTime) + "ms, latency " + reportLatency+ (onlyDumpSelf ? "ms (expired, only dump ANR app)" : "ms"));}mRunning.set(false);synchronized (mAnrRecords) {// The race should be unlikely to happen. Just to make sure we don't miss.if (!mAnrRecords.isEmpty()) {startAnrConsumerIfNeeded();}}}}

接着又调用 ProcessErrorStateRecord 的 appNotResponding 方法。

		void appNotResponding(boolean onlyDumpSelf) {mApp.mErrorState.appNotResponding(mActivityShortComponentName, mAppInfo,mParentShortComponentName, mParentProcess, mAboveSystem, mAnnotation,onlyDumpSelf);}

进入该方法 将各种 anr 的原因 拼接起来纪录下来,最终输出,并通过构造 AppNotRespondingDialog 方法 对应的message对象后,通过 hander 发送 SHOW_NOT_RESPONDING_UI_MSG 事件,进行弹框显示。整个anr 流程就结束了。

	void appNotResponding(String activityShortComponentName, ApplicationInfo aInfo,String parentShortComponentName, WindowProcessController parentProcess,boolean aboveSystem, String annotation, boolean onlyDumpSelf) {ArrayList<Integer> firstPids = new ArrayList<>(5);SparseArray<Boolean> lastPids = new SparseArray<>(20);mApp.getWindowProcessController().appEarlyNotResponding(annotation, () -> {synchronized (mService) {mApp.killLocked("anr", ApplicationExitInfo.REASON_ANR, true);}});...// Log the ANR to the main log.StringBuilder info = new StringBuilder();info.setLength(0);FrameworkStatsLog.write(FrameworkStatsLog.ANR_OCCURRED, mApp.uid, mApp.processName,activityShortComponentName == null ? "unknown" : activityShortComponentName,annotation,...final ProcessRecord parentPr = parentProcess != null? (ProcessRecord) parentProcess.mOwner : null;mService.addErrorToDropBox("anr", mApp, mApp.processName, activityShortComponentName,parentShortComponentName, parentPr, null, report.toString(), tracesFile,null, new Float(loadingProgress), incrementalMetrics, errorId);if (mApp.getWindowProcessController().appNotResponding(info.toString(),() -> {synchronized (mService) {mApp.killLocked("anr", ApplicationExitInfo.REASON_ANR, true);}},() -> {synchronized (mService) {mService.mServices.scheduleServiceTimeoutLocked(mApp);}})) {return;}synchronized (mService) {...// mUiHandler can be null if the AMS is constructed with injector only. This will only// happen in tests.if (mService.mUiHandler != null) {// Bring up the infamous App Not Responding dialogMessage msg = Message.obtain();msg.what = ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG;msg.obj = new AppNotRespondingDialog.Data(mApp, aInfo, aboveSystem);mService.mUiHandler.sendMessageDelayed(msg, anrDialogDelayMs);}}}

3. 正常响应

正常的触摸,需要移除对应的数据,移除的时机,在事件流程通过调用 doDispatchCycleFinishedLockedInterruptible 的时候,在移除wq的时候,会移除对应的 anr监听数据

void InputDispatcher::doDispatchCycleFinishedLockedInterruptible(CommandEntry* commandEntry) {...// 二次判断dispatchEntryIt = connection->findWaitQueueEntry(seq);if (dispatchEntryIt != connection->waitQueue.end()) {dispatchEntry = *dispatchEntryIt;// 删除wqconnection->waitQueue.erase(dispatchEntryIt);const sp<IBinder>& connectionToken = connection->inputChannel->getConnectionToken();// 移除对应的anr数据监听mAnrTracker.erase(dispatchEntry->timeoutTime, connectionToken);...}// Start the next dispatch cycle for this connection.startDispatchCycleLocked(now(), connection);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/462304.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

D55【python 接口自动化学习】- python基础之模块与标准库

day55 练习&#xff1a;实现求导 学习日期&#xff1a;20241101 学习目标&#xff1a;模块与标准库 -- 70 小试牛刀&#xff1a;如何使用Python为函数求导&#xff1f; 学习笔记&#xff1a; 需求分析 使用第三方模块实现函数求导 编写程序并测试 # 求导 from sympy import…

推荐一款功能强大的AI实时变声器:FliFlik Voice Changer

FliFlik VoiCE Changer是一款专注于声音变换与音频处理的创新软件&#xff0c;旨在满足从日常娱乐、游戏直播到播客制作、专业音频编辑的多种应用场景需求。无论是想在游戏中变换声音逗乐队友&#xff0c;还是在播客中塑造个性化的音效&#xff0c;这款软件都能提供灵活而强大的…

Spring Boot技术栈:打造大学城水电管理系统

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…

2024 IC行业还能不能入了?

打个有趣的比方&#xff0c;18年以前入行IC的&#xff0c;就业前就知道或者就业后才知道&#xff0c;是去吃席只不过是“农村酒席”&#xff0c;但不至于吃坏肚子。对于这种阵仗&#xff0c;不是每个人都愿意去的&#xff0c;即便是在西电这样的院校&#xff0c;当年也有一些同…

基于双向长短期记忆网络(BiLSTM)的时间序列数据预测,15个输入1个输出,可以更改数据集,MATLAB代码

1. 数据收集与预处理 数据清洗&#xff1a;处理缺失值、异常值等。特征工程&#xff1a;提取有助于预测的特征。数据标准化&#xff1a;将时间序列数据标准化&#xff0c;使其具有零均值和单位方差&#xff0c;有助于模型训练。滑动窗口划分&#xff1a;将时间序列数据划分为多…

基于Python可视化的热门微博数据分析系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于pythondjangovueMySQL的热…

推荐一款非常好用的3d设计软件:LuBan 3D

LuBan 3D是一款非常好用的3d设计软件&#xff0c;这款软件提供独特的3D的模型以及设计的理念&#xff0c;还可以自动为用户处理像三角形OBJ&#xff0c;PLY等网格模型。 基本简介 LuBan 3D是一款非常小巧但是功能十分强大的生成式设计软件。这款软件对于设计师和制作们来说非常…

Comfyui-Flux写实人像摄影风格探索

在一些小伙伴的建议下&#xff0c;我最近开始着手整理ComfyUI的相关内容。其实之前就一直在关注这个工具&#xff0c;但由于工作繁忙&#xff0c;一直没能抽出时间去总结。 与SD webui不同的是,comfyui有着极高的自由度和灵活性&#xff0c;支持高度的定制化和工作流复用&#…

qt QPicture详解

1、概述 QPicture类是Qt框架中的一个重要图形类&#xff0c;它主要用于记录和回放QPainter的绘图指令。这个类能够跨平台、无分辨率依赖地绘制图形&#xff0c;非常适合用于实现打印预览和图像操作等场景。QPicture可以将绘图操作序列化为一种独立于平台的格式&#xff0c;保存…

Axure使用动态面板制作新闻栏目高级交互

亲爱的小伙伴&#xff0c;在您浏览之前&#xff0c;烦请关注一下&#xff0c;在此深表感谢&#xff01; 课程主题&#xff1a;使用动态面板制作新闻栏目 主要内容&#xff1a;动态面板State切换、控制&#xff1b;动态面板滚动设置&#xff1b;设置选中 应用场景&#xff1a…

Jupyter lab 打开时默认使用 Notebook 而不是浏览器

Jupyter lab 打开时默认使用 Notebook 而不是浏览器 正文 正文 今天遇到了一个特别有意思的事情&#xff0c;这里我们以 Windows \textrm{Windows} Windows 系统举例。 我们知道通常我们需要使用如下代码在 Terminal \textrm{Terminal} Terminal 中打开 Jupyter lab \textr…

h5小游戏5--杀死国王(附源码)

源代码如下 1.游戏基本操作 用空格键攻击&#xff0c;kill the king。 css样式源码 charset "UTF-8";font-face {font-family: "AddLGBitmap09";src: url("https://assets.codepen.io/217233/AddLGBitmap09.woff2") format("woff2"…

HtmlAgilityPack 操作详解

目录 1.安装 HtmlAgilityPack 2. 示例 HTML 3. 使用 HtmlAgilityPack 进行 HTML 解析与操作 4. 代码详解 1.加载html文档 2.选择元素 3. 提取属性 4.修改属性 5.常用的几种获取元素的 XPath 写法 HtmlAgilityPack&#xff1a; 轻量且高效&#xff0c;适合进行常规的 H…

图形学常识 | RVT和图像处理

目录 Runtime virtual texture 实时虚拟纹理RVT RVT应用1&#xff1a; 引擎中开启Virtual Texture support vLevel floor[F d(uv)/dx, d(uv)/dy) Random(-0.25,0.25)] RVT的应用2 svt和rvt的区别 双线性过滤和三线性过滤的区别 UE的PixelNormalWS节点 数字图像处理 …

操作符详解

操作符也被叫做&#xff1a;运算符。 操作符的分类 算术操作符&#xff1a; 、- 、* 、/ 、%赋值操作符&#xff1a; 、 、 - 、 * 、 / 、% 、<< 、>> 、& 、| 、^移位操作符&#xff1a;<< >>位操作符&#xff1a;& | ^ ~单目操作符&#…

7、lvm逻辑卷和磁盘配额

lvm逻辑卷概念 lvm基本概念 Lvm 是 Logical Volume Manager 的简称&#xff1a;逻辑卷管理Linux系统下管理硬盘分区的一种机制。lvm适合于管理大存储设备。用户可以动态的对硬盘进行扩容&#xff08;缩容&#xff09;。我们只关心使用层面&#xff0c;对于物理底层&#xff0…

WebGPU跨平台应用开发

对于 Web 开发人员来说&#xff0c;WebGPU 是一个 Web 图形 API&#xff0c;可提供对 GPU 的统一和快速访问。WebGPU 公开了现代硬件功能&#xff0c;并允许在 GPU 上进行渲染和计算操作&#xff0c;类似于 Direct3D 12、Metal 和 Vulkan。 虽然这是真的&#xff0c;但这个故事…

Java项目实战II基于Java+Spring Boot+MySQL的智能推荐的卫生健康系统(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 基于Java、…

DDR Study - PIM Technical

参考来源&#xff1a;In-memory processing - Wikipedia&#xff0c;What is processing in memory (PIM) and how does it work? (techtarget.com)&#xff0c;《Processing-in-memory: A workload-driven perspective》 LPDDR Initial → LPDDR Write Leveling and DQ Train…

企业如何通过架构蓝图实现数字化转型

数字化转型的关键——架构蓝图的力量 在当今的商业世界&#xff0c;数字化转型已经不再是一个选择&#xff0c;而是企业生存与发展不可回避的战略行动。企业希望通过数字化提高效率、增强灵活性&#xff0c;并为客户提供更好的体验。然而&#xff0c;数字化转型不仅仅涉及技术…