当前位置: 首页 > news >正文

update_engine-FilesystemVerifierAction和PostinstallRunnerAction

在介绍完了DownloadAction之后,还剩下FilesystemVerifierAction和PostinstallRunnerAction,下面开始对其进行分析。

FilesystemVerifierAction

在数据下载完成后,在DownloadAction中会切换到FilesystemVerifierAction

void DownloadAction::TransferComplete(HttpFetcher* fetcher, bool successful) {
  if (writer_) {
   ........
  // Write the path to the output pipe if we're successful.
  if (code == ErrorCode::kSuccess && HasOutputPipe())
    SetOutputObject(install_plan_);
  processor_->ActionComplete(this, code);
}

最后的ActionComplete会开始执行FilesystemVerifierAction。

src/system/update_engine/payload_consumer/filesystem_verifer_action.cc

 1 void FilesystemVerifierAction::PerformAction() {2   // Will tell the ActionProcessor we've failed if we return.3   ScopedActionCompleter abort_action_completer(processor_, this);4 5   if (!HasInputObject()) {6     LOG(ERROR) << "FilesystemVerifierAction missing input object.";7     return;8   }9   install_plan_ = GetInputObject();   //获取上一个Action传过来的install_plan_
10 
11   if (install_plan_.partitions.empty()) {
12     LOG(INFO) << "No partitions to verify.";
13     if (HasOutputPipe())
14       SetOutputObject(install_plan_);
15     abort_action_completer.set_code(ErrorCode::kSuccess);
16     return;
17   }
18 
19   StartPartitionHashing();      //开始计算分区的hash
20   abort_action_completer.set_should_complete(false);
21 }

  接着看StartPartitionHashing

 1 void FilesystemVerifierAction::StartPartitionHashing() {2   if (partition_index_ == install_plan_.partitions.size()) {       //判断是否验证到了最后一个分区3     Cleanup(ErrorCode::kSuccess);4     return;5   }6   InstallPlan::Partition& partition =7       install_plan_.partitions[partition_index_];8 9   string part_path;         
10   switch (verifier_step_) {                    //默认值是KVerifyTargetHash
11     case VerifierStep::kVerifySourceHash:
12       part_path = partition.source_path;
13       remaining_size_ = partition.source_size;
14       break;
15     case VerifierStep::kVerifyTargetHash:
16       part_path = partition.target_path;         //分区的路径
17       remaining_size_ = partition.target_size;   //大小
18       break;
19   }
20   LOG(INFO) << "Hashing partition " << partition_index_ << " ("
21             << partition.name << ") on device " << part_path;
22   if (part_path.empty())
23     return Cleanup(ErrorCode::kFilesystemVerifierError);
24 
25   brillo::ErrorPtr error;
26   src_stream_ = brillo::FileStream::Open(             //打开对应的分区文件
27       base::FilePath(part_path),
28       brillo::Stream::AccessMode::READ,
29       brillo::FileStream::Disposition::OPEN_EXISTING,
30       &error);
31 
32   if (!src_stream_) {
33     LOG(ERROR) << "Unable to open " << part_path << " for reading";
34     return Cleanup(ErrorCode::kFilesystemVerifierError);
35   }
36 
37   buffer_.resize(kReadFileBufferSize);   //重置缓存区的大小
38   read_done_ = false;                    //未被读取完成
39   hasher_.reset(new HashCalculator());   //设置HashCalculator
40 
41   // Start the first read.
42   ScheduleRead();               //开始读取
43 }

 首先判断是否验证的分区的所有hash,如果验证完成了,调用CleanUp做最后的工作。

CleanUp

 1 void FilesystemVerifierAction::Cleanup(ErrorCode code) {2   src_stream_.reset();3   // This memory is not used anymore.4   buffer_.clear();5 6   if (cancelled_)7     return;8   if (code == ErrorCode::kSuccess && HasOutputPipe())9     SetOutputObject(install_plan_);
10   processor_->ActionComplete(this, code);
11 }

可以看到主要就是清空缓存区,设置install_plan_,切换到下一个Action。如果没有验证完成,就获取要验证的分区路径和大小,这个大小只是要验证的大小,不一定是分区的真正大小。对于镜像文件而言1G的大小能被安装在2G的分区上。接下来调用ScheduleRead()开始进行验证。

ScheduleRead()

 1 void FilesystemVerifierAction::ScheduleRead() {2   size_t bytes_to_read = std::min(static_cast<int64_t>(buffer_.size()), 3                                   remaining_size_);  //获取要读取数据的大小4   if (!bytes_to_read) {   //读取完成5     OnReadDoneCallback(0);6     return;7   }8 9   bool read_async_ok = src_stream_->ReadAsync(
10     buffer_.data(),
11     bytes_to_read,
12     base::Bind(&FilesystemVerifierAction::OnReadDoneCallback,
13                base::Unretained(this)),
14     base::Bind(&FilesystemVerifierAction::OnReadErrorCallback,
15                base::Unretained(this)),
16     nullptr);  //开始读取
17 
18   if (!read_async_ok) {
19     LOG(ERROR) << "Unable to schedule an asynchronous read from the stream.";
20     Cleanup(ErrorCode::kError);
21   }
22 }

获取读取数据的真实大小,开始读取数据。

 1 void FilesystemVerifierAction::OnReadDoneCallback(size_t bytes_read) {2   if (bytes_read == 0) {        //读取完成3     read_done_ = true;4   } else {5     remaining_size_ -= bytes_read;  6     CHECK(!read_done_);                                     7     if (!hasher_->Update(buffer_.data(), bytes_read)) {   //计算hash8       LOG(ERROR) << "Unable to update the hash.";9       Cleanup(ErrorCode::kError);
10       return;
11     }
12   }
13 
14   // We either terminate the current partition or have more data to read.
15   if (cancelled_)
16     return Cleanup(ErrorCode::kError);
17 
18   if (read_done_ || remaining_size_ == 0) {
19     if (remaining_size_ != 0) {
20       LOG(ERROR) << "Failed to read the remaining " << remaining_size_
21                  << " bytes from partition "
22                  << install_plan_.partitions[partition_index_].name;
23       return Cleanup(ErrorCode::kFilesystemVerifierError);
24     }
25     return FinishPartitionHashing();   //计算完成后
26   }
27   ScheduleRead();   //如果没有计算完成,继续计读取计算
28 }

