
简介面向JAVA ME游戏入门者的《9688雷霆战机》个人移植版源码及解析资料包以经典飞行射击游戏为实例展示在J2ME平台下借助KVM虚拟机、MIDP/CLDC配置与Profile完成移动端游戏开发的完整路径同时说明Java语言子集在受限环境中的使用要点。压缩包约4.4MB以Java源码、图片资源与wav/mid音频文件为主包含gameLogic、ui、sound、resource等模块目录结构清晰便于对照学习。已有1134人学习下载。内容覆盖主程序入口与游戏循环设计、飞机/敌机/子弹对象模型、碰撞检测、游戏状态机管理、Canvas图形绘制、触摸事件处理、图像与音频资源加载播放以及帧率控制、内存分配和缓存策略等关键实现。通过学习这套源码可以快速掌握JAVA ME游戏从界面渲染、用户交互到逻辑控制的完整流程并提升代码阅读与问题调试能力既适合自学也适合作为相关课程或毕业设计的参考。1. 9688雷霆战机一个 JAVA ME 移植工程里的“配置包”模拟器上跑得满帧的 JAVA ME 游戏塞进 9688 这台老直板机里可能连游戏都进不去这不是设备性能低而是移植时根本没按目标机的边界来写。个人移植“9688雷霆战机”这件事核心不是换几张贴图、调一下音乐而是把游戏逻辑、渲染、输入、资源管理全部重排到目标设备的内存和屏幕限制里。9688 是我给手头一批测试机起的配置包代号这个工程里跑的是一套弹幕射击玩法。这篇文章就沿着我自己移植这条线的顺序来写先定边界再拆渲染与逻辑然后写卷轴、碰撞和 Boss 状态机最后是五条真机踩坑记录和一套把弹道手感变成数据的校准方法。新手能照步骤把工程搭起来熟手可以跳过教程直接看避坑和参数取舍。2. 先摸清9688的边界分辨率、堆内存和 MIDP 基线2.1 “9688配置包”到底是什么为什么移植先定基线个人移植和商业移植最大的区别是商业项目有测试设备库个人手里往往只有一两台真机。没有真机列表就必须把“目标设备”抽象成一份配置表。9688 不是某个公开型号它是我给工程起的内部代号代表一批还在服役的老直板机。第一批测试机全在这份表里配置项9688 基线说明屏幕分辨率176x208部分为 128x160按最高兼容做色彩深度16 位65536 色不要依赖 alpha 混合堆内存上限约 1.5MB 空闲加载资源后剩余多少才是关键MIDP 配置MIDP 2.0 / CLDC 1.1可以用 Game API但我不推荐按键五向键 左右软键无触摸屏按键事件延迟明显浮点能力有 float但极慢弹道计算一律用定点数为什么先定这张表因为后面每一个设计决策都由它决定。屏幕小贴图就不能大Boss 动作细节要靠帧动画帧数而不是单帧尺寸来补堆内存只有 1.5MB一张 176x208 的缓冲 Image 在 16 位色下约占 70KB能画几层缓冲、能放几张背景图算一算就知道。我一般会先把“资源预算表”算出来再开始写代码背景图、帧动画、音效、缓冲、对象池每项占多少预算内分配。这个习惯能省掉后面九成的内存翻车。2.2 搭一个最小 MIDlet 工程JAD 字段和启动骨架移植前先建一个能安装到真机上的空 MIDlet验证打包、签名、安装链路是通的。用常用的 WTK 建工程工程目录保持 src、res、bin 三层。JAD 描述文件是重点真机安装时先读 JAD 再取 JAR字段写错直接“无法安装”。MIDlet-1: 9688Thunder, , com.local.games.thunder.ThunderMIDlet MIDlet-Name: 9688Thunder MIDlet-Vendor: local MIDlet-Version: 1.0.0 MicroEdition-Configuration: CLDC-1.1 MicroEdition-Profile: MIDP-2.0 MIDlet-Jar-URL: 9688Thunder.jar MIDlet-Jar-Size: 0MIDlet-1 这一行分三段显示名、图标路径、入口类名图标可为空。MicroEdition-Profile 决定你能用哪些 API写 MIDP-2.0 才能用 Game API 和部分高级 UI。MIDlet-Jar-Size 在打包时由工具回填手写 0 虽然能装但个别真机会在安装时校验文件大小并拒绝执行。还有一点容易踩JAR 文件名大小写必须和 JAD 里完全一致老直板机的文件系统是大小写敏感的9688thunder.jar和9688Thunder.jar会被当成两个文件。入口类的骨架要做得干净构造函数里不要加载任何资源import javax.microedition.midlet.MIDlet; import javax.microedition.lcdui.Display; public class ThunderMIDlet extends MIDlet { private ThunderCanvas canvas; public void startApp() { if (canvas null) { canvas new ThunderCanvas(); } Display.getDisplay(this).setCurrent(canvas); } public void pauseApp() { if (canvas ! null) canvas.pauseGame(); } public void destroyApp(boolean unconditional) { if (canvas ! null) canvas.shutdown(); } }startApp 里才创建 Canvas 并 setCurrent而不是在构造函数里做这件事。模拟器对时序宽容真机严格按 MIDlet 生命周期调度构造函数里 setCurrent 会出现“启动后退回桌面”的故障。pauseApp 里停线程destroyApp 里释放资源这三者的调用时机在每台真机上不完全一致但顺序不能变先停逻辑再停绘制最后释放图片。3. 拆解移植顺序先把逻辑和渲染分开再谈弹幕手感3.1 为什么直接搬源码会翻车Canvas、repaint 和线程模型很多人拿到一套 JAVA ME 雷霆战机源码第一反应是全局搜索分辨率、改贴图路径、重新打包。这个流程在模拟器上通常能跑通到真机上就原形毕露。原因在 Canvas 的绘制模型paint(Graphics g)不是你想调就能调的repaint()只是告诉系统“该重绘了”具体什么时候调用paint由设备决定。老直板机没有垂直同步绘制线程和游戏逻辑线程混在一起就会出现逻辑已经跑到第 20 帧、画面还在第 12 帧的错位。更严重的是原版源码往往把逻辑写在paint里或者每帧在paint中创建临时对象。这在模拟器上只是慢一点在真机上会直接 GC 卡死表现就是“玩着玩着突然停顿一下然后又恢复”。所以移植第一步不是调画面而是把游戏循环重写成“逻辑更新与渲染分离”的结构一个线程跑固定步长的逻辑渲染结果画到离屏缓冲paint里只做一次缓冲贴图。我一般会放弃 MIDP 2.0 Game API 里的Sprite和LayerManager。它们封装的变换和碰撞检测在模拟器上很理想但部分真机的实现效率差异很大属于不可控的黑匣子。弹幕游戏对碰撞精度和帧率要求极高自己维护坐标和判定反而更稳。3.2 重写帧循环双缓冲、固定步长与掉帧保护弹幕射击的逻辑帧率我通常锁定 25FPS对应单帧 40ms。有人觉得太低但老直板机的 LCD 响应时间和像素刷新方式决定了高帧率收益很小反而会把 CPU 时间烧在绘制上。核心循环长这样public void run() { long frameTime System.currentTimeMillis(); int frameSkip 40; // 25 FPS while (running) { handleInput(); // 第 1 步读按键放进输入队列 updateLogic(); // 第 2 步推进敌机、子弹、Boss 状态 renderToBuffer(); // 第 3 步画到离屏缓冲 repaint(); // 第 4 步请求系统拷贝缓冲到屏幕 frameTime frameSkip; long sleepTime frameTime - System.currentTimeMillis(); if (sleepTime 0) { try { Thread.sleep(sleepTime); } catch (InterruptedException e) { // 忽略下一帧继续 } } else { // 真机没跟上不追帧重置基准丢弃已经超时的帧 frameTime System.currentTimeMillis(); } } }handleInput、updateLogic、renderToBuffer三者的顺序是固定的先读输入再改游戏状态最后画出来。这样才能保证“按键在第 10 帧生效”而不是“在第 10 帧和第 11 帧之间随机生效”。掉帧时我选择直接对齐当前时间而不是疯狂补帧因为补帧会让 Boss 弹幕在设备卡顿时突然加速玩家会觉得“这一波弹幕变快了”。弹幕游戏的手感来自稳定速度不是高帧率。renderToBuffer 内部也不要直接操作屏幕 Graphics而是拿缓冲 Image 的 Graphics 来画最后在paint里一次贴出。3.3 输入也要排队键盘事件与逻辑帧的对齐Canvas 的keyPressed是事件回调它由系统线程触发和逻辑线程是并发的。如果直接在keyPressed里改玩家坐标会出现一个经典问题按键被记录时逻辑帧刚好在更新玩家位置两个线程抢同一个变量结果这一帧的移动量加倍或丢失。移植时我会用一组合法按键位图做队列事件回调只写队列逻辑帧开头统一消费private boolean[] keyQueue new boolean[16]; public void keyPressed(int keyCode) { int action getGameAction(keyCode); if (action 0 action keyQueue.length) { keyQueue[action] true; } } public void handleInput() { // 读取后立即复位保证一个逻辑帧只响应一次 boolean moveLeft keyQueue[LEFT]; keyQueue[LEFT] false; if (moveLeft) { player.x - player.speed; } }getGameAction会把按键码映射成Canvas.LEFT、Canvas.RIGHT、Canvas.FIRE等抽象动作这样移植不同按键布局的真机时不用改逻辑代码。input 队列不要用VectorCLDC 1.1 的Vector每个元素都是 Object存取有装箱和类型转换开销而且带同步锁弹幕游戏每帧读按键几十次纯数组最划算。这个队列的尺寸就 16覆盖方向键、软键和数字键足够。4. 写弹幕引擎的三个核心件卷轴、碰撞与 Boss 状态机4.1 卷轴背景两块图交替滚动别用大图硬拼雷霆战机这一类的垂直接卷轴射击背景滚动是“伪滚动”贴图本身不动改变的是绘制时的偏移量。最大的一张背景图不要超过屏幕高度的两倍否则内存里长期躺着一张大图其他资源就没地方放了。我用两张高度等于屏幕高度的图交替拼效果一样内存省一半private int scrollY; // 累计滚动偏移 private final Image bgTop; // 上半张背景 private final Image bgBottom; // 下半张背景无缝拼接 private void drawBackground(Graphics g) { int h getHeight(); int y1 scrollY % h; int y2 y1 - h; g.drawImage(bgTop, 0, y1, Graphics.TOP | Graphics.LEFT); g.drawImage(bgBottom, 0, y2, Graphics.TOP | Graphics.LEFT); }scrollY 使用取模运算避免数值无限增大同时保证 y1 永远落在屏幕高度范围内。y2 是负值表示它从屏幕顶端上方进入。两张图要设计成上下边缘图案连续否则每次循环都会看到一条接缝。滚动速度如果按帧累加建议用“每 N 帧移动 1 像素”的写法代替每帧加浮点数if ((frameCounter 3) 0) scrollY;速度就是每 4 帧 1 像素稳定且无浮点误差。弹幕的滚动背景速度还会影响玩家对敌机移动速度的判断背景和敌机同向滚动时相对速度变小弹幕会更难躲这个参数要单独调。4.2 对象池杜绝弹幕里的 new 对象弹幕游戏最忌讳的就是每帧new Bullet()。一次扩散弹幕可能射出 30 颗子弹每颗都要 new一秒钟十几次GC 就会频繁启动。CLDC 1.1 的 GC 是 stop-the-world 式的卡顿直接体现在画面上。移植时我把所有子弹、敌机、爆炸特效都放进对象池。class Bullet { boolean alive; int x, y; // 当前坐标 int vx, vy; // 像素/帧速度 int patternId; // 弹道模板 ID } class BulletPool { private Bullet[] pool; BulletPool(int size) { pool new Bullet[size]; for (int i 0; i size; i) { pool[i] new Bullet(); } } Bullet obtain() { for (int i 0; i pool.length; i) { if (!pool[i].alive) { pool[i].alive true; return pool[i]; } } return null; // 池满本次发射放弃 } void release(Bullet b) { b.alive false; } }obtain返回池中一个空闲对象release只是把 alive 置 false对象本身不销毁。池满时返回 null调用方直接跳过这次发射这个策略在弹幕上限场景下反而更安全屏幕上的子弹数量被硬性限制设备帧率不会因为失控的弹幕而雪崩。这个池的容量不要拍脑袋定先按“最密一波弹幕数量 x 1.5”来算比如 9688 上我设置了 120 颗子弹加 30 个敌机跑满时内存占用大约 8KB非常便宜。4.3 碰撞判定用小判定框别用贴图尺寸新手做碰撞通常直接拿贴图宽高做矩形相交结果玩家总觉得“我明明躲开了还被击中”。弹幕射击的公平感来自判定框比贴图小而且是双方都小。原版雷霆战机玩起来“手感很顺”很大程度是判定的功劳。移植时我定义独立的判定框常量private static final int ENEMY_BOX_W 10; private static final int ENEMY_BOX_H 10; private static final int BULLET_BOX_W 6; private static final int BULLET_BOX_H 6; private boolean hit(Enemy e, Bullet b) { // 取敌机和子弹判定框的中心点距离 int dx (e.x ENEMY_BOX_W / 2) - (b.x BULLET_BOX_W / 2); int dy (e.y ENEMY_BOX_H / 2) - (b.y BULLET_BOX_H / 2); // 半宽之和判定框相交就视为命中 int halfW (ENEMY_BOX_W BULLET_BOX_W) / 2; int halfH (ENEMY_BOX_H BULLET_BOX_H) / 2; return Math.abs(dx) halfW Math.abs(dy) halfH; }这段代码用的是“中心距离法”等价于 AABB 矩形相交但少了几次加减比较。判断条件用而不是意味着只有判定框真正重叠才算命中边缘擦过不算这是给玩家的宽容度。Boss 的判定框要更大一点并且我一般把 Boss 的判定框放在机身中下部而不是贴着顶部贴图因为弹幕从上方来袭时玩家瞄准的是机头下方区域判定框偏上会造成“明明打中了却没判定”的挫败感。4.4 Boss 状态机switch 状态字段最省Boss 战是最容易写出面条代码的地方一堆 if 嵌套加上计时器变量改一个弹幕参数要翻半天。我习惯用状态字段加 switch 来组织每个状态是一个“行为块”进入条件、执行动作、退出条件都写在同一个 case 里。private int bossState; private int stateTimer; private void updateBoss() { stateTimer; switch (bossState) { case 0: // 出场移到预定 y 坐标 boss.y - 2; if (boss.y 60) { boss.y 60; bossState 1; stateTimer 0; } break; case 1: // 弹幕 A每 20 帧发射一轮扇形弹 if (stateTimer % 20 0) { fireFan(boss.x, boss.y, 5, 8); // 5 颗每颗间隔 8 度 } if (boss.hp boss.maxHp / 2) { bossState 2; stateTimer 0; } break; case 2: // 弹幕 B追踪弹频率更高 if (stateTimer % 12 0) { fireHoming(boss.x, boss.y, player.x, player.y); } break; default: bossState 0; break; } }每个 case 都遵守“计时器驱动 状态切换条件”的模式Boss 的行为完全由状态和时间决定不依赖随机数这样回放和调试才可能。弹幕参数比如扇形的弹幕密度、旋转角速度我都抽成fireFan这类函数的参数这样调“手感”时不用改状态机结构只改参数。为什么不用状态模式类一个状态一个类会让类数量膨胀JAVA ME 的类加载和内存都有压力switch 写法在低端机上最稳压混淆器也省心。5. JAVA ME 真机避坑白屏、卡顿、残影、迟滞与内存泄漏5.1 白屏或启动后退回桌面生命周期时序问题现象模拟器一切正常装到老直板机上一点图标屏幕白一下直接退回桌面。原因多半是startApp()里做了重量级初始化或者Display.setCurrent()没在startApp()里执行。真机的 MIDlet 生命周期管理比模拟器严格startApp返回后系统才认为“启动完成”在这之前如果又创建线程又加载图片可能超时被系统杀掉。另一个常见原因是paint()里抛出异常真机捕获异常后直接终止应用而模拟器只打印一行错误。解决把所有资源加载从构造函数和startApp()中移走启动只做“创建 Canvas 并 setCurrent”。实际图片、音频放在游戏线程自己的初始化方法里进入游戏循环前先加载。我在 9688 上踩过这个坑之后养成了“启动路径上不写超过 10 行代码”的习惯。如果paint()必须做复杂绘制用 try-catch 包住并写一个兜底清屏至少能让玩家看到画面而不是白屏退出。5.2 越玩越卡GC 停顿与对象复用现象游戏前 30 秒流畅越到后面越卡特别是弹幕密集时画面会周期性停顿半秒。原因逻辑代码里藏了new。每帧创建 String 拼接分数、每次爆炸创建特效对象、每帧创建临时数组这些东西在模拟器上不显眼在真机上会成为 GC 触发源。CLDC 的 GC 没有并发整理一次 GC 可能停 50ms 到 200ms表现在游戏里就是“卡一下”。解决把代码里所有new集中清理一遍。字符串拼接用StringBuffer分数显示先setScore(int)再在paint里用预分配的StringBuffer输出特效和子弹统一走对象池每帧使用到的临时点坐标用成员变量复用而不要在方法里新建。排查方法是把 WTK 的-Xrunhprof打开看 GC 频率但真机上没法这么干我就写了个计数变量每 100 帧输出一次Runtime.getRuntime().freeMemory()看曲线趋势。内存平稳卡顿就基本来自 GC对象复用是唯一的后悔药。5.3 画面撕裂和残影双缓冲的位置不对现象背景滚动时屏幕上下两部分错位或者上一帧的影子残留在下一帧。原因JAVA ME 规范没有要求 Canvas 必须双缓冲真机实现各不相同。我自己做了离屏缓冲并在paint里贴图但部分设备在“贴图”这个环节不是一次性刷新整屏而是分区域刷新如果贴图和硬件刷新不同步就会出现撕裂。另一种情况是缓冲 Image 带 alpha 通道真机绘制带 alpha 的图像要额外做混合计算速度变慢后也能看到残影。解决先用设备屏幕的实际像素格式创建缓冲不要用Image.createImage(w, h)的默认格式如果设备支持createRGBImage再用它。然后是绘制链路我最终在 9688 上用 GameCanvas 的flushGraphics()代替了 Canvas repaint把画面画到内部缓冲后主动刷新刷新时机掌握在自己手里。// GameCanvas 模式下绘制完直接刷新到屏幕 public void renderFrame() { Graphics g getGraphics(); // 注意是 getGraphics不是 paint drawBackground(g); drawBullets(g); drawPlayer(g); flushGraphics(); // 主动刷新不等系统调度 }GameCanvas 的getGraphics()拿到的就是缓冲区的 Graphics绘制完成后flushGraphics()一次性拷到屏幕这比 Canvas 的 repaint paint 组合少一次缓冲拷贝也消除了“paint 被系统推迟导致残影”的问题。注意 GameCanvas 构造函数要super(true)开启按键状态查询否则getKeyStates()拿不到输入。5.4 按键迟滞轮询和事件两种输入模型的取舍现象模拟器上按方向键飞机立刻移动真机上手感慢半拍快速连按还会丢方向。原因真机的按键事件从物理按键到应用层有延迟事件回调进入keyPressed的时机不稳定。如果keyPressed里直接改玩家坐标这一帧的更新就发生在“逻辑更新之后、绘制之前”的某个随机点画面体现出来就是“有的帧动了有的帧没动”体感迟钝。解决从事件驱动改成轮询驱动。GameCanvas 的getKeyStates()会返回当前所有按下按键的位掩码每帧在逻辑开头读一次读完后状态自动清零。这样输入和逻辑强制对齐到帧边界不会出现半帧输入。public int readInput() { int raw getKeyStates(); int result 0; if ((raw LEFT_PRESSED) ! 0) result | DIR_LEFT; if ((raw RIGHT_PRESSED) ! 0) result | DIR_RIGHT; if ((raw UP_PRESSED) ! 0) result | DIR_UP; if ((raw DOWN_PRESSED) ! 0) result | DIR_DOWN; if ((raw FIRE_PRESSED) ! 0) result | DIR_FIRE; return result; }移植到不支持 GameCanvas 的设备时就退回第 3 章的 keyQueue 方案但消费时机必须在逻辑帧开头。注意getKeyStates的位掩码在不同真机上对个别按键的映射不一致必须在目标机上逐个按键实测我就在某台直板机上遇到过FIRE_PRESSED常量与硬件按键反转的怪事这类问题不真机对比永远发现不了。5.5 中途内存溢出图片资源和时序释放现象玩到第二关或第三关弹出一段堆栈后退出提示 OutOfMemoryError。原因资源加载策略问题。帧动画如果是每帧一张 Image一套攻击动画 20 帧每帧 32x32看起来不大但加上背景、Boss、特效图片总数量可能破百。每张 Image 在堆里都有对象头还有像素缓冲区100 张小图内存碎片化加上对象池占用的空间1.5MB 说爆就爆。另一个原因是从不释放旧资源切关时加载新背景旧背景没有置 null还挂在某个静态变量里。解决帧动画改成“一张横向大图 drawRegion裁剪”让单张 Image 承载多帧切场景时写一个unloadLevel()把该场景的 Image 引用置 null 并调用System.gc()请求回收。System.gc()只是建议但老直板机的 KVM 通常真的会执行一次完整回收足够释放切关时的尖峰内存。public void drawFrame(Graphics g, Image sheet, int frameIndex, int frameW, int frameH, int x, int y) { int sx frameIndex * frameW; g.drawRegion(sheet, sx, 0, frameW, frameH, Sprite.TRANS_NONE, x, y, Graphics.TOP | Graphics.LEFT); }裁剪绘制比创建子图更稳因为createImage的拷贝耗时在真机上非常可观。还有一条血泪经验不要在每帧绘制里调用getWidth()和getHeight()获取 Canvas 尺寸把屏幕宽高缓存在成员变量里。这些方法在某些真机上会触发系统调用慢一个数量级。6. 弹道校准技巧用一套回放日志把“手感”变成可对比的数据6.1 给弹幕装一个“黑匣子”回放日志的数据结构弹幕手感是玄学但玄学背面是数据每一颗子弹在哪一帧发射、从哪出发、速度多少、加速度多少全是可以记录的。移植时经常遇到“弹幕效果和原版不太一样说不清哪里不一样”的情况肉眼盯弹幕只能感觉“怪”说不出是哪一帧、哪颗子弹出了问题。我在 9688 这个工程里给子弹发射点装了一套回放日志逻辑层每次生成一颗子弹就写一条记录到环形缓冲区。class ReplayEntry { int frame; // 全局逻辑帧号 int bulletId; // 子弹编号 int x, y; // 发射位置 int vx, vy; // 速度 int patternId; // 弹道模板 ID } class ReplayBuffer { ReplayEntry[] entries; int head, count; ReplayBuffer(int capacity) { entries new ReplayEntry[capacity]; for (int i 0; i capacity; i) { entries[i] new ReplayEntry(); // 预分配回放过程不 new } } void record(int frame, int bulletId, int x, int y, int vx, int vy, int patternId) { ReplayEntry e entries[head]; e.frame frame; e.bulletId bulletId; e.x x; e.y y; e.vx vx; e.vy vy; e.patternId patternId; head (head 1) % entries.length; if (count entries.length) count; } }环形缓冲区容量开 4096足够记录一整场 Boss 战的每一颗子弹。缓冲区里的对象全部预分配回放过程不会产生新对象不会反过来干扰性能。记录放在fireBullet函数入口处这样所有发射路径都覆盖不会漏掉某一种弹幕。6.2 用四分之一速率重放对比弹道偏差有了日志校准方式就变成科学的了。我在模拟器上以正常游戏的 1/4 速率重放把参考弹道从原版录制的坐标序列或自己设计的预期轨迹和移植版的日志逐帧对比偏差超过阈值的子弹单独标出来。private boolean near(ReplayEntry a, ReplayEntry b, int tolerance) { return Math.abs(a.x - b.x) tolerance Math.abs(a.y - b.y) tolerance Math.abs(a.vx - b.vx) 1 Math.abs(a.vy - b.vy) 1; }对比逻辑很简单同一帧号、同一颗子弹、坐标和速度都在容差内就算通过。容差我用 8 像素超过这个范围玩家肉眼就能看出弹道偏移。重放时弹幕速度降低四分之一人眼能看清楚每一颗子弹的飞行路径。这个技巧帮我在这个工程里定位到两个典型问题一个是扇形弹的初始角度用了角度制而原版用的弧度制导致每颗子弹偏离了 3 到 5 度另一个是追踪弹的转向速度参数在移植时少除一个 2子弹画出来的轨迹比原版“弯得更急”。这两个问题在正常速度下都很难察觉但日志一对比坐标偏差直接暴露了问题。我现在每移植一个射击游戏最先做的事永远是先装这套回放日志再开始调弹幕——因为手感这东西靠感觉调永远有下一轮靠数据调一次就能收敛。希望帮到你。本文还有配套的精品资源点击获取