news 2026/10/8 4:17:43

Unity WebGL 下 Addressables 加载进度条实现与常见坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity WebGL 下 Addressables 加载进度条实现与常见坑

简介:围绕Unity Addressables框架实现资源与场景异步加载,并展示加载进度条的完整项目,适用于需要在WebGL平台优化资源加载体验的Unity开发者。压缩包共2000个文件,整体约940.41MB,文件类型涵盖813个bin资源数据、285个meta配置、180个md说明文档、115个mat材质、70张jpg与47张png贴图、36个asset资产文件,并包含少量prefab、fbx和cs脚本等,基本覆盖工程配置、场景素材、文档说明与脚本代码,目录结构清晰便于检索。目前已有888人学习下载。这套资料详细演示了Addressables分组配置、LoadAssetAsync/LoadSceneAsync异步调用、进度监听、资源释放等核心API用法,还结合HTML5 progress元素给出WebGL进度条实现方案,并附有构建缓存与打包记录,可帮助开发者快速复现完整加载流程、掌握WebGL平台下的资源优化与排错思路。

1. WebGL 里用 Addressables 加载资源和场景,进度条为什么反而成了最麻烦的一环

在 WebGL 上做加载资源和场景,目前 Unity 项目里最常用的方案就是 Addressables。它把资源按组打成 AssetBundle,运行时在浏览器里通过网络按需下载,加载资源和场景也走同一套异步管线。也正因为“浏览器 + 网络 + 异步”三件事叠在一起,等待时间被明显拉长,进度条从可选变成了必须。不少团队在 PC 上能靠一句异步等待草草了事,WebGL 版却翻了车:进度条卡在 90%、切场景后内存翻倍、甚至白屏。这篇文章围绕“unity addressables 加载资源和场景 + 显示进度条”这条链,先把加载管线拆开,再给出可复现的进度条实现,最后把我踩过的坑一条条列出来。适合正在做 WebGL 导出、微信小游戏打包或关注包体优化的 Unity 开发者。

2. 先摸清 Addressables 的加载管线:资源组、Key 与 AsyncOperationHandle 的配合

很多人一上来就拖个 Slider,然后每帧读一个叫“进度”的字段,却发现进度条要么不动要么乱跳。这是因为 Addressables 的加载不是一条线,而是一棵依赖树。树的每个分支可能是一个本地 AssetBundle,也可能是一个远程 AssetBundle,UnityWebRequest 每完成一个分支,进度就跳一下,所以最终呈现出来的 PercentComplete 天然是台阶状的。想做出稳定的进度条,先得把这条管线里谁先谁后搞清楚。

2.1 一次加载请求背后发生了什么

Addressables 初始化时才加载 Catalog,Catalog 里记录的是每个 Key 对应的所有 AssetBundle 及其依赖关系。调 LoadAssetAsync 或 LoadSceneAsync 时,流程大致是四步:

  1. 在 Catalog 里查 Key 对应的 Location。
  2. 根据 Location 找到目标 AssetBundle,以及它依赖的所有 Bundle。
  3. 把所有依赖 Bundle 下载并加载完,如果是远程地址则走 UnityWebRequest。
  4. 依赖就绪后,反序列化资源对象,再往前走回调或实例化。

对 WebGL 来说,第三步是进度条最难缠的地方:依赖包里可能有几十个 Bundle,其中有本地(Local)也有远程(Remote)的。只要其中一个 Bundle 特别大,进度条就会长时间停在某个值上,看起来像死机。所以写进度条不能只盯着最终返回值,还要知道当前到底卡在哪一步。

2.2 加载同一个对象,用 Address 还是 Label

Addressables 有三种常见 Key 类型:Address、Label、AssetReference。它们的加载粒度完全不同,直接影响进度条的平稳程度。

Key 类型加载粒度对进度条的影响适用场景
Address 字符串定位到单一资源进度只受该资源的依赖 Bundle 影响,相对可控场景加载、单个 UI 弹窗、单个角色
Label 标签一次加载整组资源进度是整组 Bundle 的加权值,跳动明显批量预热、按模块预下载
AssetReference序列化引用和 Address 一样走 handle 进度只在预制体里拖引用时推荐