在这个方法中会对读取的数据进行hash计算,每次计算其实都是基于前一次的计算结果来进行的,不然就会有太对的数据加载到内存中,导致内存不足。当计算完成后

 1 void FilesystemVerifierAction::FinishPartitionHashing() {2   if (!hasher_->Finalize()) {3     LOG(ERROR) << "Unable to finalize the hash.";4     return Cleanup(ErrorCode::kError);5   }6   InstallPlan::Partition& partition =7       install_plan_.partitions[partition_index_];8   LOG(INFO) << "Hash of " << partition.name << ": "9             << Base64Encode(hasher_->raw_hash()); 
10 
11   switch (verifier_step_) {
12     case VerifierStep::kVerifyTargetHash:
13       if (partition.target_hash != hasher_->raw_hash()) {   //对保存的targethash和计算得到的hash进行一个比较
14         LOG(ERROR) << "New '" << partition.name
15                    << "' partition verification failed.";
16         if (partition.source_hash.empty()) {
17           // No need to verify source if it is a full payload.
18           return Cleanup(ErrorCode::kNewRootfsVerificationError);
19         }
20         // If we have not verified source partition yet, now that the target
21         // partition does not match, and it's not a full payload, we need to
22         // switch to kVerifySourceHash step to check if it's because the source
23         // partition does not match either.
24         verifier_step_ = VerifierStep::kVerifySourceHash;  //计算source hash
25       } else {
26         partition_index_++;   //计算下一个分区
27       }
28       break;
29     case VerifierStep::kVerifySourceHash:
30       if (partition.source_hash != hasher_->raw_hash()) {  //保存的source hash和计算得到的也不相同
31         LOG(ERROR) << "Old '" << partition.name
32                    << "' partition verification failed.";
33         LOG(ERROR) << "This is a server-side error due to mismatched delta"
34                    << " update image!";
35         LOG(ERROR) << "The delta I've been given contains a " << partition.name
36                    << " delta update that must be applied over a "
37                    << partition.name << " with a specific checksum, but the "
38                    << partition.name
39                    << " we're starting with doesn't have that checksum! This"
40                       " means that the delta I've been given doesn't match my"
41                       " existing system. The "
42                    << partition.name << " partition I have has hash: "
43                    << Base64Encode(hasher_->raw_hash())
44                    << " but the update expected me to have "
45                    << Base64Encode(partition.source_hash) << " .";
46         LOG(INFO) << "To get the checksum of the " << partition.name
47                   << " partition run this command: dd if="
48                   << partition.source_path
49                   << " bs=1M count=" << partition.source_size
50                   << " iflag=count_bytes 2>/dev/null | openssl dgst -sha256 "
51                      "-binary | openssl base64";
52         LOG(INFO) << "To get the checksum of partitions in a bin file, "
53                   << "run: .../src/scripts/sha256_partitions.sh .../file.bin";
54         return Cleanup(ErrorCode::kDownloadStateInitializationError);
55       }
56       // The action will skip kVerifySourceHash step if target partition hash
57       // matches, if we are in this step, it means target hash does not match,
58       // and now that the source partition hash matches, we should set the error
59       // code to reflect the error in target partition.
60       // We only need to verify the source partition which the target hash does
61       // not match, the rest of the partitions don't matter.
62       return Cleanup(ErrorCode::kNewRootfsVerificationError);
63   }
64   // Start hashing the next partition, if any.
65   hasher_.reset();   //重置hash计算器
66   buffer_.clear();  //清空缓存
67   src_stream_->CloseBlocking(nullptr);
68   StartPartitionHashing(); //接着计算
69 }

 可见当一个分区的hash被计算出来的时候就会根据保存好的进行比较,如果target的hash不一致就会转向比较该分区的source hash,其实比较source hash主要就是为了确定错误的类型,只要target hash不一致,无论source hash是否一致都不会继续下一个分区的计算了。就这样一直到最后一个分区验证完后,执行最后一个Action,PostinstallRunnerAction。

