简介:本资源是一套面向高校自动化、机器人工程及机电类专业本科生的ROS课程设计实践项目,聚焦机械臂在ROS环境下的运动仿真与点焊工艺控制实现。项目基于Python开发,完整复现了从URDF建模、MoveIt!运动规划、RVIZ可视化仿真到末端执行器轨迹控制的全流程,可直接用于课程设计、期末大作业或ROS入门实践。压缩包共57个文件,涵盖12个launch启动脚本(负责仿真环境与控制器加载)、9个YAML配置文件(含关节限位、控制器参数等)、9个STL三维模型(构成机器人本体与焊枪)、5个核心Python控制脚本(实现点焊路径生成与动作触发)以及URDF/SRDF/RVIZ等关键描述文件,总大小仅430KB,结构规范、模块清晰。已有284人学习下载,项目经导师指导获评97分高分,代码无冗余、注释完整、依赖明确,解压后按说明即可一键运行,无需额外调试。 写这篇文章的契机,是前几天一个学弟把“基于ROS的点焊机器人仿真与控制python源码”这个课程设计项目拿给我看。他问的最多的一句话是:“这些代码我都能跑通,但答辩的时候老师要是问我为什么这么设计,我该怎么答?”这个提问其实比很多拿到源码就完事的同学更关键。点焊机器人仿真与控制这个课题,表面上是一个ROS课程设计,实际上覆盖了机器人建模、运动学解算、运动规划、工艺时序控制四条主线。你只有把这条链路从头到尾吃透,才能在答辩和实际项目中真正说得清楚、改得动代码。
这篇文章就以这个课程设计为主体,从方案选型、仿真环境搭建、运动学与规划实现、Python控制系统设计、完整运行流程,到调试中的经典坑位,全部拆开讲一遍。不管你是正在做类似课题的学生,还是想用ROS快速搭一套机械臂仿真控制验证环境的工程师,这篇内容都可以直接拿去当参考。
1. 课程设计的整体目标与方案选型
1.1 为什么选择ROS作为仿真与控制系统框架
ROS,全称Robot Operating System,翻译过来是“机器人操作系统”,但它本质上不是传统意义的操作系统,而是一套分布式通信框架和机器人开发工具集。在点焊机器人这个课题里,选择ROS首先是因为生态太齐全了:URDF/Xacro用来描述机器人结构,Gazebo用物理引擎做仿真,RViz用来可视化,MoveIt用来做运动规划,ros_control用来做关节控制,全部是现成组件,不需要自己从头造轮子。
第二个原因是Python支持。ROS的rospy库提供了完整的Python接口,而Python恰好是课程设计最友好的语言。用Python写机器人控制逻辑,迭代速度快,打印调试方便,对不熟悉C++的同学非常友好。你用rospy去发一个关节指令,只需要几行代码,但对初学者来说,这几行代码背后的节点通信机制、消息类型、话题回调,才是真正要理解和掌握的东西。
第三个原因是ROS的节点通信模型与工业机器人控制抽象层次高度匹配。以点焊机器人为例,上层流程逻辑负责“接下来去哪个焊点”,运动规划节点负责“怎么到达”,关节控制层负责“关节实际转多少度”,焊枪控制节点负责“什么时候闭合焊接”。这些模块如果用普通串行程序硬写,耦合度会很高,改动一个环节就得动全局。而ROS里每个模块是一个独立节点,通过话题(Topic)和服务(Service)通信,天然解耦,非常契合课程设计“展示模块化设计能力”的评分点。
1.2 点焊机器人的仿真与控制包含哪些核心需求
把标题里“仿真”和“控制”拆开看,这两个词各自有非常具体的含义。
仿真不是指把3D模型丢进Gazebo里看它站着不动。完整的仿真应该做到三件事:第一,机器人模型有真实的质量和惯性参数,物理引擎下能稳定站立,不会飘、不会抖;第二,关节能接受指令并产生符合预期的运动,也就是说控制闭环是通的;第三,环境中有障碍物可以验证规划的避障能力。能做到这三点,仿真环境才算真正“立住”。
控制则分为两个层面。第一层是运动控制,核心包括正运动学、逆运动学和轨迹规划。正运动学负责由关节角推算末端位姿,逆运动学负责由末端位姿反解关节角,轨迹规划则负责在关节空间中生成从当前位姿到目标位姿的平滑路径。第二层是工艺控制,点焊不是简单把焊枪移过去就可以,它有严谨的时序:焊钳闭合加压、通电加热、维持冷却、打开焊钳,这个过程叫“加压—焊接—维持—休止”。工艺控制必须和运动控制严格互锁,不允许“还没到位就焊接”这种逻辑错误。
除了这两个核心层面,完整系统还应当包括系统集成需求,比如用状态机来管理整个流程,模拟PLC的外围信号交互,以及把机器人的关节状态、焊接状态实时发布出来供监控查看。这些需求决定了源码的模块划分,我的整体设计按“模型—仿真—规划—控制—监视”五层来组织。
1.3 方案架构与源码文件布局设计
课程设计源码和工程项目不一样,工程项目追求代码的极致抽象,课程设计更看重逻辑清晰、可读性强、方便答辩展示。因此在一开始,我把源码规划为ROS工作空间标准布局,一切都按照规范来组织:
point_weld_robot/ ├── urdf/ # 机器人模型文件(xacro宏定义) ├── config/ # 控制器参数、焊接工艺参数 ├── launch/ # 启动文件 ├── worlds/ # Gazebo环境模型 ├── scripts/ │ ├── kinematics.py # 正/逆运动学求解节点 │ ├── weld_controller.py # 焊枪与焊接工艺控制节点 │ ├── motion_planner.py # 运动规划封装节点 │ └── main_state.py # 主状态机节点 ├── msg/ # 自定义消息类型 └── rviz/ # RViz可视化配置这种结构的核心思想是分工明确:模型层只负责描述“机器人长什么样”,控制层负责“关节怎么动”,逻辑层负责“下一步干什么”,互不干扰。后面几章的内容,都会围绕这套结构逐一展开。我建议你把这一节当作全文的索引,读到哪里就对应到源码里的哪个目录,这样理解起来会特别顺。
2. 仿真环境搭建与核心配置
2.1 环境准备:Ubuntu版本与ROS发行版选择
在机器人仿真这个领域,版本匹配是最大的坑。ROS发行版和Ubuntu版本严格绑定,选错了后面全是问题。我整理了一张对照表供参考:
| Ubuntu版本 | 对应ROS发行版 | Python版本 |
|---|---|---|
| 16.04 | Kinetic | Python2 |
| 18.04 | Melodic | Python2为主 |
| 20.04 | Noetic | Python3 |
| 22.04 | Rolling/自编译 | Python3 |
如果是从零开始,我强烈建议直接选择Ubuntu 20.04加Noetic。原因有两点:第一,Noetic是最后一个官方支持Ubuntu 20.04的长期维护版,而且完整支持Python3,你的Python源码在环境配置上几乎不会遇到兼容性问题;第二,网上关于Noetic的Gazebo和MoveIt教程是最多的,遇到问题很好搜索。
安装ROS本体后,还需要安装配套的仿真和规划组件。在终端执行:
sudo apt install ros-noetic-desktop-full sudo apt install ros-noetic-moveit ros-noetic-gazebo-ros ros-noetic-gazebo-ros-control sudo apt install ros-noetic-ros-control ros-noetic-ros-controllers这里有个小技巧:如果apt安装时提示有些包找不到,大概率是源没更新或者缺少ROS软件源配置,执行完下面这行再重试即可:
sudo apt update2.2 机器人模型的URDF/Xacro描述要点
URDF(Unified Robot Description Format)是ROS中描述机器人结构的标准XML格式。但直接用URDF写六自由度机械臂会非常冗长,所以我的第一个实操建议是:尽量用Xacro,它是URDF的宏定义扩展,能大幅减少重复代码。
点焊机器人本质上是一个六自由度串联机械臂加上一个焊钳。典型的关节排布是:基座腰部旋转、大臂俯仰、小臂俯仰、腕部三个旋转关节,末端法兰上再固定一个C型焊钳。在Xacro里,我习惯给每个关节写成一个宏,比如:
<xacro:macro name="joint_module" params="name parent child xyz rpy"> <joint name="${name}" type="revolute"> <parent link="${parent}"/> <child link="${child}"/> <origin xyz="${xyz}" rpy="${rpy}"/> <axis xyz="0 0 1"/> <limit lower="-3.14" upper="3.14" effort="100" velocity="1.5"/> </joint> </xacro:macro>每个link则需要定义visual(可视化模型)和collision(碰撞模型)。visual决定你看到的样子,collision决定物理碰撞时用的简化体。课程设计里很多人忽略collision,直接不写,结果Gazebo里机器人要么穿模、要么碰撞检测异常。正确做法是为每个link单独指定一个简化圆柱体或长方体作为碰撞体,不要直接用细碎的3D网格。
inertial参数同样是重点。mass和inertia如果缺失或数值明显不合理,Gazebo里机器人会直接炸开,表现为“砰一下弹飞”。很多同学调了一晚上,最后发现只是某个link的惯性矩阵没写。我给出一个可以直接套用的简化惯量写法:
<inertial> <mass value="2.0"/> <origin xyz="0 0 0" rpy="0 0 0"/> <inertia ixx="0.05" ixy="0" ixz="0" iyy="0.05" iyz="0" izz="0.02"/> </inertial>2.3 Gazebo仿真环境与ros_control控制器配置
模型文件写完,进入Gazebo之后会发现机器人根本动不了,这是因为Gazebo里机器人的关节默认是自由状态,必须通过ros_control框架挂载关节控制器,才能真正实现位置或力矩控制。
我在config目录下建了controllers.yaml:
arm_joint_controller: type: position_controllers/JointTrajectoryController joints: - joint1 - joint2 - joint3 - joint4 - joint5 - joint6 state_publish_rate: 50 weld_controller: type: position_controllers/JointTrajectoryController joints: - weld_slide state_publish_rate: 50这里arm_joint_controller用来控制六个机械臂关节,weld_controller用来控制焊钳的开合滑动关节。在launch文件里通过gazebo_ros和gazebo_ros_control插件加载控制器,启动后可以使用rostopic list查看是否出现了/arm_joint_controller/command这个标准话题。
调试这一步有一个非常好的标志:机器人能在Gazebo里稳定站立且不会坠落。如果机器人像面条一样瘫下去,说明位置控制闭环没有生效,优先检查joint名称和controllers.yaml里的拼写是否一一对应。
3. 点焊机器人运动学与轨迹规划实现
3.1 基于DH参数的正逆运动学实现
运动学是整个机器人控制的数学基础。正运动学是“已知关节角,求末端位姿”,逆运动学反过来,是“已知末端位姿,求关节角”。课程设计中,最稳妥的实现方式是提前建立机器人的DH参数表,然后通过齐次变换矩阵连乘求解。比如对于一个六轴机器人,相邻连杆间的变换矩阵是:
T_i = Rot(z, theta_i) * Trans(z, d_i) * Trans(x, a_i) * Rot(x, alpha_i)把所有关节的变换矩阵连乘,就得到末端在基坐标系下的位姿。Python里我习惯用numpy手写这个链乘,不使用额外的三维数学库,这样答辩时可以当场讲清楚每一步矩阵的含义:
import numpy as np def dh_matrix(theta, d, a, alpha): return np.array([ [np.cos(theta), -np.sin(theta)*np.cos(alpha), np.sin(theta)*np.sin(alpha), a*np.cos(theta)], [np.sin(theta), np.cos(theta)*np.cos(alpha), -np.cos(theta)*np.sin(alpha), a*np.sin(theta)], [0, np.sin(alpha), np.cos(alpha), d], [0, 0, 0, 1] ])正解的手写实现比较简单,逆解就有讲究了。六轴工业机器人如果后三轴交于一点,存在解析解,速度快而且稳定;如果构型不具备球形腕部,就需要用数值迭代方法,比如基于雅可比矩阵的阻尼最小二乘法(DLS)。在设计URDF模型时,我强烈建议让后三轴交点满足球形腕约束,这样逆解成功率会高很多,也省去了很多调参时间。
3.2 基于MoveIt!的运动规划与避障集成
MoveIt!是ROS中运动规划的核心框架,它把机器人模型、碰撞检测、运动学求解器、规划算法都整合在一起,对外提供简洁接口。在课程设计里,用moveit_commander配合rospy就能完成大部分规划任务:
import rospy import moveit_commander from moveit_commander import MoveGroupCommander moveit_commander.roscpp_initialize([""]) rospy.init_node("moveit_node") arm = MoveGroupCommander("arm_group") arm.set_planning_time(5.0) arm.set_pose_target([0.5, 0.2, 0.8, 0, 0, 0, 1]) plan = arm.plan() arm.execute(plan)这段代码里,set_pose_target传入的是[x, y, z, qx, qy, qz, qw],前三个是末端位置,后四个是四元数姿态。plan()返回规划好的轨迹,execute()执行轨迹。看起来简单,但MoveIt的配置过程相对繁琐,需要先使用Setup Assistant加载URDF,定义规划组(planning group)、预定义位姿,再生成配置文件。
有一个课程设计里很容易被忽略的坑:MoveIt规划出来的是关节空间轨迹,它不保证末端焊枪在两点之间走直线。但点焊的特点是机器人只在目标点停稳后才焊接,点间轨迹到底是什么形状,对焊接质量没有影响,所以关节空间规划完全够用。如果是做弧焊或涂胶,轨迹形状会直接影响工艺质量,那就必须改用笛卡尔空间规划。
3.3 焊点位置规划与焊枪姿态约束
点焊机器人的目标不是简单到达某个坐标点,而是要让焊钳电极对准工件上的焊点,并且焊钳中心线的方向与工件表面法线一致。这个“姿态约束”是焊点规划的核心。
实际工程中,我先把工件坐标系相对机器人基坐标系的变换矩阵T_cam_base标定出来,然后在工件坐标系里维护一份焊点清单,包括位置分量和姿态分量。转换到机器人全局坐标系后,每个焊点变成一个6自由度的目标位姿。我在源码里用CSV文件存储焊点,格式为:
point_id, x, y, z, rx, ry, rz, weld_time 1, 0.35, 0.10, 0.85, 0, 1.57, 0, 2.0 2, 0.38, 0.10, 0.85, 0, 1.57, 0, 2.0每次主状态机读取一行,就调用一次运动规划节点,让机械臂移动到对应位姿,然后通知焊枪开始执行焊接时序。这样做的好处是,焊点数据与代码逻辑完全分离,换一个工件只需要改CSV文件。
4. 控制系统设计与Python源码实现
4.1 ROS节点架构与话题通信设计
控制系统是整个项目的大脑。我采用四个Python节点来分担职责,节点之间的通信关系非常清晰:
kinematics_node:负责正逆运动学的计算服务,封装成ROS Service,供其他节点调用。motion_planner:订阅“去焊点N”的指令,调用MoveIt规划并执行运动,完成后发布到位消息。weld_controller:订阅到位消息,控制焊钳闭合并执行“加压—焊接—维持—休止”时序。main_state:主状态机节点,负责读取焊点列表、派发任务、监听所有节点状态。
话题设计上,我在msg目录下自定义了两个消息类型,一个是PointArrived,表示机器人已经到达目标焊点;另一个是WeldState,表示当前焊接处于哪个阶段。为什么不用标准消息?因为自定义消息能让代码具备“自描述”能力,读代码的人一眼就知道这条消息传达的是什么业务语义,这点在答辩时非常加分。
4.2 主状态机与焊接工艺时序控制
点焊工艺的时序控制是整个项目“最有工业味道”的部分。真实点焊控制器的典型流程包括预压、加压、通电、维持、休止等阶段。仿真中我用Python枚举和状态迁移来实现:
class WeldState(Enum): IDLE = 0 PRESSING = 1 WELDING = 2 HOLDING = 3 RELEASING = 4 state = WeldState.IDLE while not rospy.is_shutdown(): if state == WeldState.IDLE: if point_reached: close_welder() state = WeldState.PRESSING elif state == WeldState.PRESSING: if pressure_ready(): start_welding_power() state = WeldState.WELDING elif state == WeldState.WELDING: if weld_time_elapsed(): stop_welding_power() state = WeldState.HOLDING elif state == WeldState.HOLDING: if hold_time_elapsed(): open_welder() state = WeldState.RELEASING elif state == WeldState.RELEASING: if welder_opened(): state = WeldState.IDLE request_next_point()这种状态机写法看似简单,在实际调试里有几个容易翻车的地方。第一是状态间的“消息确认”必须严格,不能只靠延时来跳转,因为如果前一个动作没有真正完成就进入下一状态,后续时序会越来越乱。第二是焊接电流的启停,在仿真中我用一个weld_power话题去控制虚拟焊机节点,由它来发布通电状态,这样数据流向更符合真实系统。
4.3 焊钳开合与机器人运动的联动逻辑
焊钳开合和机器人运动之间存在强关联:机器人还没到位时焊钳绝对不能提前闭合,否则会“把工件压坏”,哪怕在仿真里这也是一个严重的逻辑错误。因此我设计了一个严格的互锁机制:motion_planner在执行轨迹前将状态置为“运动保持中”,执行完成后才发布到位消息;weld_controller只有在收到到位消息后才会调用焊钳闭合。
这个互锁逻辑在实际工作站里通常由PLC安全逻辑负责,课程设计里即使不强制要求,实现出来也很有价值。答辩时可以这样陈述:“我把工艺互锁抽象到状态机层,所有跨节点动作都通过消息确认来保证时序安全”,这一句话就能让评委看到你对工业控制安全的理解。
4.4 控制效果验证与调试参数
写完控制逻辑后,需要对系统做定量验证。我做三个常规测试,推荐你也照着做一遍。
第一是关节跟踪误差测试。给六个关节分别发送幅值不同的正弦波参考轨迹,用rosbag记录实际关节角与期望角的差,计算最大误差和均方根误差。仿真里关节跟踪误差通常在毫度级别,这本身不代表真实机器人精度,但能说明控制器参数和模型参数是否合理。
第二是点位重复精度测试。让机械臂在同一个焊点和初始位姿之间往返多次,打印末端位姿的偏差。Gazebo中没有振动和磨损,重复精度按理会非常好,如果这个数据出现明显漂移,反而要怀疑控制器或TF坐标变换是不是有bug。
第三是节拍时间测试。统计从焊点1出发到焊点2完成焊接的完整周期,这个数据直接影响“仿真产线节拍”的结论。实际调试中如果节拍太慢,可以先调大MoveIt的速度缩放因子,或者检查是否有不必要的等待时间。
5. 完整运行流程与复现步骤
5.1 启动仿真环境与加载机器人模型
整个系统启动流程可以拆成四条命令,对应四个终端窗口。
第一个终端启动Gazebo仿真环境,加载包含机器人模型的世界:
source devel/setup.bash roslaunch point_weld_robot sim_world.launch第二个终端加载ros_control控制器,让关节进入可控状态:
source devel/setup.bash roslaunch point_weld_robot control.launch第三个终端启动RViz和MoveIt,方便观察机器人状态和规划轨迹:
source devel/setup.bash roslaunch point_weld_robot moveit_plan.launch第四个终端运行主状态机Python节点,开始执行点焊流程:
source devel/setup.bash rosrun point_weld_robot main_state.py启动完成后,可以在终端里用rqt_graph查看节点通信图。看到main_state、motion_planner、weld_controller之间连线清晰,就说明通信链路没有问题。
5.2 执行一次完整的点焊流程
焊点CSV中我预设了三个焊点。启动主状态机之后,系统按以下顺序运转:
第一步,主状态机读取焊点1的数据,通过服务调用运动学节点求解逆解,然后把目标位姿发给motion_planner。第二步,motion_planner调用MoveIt规划出一条无碰撞路径并执行,机械臂从初始位姿运动到焊点1正上方。第三步,机械臂到达后发布到位消息。第四步,weld_controller收到消息后执行焊接时序,焊钳闭合,模拟通电电流,焊接完成。第五步,焊钳打开,主状态机切换到焊点2,循环执行。
整个过程中,终端会打印状态切换日志,RViz里可以看到机械臂的实时运动。焊点位置和焊钳状态也可以用可视化marker显示,让整个焊接过程一目了然。
5.3 从运行结果解读系统设计合理性
运行结束后,我习惯用rostopic echo回放关键话题的数据,检查整个周期是否和预设的工艺节拍一致。如果从焊点1开始到焊点3完成,总耗时和预期偏差在合理范围内,说明时序逻辑没有冗余等待;如果偏差很大,就需要逐段查看哪个状态耗时异常。
另外一个要检查的是TF树是否完整。运行rosrun rqt_tf_tree rqt_tf_tree,确认从base_link到weld_tip的坐标变换链条没有任何断裂。TF树是机器人仿真的“坐标系地图”,一旦某一段缺失,MoveIt规划或者坐标转换就会直接报错。我在调试中遇到的多次“莫名其妙的位置偏移”,最后几乎都定位到TF问题。
6. 常见问题、踩坑记录与扩展建议
6.1 课程设计高频排查问题速查表
我把实际调试中遇到过的问题整理成表,按出现频率排序,方便遇到问题时直接对照:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| Gazebo启动瞬间机器人弹飞 | link的inertial参数缺失或数值异常 | 检查每个link的mass和inertia,不能为0或超出量级 |
| 关节一直不动但有指令发出 | 控制器未加载或joint名称不匹配 | 对比URDF和controllers.yaml中的joint名称 |
| MoveIt规划失败率高 | 目标点不可达或碰撞模型过保守 | 确认目标在工作空间内,简化collision体 |
| 机械臂到达后焊钳不闭合 | 主状态机未收到到位消息 | 检查PointArrived话题发布端和订阅端是否同名 |
| Python节点启动后卡死 | rospy.spin()阻塞导致状态机不运行 | 状态机主循环不要调用spin,用Rate控制周期 |
| 规划轨迹绕大圈 | TF树不完整或IK解迭代不收敛 | 检查TF树,必要时调整目标姿态初值 |
| Gazebo运行非常卡 | 物理步长过小或模型网格过密 | 调整更新频率,简化碰撞模型 |
6.2 我实际调试中踩过的三个特别值得说的坑
第一个坑和KDL解算器有关。MoveIt默认的运动学求解器是KDL,它对奇异位形比较敏感。有一次我设置的焊点姿态刚好接近腕部奇异,结果机械臂规划的路径绕了一个大圈才到位,看起来非常滑稽。调试了半天才发现是目标姿态问题。后来我在源码里加了一段预检测:调用逆解之前先计算雅可比矩阵的条件数,条件数过大就提醒用户该位姿接近奇异。
第二个坑是焊钳的控制频率和焊接延时冲突。最初我把焊接时间写在“焊钳开始闭合”的同时开始计时,导致控制器还没执行到位,延时就已经结束,焊点质量数据完全不可信。后来改成由焊钳的实际到位反馈触发“焊接”状态,这是整个源码里一处不太起眼但很关键的修正。
第三个坑是Gazebo中物理稳定性。我给大臂的collision体用了一个特别贴近原始模型的网格,结果碰撞检测的计算量非常巨大,仿真帧率掉到个位数。后来我把所有碰撞体换成圆柱体和长方体组合,仿真立刻流畅起来。这让我意识到,碰撞模型追求“精确”不一定好,重要的是“够用且稳定”。
6.3 课程设计之外:这套源码还能怎么扩展
如果课程设计做完还有余力,这个项目可以往三个方向扩展。
第一个方向是加一个人机交互界面。使用Qt或者Web端控制面板,在界面中显示当前焊点编号、机器人关节角度、焊接进度,并允许操作员点击按钮暂停或继续流程。这一层加上后,系统就从“能跑的代码”升级成“可演示的人机系统”,在很多课程设计答辩中是高分亮点。
第二个方向是加入视觉定位。在Gazebo世界放置一个虚拟摄像头,通过OpenCV识别工件上的焊点标记,自动生成焊点坐标,替代预先写死的CSV。这个扩展引入了视觉伺服的概念,复杂度提升一大截,但对理解“手眼系统”特别有帮助。
第三个方向是往真实机器人靠近。将控制输出从“仿真关节指令”改成“真实机器人驱动指令”,或者接入真实焊机PLC的IO信号。这个方向工作量很大,但它是工业点焊工作站最真实的形态。理解焊钳伺服控制方法、TCP标定流程、与PLC的IO联动,以后去工业自动化岗位面试会非常有底气。
我在实际调试这个课题时最大的体会是,仿真里看起来简简单单的“到达—焊接—离开”,真正写成代码后要考虑消息同步、状态互锁、异常恢复,工作量比想象中大得多。但也正是这些细节,让我对机器人控制系统里模块划分和通信机制有了最直观的感受。最后再分享一个小技巧:如果Gazebo仿真卡顿,可以适当降低物理更新频率,把固定时间步长从1kHz降到500Hz,多数课程设计场景下不影响结果真实性,但流畅度会有明显改善。
本文还有配套的精品资源,点击获取