要做进度条时,我一般更推荐以 Address 作 Key,粒度越细,进度越好预判。Label 加载时只要其中一个 Bundle 体积大,进度就会在某个百分比上停很久,观感非常差。Label 更适合做启动阶段的无界面预下载,做可见进度条是给自己找麻烦。

2.3 进度到底读哪个字段:PercentComplete 与 GetDownloadStatus 的配合

进度条的数值来源在 Addressables 里有两套,分别是 AsyncOperationHandle.PercentComplete 和 GetDownloadStatus().Percent。它们不是同一个东西:

字段计算范围典型表现
PercentComplete整条加载链路的综合进度依赖多时呈台阶状,可能先满后回跳
GetDownloadStatus().Percent只统计还有多少字节没下载完对远程 Bundle 更线性,但加载阶段会报满

配合方式很简单:拿到 handle 后,在 while 循环里优先看下载状态,下载结束后再用 PercentComplete 覆盖后半段。下面是一个最小脚本,可以直接挂到 Canvas 上跑:

using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.UI; using System.Collections; public class AddressableProgressBar : MonoBehaviour { public Slider progressSlider; public Text progressText; IEnumerator Start() { // 手动初始化,避免后续加载撞上 Catalog 未就绪 var initHandle = Addressables.InitializeAsync(); yield return initHandle; // 用 Address 加载单一预制体 var handle = Addressables.LoadAssetAsync<GameObject>("enemy_boss"); while (!handle.IsDone) { float progress = Mathf.Clamp01(handle.PercentComplete); progressSlider.value = progress; progressText.text = $"{(progress * 100f):F0}%"; yield return null; } if (handle.Status == AsyncOperationStatus.Succeeded) { Instantiate(handle.Result, transform.position, Quaternion.identity); } else { Debug.LogError($"加载失败: {handle.OperationException}"); } // 这里释放的是句柄,实例化出来的物体不受影响 Addressables.Release(handle); } }

这段代码的逻辑是先初始化 Addressables,再按 Key 加载资源。每帧读取 PercentComplete,等句柄 IsDone 后取 Result。参数说明里有几个值得注意的点:一是 Slider 的 value 要提前设到 0,不要留 Editor 里的旧值;二是handle.Status == AsyncOperationStatus.Succeeded一定要判断,WebGL 在弱网下经常返回 Failed,只等 IsDone 会误认为成功;三是 Release 必须和 LoadAssetAsync 成对出现,如果你换成 InstantiateAsync,对应就要用 ReleaseInstance,用错会连带把实例销毁。

2.4 句柄释放与进度条并存:一个常见误用

进度条本身不持有资源,持有资源的一直是句柄。很多项目把释放逻辑写在进度条回调里,这本身没错,但经常犯一个顺序问题:进度条刚到 100%,玩家还站在场景里,代码就把 load 句柄 release 了,结果角色身上的贴图、声音全部失效。释放逻辑的正确姿势是“什么时候确定没人再引用这块资源,什么时候释放”,和进度条走到头没有必然关系。下面这种写法是反例:

// 错误示范:加载结束立刻释放 var handle = Addressables.LoadAssetAsync<GameObject>("ui_icon"); yield return handle; var prefab = handle.Result; var obj = Instantiate(prefab); Addressables.Release(handle); // 如果后续还依赖 prefab,这里就会出问题

正确的做法是把句柄存到成员变量或专门的资源登记表里,等 UI 关闭、场景退出时再释放。进度条只负责展示加载状态,不负责替资源生命周期做决定。

3. 从资源到场景:把加载进度条落到 WebGL 发布版也能用的实现

先加载单个资源再扩展场景,是大多数人熟悉的路子,但场景加载在 WebGL 上有几个和编辑器里完全不同的表现:远程 Bundle 要先下载、下载完还要反序列化、场景激活前还有一段卡顿。所以场景进度条不能照搬“资源加载的进度条”,要做两段式。这一章给出的是我实际在项目里用过的完整写法。

3.1 加载单个资源的最小进度条实现

先说单个资源。这个进度条适合加载弹窗、道具、角色模型这类场景内单点资源。用 LoadAssetAsync 加一个 while 循环,每帧把 PercentComplete 映射到 Slider 上:

using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.UI; using System.Collections; public class SingleAssetLoader : MonoBehaviour { public Slider progressSlider; public Text progressText; public void LoadDialog() { StartCoroutine(LoadAsset("ui/dialog_confirm")); } IEnumerator LoadAsset(string key) { var handle = Addressables.LoadAssetAsync<GameObject>(key); while (!handle.IsDone) { float p = Mathf.Clamp01(handle.PercentComplete); progressSlider.value = p; progressText.text = $"{(p * 100f):F0}%"; yield return null; } if (handle.Status == AsyncOperationStatus.Succeeded) { // Result 是预制体,实例化后放到指定父节点 var go = Instantiate(handle.Result, transform); go.transform.localPosition = Vector3.zero; } else { Debug.LogError($"资源加载失败: {key}, {handle.OperationException}"); } Addressables.Release(handle); } }

逻辑说明:这里用一个协程替代 Update,协程的好处是每个加载任务互不阻塞。参数上,key必须是 Addressables 配置好的 Address,如果填成 AssetBundle 文件名反而找不到。Instantiate之后立刻 Release 是安全的,因为handle.Result引用的资源在实例化后,对象自身已持有引用数据,释放句柄只是让 Addressables 不再追踪这块资源。真正需要注意的是如果后续还要Instantiate第二次,就不能在这个时刻释放。

3.2 场景切换的加载进度条:LoadSceneAsync 的完整用法

场景加载比资源加载多的一个环节是场景激活。Unity 在激活新场景前会把旧的场景卸载掉,这个动作在 WebGL 上可能造成几百毫秒的卡顿,而handle.PercentComplete在到达 0.9 左右后就不会继续涨,直到激活完成才直接跳到 1。所以进度条会在 90% 左右停一会儿,这是正常现象,不代表死锁。

下面这段代码是最小可用的场景加载进度条:

using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.SceneManagement; using UnityEngine.UI; using System.Collections; public class SceneLoader : MonoBehaviour { public Slider progressSlider; public Text progressText; public string sceneKey = "Scenes/Level_Forest"; public void LoadLevel() { StartCoroutine(LoadSceneWithProgress(sceneKey)); } IEnumerator LoadSceneWithProgress(string key) { var handle = Addressables.LoadSceneAsync(key, LoadSceneMode.Single); while (!handle.IsDone) { float p = Mathf.Clamp01(handle.PercentComplete); progressSlider.value = p; progressText.text = $"场景加载 {(p * 100f):F0}%"; yield return null; } if (handle.Status == AsyncOperationStatus.Succeeded) { // 新场景已经激活,此时释放旧场景句柄,防止 Old Bundle 占用内存 Addressables.Release(handle); } else { Debug.LogError($"场景加载失败: {handle.OperationException}"); } } }

这里的LoadSceneMode.Single会在加载完成时卸载旧场景。把Addressables.Release(handle)放在 Succeeded 之后是给旧场景的 Addressable 资源做收尾,场景本身已经由场景系统接管,不会因为释放句柄而被卸载。

3.3 WebGL 上把进度拆成“下载依赖 + 加载场景”两段

如果你把上面的代码打包到 WebGL,并且场景的第一个 Bundle 是从远程 CDN 拉取的,就会发现问题:进度条在前 1 秒几乎不动。因为LoadSceneAsync是先下载依赖、再加载场景,下载阶段和加载阶段混在一个 PercentComplete 里。更稳的做法是显式调用DownloadDependenciesAsync,把两个阶段分开,给玩家一个更真实的反馈:

const float downloadWeight = 0.7f; IEnumerator LoadSceneWithTwoStageProgress(string key) { // 第一阶段:把场景依赖的远程 Bundle 全部拉下来 var downloadHandle = Addressables.DownloadDependenciesAsync(key); while (!downloadHandle.IsDone) { var status = downloadHandle.GetDownloadStatus(); float percent = status.Percent; progressSlider.value = percent * downloadWeight; progressText.text = $"下载 {(percent * 100f):F0}%"; yield return null; } if (downloadHandle.Status != AsyncOperationStatus.Succeeded) { Debug.LogError($"依赖下载失败: {downloadHandle.OperationException}"); yield break; } // 第二阶段:加载场景本体,进度从 70% 继续走到 100% var sceneHandle = Addressables.LoadSceneAsync(key, LoadSceneMode.Single); while (!sceneHandle.IsDone) { float p = Mathf.Clamp01(sceneHandle.PercentComplete); progressSlider.value = downloadWeight + (1f - downloadWeight) * p; progressText.text = $"加载场景 {(p * 100f):F0}%"; yield return null; } if (sceneHandle.Status == AsyncOperationStatus.Succeeded) { Addressables.Release(sceneHandle); progressText.text = "进入场景"; } else { Debug.LogError($"场景加载失败: {sceneHandle.OperationException}"); } }

这段代码的关键点是GetDownloadStatus()。DownloadStatus 会返回 TotalBytes 和 DownloadedBytes,Percent 就是两者的比值,它反映的是真实下载量,而不是内部句柄的加权估算。把下载阶段映射到 0~70%,加载阶段映射到 70%~100%,看起来会比单个 PercentComplete 顺滑很多。WebGL 在浏览器里会把下载完的 Bundle 缓存进 IndexedDB,第二次进同一个场景时下载阶段会极快跳过,进度条会直接进入加载场景阶段;如果你看到两次运行进度条节奏不一样,多半是浏览器缓存生效了。

3.4 进度条文本别每帧刷新,WebGL 上尤其如此

大多数示例代码里progressText.text = $"{...}%"写在 while 循环内部,这在 PC 上没有感觉,在 WebGL 上会拖慢帧率。因为浏览器环境没有真正的多线程,字符串拼接和 UGUI 脏矩形重建每帧都在和主线程抢 CPU。进度条本身用 Slider.value 每帧赋值没问题,但文本建议 0.1 秒更新一次:

float lastUpdateTime = 0f; while (!handle.IsDone) { if (Time.unscaledTime - lastUpdateTime > 0.1f) { lastUpdateTime = Time.unscaledTime; progressSlider.value = handle.PercentComplete; progressText.text = $"{(handle.PercentComplete * 100f):F0}%"; } yield return null; }

这里的Time.unscaledTime不受游戏暂停影响,加载场景时如果有人改过 Time.timeScale,用 unscaledTime 更保险。文本 0.1 秒刷新一次人眼完全感知得到,帧率却能省下一大批重建开销。

4. Addressables + WebGL 避坑清单:卡 90%、内存翻倍、包体没变小怎么办

下面是几条我从 WebGL 发布项目里一层层排查出来的经验,每条按“现象 → 原因 → 解决”的顺序写,基本覆盖了进度条上线后最常见的几类翻车。

4.1 进度条卡在 90% 后不动,玩家以为白屏了

现象:进度条一路走到 90% 左右,然后停住,短则两三秒,长则十秒以上,操作也没反应。

原因有两类。一类是场景激活前的卡顿,也就是 LoadSceneAsync 在等待旧场景卸载和新场景激活,WebGL 单线程下这个过程会占满主线程。另一类是资源反序列化,Shader 变体编译和材质加载都在这个阶段执行,Bundle 越大卡得越久。

解决:先在场景激活前给玩家一个过渡提示,把 90% 到 100% 改成“进入场景中”,或者用一个循环转圈动画替代精确百分比。如果卡顿集中在某个大 Bundle,用 Addressables Profiler 看是哪个资源拖慢,把大资源拆小,或者提前在下载阶段预加载。进度条卡住本身不可怕,可怕的是没有任何反馈让玩家以为崩了。

4.2 多次切场景后浏览器内存一路涨,最后崩溃

现象:连续切换三四个场景后,浏览器标签页的内存占用一路增长,最终白屏或者直接提示页面无响应。

原因:旧场景的 Addressable 句柄没释放。LoadSceneAsync 用 Single 模式确实会卸载旧场景,但旧场景用到的 AssetBundle 不会自动卸载,因为 Addressables 不知道你是否还要复用这些资源。越切场景,驻留的无主 Bundle 越多。

解决:保存旧场景的加载句柄,新场景激活后再Addressables.Release(旧句柄)。如果旧场景是通过InstantiateAsync生成的物体,还需要遍历场景里的物体调Addressables.ReleaseInstance,否则即使句柄释放了,实例和资源仍然被引用。这里可以配合内存分析:在浏览器 DevTools 里拍一个 Heap Snapshot,能看到大量 2~5MB 的 ArrayBuffer,基本就是没释放的 Bundle。

4.3 包体优化没有生效:远程资源全部打进了首包

现象:发布到 WebGL 后,首包体积和原本打包前差不多,甚至更大。打开 Addressables Groups 窗口后发现,所有资源都在 Default Local Group 里。

原因:资源没有被分配到 Remote Group。只有把资源组的 Content Packing & Loading 设置改成 Remote,构建时才会单独生成远程 Bundle,并配上 Remote Load Path 对应的 URL。如果一直沿用默认的 Local,所有资源都会打进本地 Bundle。

解决:打开 Addressables Groups 窗口,把场景资源、关卡美术、音频这类后加载的内容移到单独的 Remote Group。Remote Build Path 指构建输出目录,Remote Load Path 指运行时下载的 URL,这里要填 CDN 或服务器地址。构建前用 Build Bundles 跑一遍,确认输出目录里有对应.bundle文件,且不在首包里。包体优化做到最后,就是首包只装核心逻辑,把后续关卡 Bundle 全挪到远程。

4.4 粒子特效在 WebGL 上反复加载,内存明显上浮

现象:某个场景切出去再切回来,粒子特效的材质、贴图内存明显上涨,多次切换后上涨趋势不回落。

原因:粒子特效依赖的 Shader、材质和 MonoBehaviour 很可能挂在同一个 Bundle 里。第一次加载时这些资源进入了 WebGL 的 GPU 内存,第二次加载如果走了另一个分组或另一个 Catalog 入口,旧的一份没有被释放,两份同时驻留。粒子特效本身的内存泄漏在 WebGL 上比模拟器更隐蔽,因为编辑器里 Play Mode 的内存可以快速回收,浏览器不行。

解决:把粒子特效按场景分到独立 Group,场景退出时主动卸载该 Group 的 Bundle。如果粒子特效跨场景复用,就把它的 Shader 和材质放到全局常驻组,只加载一次;不要把同一份特效放在多个场景 Bundle 里到处复制。最关键还是释放:粒子系统停掉后,调用ReleaseInstance并把引用置空,别等着 GC 去收,WebGL 的 GC 表现和编辑器差很多。

4.5 进度条到 100%,画面还是白屏

现象:进度条走满,玩家点击开始,界面切到新场景,但画面持续白屏,没有报错。

原因:最常见的是场景虽然加载成功了,但场景里的引导脚本在 Awake 或 OnEnable 阶段访问了尚未加载完成的资源。WebGL 上异步初始化顺序更严格,如果依赖的资源还在下载中,脚本会自动静默跳过初始化,导致场景空转。另一个原因是手动初始化时序不对,进度条走完后脚本才发现 Catalog 还没就绪。

解决:在场景加载前显式调用Addressables.InitializeAsync(),确保 Catalog 先就绪。场景里的核心脚本不要依赖 Awake 做资源初始化,改成监听SceneManager.sceneLoaded事件,或者用协程等一个自定义的“场景资源就绪”标记。白屏问题排查时,第一步先看 Console 有没有异常,第二步检查场景中脚本的初始化顺序,第三步再怀疑渲染层。

5. 进度条之后的一公里:用 Profiler 核对加载耗时与内存峰值

进度条能跑只是及格线,真正要回答的问题是:加载到底慢在哪、内存峰值出在哪。WebGL 上不能用 Unity Profiler 直接抓浏览器网络线程,我惯用的做法是分段打点,把每一段耗时打印出来,再对照资源大小判断瓶颈。

using System.Diagnostics; IEnumerator LoadSceneAndLog(string key) { var sw = Stopwatch.StartNew(); var downloadHandle = Addressables.DownloadDependenciesAsync(key); yield return downloadHandle; sw.Stop(); Debug.Log($"依赖下载: {sw.ElapsedMilliseconds} ms, 大小: {downloadHandle.GetDownloadStatus().TotalBytes / 1024f} KB"); sw.Restart(); var sceneHandle = Addressables.LoadSceneAsync(key, LoadSceneMode.Single); yield return sceneHandle; sw.Stop(); Debug.Log($"场景加载: {sw.ElapsedMilliseconds} ms"); }

这种日志在 WebGL 的浏览器 Console 里直接能看到,比猜进度快得多。下载阶段慢,就去查 CDN 带宽和 Bundle 体积;场景加载阶段慢,就去查反序列化和 Shader 编译时间。我在项目里还养成了一个习惯:所有 Addressables 句柄都放一个静态登记表,加载时登记,释放时移除,每次发版前跑一遍加载逻辑,看日志里有没有“句柄已释放但资源仍被引用”的警告。

再验证一步,在 Addressables Groups 窗口里用 Analyze 检查重复引用和不可达资源。这个工具能直接列出哪些 Bundle 被多个场景引用、哪些资源没有归属,比手动翻代码可靠得多。

如果以后要做微信小游戏打包,这套逻辑也基本通用,只是需要额外留意平台对单个 Bundle 体积的限制。进度条最终是要给玩家看的,但它更是一面镜子,把加载链路的每一段照得清清楚楚。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 4:17:42

AI Agent Skill 编写指南:从结构设计到测试迭代的实战经验

1. 为什么“写 Skill”这件事值得单独拿出来聊很多人第一次接触 Skill 这个概念&#xff0c;是在 AI Agent 工具链里。你给它一段自然语言描述&#xff0c;它就能调用某个能力去完成一件事——查数据、跑脚本、生成文档、做格式转换。看起来很简单&#xff0c;但真正动手写的时…

作者头像 李华
网站建设 2026/10/8 4:17:15

算法复杂度与摩尔定律:程序性能优化的核心推导与实践

我在公司的压测群里经常看到一类问题&#xff1a;某个任务跑了好几个小时就是不出结果&#xff0c;机器CPU飙满&#xff0c;内存居高不下&#xff0c;但谁也说不清到底是哪个环节在拖后腿。问了一圈&#xff0c;有人说“买更好的服务器”&#xff0c;也有人说“多开几个线程”。…

作者头像 李华
网站建设 2026/10/8 4:16:41

运维转型大模型全栈:FastAPI与Ollama实战指南

1. 从命令行到模型推理&#xff1a;一个运维人的转型起点两年前我还在机房和监控大屏打交道&#xff0c;每天的工作是盯着Zabbix告警、处理K8s集群的Pod漂移、写Ansible脚本批量刷配置。那时候我对“大模型”这三个字的理解&#xff0c;仅限于“又一个需要部署的中间件”。直到…

作者头像 李华
网站建设 2026/10/8 4:16:24

Agent与LLM技术栈全景拆解:从vLLM部署到GraphRAG实战

1. 从一份日报标题说起&#xff1a;Agent 与 LLM 技术栈的全景拆解看到“Agent / LLM 技术精选日报”这个标题&#xff0c;很多人第一反应是“又一个资讯聚合”。但如果你真正在一线做过 Agent 系统&#xff0c;就会知道这类日报背后其实藏着一张技术地图&#xff1a;Agent 架构…

作者头像 李华
网站建设 2026/10/8 4:16:09

校园视频平台毕设开发指南:SpringBoot+Vue全栈实现与避坑总结

1. 为什么这个校园视频平台是毕设的“满分选题”每年到了毕业季&#xff0c;我都会收到一堆私信问“到底选什么题目才能既好做又容易过”。说实话&#xff0c;校园视频平台这个选题&#xff0c;我几乎每年都会推荐给找我咨询的学弟学妹。不是因为题目新&#xff0c;恰恰是因为这…

作者头像 李华
网站建设 2026/10/8 4:16:04

Python数据处理与自动化办公实战:两小时掌握高效技能

先说个我自己的事。前两年在电商公司做运营分析&#xff0c;每月底要对销售、库存、售后三张表做汇总匹配。刚开始我用Excel的VLOOKUP一个个手拖&#xff0c;三张表二十多万行&#xff0c;拖一次卡五分钟&#xff0c;来回折腾七八个小时才弄完&#xff0c;还得反复核对有没有匹…

作者头像 李华