PostinstallRunnerAction

PostinstallRunnerAction执行每个分区更新完后的postinstall script。但是在高通平台的,android8.0上无论是全包还是差分包升级并没有实质性的postinstall script。在PostinstallRunnerAction中仅仅是将target_slot标记为active状态。目前只分析于执行相关的代码。

src/system/update_engine/payload_consumer/postinstall_runner_action.cc

 1 void PostinstallRunnerAction::PerformAction() {2   CHECK(HasInputObject());3   install_plan_ = GetInputObject();   //获取install_plan_4 5   if (install_plan_.powerwash_required) {    //是否需要进行数据的擦除6     if (hardware_->SchedulePowerwash()) {7       powerwash_scheduled_ = true;8     } else {9       return CompletePostinstall(ErrorCode::kPostinstallPowerwashError);
10     }
11   }
12 
13   // Initialize all the partition weights.
14   partition_weight_.resize(install_plan_.partitions.size());  //初始化每个分区的权重
15   total_weight_ = 0;
16   for (size_t i = 0; i < install_plan_.partitions.size(); ++i) {
17     // TODO(deymo): This code sets the weight to all the postinstall commands,
18     // but we could remember how long they took in the past and use those
19     // values.
20     partition_weight_[i] = install_plan_.partitions[i].run_postinstall;
21     total_weight_ += partition_weight_[i];  //计算总的权重
22   }
23   accumulated_weight_ = 0;
24   ReportProgress(0);                      //更新进度
25 
26   PerformPartitionPostinstall();          //开始真正的流程
27 }

来看PerformPartitionPostinstall()

 1 void PostinstallRunnerAction::PerformPartitionPostinstall() {2   if (install_plan_.download_url.empty()) {3     LOG(INFO) << "Skipping post-install during rollback";4     return CompletePostinstall(ErrorCode::kSuccess);5   }6 7   // Skip all the partitions that don't have a post-install step.8   while (current_partition_ < install_plan_.partitions.size() &&9          !install_plan_.partitions[current_partition_].run_postinstall) {   //run_postinstall为false
10     VLOG(1) << "Skipping post-install on partition "
11             << install_plan_.partitions[current_partition_].name;
12     current_partition_++;
13   }
14   if (current_partition_ == install_plan_.partitions.size())
15     return CompletePostinstall(ErrorCode::kSuccess);
16   ...................
17   ...................
18   ...................
19 }

