using System; using System.Collections.Generic; using System.IO; using System.Linq; using Sirenix.OdinInspector; using Sirenix.OdinInspector.Editor; using UnityEditor; using UnityEditor.Build.Reporting; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; namespace Stary.Evo.Editor { /// /// Android APK打包工具窗口 /// public class BuildApkWindow : OdinEditorWindow { #region 字段与属性 private static OdinEditorWindow window; private bool _isBuilding; private UnityEditor.Build.Reporting.BuildReport _lastBuildReport; private string[] _scenePaths; [Title("设备类型选择", titleAlignment: TitleAlignments.Centered)] [EnumToggleButtons, HideLabel] public DeviceType deviceType = DeviceType.Xreal; [Title("包裹列表", titleAlignment: TitleAlignments.Centered)] [LabelText("Domain子包")] [ValueDropdown("GetAvailablePackageNames")] [OnValueChanged("OnPackageNameChanged")] public string selectedPackageName; [Title("打包配置", titleAlignment: TitleAlignments.Centered)] [ToggleLeft] [LabelText("自动递增版本号")] public bool autoIncrementVersion = true; [Title("打包配置", titleAlignment: TitleAlignments.Centered)] [ToggleLeft] [LabelText("是否添加水印")] public bool isWatermark = false; [ToggleLeft] [LabelText("构建完成后打开文件夹")] public bool openFolderOnComplete = true; [BoxGroup("缓存管理", centerLabel: true)] [Button("清空打包缓存", ButtonSizes.Large)] private void ClearBuildCache() { ClearCache(); } [ButtonGroup] [Button("本地普通包", ButtonSizes.Large, ButtonStyle.FoldoutButton)] private void BuildNormalPackage() { StartBuild(PLayerMode.LOCAL_PLAYMODE); } [Button("服务器热更包", ButtonSizes.Large, ButtonStyle.FoldoutButton)] private void BuildServerPackage() { StartBuild(PLayerMode.HOST_PLAYMODE); } [Title("打包状态", titleAlignment: TitleAlignments.Centered)] [ReadOnly] [LabelText("当前状态")] public string buildStatus = "就绪"; [ProgressBar(0, 100)] [ReadOnly] [LabelText("打包进度")] public int buildProgress; #endregion #region 菜单与初始化 [MenuItem("Evo/Dev/Apk打包工具", false, 4)] private static void ShowWindow() { window = GetWindow(false, "APK打包工具", true); window.maxSize = new Vector2(500, 700); window.minSize = new Vector2(400, 600); window.Show(); } protected override void Initialize() { base.Initialize(); FindScenesWithLoadDll(); UpdateAvailablePackages(); } #endregion #region 核心功能 /// /// 开始打包流程 /// /// 是否为水印包 private void StartBuild(PLayerMode playMode) { if (_isBuilding) { Debug.LogWarning("打包正在进行中,请稍候..."); return; } _isBuilding = true; buildStatus = "准备打包..."; buildProgress = 0; Repaint(); try { // 执行打包 BuildAndroid(playMode); } catch (Exception e) { buildStatus = $"打包失败: {e.Message}"; Debug.LogError($"打包过程中发生错误: {e}"); ShowNotification(new GUIContent("APK打包失败!"), 3f); } finally { _isBuilding = false; buildProgress = 100; Repaint(); } } /// /// Android打包核心逻辑 /// private void BuildAndroid(PLayerMode pLayerMode) { buildStatus = "加载配置文件..."; Repaint(); buildStatus = "配置包名..."; Repaint(); ConfigurePackageInfo(); buildStatus = "配置场景列表..."; Repaint(); string[] sceneList = ConfigureScenes(); buildStatus = "配置水印信息..."; Repaint(); if (isWatermark) { ConfigureWatermark(); } // 打包前检查 if (!PreBuildCheck()) { return; } buildStatus = "设置打包模式..."; Repaint(); ChangePlayerSchema.SetPlayerMode(pLayerMode); buildStatus = "执行打包..."; Repaint(); // 配置构建选项 BuildPlayerOptions buildOptions = ConfigureBuildOptions(sceneList, isWatermark); // 执行打包 var report = BuildPipeline.BuildPlayer(buildOptions); _lastBuildReport = report; // 处理打包结果 HandleBuildResult(report, buildOptions.locationPathName); } /// /// 配置包名和产品名称 /// private void ConfigurePackageInfo() { string productName; string packageName; if (!IsValidPackageName(selectedPackageName)) { // 不是有效包名格式,添加前缀 com.xosmo. productName = selectedPackageName; packageName = "com.xosmo." + selectedPackageName; } else { // 分割包名字符串,使用最后一个部分作为产品名称 string[] segments = selectedPackageName.Split('.'); productName = segments.Last(); packageName = selectedPackageName; } PlayerSettings.productName = productName; PlayerSettings.applicationIdentifier = packageName; // 自动递增版本号 if (autoIncrementVersion) { IncrementBuildVersion(); } } /// /// 配置场景列表 /// /// 是否为水印包 /// 资源配置 /// 场景路径数组 private string[] ConfigureScenes() { List scenes = new List(); string mainScenePath = $"Assets/Main/main_{deviceType.ToString()}.unity"; // 验证主场景是否存在 if (!File.Exists(mainScenePath)) { throw new Exception($"主场景文件不存在: {mainScenePath}"); } if (deviceType == DeviceType.Xreal && isWatermark) { // 加载资源配置 HotfixMainResDomain hotfixMainResDomain = Resources.Load("HotfixMainResDomain"); if (hotfixMainResDomain == null) { throw new Exception("HotfixMainResDomain资源在Resources下不存在,请检查!"); } // 水印包包含水印场景 string loadingScenePath = hotfixMainResDomain.projectInfo.loadingScenePath; if (!string.IsNullOrEmpty(loadingScenePath) && File.Exists(loadingScenePath)) { scenes.Add(loadingScenePath); } else { Debug.LogWarning($"加载场景不存在或未配置,仅打包主场景: {loadingScenePath}"); } } foreach (var scenePath in _scenePaths) { string path = scenePath.Replace("\\", "/"); if (mainScenePath == path) { LoadSceneForEditing(mainScenePath); scenes.Add(mainScenePath); } } return scenes.ToArray(); } /// /// 配置水印信息 /// private void ConfigureWatermark() { PlayerSettings.productName += "_watermark"; PlayerSettings.applicationIdentifier += "_watermark"; Sprite logoSprite = Resources.Load("logo"); if (logoSprite != null) { PlayerSettings.virtualRealitySplashScreen = logoSprite.texture; PlayerSettings.SplashScreen.showUnityLogo = false; PlayerSettings.SplashScreen.logos = new[] { PlayerSettings.SplashScreenLogo.Create(3f, logoSprite) }; } else { Debug.LogWarning("Resources下未找到logo图片,水印包将不显示自定义logo"); } } /// /// 配置构建选项 /// /// 场景列表 /// 是否为水印包 /// 构建选项 private BuildPlayerOptions ConfigureBuildOptions(string[] scenes, bool isWatermark) { string buildPath = GetBuildPath(isWatermark); // 确保构建目录存在 string buildDir = Path.GetDirectoryName(buildPath); if (!Directory.Exists(buildDir)) { Directory.CreateDirectory(buildDir); } BuildOptions buildOptions = BuildOptions.None; return new BuildPlayerOptions { scenes = scenes, locationPathName = buildPath, target = BuildTarget.Android, options = buildOptions }; } /// /// 获取构建路径 /// /// 是否为水印包 /// 构建路径 private string GetBuildPath(bool isWatermark) { string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); string buildFileName = $"{PlayerSettings.productName}_{timestamp}.apk"; return Path.Combine("Builds", "Android", buildFileName); } /// /// 处理打包结果 /// /// 构建报告 /// 构建路径 private void HandleBuildResult(UnityEditor.Build.Reporting.BuildReport report, string buildPath) { BuildSummary summary = report.summary; if (summary.result == BuildResult.Succeeded) { buildStatus = $"打包成功!大小: {FormatBytes(summary.totalSize)}"; Debug.Log($"构建成功: {buildPath},大小: {FormatBytes(summary.totalSize)}"); ShowNotification(new GUIContent("APK打包成功!"), 3f); // 构建完成后打开文件夹 if (openFolderOnComplete) { OpenBuildFolder(Path.GetDirectoryName(buildPath)); } } else { buildStatus = "打包失败!"; Debug.LogError("构建失败"); ShowNotification(new GUIContent("APK打包失败!"), 3f); // 显示详细错误信息 foreach (var step in report.steps) { foreach (var message in step.messages) { if (message.type == LogType.Error) { Debug.LogError($"构建错误: {message.content}"); } } } } } #endregion #region 辅助功能 /// /// 打包前检查 /// /// 检查是否通过 private bool PreBuildCheck() { // 检查是否选择了包裹 if (string.IsNullOrEmpty(selectedPackageName)) { ShowNotification(new GUIContent("请先选择一个包裹!"), 2f); return false; } // 检查当前平台 if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.Android) { if (EditorUtility.DisplayDialog("平台提示", "当前平台不是Android,是否切换到Android平台?", "是", "否")) { EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.Android, BuildTarget.Android); return false; // 切换平台后需要重新检查 } return false; } // 检查场景是否保存 if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) { return false; } return true; } /// /// 清空打包缓存 /// private void ClearCache() { try { buildStatus = "正在清理缓存..."; Repaint(); string projectPath = Application.dataPath; string libraryPath = Path.GetFullPath(Path.Combine(projectPath, "../Library")); // 要清理的缓存目录 string[] cacheDirectories = { Path.Combine(libraryPath, "BuildCache"), Path.Combine(libraryPath, "ScriptAssemblies"), Path.Combine(libraryPath, "ArtifactDB") }; int clearedCount = 0; foreach (string cacheDir in cacheDirectories) { if (Directory.Exists(cacheDir)) { Directory.Delete(cacheDir, true); clearedCount++; Debug.Log($"成功删除缓存目录: {cacheDir}"); } } // 清理构建输出目录 string buildOutputPath = Path.Combine(Application.dataPath, "../Builds/Android"); if (Directory.Exists(buildOutputPath)) { // 只清理旧的APK文件,保留目录结构 string[] oldApks = Directory.GetFiles(buildOutputPath, "*.apk", SearchOption.AllDirectories); foreach (string apkFile in oldApks) { File.Delete(apkFile); } Debug.Log("成功清理旧的APK文件"); } AssetDatabase.Refresh(); buildStatus = "缓存清理完成!"; ShowNotification(new GUIContent("缓存清理完成!"), 2f); Debug.Log($"缓存清理完成,共清理 {clearedCount} 个缓存目录"); } catch (Exception e) { buildStatus = "缓存清理失败!"; Debug.LogError($"缓存清理失败: {e.Message}"); ShowNotification(new GUIContent("缓存清理失败!"), 2f); } } /// /// 获取可用的包裹名称列表 /// /// 包裹名称列表 private List GetAvailablePackageNames() { UpdateAvailablePackages(); return CreatAssetWindow.GetCreatDomainAllName().ToList(); } /// /// 更新可用包裹列表 /// private void UpdateAvailablePackages() { var availablePackages = CreatAssetWindow.GetCreatDomainAllName().ToList(); if (availablePackages.Any() && string.IsNullOrEmpty(selectedPackageName)) { selectedPackageName = availablePackages.First(); } } /// /// 当包裹名称改变时的回调 /// private void OnPackageNameChanged() { buildStatus = "就绪"; } /// /// 自动递增版本号 /// private void IncrementBuildVersion() { Version version = new Version(PlayerSettings.bundleVersion); Version newVersion = new Version(version.Major, version.Minor, version.Build + 1); PlayerSettings.bundleVersion = newVersion.ToString(); Debug.Log($"版本号已递增: {newVersion}"); } /// /// 打开构建文件夹 /// /// 文件夹路径 private void OpenBuildFolder(string folderPath) { if (Directory.Exists(folderPath)) { EditorUtility.RevealInFinder(folderPath); } } /// /// 检测字符串是否是有效的包名格式 /// /// 要检测的字符串 /// 是否是有效的包名格式 private bool IsValidPackageName(string packageName) { if (string.IsNullOrEmpty(packageName)) return false; // 包名只能包含小写字母、数字和点 foreach (char c in packageName) { if (!char.IsLower(c) && !char.IsDigit(c) && c != '.') return false; } // 不能以点开头或结尾 if (packageName.StartsWith(".") || packageName.EndsWith(".")) return false; // 不能包含连续的点 if (packageName.Contains("..")) return false; // 至少包含一个点 if (!packageName.Contains(".")) return false; return true; } /// /// 格式化字节大小 /// /// 字节数 /// 格式化后的字符串 private string FormatBytes(ulong bytes) { string[] suffixes = { "B", "KB", "MB", "GB" }; int index = 0; double size = bytes; while (size >= 1024 && index < suffixes.Length - 1) { size /= 1024; index++; } return $"{size:F2} {suffixes[index]}"; } #endregion #region 场景处理 /// /// 查找所有包含LoadDll的场景文件 /// private void FindScenesWithLoadDll() { List sceneList = new List(); // 搜索所有unity场景文件 string[] allScenes = Directory.GetFiles(Application.dataPath, "*.unity", SearchOption.AllDirectories); foreach (string scenePath in allScenes) { // 读取场景文本内容,检查是否包含LoadDll try { string sceneContent = File.ReadAllText(scenePath); if (sceneContent.Contains("m_Name: LoadDll")) { // 转换为Assets相对路径 string assetsPath = scenePath.Replace(Application.dataPath, "Assets"); sceneList.Add(assetsPath); } } catch (Exception e) { Debug.LogError($"读取场景文件失败: {scenePath}, 错误: {e.Message}"); } } _scenePaths = sceneList.ToArray(); } /// /// 加载场景进行编辑 /// private void LoadSceneForEditing(string scenePath) { try { // 加载场景 EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single); GameObject loadDllObj = GameObject.Find("LoadDll"); // 收集HybridClREntrance组件 HybridClREntrance entrance = loadDllObj.GetComponent(); entrance.loadDomain = selectedPackageName; Debug.Log($"成功加载场景: {scenePath}"); } catch (Exception e) { Debug.LogError($"加载场景失败: {scenePath}, 错误: {e.Message}"); } } #endregion #region 枚举 /// /// 设备类型 /// public enum DeviceType { Xreal, Rokid } #endregion } }