3.YooAsset工具窗口

  1. 3.YooAsset工具窗口
    1. 3.1 知识点
      1. YooAsset 的菜单功能列表
      2. AssetBundle Collector 资源配置界面
        1. 顶部功能栏细节
        2. 全局设置项解析
        3. 包裹设置项说明
        4. 资源分组配置细节
        5. 搜集器配置详解
      3. AssetBundle Builder 资源构建界面
        1. 界面功能模块
        2. 构建操作与结果
        3. 重要概念与注意事项
      4. AssetBundle Reporter 构建报告界面
        1. 概览视图
        2. 资源对象列表视图
        3. 资源包列表视图
      5. AssetBundle Debugger 调试器界面
        1. 调试器核心功能
        2. 平台支持与调试配置
        3. 资源对象列表视图
        4. 资源包列表视图
        5. 异步操作任务列表视图
      6. AssetArt Scanner 资产扫描器
        1. 工具界面组成
        2. 基础操作按钮
        3. 扫描器配置项详解
        4. 扫描器检视界面
        5. 扫描器接口扩展
          1. 扩展示例(太空战机DEMO中的纹理扫描器)
        6. 扫描报告输出
        7. 典型应用场景
      7. AssetArt Reporter 资产扫描报告
        1. 报告核心价值
        2. 核心功能模块
          1. 报告导入与导出
          2. 问题修复功能
          3. 显示控制选项
        3. 搜索栏高级语法
          1. 通用关键字搜索
          2. 指定项关键字搜索
          3. 指定项数值比较
        4. 操作示例场景
      8. 示例工程中的配置文件
        1. 文件一览与用途
          1. AssetBundleCollectorSetting.asset(必须有,唯一)
          2. AssetBundleCollectorConfig(可选的备份/交换格式)
          3. AssetArtScannerSetting.asset(可选)
          4. ScannerSchema/*(示例 Schema 与参数资产)
        2. 它们从哪里来?怎么“正确生成”
        3. 放置与迁移建议
        4. 运行时与打包期的关系

3.YooAsset工具窗口


3.1 知识点

YooAsset 的菜单功能列表

  • Home Page:跳转至 YooAsset 官方主页、文档或相关介绍页面,方便用户获取工具说明与支持。 查看官方文档网站

  • AssetBundle 相关工具

    • AssetBundle Collector:资源配置界面。收集项目资源,确定哪些资源需要打包成 AssetBundle。
    • AssetBundle Builder:资源构建界面。构建工具,用于生成 AssetBundle 包,配置打包参数。
    • AssetBundle Reporter:构建报告界面。生成构建报告,展示 AssetBundle 依赖关系、大小等信息,便于分析优化。
    • AssetBundle Debugger:调试工具,用于排查 AssetBundle 加载、依赖错误等运行时问题。
  • AssetArt 相关工具

    • AssetArt Scanner:扫描项目美术资源(如模型、贴图),检测格式、尺寸等规范问题。
    • AssetArt Reporter:生成美术资源分析报告,汇总扫描结果,辅助资源管理与优化。

AssetBundle Collector 资源配置界面

顶部功能栏细节

  • Fix修复按钮:当配置中的文件夹位置变动时,修正资源路径关联,确保资源引用正常。比如默认找不到资源路径,点击修复按钮后找到了。
  • 导入按钮:导入保存的 XML 配置文件,快速复用历史配置,减少重复操作。
  • 导出按钮:将当前配置数据导出为 XML 文件,便于备份、团队协作共享配置。


原来找不到,点击修复按钮后找到了

全局设置项解析

  • Show Package:控制是否展示资源包列表视图,勾选后直观呈现项目资源包结构。一般默认只有一个DefaultPackage。

  • Show Editor Alias:是否显示为中文模式。切换界面语言显示模式,开启后以中文展示相关功能说明。

  • Unique Bundle Name:为资源包名添加 PackageName 前缀,多 Package 场景下避免包名冲突,建议开启。

包裹设置项说明

  • Enable Addressable:启用可寻址资源定位系统,支持代码加载资源,同时兼容全路径加载方式。相当于可以使用全路径加载,建议开启!。
  • Location To Lower:使资源定位地址大小写不敏感,简化资源加载时的地址匹配逻辑。
  • Include Asset GUID:在资源清单中记录资源 GUID 信息,精准追踪管理资源。
  • Auto Collect Shaders:自动将所有着色器打包到独立资源包,优化着色器管理与加载效率。就是开启后,系统会自动把所有着色器打包到一个独立的 “shader 资源包” 里;不用你手动整理,加载时更高效(比如切换场景时,着色器不用重复加载),管理也更方便。
  • File Ignore Rule:设置全局文件忽略规则(支持扩展),如原生文件配置可选择 RawFileIgnoreRule 过滤特定文件。相当于一个筛选规则比如选 RawFileIgnoreRule,可以设置哪些文件 “不用处理”(比如某些原生格式、临时文件);这样打包的时候,这些文件会被过滤掉,避免包体变大或者塞进去没用的东西。

资源分组配置细节

  • Active Rule:分组激活规则(支持自定义扩展)。用于控制 资源分组是否生效(是否参与打包、加载等流程)。内置规则如 EnableGroup(启用分组)、DisableGroup(禁用分组),DisableGroup示例代码如下:

    using YooAsset; // 必须引入YooAsset命名空间
    public class DisableGroup : IActiveRule {  
        public bool IsActiveGroup(GroupData groupData) {  
            // 这里可以扩展更复杂的逻辑,比如:
            // if (groupData.GroupName == "battle") return false; // 根据分组名称判断
            // 现在是固定返回false,让分组永远不激活
            return false;  
        }  
    }   
    


    也可以自己实现继承IActiveRule的类,自定义IsActiveGroup逻辑。这样在这个下拉框就可以选择里自定义的那个类,走自定义规则。

  • Grouper Name:定义分组名称(如 battle),标识资源分组用途。

  • Grouper Desc:添加分组备注信息,说明分组内容,方便团队协作理解。

  • Asset Tags:为分组内资源添加分类标签(多标签用分号分隔,如 level1;level2),便于资源筛选。

搜集器配置详解

  • Collect Path:指定需要收集的资源路径,可以是文件夹(如 Assets/Res/UI)或单个文件(如 Assets/Res/Models/player.prefab)。只有该路径下的资源会被当前搜集器处理,避免无关资源混入。

  • Collector Type:定义收集器类型:

    • MainAssetCollector: 收集资源并写入资源清单(Manifest),支持通过代码加载(如 YooAsset.Load)。适用于大部分业务资源(UI、模型、音效等需主动加载的资源)。
    • StaticAssetCollector: 收集资源但不写入清单,仅用于定制化打包(如内置到安装包的基础资源)。适用于无需代码动态加载的固定资源(如启动图、全局配置)。
    • DependAssetCollector:自动收集 “主资源依赖的资源”(如材质依赖的纹理、预制体依赖的动画),未被引用的依赖会自动剔除。适用于处理资源间的依赖关系,避免手动收集依赖导致的遗漏或冗余。
  • AddressRule:可寻址规则(支持自定义)。或者说是资源的 “加载地址生成规则”。定义代码加载资源时使用的 “地址字符串”,决定如何通过字符串定位资源。

    • AddressByFileName:按文件名生成地址,只取 文件名(去掉扩展名) 作为地址。适用于资源路径唯一(如所有资源放在同一文件夹),或确保同名文件不会出现。例如Assets/Res/UI/btn.png和Assets/Res/Icon/btn.png地址都是btn,会冲突。

      [DisplayName("定位地址: 文件名")]
      public class AddressByFileName : IAddressRule
      {
          string IAddressRule.GetAssetAddress(AddressRuleData data)
          {
              return Path.GetFileNameWithoutExtension(data.AssetPath);
          }
      }
      
    • AddressByFolderAndFileName:按文件夹+文件名生成地址,取“文件夹路径(相对资源根目录)+ 文件名(去掉扩展名)”作为地址。适用于需要区分不同文件夹下的同名文件,避免跨文件夹冲突。例如Assets/Res/UI/btn.png地址是UI/btn,Assets/Res/Icon/btn.png地址是Icon/btn,不会冲突;但Assets/Res/UI/btn.png和Assets/Res/UI/btn.jpg地址都是UI/btn,会冲突。

      [DisplayName("定位地址: 文件夹名_文件名")]
      public class AddressByFolderAndFileName : IAddressRule
      {
          string IAddressRule.GetAssetAddress(AddressRuleData data)
          {
              string fileName = Path.GetFileNameWithoutExtension(data.AssetPath);
              FileInfo fileInfo = new FileInfo(data.AssetPath);
              return $"{fileInfo.Directory.Name}_{fileName}";
          }
      }  
      
    • AddressByGroupAndFileName:按分组+文件名生成地址,取“资源所属分组名称 + 文件名(去掉扩展名)”作为地址。适用于多业务分组管理资源,通过分组隔离同名文件。例如属于“UI分组”的Assets/Res/UI/btn.png地址是UI/btn,属于“Icon分组”的Assets/Res/Icon/btn.png地址是Icon/btn,不会冲突;但同一分组内的Assets/Res/UI/btn.png和Assets/Res/UI/btn.jpg地址都是UI/btn,会冲突。

      [DisplayName("定位地址: 分组名_文件名")]
      public class AddressByGroupAndFileName : IAddressRule
      {
          string IAddressRule.GetAssetAddress(AddressRuleData data)
          {
              string fileName = Path.GetFileNameWithoutExtension(data.AssetPath);
              return $"{data.GroupName}_{fileName}";
          }
      }  
      
    • AddressDisable:禁用可寻址地址,生成空值或无效标识(如固定字符串“disable”),使资源无法通过地址直接加载。适用于仅作为其他资源的依赖项(如材质引用的纹理、预制体依赖的动画),无需单独加载的资源。例如任何资源使用此规则后,地址均为无效值,调用YooAsset.LoadAsset(“xxx”)会加载失败。

      [DisplayName("定位地址: 禁用")]
      public class AddressDisable : IAddressRule
      {
          string IAddressRule.GetAssetAddress(AddressRuleData data)
          {
              return string.Empty;
          }
      }
      
    • AddressByFileNameAndExt:按文件名+扩展名生成地址,保留“文件名+扩展名”作为完整地址。适用于同文件夹内需要区分不同格式的同名文件(如图片和脚本同名)。例如Assets/Res/UI/btn.png地址是btn.png,Assets/Res/UI/btn.jpg地址是btn.jpg,不会冲突;但Assets/Res/UI/btn.png和Assets/Res/Icon/btn.png地址都是btn.png,会冲突。

      [DisplayName("定位地址: 文件名.智能尾缀")]
      public class AddressByFileNameAndExt : IAddressRule
      {
          public string GetAssetAddress(AddressRuleData data)
          {
              var ext = Path.GetExtension(data.AssetPath);
              if (ext == ".asset")
              {
                  var a = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(data.AssetPath);
                  if (a == null) return ".errortype";
                  var type = a.GetType();
                  var dt = Path.GetFileNameWithoutExtension(data.AssetPath);
                  return dt + $".{type.Name.ToLowerInvariant()}";
              }
      
              return Path.GetFileName(data.AssetPath);
          }
      } 
      
    • AddressByFullPath:需要自定义添加代码,但其实完全没必要,因为开启Enable Addressable就行。如果按资源完整路径生成地址,直接返回 data.AssetPath(资源在项目中的完整路径)作为地址。适用于需要绝对唯一地址、不希望任何冲突的场景,尤其适合资源路径规范严格的项目。例如Assets/Res/UI/btn.png地址是Assets/Res/UI/btn.png,Assets/Res/Icon/btn.png地址是Assets/Res/Icon/btn.png,完全不会冲突(因为项目中每个资源的完整路径必然唯一);即使同文件夹内有同名不同扩展名的资源(如Assets/Res/UI/btn.png和Assets/Res/UI/btn.jpg),地址也分别为完整路径,同样不冲突。但需注意:此规则生成的地址较长,且如果资源移动路径(如从UI文件夹移到Icon文件夹),代码中对应的加载地址也需要同步修改,维护成本略高。

      [DisplayName("定位地址: 完整路径")]
      public class AddressByFullPath : IAddressRule
      {
          string IAddressRule.GetAssetAddress(AddressRuleData data)
          {
              // 直接返回资源的完整路径作为地址,利用路径唯一性避免冲突
              return data.AssetPath;
          }
      }  
      
  • PackRule:打包规则(支持自定义)。决定资源如何合并为 AssetBundle 包,直接影响 加载效率(包体大小 → 加载时间,包数量 → 并发请求数)。通过规则类定义“资源包的划分逻辑”,即 哪些资源该打进同一个包

    • DefaultPackRule(打包规则配置类)
      下面的规则依赖 DefaultPackRule 统一管理包的后缀、默认名称:

      public class DefaultPackRule
      {
          /// <summary>
          /// AssetBundle文件的后缀名
          /// </summary>
          public const string AssetBundleFileExtension = "bundle";
      
          /// <summary>
          /// 原生文件的后缀名
          /// </summary>
          public const string RawFileExtension = "rawfile";
      
          /// <summary>
          /// 默认的Unity着色器资源包名称
          /// </summary>
          public const string ShadersBundleName = "unityshaders";
      
          /// <summary>
          /// 默认的Unity脚本资源包名称
          /// </summary>
          public const string MonosBundleName = "unitymonos";
      
          public static PackRuleResult CreateShadersPackRuleResult()
          {
              PackRuleResult result = new PackRuleResult(ShadersBundleName, AssetBundleFileExtension);
              return result;
          }
          public static PackRuleResult CreateMonosPackRuleResult()
          {
              PackRuleResult result = new PackRuleResult(MonosBundleName, AssetBundleFileExtension);
              return result;
          }
      }
      
    • PackSeparately(资源包名: 文件路径)

      • 作用每个资源单独打包,包名由文件路径处理生成(小写、替换分隔符,确保唯一性)。适用于 超大资源(如高清视频)需精准热更(仅更新单个资源) 的场景。
      • 例子
        • Assets/UIPanel/Shop/Image/backgroud.pngassets_uipanel_shop_image_backgroud.bundle
        • Assets/UIPanel/Shop/View/main.prefabassets_uipanel_shop_view_main.bundle(不同资源单独成包,互不影响)。
      • 代码实现
        [DisplayName("资源包名: 文件路径")]
        public class PackSeparately : IPackRule {  
            PackRuleResult IPackRule.GetPackRuleResult(PackRuleData data) {  
                string bundleName = PathUtility.RemoveExtension(data.AssetPath); // 移除扩展名,统一格式  
                return new PackRuleResult(bundleName, DefaultPackRule.AssetBundleFileExtension);  
            }  
        }  
        
    • PackDirectory(资源包名: 父类文件夹路径)

      • 作用同一文件夹下的资源合并打包,包名 = 文件夹路径(相对项目根目录)。适用于 按模块聚合资源(如 UI 模块、战斗模块各自成包,提升模块内资源复用率)。
      • 例子
        • Assets/UIPanel/Shop/Image 文件夹下的 backgroud.pngicon.png → 统一打进 assets_uipanel_shop_image.bundle(同文件夹资源合并)。
      • 代码实现
        [DisplayName("资源包名: 父类文件夹路径")]
        public class PackDirectory : IPackRule {  
            PackRuleResult IPackRule.GetPackRuleResult(PackRuleData data) {  
                string bundleName = Path.GetDirectoryName(data.AssetPath); // 取文件所在文件夹路径  
                return new PackRuleResult(bundleName, DefaultPackRule.AssetBundleFileExtension);  
            }  
        }  
        
    • PackTopDirectory(资源包名: 收集器下顶级文件夹路径)

      • 作用:以 收集器内的“顶级文件夹” 为单位打包(若资源嵌套多级文件夹,取最上层文件夹名)。适用于 合并多级子文件夹资源,但希望按顶层模块聚合(如所有 UI 相关资源打一个包)。
      • 例子
        • 收集器路径为 Assets/UIPanel,资源 Assets/UIPanel/Shop/Image/backgroud.pngAssets/UIPanel/Shop/View/main.prefab → 顶级文件夹是 Shop,统一打进 assets_uipanel_shop.bundle(多级子文件夹合并到顶级文件夹)。
      • 代码实现
        [DisplayName("资源包名: 收集器下顶级文件夹路径")]
        public class PackTopDirectory : IPackRule {  
            PackRuleResult IPackRule.GetPackRuleResult(PackRuleData data) {  
                string assetPath = data.AssetPath.Replace(data.CollectPath, string.Empty).TrimStart('/');  
                string[] splits = assetPath.Split('/');  
                if (splits.Length == 0 || Path.HasExtension(splits[0]))  
                    throw new Exception($"未找到顶级文件夹:{assetPath}");  
                string bundleName = $"{data.CollectPath}/{splits[0]}"; // 收集器路径 + 顶级文件夹名  
                return new PackRuleResult(bundleName, DefaultPackRule.AssetBundleFileExtension);  
            }  
        }  
        
    • PackCollector(资源包名: 收集器路径)

      • 作用当前收集器的所有资源合并为一个包,包名 = 收集器路径(相对项目根目录)。适用于 资源强关联(如一个功能模块的所有资源)资源量小,需最小化包数量(减少加载请求)。
      • 例子
        • 收集器路径为 Assets/UIPanel,该路径下的 btn.pngpanel.prefabShop/btn.png → 全部打进 assets_uipanel.bundle(收集器内资源全合并)。
      • 代码实现
        [DisplayName("资源包名: 收集器路径")]
        public class PackCollector : IPackRule {  
            PackRuleResult IPackRule.GetPackRuleResult(PackRuleData data) {  
                string bundleName = AssetDatabase.IsValidFolder(data.CollectPath)  
                    ? data.CollectPath // 收集器是文件夹,直接用路径  
                    : PathUtility.RemoveExtension(data.CollectPath); // 收集器是文件,移除扩展名  
                return new PackRuleResult(bundleName, DefaultPackRule.AssetBundleFileExtension);  
            }  
        }  
        
    • PackGroup(资源包名: 分组名称)

      • 作用:按 资源分组名称 打包,同一分组的资源合并为一个包(分组在 YooAsset 配置中定义)。适用于 多业务分组管理(如不同团队维护独立分组,确保分组内资源统一打包)。
      • 例子
        • 资源分组为 BattleGroup,旗下的 sword.prefabeffect.particle → 打进 BattleGroup.bundle(同分组资源合并)。
      • 代码实现
        [DisplayName("资源包名: 分组名称")]
        public class PackGroup : IPackRule {  
            PackRuleResult IPackRule.GetPackRuleResult(PackRuleData data) {  
                string bundleName = data.GroupName; // 直接用分组名称作为包名  
                return new PackRuleResult(bundleName, DefaultPackRule.AssetBundleFileExtension);  
            }  
        }  
        
    • PackRawFile(打包原生文件)

      • 作用:资源以 原生格式(如 txt、json)直接打包,不经过 Unity 引擎处理(保留文件原始内容),包后缀为 rawfile。适用于 需直接读取原始文件内容(如配置文件、自定义格式数据),避免 Unity 序列化干扰。
      • 例子
        • Assets/Res/Config/game.jsonAssets/Res/Config/game.json.rawfile(加载时按原生文件处理)。
      • 代码实现
        [DisplayName("打包原生文件")]
        public class PackRawFile : IPackRule {  
            PackRuleResult IPackRule.GetPackRuleResult(PackRuleData data) {  
                string bundleName = data.AssetPath; // 包名保留原路径  
                return new PackRuleResult(bundleName, DefaultPackRule.RawFileExtension);  
            }  
        }  
        
    • PackVideoFile(打包视频文件)

      • 作用:视频文件 单独打包,包后缀保留原视频格式(如 mp4、mov),便于播放器直接读取。适用于 视频资源需原生格式加载(如 Unity 视频播放器直接播放),避免格式转换。
      • 例子
        • Assets/Res/Video/intro.mp4Assets/Res/Video/intro.mp4.mp4(后缀为原格式 mp4)。
      • 代码实现
        [DisplayName("打包视频文件")]
        public class PackVideoFile : IPackRule {  
            PackRuleResult IPackRule.GetPackRuleResult(PackRuleData data) {  
                string fileExtension = Path.GetExtension(data.AssetPath).Substring(1); // 提取扩展名(如 mp4)  
                string bundleName = data.AssetPath;  
                return new PackRuleResult(bundleName, fileExtension);  
            }  
        }  
        
    • PackShader(打包着色器文件)

      • 作用:所有着色器(Shader)合并为统一包,包名为 unityshaders,优化着色器加载效率(减少冗余编译)。适用于 项目着色器数量多,需批量加载,降低运行时着色器编译开销。
      • 例子
        • Assets/Shaders/Lit.shaderAssets/Shaders/Transparent.shader → 统一打进 unityshaders.bundle(着色器集合打包)。
      • 代码实现
        [DisplayName("打包着色器文件")]
        public class PackShader : IPackRule {  
            public PackRuleResult GetPackRuleResult(PackRuleData data) {  
                return DefaultPackRule.CreateShadersPackRuleResult(); // 复用默认着色器包名  
            }  
        }  
        
    • PackShaderVariants(打包着色器变种集合文件)

      • 作用:着色器变种(如同一 Shader 的不同 Pass 变体)合并为统一包,包名为 unityshaders,减少冗余变体的重复打包。适用于 需合并 Shader 变体,避免同一 Shader 的不同变体分散打包导致的体积膨胀。
      • 例子
        • Assets/Shaders/Lit variants.shadervariants → 打进 unityshaders.bundle(变种集合打包)。
      • 代码实现
        [DisplayName("打包着色器变种集合文件")]
        public class PackShaderVariants : IPackRule {  
            public PackRuleResult GetPackRuleResult(PackRuleData data) {  
                return DefaultPackRule.CreateShadersPackRuleResult(); // 复用默认着色器包名  
            }  
        }  
        
  • FilterRule:过滤收集的资源类型。决定当前搜集器 “只收集哪些类型的资源”,过滤掉无关资源(如临时文件、非目标类型文件)。

    • 内置如 CollectScene(只收集场景文件),示例:
      public class CollectScene : IFilterRule {  
          public bool IsCollectAsset(FilterRuleData data) {  
              return Path.GetExtension(data.AssetPath) == ".unity";  
          }  
      }  
      
  • UserData:自定义数据,辅助定制 AddressRulePackRule,满足个性化需求。在 PackRule 中,可通过 data.UserData 获取配置的参数(如 maxSize=5),动态调整打包策略。

  • Asset Tags:为当前搜集器收集的资源统一添加标签(如 activity;2024),与分组的 Asset Tags 配合,便于后续按标签筛选资源(如统计所有 “活动” 标签的资源)。

AssetBundle Builder 资源构建界面

界面功能模块

  • Build Package:资源包裹列表,通过下拉选择确定需构建的资源包裹范围。

  • Build Pipeline:构建管线列表,包含多种类型:

    • EditorSimulateBuildPipeline:编辑器模拟构建管线。在编辑器内 模拟打包流程,生成资源清单(Manifest)但 不实际生成 AssetBundle 文件,快速验证资源依赖和寻址规则。比如开发阶段修改资源后,想快速检查 “哪些资源会被打包、地址是否冲突”,无需等待真实打包(真实打包可能耗时数分钟)。适用于开发调试(验证资源配置、依赖关系);快速迭代,避免频繁全量打包的时间消耗。
    • BuiltinBuildPipeline:内置构建管线,常规构建使用。使用 Unity 原生的 AssetBundle 构建逻辑,常规项目默认可选择。一般项目无特殊打包需求(如加密、定制 Bundle 结构),可以直接用此管线构建生产环境的资源包。
    • ScriptableBuildPipeline:可编程构建管线(Unity 2021.3+推荐),允许开发者控制打包流程。可以通过 自定义脚本控制打包全流程(如资源加密、拆分 Bundle、注入自定义数据),灵活度极高。比如对 AssetBundle 进行加密(打包时自动加密,加载时解密);按自定义规则拆分 Bundle(如按资源大小、标签动态分组)。适用于大型项目需深度定制打包流程(如加密、性能优化)。
    • RawFileBuildPipeline:原生文件构建管线,处理Unity无法识别的资源(如FMOD音频文件)。直接 复制原生文件(不经过 Unity 处理) 到输出目录,保留文件原始格式(如自定义二进制、FMOD 音频文件)。
  • Build Output:构建输出目录,根据Unity当前平台划分构建结果,便于管理不同平台资源。

  • Build Version:设置构建的资源包版本,用于标识补丁包等内容。

  • Clear Build Cache:清理构建缓存,勾选则重新构建所有资源包;不勾选启用增量打包,提升构建速度。

  • Use Asset Depend DB:开启后使用资源依赖关系数据库,加快资源收集与构建效率。

  • Encryption:加密类列表,支持自定义加密(需实现 IEncryptionServices 接口),包含文件偏移、内存、流等解密加载方式。

  • Compression:设置资源包压缩方式,优化包体大小。

  • File Name Style:定义输出资源包文件名样式,如 HashName(哈希值)、BundleName(资源包名)、BundleName_HashName(资源包名+哈希值)。

  • Copy Buildin File Option:首包资源拷贝方式:

    • None:不拷贝文件。首包目录不包含任何资源,所有资源依赖后续热更新下载。安装包体积最小,但弱网环境可能无法加载资源,依赖热更系统兜底。适用于纯热更游戏(如长期运营的网游),首包仅含启动代码,资源全通过网络动态加载。
    • ClearAndCopyAll:清空后拷贝所有文件。先清空首包目录原有资源,再将所有符合构建条件的资源拷贝进去。首包包含完整资源,无需依赖热更,但安装包体积最大,构建时间较长。适用于游戏大版本更新(如 UI 全面改版、核心玩法重构),需替换首包内所有旧资源,确保首包资源为最新全量。
    • ClearAndCopyByTags:清空后按资源标签拷贝。先清空首包目录,再 仅拷贝带指定标签的资源(标签需在资源配置中预先定义)。首包体积可控(仅含核心资源),适合标签化管理清晰的项目,但需确保标签覆盖所有必要资源。比如首包仅需 “核心玩法” 资源(如主场景、基础角色,标签为 core),清空旧资源后,将 core 标签的资源集中拷贝进首包。
    • OnlyCopyAll:不清空直接拷贝所有文件。保留首包目录原有资源,直接追加拷贝所有新资源(可能导致重复,需谨慎使用)。 相对来说迭代效率高,但长期使用易导致首包体积冗余,需人工清理重复资源。 适用于小版本更新(如修复 Bug、新增少量道具),首包已有基础资源,新增资源直接追加,避免覆盖旧内容(需保证资源地址无冲突)
    • OnlyCopyByTags:不清空直接按标签拷贝。保留首包目录原有资源,仅追加拷贝带指定标签的新资源。适合活动资源兜底(首包预先存放部分活动资源,降低热更压力),需维护标签一致性,避免遗漏。比如首包已有 core 标签的核心资源,新增暑期活动资源(标签 activity_summer),仅追加该标签的资源,避免影响核心资源。

构建操作与结果

  • 构建执行:点击“构建”按钮启动多节点流程,任一节点错误则构建失败,错误信息可在控制台查看。

  • 补丁包:构建成功后,输出目录生成以“资源版本号”命名的补丁包文件夹,包含:
    • 资源清单
      • DefaultPackage.version:资源版本文件。
      • DefaultPackage_xxx.hash:记录资源清单哈希值。
      • DefaultPackage_xxx.json:Json格式,用于预览信息。
      • DefaultPackage_xxx.bytes:二进制格式,供程序读取加载。
    • 构建报告DefaultPackage_xxx.report,可通过构建报告窗口查看详细构建信息。

重要概念与注意事项

  • 增量构建:利用Unity资源构建缓存,避免重复构建,提升打包效率。也就是不勾选Clear Build Cache。

  • 首包资源:构建应用时打入首包的资源,存放于 StreamingAssets/yoo/ 目录,支持热更新。也就是选择Copy Buildin File Option首包资源拷贝方式。

  • Jenkins自动化构建:参考示例代码配置构建参数(如输出目录、版本、加密等),通过代码执行构建流程,实现自动化。如果需要自动化构建,可以参考如下几个不同构建的代码范例。

  • SBP构建管线注意事项:需设置内置着色器资源包名称,且与自动收集的着色器资源包名一致,确保构建正确。

using UnityEditor;
using UnityEngine;
using YooAsset;
using YooAsset.Editor;

public static class TestPackageBuilder
{
    /// <summary>
    /// 构建资源包
    /// </summary>
    public static PackageInvokeBuildResult BuildPackage(PackageInvokeBuildParam buildParam)
    {
        string packageName = buildParam.PackageName;
        string buildPipelineName = buildParam.BuildPipelineName;

        if (buildPipelineName == EBuildPipeline.EditorSimulateBuildPipeline.ToString())
        {
          //...
        }
        else if (buildPipelineName == EBuildPipeline.ScriptableBuildPipeline.ToString())
        {
            string projectPath = EditorTools.GetProjectPath();
            string outputRoot = $"{projectPath}/Bundles/Tester_SBP";

            // 内置着色器资源包名称
            var builtinShaderBundleName = GetBuiltinShaderBundleName(packageName);
            //...
        }
        else if (buildPipelineName == EBuildPipeline.BuiltinBuildPipeline.ToString())
        {
            //...
        }
        else if (buildPipelineName == EBuildPipeline.RawFileBuildPipeline.ToString())
        {
           //...
        }
        else
        {
            throw new System.NotImplementedException(buildPipelineName);
        }
    }

    /// <summary>
    /// 内置着色器资源包名称
    /// 注意:和自动收集的着色器资源包名保持一致!
    /// </summary>
    private static string GetBuiltinShaderBundleName(string packageName)
    {
        var uniqueBundleName = AssetBundleCollectorSettingData.Setting.UniqueBundleName;
        var packRuleResult = DefaultPackRule.CreateShadersPackRuleResult();
        return packRuleResult.GetBundleName(packageName, uniqueBundleName);
    }
}

AssetBundle Reporter 构建报告界面

点击import,选择Report文件

YooAsset 构建报告工具支持查看概览信息、资源对象视图、资源包视图,且仅适配 Unity 2019.4+ 版本。要import后选择打包出来的report文件才能查看。各视图功能如下:

概览视图

  • 核心作用:呈现构建报告整体概况,包含资源包数量、资源对象总数、构建耗时、错误与警告统计等关键数据。通过概览可快速判断构建是否成功,是否存在需处理的异常问题(如大量错误/警告),从宏观层面把握构建结果。

资源对象列表视图

  • 内容展示
    • 罗列所有参与构建的资源对象,显示资源名称、类型、路径等基础信息。
    • 标注每个资源对象依赖的资源包,便于追踪资源与资源包的依赖关系。例如,分析模型资源对材质、纹理资源包的引用,辅助优化资源引用逻辑,避免依赖冗余或缺失。

资源包列表视图

  • 内容展示
    • 展示所有生成的资源包,包含资源包名称、大小、版本等信息。
    • 列出每个资源包内包含的资源对象,清晰呈现资源包内容构成。通过该视图可校验资源包是否包含预期资源,是否混入无关内容,进而优化资源包划分策略,确保资源打包的合理性与高效性。

AssetBundle Debugger 调试器界面

调试器核心功能

  1. 作用:在游戏运行时实时监控资源包加载状态,追踪资源对象引用计数,辅助定位资源泄漏问题。
  2. 资源泄漏定位:通过引用计数与资源包卸载条件,快速识别未释放的资源。
  3. 性能优化:分析加载耗时与异步任务执行链,优化资源加载策略。
  4. 依赖关系验证:校验资源包依赖关系,避免冗余加载或错误引用。

平台支持与调试配置

  • 支持版本:仅适配 **Unity 2019.4+**。
  • 安卓平台调试
    • Unity 2020/2021:直接支持。
    • Unity 2019:需通过 ADB 命令转发调试端口:
      adb forward tcp:34999 localabstract:Unity-包名  
      
  • 真机远程调试
    • 构建安装包时:勾选 Development BuildAutoconnect Profiler,确保调试器可连接。

资源对象列表视图

  • 功能:展示当前加载的所有资源对象及其关键信息:
    • 资源名称/路径:标识资源唯一性。
    • 加载耗时:资源加载耗时(毫秒),定位性能瓶颈。
    • 加载状态:如 LoadingLoadedUnloaded 等。
    • 引用计数:统计资源被引用的次数,引用计数归零时触发卸载。
    • 依赖资源包列表(Depend Bundles):选中资源加载所依赖的资源包,帮助分析资源包依赖链。

资源包列表视图

  • 功能:监控当前加载的资源包及其关联信息:
    • 资源包名称/大小:标识资源包基础属性。
    • 加载状态:如 LoadedUnloaded
    • 引用计数:资源包被引用的次数。
    • 使用中的资源(Using Asset):显示该资源包当前正在使用的资源对象列表。
    • 引用关系(Reference Bundle):展示哪些资源包引用了当前选中的资源包。
  • 资源包卸载条件
    • 资源包的 Using Asset 列表内所有资源引用计数为零。
    • 资源包的 Reference Bundle 列表内所有资源包引用计数为零。

异步操作任务列表视图

  • 功能:跟踪所有异步任务执行详情:
    • 任务类型:如资源加载、卸载、场景切换等。
    • 开始时间/耗时:记录任务启动时间与执行时长。
    • 任务状态:如 RunningCompletedFailed
    • 附加调试信息:包括错误日志、操作链上下文等。
  • 操作链分析
    • 选中异步任务后,底部显示完整操作链信息,帮助定位任务执行路径中的问题节点。

AssetArt Scanner 资产扫描器

工具界面组成

  • 左侧:资产扫描器列表,显示所有可用扫描器。
  • 中间:选中扫描器的配置界面,设置扫描规则与参数。
  • 右侧:扫描器参数检视界面,展示自定义扫描器的专属配置。

基础操作按钮

  • 导入按钮:加载保存的 XML 配置文件,复用历史扫描规则。
  • 导出按钮:将当前配置导出为 XML 文件,便于备份或团队共享。
  • 扫描所有按钮:执行全部扫描器任务,生成多份扫描报告。

扫描器配置项详解

  • Scanner Name:定义扫描器名称(如 TextureScanner),标识扫描器功能。
  • Scanner Desc:添加扫描器备注信息,说明扫描用途或规则。
  • Scanner Schema:关联扫描器模式文件(继承 ScannerSchema 的 ScriptableObject)。
  • Output Folder:设置报告文件输出目录,默认为 Assets/ScanReports
  • Collector:指定扫描路径,可配置多个文件夹或文件范围。

扫描器检视界面

  • 功能:开发者实现自定义扫描器时,可编写专属参数检视面板。
  • 实现方式:继承 SchemaInspector 类,通过 EditorGUI 绘制可视化配置项。

扫描器接口扩展

需继承基类 ScannerSchema 并实现以下核心方法:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace YooAsset.Editor
{
    public abstract class ScannerSchema : ScriptableObject
    {
        /// <summary>
        /// 获取用户指南信息
        /// </summary>
        public abstract string GetUserGuide();

        /// <summary>
        /// 运行生成扫描报告
        /// </summary>
        public abstract ScanReport RunScanner(AssetArtScanner scanner);

        /// <summary>
        /// 修复扫描结果
        /// </summary>
        public abstract void FixResult(List<ReportElement> fixList);

        /// <summary>
        /// 创建检视面板
        /// </summary>
        public virtual SchemaInspector CreateInspector()
        {
            return null;
        }
    }
}
扩展示例(太空战机DEMO中的纹理扫描器)
#if UNITY_2019_4_OR_NEWER
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor;
using YooAsset.Editor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;

[CreateAssetMenu(fileName = "TextureSchema", menuName = "YooAssetArt/Create TextureSchema")]
public class TextureSchema : ScannerSchema
{
    /// <summary>
    /// 图片最大宽度
    /// </summary>
    public int MaxWidth = 1024;

    /// <summary>
    /// 图片最大高度
    /// </summary>
    public int MaxHeight = 1024;

    /// <summary>
    /// 测试列表
    /// </summary>
    public List<string> TestStringValues = new List<string>();
    

    /// <summary>
    /// 获取用户指南信息
    /// </summary>
    public override string GetUserGuide()
    {
        return "规则介绍:检测图片的格式,尺寸";
    }

    /// <summary>
    /// 运行生成扫描报告
    /// </summary>
    public override ScanReport RunScanner(AssetArtScanner scanner)
    {
        // 创建扫描报告
        string name = "扫描所有纹理资产";
        string desc = GetUserGuide();
        var report = new ScanReport(name, desc);
        report.AddHeader("资源路径", 600, 500, 1000).SetStretchable().SetSearchable().SetSortable().SetCounter().SetHeaderType(EHeaderType.AssetPath);
        report.AddHeader("图片宽度", 100).SetSortable().SetHeaderType(EHeaderType.LongValue);
        report.AddHeader("图片高度", 100).SetSortable().SetHeaderType(EHeaderType.LongValue);
        report.AddHeader("内存大小", 120).SetSortable().SetUnits("bytes").SetHeaderType(EHeaderType.LongValue);
        report.AddHeader("苹果格式", 100);
        report.AddHeader("安卓格式", 100);
        report.AddHeader("错误信息", 500).SetStretchable();

        // 获取扫描资源集合
        var searchDirectorys = scanner.Collectors.Select(c => { return c.CollectPath; });
        string[] findAssets = EditorTools.FindAssets(EAssetSearchType.Texture, searchDirectorys.ToArray());

        // 开始扫描资源集合
        var results = SchemaTools.ScanAssets(findAssets, ScanAssetInternal);
        report.ReportElements.AddRange(results);
        return report;
    }
    private ReportElement ScanAssetInternal(string assetPath)
    {
        var importer = TextureTools.GetAssetImporter(assetPath);
        if (importer == null)
            return null;

        // 加载纹理对象
        var texture = AssetDatabase.LoadAssetAtPath<Texture>(assetPath);
        var assetGUID = AssetDatabase.AssetPathToGUID(assetPath);
        var iosFormat = TextureTools.GetPlatformIOSFormat(importer);
        var androidFormat = TextureTools.GetPlatformAndroidFormat(importer);
        var memorySize = TextureTools.GetStorageMemorySize(texture);

        // 获取错误信息
        string errorInfo = string.Empty;
        {
            // 苹果格式
            if (iosFormat != TextureImporterFormat.ASTC_4x4)
            {
                errorInfo += " | ";
                errorInfo += "苹果格式不对";
            }

            // 安卓格式
            if (androidFormat != TextureImporterFormat.ASTC_4x4)
            {
                errorInfo += " | ";
                errorInfo += "安卓格式不对";
            }

            // 多级纹理
            if (importer.isReadable)
            {
                errorInfo += " | ";
                errorInfo += "开启了可读写";
            }

            // 超大纹理
            if (texture.width > MaxWidth || texture.height > MaxHeight)
            {
                errorInfo += " | ";
                errorInfo += "超大纹理";
            }
        }

        // 添加扫描信息
        ReportElement result = new ReportElement(assetGUID);
        result.AddScanInfo("资源路径", assetPath);
        result.AddScanInfo("图片宽度", texture.width);
        result.AddScanInfo("图片高度", texture.height);
        result.AddScanInfo("内存大小", memorySize);
        result.AddScanInfo("苹果格式", iosFormat.ToString());
        result.AddScanInfo("安卓格式", androidFormat.ToString());
        result.AddScanInfo("错误信息", errorInfo);

        // 判断是否通过
        result.Passes = string.IsNullOrEmpty(errorInfo);
        return result;
    }

    /// <summary>
    /// 修复扫描结果
    /// </summary>
    public override void FixResult(List<ReportElement> fixList)
    {
        SchemaTools.FixAssets(fixList, FixAssetInternal);
    }
    private void FixAssetInternal(ReportElement result)
    {
        var scanInfo = result.GetScanInfo("资源路径");
        var assetPath = scanInfo.ScanInfo;
        var importer = TextureTools.GetAssetImporter(assetPath);
        if (importer == null)
            return;

        // 苹果格式
        var iosPlatformSetting = TextureTools.GetPlatformIOSSettings(importer);
        iosPlatformSetting.format = TextureImporterFormat.ASTC_4x4;
        iosPlatformSetting.overridden = true;

        // 安卓格式
        var androidPlatformSetting = TextureTools.GetPlatformAndroidSettings(importer);
        androidPlatformSetting.format = TextureImporterFormat.ASTC_4x4;
        androidPlatformSetting.overridden = true;

        // 可读写
        importer.isReadable = false;

        // 保存配置
        importer.SetPlatformTextureSettings(iosPlatformSetting);
        importer.SetPlatformTextureSettings(androidPlatformSetting);
        importer.SaveAndReimport();
        Debug.Log($"修复了 : {assetPath}");
    }

    /// <summary>
    /// 创建检视面板对
    /// </summary>
    public override SchemaInspector CreateInspector()
    {
        var container = new VisualElement();

        // 图片最大宽度
        var maxWidthField = new IntegerField();
        maxWidthField.label = "图片最大宽度";
        maxWidthField.SetValueWithoutNotify(MaxWidth);
        maxWidthField.RegisterValueChangedCallback((evt) =>
        {
            MaxWidth = evt.newValue;
        });
        container.Add(maxWidthField);

        // 图片最大高度
        var maxHeightField = new IntegerField();
        maxHeightField.label = "图片最大高度";
        maxHeightField.SetValueWithoutNotify(MaxHeight);
        maxHeightField.RegisterValueChangedCallback((evt) =>
        {
            MaxHeight = evt.newValue;
        });
        container.Add(maxHeightField);

        // 创建测试列表
#if UNITY_2021_3_OR_NEWER
        ReorderableListView reorderableListView = new ReorderableListView();
        reorderableListView.SourceData = TestStringValues;
        reorderableListView.HeaderName = "测试列表";
        container.Add(reorderableListView);
#endif

        SchemaInspector inspector = new SchemaInspector(container);
        return inspector;
    }
}
#endif

扫描报告输出

  • 报告内容:包含问题资源路径、错误类型、修复建议等结构化数据。
  • 输出格式:默认生成 .json 文件,在Asset目录下,支持第三方工具解析或二次处理。

典型应用场景

  1. 美术规范检查:验证纹理尺寸、模型面数等是否符合项目标准。
  2. 资源冗余检测:扫描重复资源或未引用资产,优化包体体积。
  3. 自动化修复:批量修改资源导入设置(如压缩格式、Mipmap开关)。

AssetArt Reporter 资产扫描报告


报告核心价值

  • 问题精准定位:通过多维度过滤快速定位异常资源。
  • 自动化修复:结合扫描器 FixResult 方法实现批量修复,提升效率。
  • 团队协作:导出/导入报告文件,便于跨部门协同处理资源问题。

核心功能模块

报告导入与导出
  • 单文件导入(Import):加载单个扫描报告文件(.json 格式),查看详细扫描结果。
  • 多文件合并导入(Multi-Import):同时导入多个报告文件并合并显示(需确保扫描器一致,避免数据冲突)。
  • 选择性导出(Export Select):勾选需要导出的元素,将关联资源文件拷贝至指定目录,便于批量处理问题资源。
问题修复功能
  • 一键修复所有(Fix All):自动修复所有未通过元素(自动排除白名单和隐藏元素)。
  • 选择性修复(Fix Select):修复勾选的元素(包含白名单和隐藏元素),用于精细化修复操作。
显示控制选项
  • 显示/隐藏元素:通过“眼睛”图标开关控制隐藏元素的显隐。
  • 显示通过元素:勾选后展示扫描通过的资源项,用于完整性校验。
  • 显示白名单元素:展示已加入白名单的例外资源,避免重复扫描干扰。

注意:该工具仅支持 Unity 2019.4+ 版本。

搜索栏高级语法

通用关键字搜索

直接输入文件名、路径片段等关键词,快速过滤匹配项:

example_texture  
Assets/Art/Characters  
指定项关键字搜索

通过 字段名:关键词 格式精确搜索特定字段内容(冒号为英文小写):

错误信息:安卓格式不对  
资源类型:Texture  
指定项数值比较

使用 字段名 比较符 数值 格式进行范围筛选(**支持 =, >, >=, <, <=**):

内存大小=1024      // 等于 1024 KB  
图片宽度>1024      // 宽度大于 1024 像素  
图片高度<=2048     // 高度小于等于 2048 像素  

操作示例场景

  1. 定位纹理格式问题

    错误信息:ETC2不支持  
    

    筛选出所有因 ETC2 压缩格式导致问题的纹理资源。

  2. 检测超大资源

    内存大小>2048  
    

    查找内存占用超过 2MB 的资源,优化包体体积。

  3. 批量修复白名单资源

    • 勾选白名单元素 → 点击 Fix Select,将问题资源标记为例外,避免后续扫描重复报警。

示例工程中的配置文件

这些是 YooAsset 示例工程里随包放的配置资产与样例脚本,它们全都属于编辑器期资源(不会进运行包),主要用于:

  • 保存 AssetBundle 收集/打包规则
  • 保存 美术扫描器 的配置与阈值
  • 备份/导入导出 收集规则
    你可以把它们统一搬到自己的目录,例如:
Assets/XX/Setting/YooAsset/

位置并不强制;YooAsset 是按资源类型查找这些资产,不依赖固定路径。移动后若窗口提示丢失,点一次 Fix 即可恢复引用。

文件一览与用途

AssetBundleCollectorSetting.asset(必须有,唯一)
  • 作用:YooAsset 收集/打包规则的总配置(ScriptableObject)。

    • 内含:Packages → Groups → Collectors、地址/打包/过滤/标签等所有规则。
  • 何时生成:第一次打开 AssetBundle Collector 窗口并点击 Fix / Save 时自动创建;或新增 Package/Group 后保存。

  • 放哪都行:建议放到
    Assets/LoL/Setting/YooAsset/AssetBundleCollectorSetting.asset

  • 版本管理务必提交(含 .meta),团队协作靠它保持规则一致。

  • 移动后:如果 Collector 窗口左上角变红,可点 Fix 让窗口重新定位到该资产。

AssetBundleCollectorConfig(可选的备份/交换格式)
  • 作用导入/导出 收集规则使用的文本/配置文件(示例里是 XML / JSON 之一)。

  • 如何生成:打开 AssetBundle Collector → 右上角 Export;要恢复用 Import

  • 是否必须不是必须;它只是备份/交换介质,运行与打包都不读取它。

  • 最佳实践

    • 团队协作场景保留它,便于做“规则快照”和比对;
    • 个人项目可以不保留,仅提交 AssetBundleCollectorSetting.asset 即可。
AssetArtScannerSetting.asset(可选)
  • 作用美术规范扫描器的统一配置(面板里创建的各个 Scanner、扫描范围、输出目录等)。
  • 何时生成:打开 AssetArt Scanner 窗口,配置后点击 Save / Export 时创建。
  • 是否必须:按需使用;你不用扫描器可不创建。
  • 放置建议
    Assets/LoL/Setting/YooAsset/AssetArtScannerSetting.asset
  • 版本管理:若团队固定扫描规则(尺寸/格式阈值),建议提交。
ScannerSchema/*(示例 Schema 与参数资产)
  • 目录含义:存放扫描器的 Schema 资产与示例(如 EffectTextureSchema)。

  • 作用:为某个扫描器提供阈值/规则参数(比如最大贴图尺寸、平台压缩格式等)。

  • 类型:均为继承自 ScannerSchema 的 ScriptableObject。

  • 创建方式

    • 示例项目已给出样例;
    • 你也可以在 Project 视图里 Create → YooAssetArt/Create XXXSchema 自定义创建。
  • 是否必须:只在使用 AssetArt Scanner 时需要;否则可不创建/不提交。

它们从哪里来?怎么“正确生成”

  • AssetBundleCollectorSetting.asset

    1. 打开 YooAsset → AssetBundle Collector
    2. 如果窗口顶端提示找不到配置,点一次 Fix(或新增一个 Package 再 Save)→ 自动生成
    3. 以后你每次修改 Package/Group/Collector 并 Save,都会写回这份资产
  • AssetBundleCollectorConfig

    • 在 Collector 窗口右上角点 Export 生成;Import 用于从这份备份恢复规则
  • AssetArtScannerSetting.asset

    • 打开 YooAsset → AssetArt Scanner,在左侧添加/配置扫描器后 Save 生成
  • ScannerSchema/*.asset

    • 右键 Create(示例提供了 Create TextureSchema 等菜单)或自定义脚本创建

放置与迁移建议

  • 推荐统一放置:

    Assets/XX/Setting/YooAsset/
      AssetBundleCollectorSetting.asset
      AssetBundleCollectorConfig        //(可选)
      AssetArtScannerSetting.asset      //(可选)
      ScannerSchema/                    //(若使用扫描器)
    
  • 移动/改名后

    • Collector / Scanner 窗口若提示缺失,点击 Fix
    • 记得连同 .meta 一起移动/提交,避免 GUID 改变导致引用失效。

运行时与打包期的关系

  • 以上资产只在编辑器期被 YooAsset 的 Collector/Scanner/Builder 使用;换句话说,这些 .asset/配置只在 Unity 编辑器里给 YooAsset 的窗口用,用来决定怎么打包,它们不会被游戏运行时代码读取;不会进最终包体。
  • Build 后,YooAsset 根据“配方”做出真正给游戏运行用的文件,包括AB包,清单,版本文件等;
  • 因此:把它们放进 Resources/ 或 Addressables/YooAsset 里都没有意义;别让它们误入首包。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