在当前分析中run_postinstall为false,会跳过post-install。之后会直接执行CompletePostinstall(ErrorCode::kSuccess)

 1 void PostinstallRunnerAction::CompletePostinstall(ErrorCode error_code) {2   // We only attempt to mark the new slot as active if all the postinstall3   // steps succeeded.4   if (error_code == ErrorCode::kSuccess &&5       !boot_control_->SetActiveBootSlot(install_plan_.target_slot)) {   //设置target_slot为active6     error_code = ErrorCode::kPostinstallRunnerError;7   }8 9   ScopedActionCompleter completer(processor_, this);
10   completer.set_code(error_code);
11 
12   if (error_code != ErrorCode::kSuccess) {
13     LOG(ERROR) << "Postinstall action failed.";
14 
15     // Undo any changes done to trigger Powerwash.
16     if (powerwash_scheduled_)
17       hardware_->CancelPowerwash();
18 
19     return;
20   }
21 
22   LOG(INFO) << "All post-install commands succeeded";
23   if (HasOutputPipe()) {                      //设置输出的install_plan
24     SetOutputObject(install_plan_);
25   }
26 }

最终将target_slot设置为active在重启之后就会从target_slot开始启动了。

分析到这里就算是对update_engine的核心过程有了个大概的了解,除了对升级的知识点的认识,还体会到了它的架构。不足之处就是还有很多的细节未涉及。

相关文章:

update_engine-FilesystemVerifierAction和PostinstallRunnerAction

在介绍完了DownloadAction之后&#xff0c;还剩下FilesystemVerifierAction和PostinstallRunnerAction&#xff0c;下面开始对其进行分析。 FilesystemVerifierAction 在数据下载完成后&#xff0c;在DownloadAction中会切换到FilesystemVerifierAction void DownloadAction:…...

深度学习乳腺癌分类 计算机竞赛

文章目录 1 前言2 前言3 数据集3.1 良性样本3.2 病变样本 4 开发环境5 代码实现5.1 实现流程5.2 部分代码实现5.2.1 导入库5.2.2 图像加载5.2.3 标记5.2.4 分组5.2.5 构建模型训练 6 分析指标6.1 精度&#xff0c;召回率和F1度量6.2 混淆矩阵 7 结果和结论8 最后 1 前言 &…...

【Python百宝箱】掌握Python Web开发三剑客:Flask、Django、FastAPI一网打尽

前言 在当今互联网时代&#xff0c;Web应用的开发变得愈发重要和复杂。选择一个合适的Web框架&#xff0c;掌握安全性与认证、数据库与ORM库、前端框架与交互、测试与调试工具等关键知识点&#xff0c;是每个Web开发者都必须面对的挑战。本文将带你深入了解三个流行的Python W…...

【人工智能时代的刑法体系与责任主体概述】

第一节&#xff1a;引言 随着科技的快速发展&#xff0c;人工智能 (Artificial Intelligence, AI) 正日益成为我们生活中不可或缺的一部分。从自动驾驶汽车到语音助手&#xff0c;从智能家居到金融机器人&#xff0c;AI 的广泛应用正不断改变着我们的生活方式和社会结构。然而…...

透视maven打包编译正常,intellj idea编译失败问题的本质

前言 maven多模块类型的项目&#xff0c;在Java的中大型应用中非常常见&#xff0c; 在 module 很多的情况&#xff0c;经常会出现各种各样的编辑依赖错误问题&#xff0c;今天记录一种比较常见的 case &#xff1a; A 子模块依赖 B 子模块&#xff0c;在 Terminal 上终端上 …...

npm报错

npm报错 npm ERR! Fix the upstream dependency conflict, or retry npm ERR! this command with --force or --legacy-peer-deps npm ERR! to accept an incorrect (and potentially broken) dependency resolution. npm ERR! npm ERR! npm ERR! For a full report s…...

【FFmpeg实战】ffmpeg播放器-音视频解码流程

