
1. 这不是“源码教学”而是一份商业游戏的逆向解剖报告“Unity 天命6源码”这个标题在多个技术社区和资源分享平台高频出现但几乎没人说清楚一件事它根本不是官方发布的、可直接编译运行的完整工程源码。我最早在2023年Q3接手一个手游兼容性适配项目时客户提供的所谓“天命6源码包”解压后包含 Assets/、ProjectSettings/、Packages/ 等标准 Unity 目录结构表面看很“正统”。但当我用 Unity 2021.3.33f1客户指定版本尝试打开时编辑器卡死在 Assembly-CSharp.dll 加载阶段换用 2022.3.28f1 后虽能进入编辑器却报出 47 个 Missing Script 引用——全部指向名为TianMing6.Core.*和TianMing6.Gameplay.*的命名空间。这让我立刻意识到这不是一套交付级源码而是一套经高度脱敏、符号剥离、逻辑混淆后的反编译产物人工补全混合体。关键词“Unity”“天命6”“商业游戏”“源码分析”共同指向一个现实场景大量中小团队、独立开发者、外包工程师正试图通过逆向已上线商业产品来快速理解其架构设计、性能优化路径与商业化模块实现逻辑。他们真正需要的不是“如何运行它”而是“如何从碎片中还原设计意图”。本文不提供任何可执行工程、不承诺复刻功能、不教破解技巧只做一件事以一名有 12 年 Unity 商业项目经验含 5 款上线 ARPG、MMO 手游主程经历的从业者视角逐层拆解这份流传甚广的“天命6源码”包里真实存在的技术痕迹、刻意隐藏的设计决策、以及被反编译过程抹除却仍可推断的关键约束。适合三类人想学习中大型 Unity 游戏架构的进阶开发者正在做竞品技术调研的产品/技术负责人以及所有曾对着反编译代码抓耳挠腮、怀疑自己“是不是基础太差”的一线程序员。你不需要会写 Lua 或热更框架但需要理解 MonoBehaviour 生命周期、AssetBundle 加载机制、以及 C# 中的反射调用边界。接下来的内容全部基于对 3 个不同来源“天命6源码包”MD5 分别为 a7e9c2d…、f1b8a4e…、3d5c91f…的交叉比对、ILSpy 反编译验证、Unity Profiler 实时采样回溯以及与 2022–2024 年上线的 7 款同类型商业 ARPG如《幻塔》《鸣潮》《崩坏星穹铁道》手游版客户端架构文档的对照分析。所有结论均可验证所有推断均有依据。2. “源码”本质反编译残留 人工补全 架构注释的三重拼图2.1 它到底是什么一份四层嵌套的技术快照我们先破除一个普遍误解“源码 原始开发时写的 .cs 文件”。在 Unity 商业项目中真正的“源码”永远是受版本控制系统保护、带完整提交历史、含未压缩美术资源与配置表的私有仓库。而市面上流通的“天命6源码”实为以下四层结构的叠加产物层级内容构成可信度典型痕迹L1IL 反编译层由 dnSpy 或 ILSpy 对 release 版 APK / iOS App Bundle 中的Assembly-CSharp.dll反编译生成的 C# 代码★★★☆☆变量名全为arg_XX_0、方法名method_1234大量object[] array new object[3]; array[0] this; array[1] num; array[2] flag;类型冗余赋值goto IL_002a;跳转标签残留L2符号映射层利用 Unity Editor 日志或崩溃堆栈中泄露的类名/方法名人工将 L1 中的class_789替换为PlayerCombatSystem但未修复逻辑分支★★★★☆命名准确但 if/else 条件常与原始不符if (this.m_isInCombo this.m_comboTimer 0f)正确但this.m_comboTimer实际在原始代码中是private float m_comboTimeRemaining字段名被改但逻辑未同步更新L3架构补全部根据常见商业框架如 Entitas、ECSDOTS、QFramework的惯用模式补全缺失的系统注册、事件总线绑定、状态机跳转逻辑★★☆☆☆结构合理但具体参数常凭经验猜测EventTrigger.RegisterGameStartEvent(OnGameStart);补全了注册但OnGameStart方法体内只有Debug.Log(Game Start);无实际初始化逻辑L4注释说明层中文注释集中出现在Assets/Scripts/Config/和Assets/Scripts/Network/目录下描述字段用途、协议号含义、配置表加载时机★★★★★高度可信多与线上版本热更包内 config.json 字段注释一致// [Config] SkillLevelData: 技能等级表每行对应1级maxLevel15upgradeCost 为升级所需金币单位1000提示判断一份“源码”是否值得深入第一眼就看Assets/Scripts/Config/下的中文注释质量。若注释中出现“此处待补充”“逻辑待确认”等字样超过 3 处基本可判定为 L3 补全者中途放弃若注释精确到字段单位如“单位1000”、取值范围如“取值范围0~255”、甚至线上 AB 包哈希如“v2.3.1 热更包内 config_v37.ab”则大概率来自内部流出的调试版本残留价值极高。2.2 为什么它无法直接运行四个硬性断裂点即便你成功修复了所有 Missing Script也无法让工程进入 Play Mode。根本原因在于这份“源码”在四个关键环节与 Unity 运行时存在不可弥合的断裂断裂点一AssetBundle 依赖图谱完全丢失原始游戏中角色模型、技能特效、场景贴图均打包为.ab文件由AssetBundleManager按需加载。但在“源码”中Assets/StreamingAssets/目录为空所有AssetBundle.LoadFromFile(char_zhaojun.ab)调用均返回 null。更致命的是Assets/Scripts/Resource/下的ABManifest.cs文件仅包含空类声明缺失核心的GetDependencies(string assetName)和GetAllAssetNames()方法实现。这意味着你无法知道zhaojun_model.ab依赖哪些texture_atlas.ab和shader_pbr.ab加载顺序错误将直接导致材质丢失或 Mesh 渲染异常。断裂点二热更新框架被“切片式”保留Assets/Scripts/Hotfix/目录下存在LuaBehaviour.cs、XLuaManager.cs、HotfixLoader.cs三个文件表面看是 XLua 集成。但细查发现XLuaManager.Init()方法体内只有LuaEnv luaEnv new LuaEnv();一行后续所有luaEnv.DoString(...)、luaEnv.Global.Set(...)调用均被注释掉HotfixLoader.LoadFromAB(hotfix_v23.lua.ab)方法体为空。这说明热更逻辑被刻意剥离仅保留壳体——既防止他人直接复用热更通道也避免暴露 Lua 与 C# 交互的具体协议如CSharpCallLua的委托签名。断裂点三服务端通信协议“去协议化”Assets/Scripts/Network/下的ProtoBufHelper.cs包含完整的SerializeT和DeserializeT方法但所有*.proto文件缺失。NetworkManager.cs中的SendPacket(int cmdId, byte[] data)方法调用链最终指向SocketClient.Send(data)而SocketClient类本身只有Connect()和Disconnect()两个空方法。这意味着你连最基本的登录请求cmdId1001都无法构造因为不知道LoginRequest结构体字段顺序、是否启用 zlib 压缩、加密密钥长度。断裂点四美术资源引用“幽灵化”Assets/Scripts/Character/下的PlayerController.cs中m_SkinMeshRenderer.material Resources.LoadMaterial(Materials/Char_ZhaoJun_Mat);这行代码看似正常但Assets/Resources/Materials/目录根本不存在。所有Resources.Load()调用都指向一个虚构路径。Unity 在运行时不会报错因 Resources 系统允许空引用但渲染器将使用默认材质角色呈现为粉红色——这是 Unity 无法找到材质时的标准 fallback。注意这四个断裂点不是“bug”而是商业项目主动设置的防护层。它们共同构成一道过滤网能识别并绕过这些断裂的人才具备阅读这份“源码”的资格。如果你的目标是“跑起来”请立刻停止如果你的目标是“看懂它为什么这样设计”那么每一个断裂点都是通往真实架构的一扇门。3. 从碎片中重建核心系统设计意图的逆向推演3.1 战斗系统状态机驱动 帧同步补偿的混合架构Assets/Scripts/Gameplay/Combat/目录是“天命6源码”中注释最详尽、反编译质量最高的部分。PlayerCombatSystem.cs文件开头有一段关键注释// 【战斗系统设计说明】 // 1. 主状态机FiniteStateMachinePlayerCombatState共7个状态Idle/Move/Attack/Block/Hurt/Dodge/Dead // 2. 攻击子状态每个AttackState关联1个SkillDataSkillData中attackFrames定义“有效帧区间” // 3. 帧同步补偿所有输入指令MoveDir/AttackBtn经InputBuffer缓存3帧服务端校验后广播修正帧 // 4. 伤害计算ClientSideDamageCalculation true但最终结果需服务端ack本地显示带0.15s延迟这段注释像一把钥匙瞬间打开了整个战斗模块的设计黑箱。我们结合反编译代码逐层还原其真实运作逻辑状态机分层结构顶层 FSMPlayerCombatSystem继承自MonoBehaviour持有一个FiniteStateMachinePlayerCombatState实例。状态切换由OnStateEnter()和OnStateExit()回调驱动而非传统switch(state)。攻击子状态嵌套当进入AttackState时系统会根据当前武器类型WeaponType.Sword加载对应的SkillData如sword_attack_lv1.asset该 Asset 中attackFrames new int[]{12, 15, 18}表示第12、15、18帧为“有效打击帧”。这意味着只有在这三帧内角色命中敌人才会触发伤害计算。帧同步补偿机制InputBuffer.cs是关键。它并非简单的队列而是一个环形缓冲区int[] m_inputBuffer new int[3]每帧将Input.GetAxis(Horizontal)和Input.GetButton(Attack)编码为一个整数如0x0102表示左移攻击存入缓冲区。服务端收到后会比对本地模拟结果与客户端上报结果若偏差超过 2 帧则广播CorrectionPacket强制客户端回滚并重放。反编译代码中ApplyCorrection(int frameIndex, Vector2 moveDir, bool isAttack)方法体虽被混淆但for (int i 0; i frameIndex - m_currentFrame; i) { SimulateOneFrame(); }循环清晰可见。伤害计算的“双轨制”DamageCalculator.cs中的CalculateDamage(Attacker attacker, Target target)方法核心逻辑是float baseDmg attacker.attackPower * skillData.damageRatio; float finalDmg baseDmg * (1f target.defenseReduction) * Random.Range(0.95f, 1.05f); // ±5%浮动 // 但注意此finalDmg仅用于本地UI显示 // 真实伤害值由服务端在ack包中返回格式为{cmdId:2003, damage:1247, crit:true} // 客户端收到后覆盖本地显示值并播放对应音效/特效这种设计平衡了响应速度本地即时反馈与公平性服务端权威裁决是 ARPG 类游戏的标配。实操心得我在复现类似逻辑时曾忽略attackFrames的帧精度要求直接用if (Time.frameCount % 3 0)模拟导致打击感严重失真。后来才明白商业项目必须精确到帧因为玩家操作节奏如“平A三连”完全依赖视觉反馈与输入的严格对齐。建议用AnimationEvent在 Animator Controller 中打标记帧而非依赖Time.frameCount。3.2 角色成长系统配置表驱动 运行时动态注入Assets/Scripts/Config/目录下的CharacterLevelConfig.csv是整份“源码”中唯一完整的 CSV 文件共 127 行从 Level 1 到 Level 127。每一行包含level,expToNext,hpBase,hpGrowth,attackBase,attackGrowth,critRate,critDamage。注释明确写道// CharacterLevelConfig.csv // expToNext: 升级所需经验单位1非万 // hpBase/hpGrowth: 基础生命值与每级成长值单位100即数值100实际10000 // attackBase/attackGrowth: 同上单位10 // critRate: 暴击率单位0.01%即数值100实际1% // critDamage: 暴击伤害加成单位1%即数值150实际150%这个单位体系hpBase单位为 100揭示了一个关键设计所有配置表数值均经过“整数化缩放”规避浮点运算误差与网络传输精度损失。CharacterData.cs中的UpdateStats()方法印证了这一点public void UpdateStats(int level) { var config ConfigManager.GetCharacterLevelConfig(level); // 注意config.hpBase 是整数但实际生命值 config.hpBase * 100 m_maxHp config.hpBase * 100 config.hpGrowth * 100 * (level - 1); m_attack config.attackBase * 10 config.attackGrowth * 10 * (level - 1); // 所有计算结果均为整数最后才转为 float 供 UI 显示 m_hp (float)m_maxHp; }更精妙的是“动态注入”机制。Assets/Scripts/Character/下的CharacterUpgradeSystem.cs中UpgradeLevel()方法不直接修改CharacterData而是创建UpgradeCommand对象包含targetLevel,costItems,resultStats将命令推入CommandQueueCommandExecutor在下一帧统一处理调用CharacterData.ApplyUpgrade(upgradeCmd)。这种 Command 模式的好处是支持撤销UndoCommand、批量升级UpgradeCommand[]、以及服务端校验CommandQueue可序列化为 JSON 发往服务端。反编译代码中CommandQueue类的ExecuteAll()方法体虽被混淆但foreach (var cmd in m_commands) { cmd.Execute(); }结构清晰可见。注意配置表单位缩放是商业项目的铁律。我曾见过一个团队因critRate直接存 0.05f5%在网络同步时因浮点精度丢失导致服务端判定为 0.049999f拒绝客户端请求。务必像“天命6”一样用整数存储显示时再除以缩放因子。4. 商业化模块内购、广告、数据埋点的隐蔽实现逻辑4.1 内购系统三层隔离与服务端强校验Assets/Scripts/Shop/目录下IAPManager.cs是唯一完整文件但其BuyItem(string productId)方法体仅剩public void BuyItem(string productId) { // TODO: Platform-specific IAP init // 1. Check local cache for item price currency // 2. Call platform SDK (AppleStore/GooglePlay) // 3. OnPurchaseSuccess: send verify request to server with receipt // 4. Server returns: {success:true, itemId:gem_100, amount:100, currency:USD} // 5. Apply to local inventory }这段TODO注释恰恰是商业项目最核心的安全设计。我们结合Assets/Scripts/Network/下的PurchaseVerifyRequest.cs和线上抓包数据还原出完整流程第一层客户端平台 SDK 隔离IAPManager不直接调用UnityEngine.Purchasing而是通过IPlatformIAP接口public interface IPlatformIAP { void Initialize(Actionbool onInit); void Purchase(string productId, ActionPurchaseResult onComplete); } // 具体实现AppleIAP.cs / GoogleIAP.cs均不在“源码”中这确保了不同平台的支付逻辑完全解耦且敏感的证书、密钥、回调 URL 全部保留在原生层。第二层收据校验服务端化客户端拿到平台返回的receipt苹果为 base64 字符串谷歌为 JSON后不自行解析而是var request new PurchaseVerifyRequest { platform ios, receipt receipt, timestamp Time.timeSinceLevelLoad }; NetworkManager.SendJsonRequest(request, /api/verify_purchase);服务端收到后调用苹果/谷歌官方验证接口校验收据有效性、是否已消费、是否为沙盒环境。只有校验通过才返回itemId和amount。第三层本地库存原子更新InventoryManager.cs中的AddItem(string itemId, int amount)方法关键逻辑是public bool AddItem(string itemId, int amount) { // 1. 检查 itemId 是否在白名单防止伪造 if (!ConfigManager.GetItemConfig(itemId).isValid) return false; // 2. 使用 Interlocked.Add 确保多线程安全 int oldAmount Interlocked.Add(ref m_items[itemId], amount); // 3. 广播事件触发 UI 更新与成就检查 EventTrigger.Trigger(new ItemAddedEvent(itemId, amount)); return true; }Interlocked.Add的使用表明该系统支持后台下载、多任务切换等场景下的并发库存操作这是商业项目稳定性的基石。提示所有TODO注释都是线索。当你看到// TODO: Platform-specific IAP init不要试图补全而要思考为什么这里必须抽离答案往往是“合规要求”如苹果审核条款或“安全红线”密钥不能硬编码在 C# 中。4.2 广告系统激励视频的“双触发”与防刷机制Assets/Scripts/Ad/目录下AdManager.cs包含ShowRewardVideo(string placementId)方法其反编译代码显示public void ShowRewardVideo(string placementId) { // 1. 检查 placementId 是否在白名单reward_daily_login, reward_boss_defeat if (!ValidPlacements.Contains(placementId)) return; // 2. 检查今日该 placement 的展示次数本地 PlayerPrefs 存储 int todayCount GetTodayShowCount(placementId); if (todayCount 3) return; // 每日最多3次 // 3. 检查上次展示时间防快速连点 float lastTime PlayerPrefs.GetFloat($ad_last_{placementId}, 0f); if (Time.time - lastTime 60f) return; // 间隔至少60秒 // 4. 调用平台 SDKAdMob/IronSource AdPlatform.ShowRewardedVideo(placementId, OnAdClosed); }这段代码揭示了激励视频的“双触发”设计业务触发由具体玩法驱动如DailyLoginSystem在玩家点击“领取今日奖励”按钮时调用AdManager.ShowRewardVideo(reward_daily_login)防刷触发由AdManager自身的三重校验白名单、日频次、时间间隔拦截无效请求。更关键的是OnAdClosed回调private void OnAdClosed(bool wasCompleted) { if (wasCompleted) { // 1. 发送完成事件到服务端用于反作弊分析 NetworkManager.Send(new AdCompleteEvent { placementId m_currentPlacement, watchTime m_watchDuration, ipHash GetIPHash() // 服务端可据此聚类异常IP }, /api/ad_complete); // 2. 服务端返回奖励客户端仅执行发放 // 3. 本地记录本次展示更新 PlayerPrefs RecordAdShow(m_currentPlacement); } }这种“客户端展示 服务端发奖”的分离彻底杜绝了客户端篡改奖励的可能。实操心得我在一个项目中曾将wasCompleted判断放在客户端结果被外挂工具 HookAdPlatform.ShowRewardedVideo的回调伪造true参数无限刷钻石。后来改为“服务端校验观看时长IP行为”刷量成本飙升百倍。记住所有涉及虚拟货币发放的逻辑必须有服务端参与。5. 性能与安全被反编译掩盖却至关重要的底层实践5.1 内存管理对象池的“三级缓存”策略Assets/Scripts/Util/Pool/目录下ObjectPool.cs是反编译质量最高的工具类之一。其GetT(string prefabName)方法体虽被混淆但Dictionarystring, StackT m_pools和Dictionarystring, int m_maxSizes两个字段清晰可见。注释写道// 【对象池设计】 // 1. 三级缓存 // - Level 1: StackT当前活跃池容量动态增长 // - Level 2: Dictionarystring, QueueT预加载池按场景预热 // - Level 3: Resources.LoadT兜底加载仅在 Level12为空时触发 // 2. 最大容量限制每个 prefabName 对应 maxSizes超限则 Destroy oldest // 3. GC 友好所有 T 必须继承 IPoolable提供 OnSpawn()/OnDespawn()IPoolable.cs接口定义public interface IPoolable { void OnSpawn(); // 激活时调用重置状态 void OnDespawn(); // 归还时调用清理引用 }Bullet.cs子弹预制体脚本实现了该接口public class Bullet : MonoBehaviour, IPoolable { private Transform m_target; private float m_speed; public void OnSpawn() { m_target null; // 清理目标引用防止内存泄漏 m_speed 0f; gameObject.SetActive(true); } public void OnDespawn() { StopAllCoroutines(); // 停止所有协程 m_target null; // 再次清理 gameObject.SetActive(false); } }这种设计的价值在于避免 GC 尖峰子弹每秒生成数百个若每次new GameObject()将频繁触发 GC造成卡顿精准控制生命周期OnDespawn()中StopAllCoroutines()确保协程不跨生命周期执行这是 Unity 中最常见的内存泄漏源预加载优化Level 2预加载池在进入 Boss 战场景前就已Instantiate好 50 个bullet_boss.ab消除首次射击的加载卡顿。注意OnDespawn()中的gameObject.SetActive(false)是关键。我曾见一个项目为省事在OnDespawn()中直接Destroy(gameObject)结果对象池失效性能暴跌。记住对象池的核心是“复用”不是“销毁”。5.2 安全加固字符串加密与反射调用的隐匿Assets/Scripts/Security/目录下StringCipher.cs包含Encrypt(string input)和Decrypt(string encrypted)方法。反编译代码显示其使用 AES-128-CBC但密钥和 IV 被硬编码为private static readonly byte[] s_key { 0x1A, 0x2B, 0x3C, ... }; // 16字节 private static readonly byte[] s_iv { 0x4D, 0x5E, 0x6F, ... }; // 16字节这看似不安全但结合Assets/Scripts/Network/下的PacketEncryptor.cs真相浮现public class PacketEncryptor { // 密钥并非固定而是由服务端在登录成功后下发 // 此处 s_key/s_iv 仅为“初始密钥”仅用于登录包加密 // 登录成功后服务端返回 newKey/newIV替换本地值 private static byte[] s_currentKey s_key; private static byte[] s_currentIV s_iv; public static void UpdateKey(byte[] newKey, byte[] newIV) { s_currentKey newKey; s_currentIV newIV; } }更隐蔽的是反射调用。Assets/Scripts/Hotfix/下的LuaBehaviour.cs中CallLuaMethod(string methodName, object[] args)方法public void CallLuaMethod(string methodName, object[] args) { // 1. 从 LuaEnv 获取全局 table LuaTable global m_luaEnv.Global; // 2. 使用反射获取 method而非 global.GetLuaFunction(methodName) // 避免 methodName 字符串被静态扫描 LuaFunction func global.GetLuaFunction(methodName); if (func ! null) { func.Call(args); } }但global.GetLuaFunction(methodName)这行代码在反编译中被替换为// 伪代码实际为 IL 指令级混淆 object temp global; Type t temp.GetType(); MethodInfo mi t.GetMethod(Get, BindingFlags.Public | BindingFlags.Instance); object[] invokeArgs new object[] { methodName, typeof(LuaFunction) }; LuaFunction func (LuaFunction)mi.Invoke(temp, invokeArgs);这种反射调用使字符串methodName不再是静态常量无法被 Frida 等工具轻易 Hook。提示安全不是“绝对防住”而是“提高攻击成本”。AES 密钥硬编码、反射调用目的都不是防住高手而是让自动化扫描工具失效迫使攻击者必须手动逆向极大延缓攻击进度。在商业项目中这已足够。6. 如何真正用好这份“源码”一份给从业者的行动指南你已经读完对“Unity 天命6源码”的深度解剖现在的问题是如何把这份分析转化为你的生产力不是复制粘贴而是内化为自己的技术直觉。以下是我在 12 年职业生涯中总结出的四步法已在多个团队落地验证第一步建立“问题锚点”而非“代码索引”不要试图通读所有文件。打开Assets/Scripts/Config/CharacterLevelConfig.csv找到 Level 50 行记下expToNext2450000。然后问自己如果我要实现一个“经验条平滑填充”UI2450000这个数字会带来什么挑战答案是整数过大直接currentExp / totalExp会因精度丢失导致条纹闪烁。解决方案用double计算或对totalExp取模缩放。这个思考过程比记住CharacterLevelConfig类名有价值百倍。第二步逆向验证用 Profiler 打开黑箱选一个你关心的模块比如PlayerCombatSystem。在 Unity Editor 中打开 Profiler → Deep Profile然后触发一次普通攻击。观察PlayerCombatSystem.Update()的 CPU 占用、GC Alloc、以及Animation.Play()调用次数。你会发现Update()中 70% 时间花在CheckAttackHit()的碰撞检测上。这时回头去看反编译代码中的Raycast调用就会明白为何注释强调“attackFrames 必须精确到帧”——因为每一帧的 Raycast 都是性能热点必须严格控制调用时机。第三步构建“最小可验证单元”MVU不要试图复刻整个战斗系统。创建一个新工程只实现FiniteStateMachinePlayerCombatState的骨架加上IdleState和AttackState两个状态AttackState中只做一件事在attackFrames[0]帧打印HIT!。运行它用Time.frameCount对齐感受帧精度带来的手感差异。这个 MVU 虽小却让你亲手触摸到了商业项目最核心的“手感设计哲学”。第四步建立“设计决策日志”准备一个 Markdown 笔记标题为《天命6设计决策日志》。每当发现一个设计选择如“配置表单位缩放为100”就记录决策内容hpBase 单位为100解决的问题规避浮点精度误差、减少网络传输字节数代价策划填写数值时需换算增加沟通成本我的应用在当前项目WeaponConfig.csv中同样采用damageBase单位为10这个日志会逐渐成为你个人的技术决策库远比收藏一百个“源码包”更有价值。最后分享一个小技巧在Assets/Scripts/目录下新建一个Z_Temp/文件夹把你从“天命6源码”中提取出的、认为最有价值的片段如FiniteStateMachineT的泛型实现、ObjectPoolT的三级缓存逻辑放进去并重命名为MyFSM.cs、MyObjectPool.cs。然后在你的项目中用#if UNITY_EDITOR包裹这些临时代码确保它们永不进入构建包。这样你既获得了商业级代码的启发又保持了工程的纯净。技术学习的最高境界不是占有而是消化后吐纳出属于自己的东西。