Files
plugin-library/Assets/00.StaryEvoTools/Editor/Build/BuildApkWindow.cs
mzh a42a663ad2 【m】优化打包工具
1.修复包体配置项在配置文件Inspector面板不显示的问题
2.修复在打包工具面板修改包体配置时无法保存的问题
2026-03-31 17:28:20 +08:00

789 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Cysharp.Threading.Tasks;
using Sirenix.OdinInspector;
using Sirenix.OdinInspector.Editor;
using UnityEditor;
using UnityEditor.Build.Reporting;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using YooAsset;
namespace Stary.Evo.Editor
{
/// <summary>
/// Android APK打包工具窗口
/// </summary>
public class BuildApkWindow : OdinEditorWindow
{
#region
private static OdinEditorWindow window;
private bool _isBuilding;
private UnityEditor.Build.Reporting.BuildReport _lastBuildReport;
private string[] _scenePaths;
private string _buildAssetTagName;
private string buildAPKTagName;
private HotfixMainResDomain _hotfixMainResDomain => Resources.Load<HotfixMainResDomain>("HotfixMainResDomain");
[Title("设备类型选择", titleAlignment: TitleAlignments.Centered)] [EnumToggleButtons, HideLabel]
public DeviceType deviceType = DeviceType.Xreal;
#region
[TitleGroup("Domain子包", alignment: TitleAlignments.Centered)]
[LabelText("选择包体")]
[HideIf(nameof(allowMutiSelection))]
[ValueDropdown(nameof(GetAvailablePackageNames))]
[OnValueChanged(nameof(OnPackageNameChanged))]
public string selectedPackageName;
[TitleGroup("Domain子包")]
[LabelText("允许多选")]
public bool allowMutiSelection = false;
[TitleGroup("Domain子包")]
[LabelText("选择包体")]
[ShowIf(nameof(allowMutiSelection))]
[ValueDropdown(nameof(GetAvailablePackageNames), IsUniqueList = true)]
public string[] selectionOfPackages;
[TitleGroup("Domain子包")]
[LabelText("查看包体配置")]
[ShowIf(nameof(allowMutiSelection))]
[OnValueChanged(nameof(OnPackageSelectedOptionChanged))]
[ValueDropdown(nameof(selectionOfPackages))]
public string selectedOptionOfselectionOfPackages;
[BoxGroup("Domain子包/包体配置")]
[InlineProperty, HideLabel]
[ShowIf(nameof(selectedPackageInfo))]
public PackageConfigInfo selectedPackageInfo;
/// <summary>
/// 获取可用的包裹名称列表
/// </summary>
/// <returns>包裹名称列表</returns>
private List<string> GetAvailablePackageNames()
{
UpdateAvailablePackages();
return CreatAssetWindow.GetCreatDomainAllName().ToList();
}
/// <summary>
/// 更新可用包裹列表
/// </summary>
private void UpdateAvailablePackages()
{
var availablePackages = CreatAssetWindow.GetCreatDomainAllName().ToList();
if (availablePackages.Any() && string.IsNullOrEmpty(selectedPackageName))
{
selectedPackageName = availablePackages.First();
}
}
/// <summary>
/// 当包裹名称改变时的回调
/// </summary>
private void OnPackageNameChanged(string packageID)
{
buildStatus = "就绪";
selectedPackageInfo = _hotfixMainResDomain.buildConfig.Get(packageID).info;
}
/// <summary>
/// 当可选包体中选择的包体改变时的回调
/// </summary>
/// <param name="packageID"></param>
private void OnPackageSelectedOptionChanged(string packageID)
{
selectedPackageInfo = _hotfixMainResDomain.buildConfig.Get(packageID).info;
}
/// <summary>
/// 包体内容修改
/// </summary>
[BoxGroup("Domain子包/包体配置")]
[Button("保存包体配置变更")]
private void SavePackageInfo()
{
_hotfixMainResDomain.buildConfig.Set(selectedOptionOfselectionOfPackages, selectedPackageInfo);
Debug.Log(selectedOptionOfselectionOfPackages);
#if UNITY_EDITOR
AssetDatabase.SaveAssets();
#endif
}
#endregion
#region
[TitleGroup("打包配置", alignment: TitleAlignments.Centered)]
[ToggleLeft]
[LabelText("自动递增版本号")]
public bool autoIncrementVersion = true;
[TitleGroup("打包配置")]
[ToggleLeft]
[LabelText("是否添加水印")]
public bool isWatermark = false;
[TitleGroup("打包配置")]
[ToggleLeft]
[LabelText("是否服务器热更包")]
[OnValueChanged("OnServerChangedTag")]
public bool isServer = false;
[TitleGroup("打包配置")]
[ToggleLeft]
[LabelText("构建完成后打开文件夹")]
public bool openFolderOnComplete = true;
#endregion
[BoxGroup("缓存管理", centerLabel: true)]
[Button("清空打包缓存", ButtonSizes.Large)]
private void ClearBuildCache()
{
ClearCache();
}
[Button("$GetBuildTargetName", ButtonSizes.Large)]
private void ResourceManagement()
{
if (!isServer)
{
StreamingAssetsFilter.OnPostprocessBuild();
StreamingAssetsFilter.KeepFiles = new[]
{
$"{YooAssetSettingsData.GetDefaultYooFolderName()}/Main",
$"{YooAssetSettingsData.GetDefaultYooFolderName()}/{selectedPackageName}"
};
StreamingAssetsFilter.OnPreprocessBuild();
}
else
{
StreamingAssetsFilter.KeepFiles=new string[0];
StreamingAssetsFilter.OnPreprocessBuild();
}
ShowNotification(new GUIContent("拷贝完成!"), 2f);
}
[ButtonGroup]
[Button("$GetBuildAPKName", ButtonSizes.Large, ButtonStyle.FoldoutButton)]
private void BuildNormalPackage()
{
if (!isServer)
{
StartBuild(PLayerMode.LOCAL_PLAYMODE);
}
else
{
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<BuildApkWindow>(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
/// <summary>
/// 开始打包流程
/// </summary>
/// <param name="isWatermark">是否为水印包</param>
private async void StartBuild(PLayerMode playMode)
{
if (_isBuilding)
{
Debug.LogWarning("打包正在进行中,请稍候...");
return;
}
_isBuilding = true;
buildStatus = "准备打包...";
buildProgress = 0;
Repaint();
try
{
// 多选打包
if (allowMutiSelection)
{
// 已选包体列表排空
if (selectionOfPackages.Length == 0)
{
throw new( "选择了多选打包但是没有选择任何Domain子包");
}
var current = 0;
while (current < selectionOfPackages.Length)
{
// 获取包体ID
var packageID = selectionOfPackages[current];
Debug.Log($"正在打包:{packageID}");
// 获取包体配置
selectedPackageInfo = _hotfixMainResDomain.buildConfig.Get(packageID).info;
// 打包
await BuildAndroid(playMode);
current++;
}
}
// 单选打包
else
{
_ = BuildAndroid(playMode);
}
}
catch (Exception e)
{
buildStatus = $"打包失败: {e.Message}";
Debug.LogError($"打包过程中发生错误: {e}");
ShowNotification(new GUIContent("APK打包失败"), 3f);
}
finally
{
_isBuilding = false;
buildProgress = 100;
Repaint();
}
}
/// <summary>
/// Android打包核心逻辑
/// </summary>
private async UniTask BuildAndroid(PLayerMode pLayerMode)
{
buildStatus = "加载配置文件...";
Repaint();
buildStatus = "配置包名...";
Repaint();
ConfigurePackageInfo();
buildStatus = "配置场景列表...";
Repaint();
string[] sceneList = ConfigureScenes();
buildStatus = "配置水印信息...";
Repaint();
if (isWatermark)
{
ConfigureWatermark();
}
else
{
PlayerSettings.virtualRealitySplashScreen = null;
PlayerSettings.SplashScreen.showUnityLogo = false;
PlayerSettings.SplashScreen.logos = null;
}
// 打包前检查
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);
}
/// <summary>
/// 配置包名和产品名称
/// </summary>
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 = string.IsNullOrEmpty(selectedPackageInfo?.appName) ? productName : selectedPackageInfo.appName;
PlayerSettings.applicationIdentifier = packageName;
// 自动递增版本号
if (autoIncrementVersion)
{
IncrementBuildVersion();
}
}
/// <summary>
/// 配置场景列表
/// </summary>
/// <returns>场景路径数组</returns>
private string[] ConfigureScenes()
{
List<string> scenes = new List<string>();
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>("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();
}
/// <summary>
/// 配置水印信息
/// </summary>
private void ConfigureWatermark()
{
if (string.IsNullOrEmpty(selectedPackageInfo?.appName))
{
PlayerSettings.productName += "_watermark";
}
PlayerSettings.applicationIdentifier += "_watermark";
Sprite logoSprite = Resources.Load<Sprite>("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");
}
}
/// <summary>
/// 配置构建选项
/// </summary>
/// <param name="scenes">场景列表</param>
/// <param name="isWatermark">是否为水印包</param>
/// <returns>构建选项</returns>
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
};
}
/// <summary>
/// 获取构建路径
/// </summary>
/// <param name="isWatermark">是否为水印包</param>
/// <returns>构建路径</returns>
private string GetBuildPath(bool isWatermark)
{
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
string buildFileName = $"{PlayerSettings.productName}_{timestamp}.apk";
return Path.Combine("Builds", "Android", buildFileName);
}
/// <summary>
/// 处理打包结果
/// </summary>
/// <param name="report">构建报告</param>
/// <param name="buildPath">构建路径</param>
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
/// <summary>
/// 打包前检查
/// </summary>
/// <returns>检查是否通过</returns>
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;
}
/// <summary>
/// 清空打包缓存
/// </summary>
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);
}
}
/// <summary>
/// 自动递增版本号
/// </summary>
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}");
}
/// <summary>
/// 打开构建文件夹
/// </summary>
/// <param name="folderPath">文件夹路径</param>
private void OpenBuildFolder(string folderPath)
{
if (Directory.Exists(folderPath))
{
EditorUtility.RevealInFinder(folderPath);
}
}
/// <summary>
/// 检测字符串是否是有效的包名格式
/// </summary>
/// <param name="packageName">要检测的字符串</param>
/// <returns>是否是有效的包名格式</returns>
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;
}
/// <summary>
/// 格式化字节大小
/// </summary>
/// <param name="bytes">字节数</param>
/// <returns>格式化后的字符串</returns>
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
/// <summary>
/// 查找所有包含LoadDll的场景文件
/// </summary>
private void FindScenesWithLoadDll()
{
List<string> sceneList = new List<string>();
// 搜索所有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();
}
/// <summary>
/// 加载场景进行编辑
/// </summary>
private void LoadSceneForEditing(string scenePath)
{
try
{
// 加载场景
Scene scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
GameObject loadDllObj = GameObject.Find("LoadDll");
// 收集HybridClREntrance组件
HybridClREntrance entrance = loadDllObj.GetComponent<HybridClREntrance>();
entrance.loadDomain = selectedPackageName;
// 刷新AssetDatabase
AssetDatabase.Refresh();
// 保存场景
EditorSceneManager.SaveScene(scene);
// 重新加载场景以应用更改
EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
Debug.Log($"成功加载场景: {scenePath}");
}
catch (Exception e)
{
Debug.LogError($"加载场景失败: {scenePath}, 错误: {e.Message}");
}
}
private void OnServerChangedTag()
{
}
private string GetBuildTargetName()
{
return isServer ? "清空所有资源" : "拷贝打包资源";
}
private string GetBuildAPKName()
{
return isServer ? "打包服务器热更包" : "打包本地运行包";
}
#endregion
#region
/// <summary>
/// 设备类型
/// </summary>
public enum DeviceType
{
Xreal,
Rokid
}
#endregion
}
}