音视频介绍 音视频解码流程 FFmpeg解码的数据结构说明 AVFormatContext&#xff1a;封装格式上下文结构体,全局结构体,保存了视频文件封装格式相关信息AVInputFormat&#xff1a;每种封装格式&#xff0c;对应一个该结构体AVStream[0]&#xff1a;视频文件中每个视频&#xff…...

基于SSM的高校毕业选题管理系统设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…...

一个简单的Oracle Redaction实验

本实验包含了&#xff1a; 简单的Oracle Redaction演示针对指定用户的Redaction 实验环境 假设有一个19c多租户数据库&#xff0c;PDB名为orclpdb1。 我们将在orclpdb1中建立2个用户&#xff1a; redact_user: redact管理员schema_user: schema用户 基础实验 首先进入数…...

getchar函数的功能有哪些

getchar函数是C语言标准库中的一个函数&#xff0c;主要用于从标准输入&#xff08;通常是键盘&#xff09;获取一个字符。它的功能包括&#xff1a; 从标准输入获取一个字符&#xff1a;getchar函数会等待用户输入一个字符&#xff0c;然后将其返回给程序。可以通过控制台输入…...

信息机房监控系统(动环辅助监控系统)

信息机房监控系统是一个综合性的系统&#xff0c;用于对机房的所有设备及其环境进行集中监控和管理。这种系统主要针对机房的各个子系统进行监控&#xff0c;包括动力系统、环境系统、消防系统、保安系统、网络系统等。 依托电易云-智慧电力物联网&#xff0c;以下是信息机房监…...

最强英文开源模型Llama2架构与技术细节探秘

prerequisite: 最强英文开源模型LLaMA架构探秘&#xff0c;从原理到源码 Llama2 Meta AI于2023年7月19日宣布开源LLaMA模型的二代版本Llama2&#xff0c;并在原来基础上允许免费用于研究和商用。 作为LLaMA的延续和升级&#xff0c;Llama2的训练数据扩充了40%&#xff0c;达到…...

编程刷题网站以及实用型网站推荐

1、牛客网在线编程 牛客网在线编程https://www.nowcoder.com/exam/oj?page1&tab%E8%AF%AD%E6%B3%95%E7%AF%87&topicId220 2、力扣 力扣https://leetcode.cn/problemset/all/ 3、练码 练码https://www.lintcode.com/ 4、PTA | 程序设计类实验辅助教学平台 PTA | 程…...

基于STC12C5A60S2系列1T 8051单片机的SPI总线器件数模芯片TLC5615实现数模转换应用

基于STC12C5A60S2系列1T 8051单片的SPI总线器件数模芯片TLC5615实现数模转换应用 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式及配置STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式介绍SPI总线器件数模芯片TLC5615介绍通过按…...

【并发编程】Synchronized的使用

&#x1f4eb;作者简介&#xff1a;小明java问道之路&#xff0c;2022年度博客之星全国TOP3&#xff0c;专注于后端、中间件、计算机底层、架构设计演进与稳定性建设优化&#xff0c;文章内容兼具广度、深度、大厂技术方案&#xff0c;对待技术喜欢推理加验证&#xff0c;就职于…...

【Python】Python基础

文章目录 一、字面值常量和表达式二、变量2.1 定义变量2.2 变量的命名规则2.3 变量的类型2.4 不同类型大小2.5 动态类型 三、注释四、输入与输出五、运算符5.1 算术运算符5.2 关系运算符5.3 逻辑运算符5.4 赋值运算符 一、字面值常量和表达式 print(1 2 * 3) # 7 print(1 2 …...

gitlab环境准备

1.准备环境 gitlab只支持linux系统&#xff0c;本人在虚拟机下使用Ubuntu作为操作系统&#xff0c;gitlab镜像要使用和操作系统版本对应的版本&#xff0c;(ubuntu18.04,gitlab-ce_13.2.3-ce.0_amd64 .deb) book100ask:/$ lsb_release -a No LSB modules are available. Dist…...

Apache Doris (五十四): Doris Join类型 - Bucket Shuffle Join

🏡 个人主页:IT贫道_大数据OLAP体系技术栈,Apache Doris,Clickhouse 技术-CSDN博客 🚩 私聊博主:加入大数据技术讨论群聊,获取更多大数据资料。 🔔 博主个人B栈地址:豹哥教你大数据的个人空间-豹哥教你大数据个人主页-哔哩哔哩视频 目录...

【AI】行业消息精选和分析(23-11-20)

技术发展 &#x1f3a8; LCM即时绘画&#xff0c;体验所见所得&#xff1a; - LCM LoRA支持即时绘图生成&#xff0c;体验直观。 - 在线体验地址提供直接访问。 - 清华大学SimianLuo开发&#xff0c;加速稳定扩散模型运行。 &#x1f48a; VM Pill&#xff1a;可吞咽装置追踪生…...

Matplotlib实现Label及Title都在下方的最佳姿势

Matplotlib实现Label及Title都在下方的最佳姿势 1. 问题背景2. 基本思想&#xff08;可以不看&#xff09;3. 方法封装4. 调用实例5. 总结6. 起飞 1. 问题背景 用python绘制下面这种图的时候&#xff0c;一般用xlable作为子图的标题&#xff0c;这是因为plt.title()方法绘制的…...

在软件开发中正确使用MySQL日期时间类型的深度解析

在日常软件开发场景中&#xff0c;时间信息的存储是底层且核心的需求。从金融交易的精确记账时间、用户操作的行为日志&#xff0c;到供应链系统的物流节点时间戳&#xff0c;时间数据的准确性直接决定业务逻辑的可靠性。MySQL作为主流关系型数据库&#xff0c;其日期时间类型的…...

java_网络服务相关_gateway_nacos_feign区别联系

1. spring-cloud-starter-gateway 作用&#xff1a;作为微服务架构的网关&#xff0c;统一入口&#xff0c;处理所有外部请求。 核心能力&#xff1a; 路由转发&#xff08;基于路径、服务名等&#xff09;过滤器&#xff08;鉴权、限流、日志、Header 处理&#xff09;支持负…...

R语言AI模型部署方案:精准离线运行详解

R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...

uni-app学习笔记二十二---使用vite.config.js全局导入常用依赖

在前面的练习中&#xff0c;每个页面需要使用ref&#xff0c;onShow等生命周期钩子函数时都需要像下面这样导入 import {onMounted, ref} from "vue" 如果不想每个页面都导入&#xff0c;需要使用node.js命令npm安装unplugin-auto-import npm install unplugin-au…...

【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分

一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计&#xff0c;提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合&#xff1a;各模块职责清晰&#xff0c;便于独立开发…...

Map相关知识

数据结构 二叉树 二叉树&#xff0c;顾名思义&#xff0c;每个节点最多有两个“叉”&#xff0c;也就是两个子节点&#xff0c;分别是左子 节点和右子节点。不过&#xff0c;二叉树并不要求每个节点都有两个子节点&#xff0c;有的节点只 有左子节点&#xff0c;有的节点只有…...

鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南

1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发&#xff0c;使用DevEco Studio作为开发工具&#xff0c;采用Java语言实现&#xff0c;包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...

Aspose.PDF 限制绕过方案:Java 字节码技术实战分享(仅供学习)

Aspose.PDF 限制绕过方案&#xff1a;Java 字节码技术实战分享&#xff08;仅供学习&#xff09; 一、Aspose.PDF 简介二、说明&#xff08;⚠️仅供学习与研究使用&#xff09;三、技术流程总览四、准备工作1. 下载 Jar 包2. Maven 项目依赖配置 五、字节码修改实现代码&#…...

嵌入式学习笔记DAY33(网络编程——TCP)

一、网络架构 C/S &#xff08;client/server 客户端/服务器&#xff09;&#xff1a;由客户端和服务器端两个部分组成。客户端通常是用户使用的应用程序&#xff0c;负责提供用户界面和交互逻辑 &#xff0c;接收用户输入&#xff0c;向服务器发送请求&#xff0c;并展示服务…...

MyBatis中关于缓存的理解

MyBatis缓存 MyBatis系统当中默认定义两级缓存&#xff1a;一级缓存、二级缓存 默认情况下&#xff0c;只有一级缓存开启&#xff08;sqlSession级别的缓存&#xff09;二级缓存需要手动开启配置&#xff0c;需要局域namespace级别的缓存 一级缓存&#xff08;本地缓存&#…...