最近在写一套模版框架,基于Addressables和ILRuntime,免不了要导出工程、编译打包之类的工作,而这些内容多数都是重复性工作,此时一个辅助工具可以大大减少在这上面花费的时间。
1. 简介 目前主要在做移动端,所以只支持了Android和iOS两种平台,其他的平台暂时还用不到,以后若有需要可能会再加。
Android平台下,Unity支持直接打包,且支持apk和aab两种格式,当然也支持导出一个Android的工程,随后手动操作;而iOS平台,由于独具独特风格,只能通过Xcode打包编译,所以只能导出工程。
按打包方式分,这里可以分为2种,一种是完整包,即需要提交应用商店审核的包,另一种是热更新的补丁包,而这种是基于Addressables的资源更新,所以不需要通过应用商店,资源的catalog更新后,用户端会根据最新的地址加载对应的bundle文件。
打一次完整的应用包,需要配置很多参数,但大多数参数都是一次性配置,即只需要在第一次打包时配置一次,之后便不用再修改,而每次打包都需要改动的只是其中部分配置。
在写之前、以及写的过程中,我一直在犹豫这个工具需要集成进来多少配置项,像那些只需配置一次的参数有没有必要集成到这个工具中来,是追求打包工具的全面性、完整性,还是追求简洁性、最小可用性,反复思虑再三,最终选择了后者,同时兼顾了一点前者,正如标题所言,将其定位成一个辅助工具。
所有配置项中,系统自带的主要就是2个地方,一个是Build Settings窗口里的配置,还有一个是Player Settings里的配置,如果还有其他的,那就是集成的第三方库的配置了,这个要看具体需求具体操作。
2. 版本号 Android平台的版本号没有格式限制,只是一个字符串,但iOS要求是3位,为了兼顾两个平台,决定使用3位格式的版本号,即a.b.c
,前两位a.b表示功能版本,小功能迭代增加b,大功能迭代增加a,末位的c表示该版本的修复版本,每修复一次则增加1。
其中,这个修复既可以是热修复,即不通过提审发版本的修复,也可以是提审发版本的修复。热更新并不是可以解决所有的线上问题,当无法通过热修复解决时,便只能通过提交新的应用包来解决。当然,当提交新的审核时,也可以增加b的值,表示小版本功能迭代,而这个版本的内容就是bug fix。
当a或b任一值增加时,c便再次从0开始,新的起点。
因为在热更新时,只会更新资源,而不会更新应用本身的信息,所以版本号需要单独写到一个文件中,以资源的形式存在,这样可以在应用中读到最新的版本号。
3. 一次性配置项 即首次打完整应用包时需要配置的内容。简单来说,就是把Player Settings里的每一项都过一遍,然后按需配置即可,但是前提是需要知道每一个配置项的作用是什么,是用来控制指什么的,以及不同可选值的区别是什么,这些可以到官网的文档查看,每一项都有具体的介绍。
Unity Player Settings介绍文档
4. 打完整应用包流程
如果,辅助工具中集成了以上配置项。是否Hotfix不用勾选,如果没有特殊需求,导出工程也不用勾选,导出格式按需勾选,默认是apk,如果需要aab则勾选。
Profile指的是Addressables的Profile,这里面指定的是资源的Build Path和Load Path,一般按平台或者模式区分,比如ProfileAndroid、ProfileiOS,ProfileDev,如果手头富裕,甚至可以再次细分,比如ProfileAndroidDev、ProfileiOSDev等,而包名一般与Profile成一一对应的关系,但也不强求,因为资源不关心包名。
内部流程是,首先需要更新group信息,即Assets/AddressableAssetsData/AssetsGroup下的配置文件,然后就可以调用打包方法了,
1 BuildPipeline.BuildPlayer(_scenesBuild, outputPath, target, BuildOptions.None);
其中_scenesBuild参数是需要编译的场景,同Build Settings里的场景,关于这个outpuPath,当需要导出工程时它是一个目录的路径,当不需要导出工程时它是一个apk或者aab文件的路径,而target则是指当前的平台,Android/iOS。
这个方法会先生成bundle文件,如果导出工程则直接导出,如果不导出则执行IL2CPP,接着通过Gradle打包成apk/aab。
打包完成后,需要先备份Addressable的配置文件。之前的文章里提过,只要group的配置文件不变,group里的资源不变,那么这个group最后生成的bundle文件的hash就不会变。当我们想要热更的时候,需要以某个版本为基础,来判断本次需要热更的内容都有哪些,所以为了避免开发过程中group的配置文件发生变动,所以除了state bin文件,连同这些配置文件也一同备份,共同复制一份到Assets目录下的Version目录中,结构如下:
1 2 3 4 5 6 7 8 9 Assets Version | | | | | | |
AddressableAssetsData是从Assets下直接复制过来的,Bundles是本次打包后需要上传到远程资源服务器的所有bundle文件,而type.txt则是对此版本的一些数据记录,比如版本号、是完整包还是补丁包等。
测试阶段的资源服务器我用的是git code,所以对于这些bundle文件,只需要复制一份到git仓库下,然后push到远程仓库即可,如果需要其他处理方式,如上传到自己的资源服务器,我定义了一个回调,直接对其复制即可:
1 public static Action<string> UploadBundlesHandler;
备份完成后,就会调用这个Action,参数则是一个目录的全路径,其下的所有文件都需要上传到资源服务器。
5. 打热更资源的流程
若要打热更资源,只需要把Hotfix勾选上,没用的配置都会隐藏,只剩下几个必须的选项。对于当下选择的Profile,基于的版本里会列出所有可用的版本,以及每个版本的信息,版本名则按照所选的基于的版本设置即可。
当开始构建后,首先会根据选择的基于版本,用该版本的addressable配置文件替换当前的配置文件,然后在此配置的基础上,刷新group配置生成最新的配置,然后调用方法,检测哪些资源发生了变化,并提示:
如上图,本次只改了一个版本号,所以只有一个文件发生变化,注意,如果是远程可变的group发生了变化,则不会在这里提示。然后将这些资源抽取出来,单独放到一个group中,一般叫做ContentUpdate,即补丁包。此时的group配置状态,则是最终的状态,根据此状态下的group,打包资源。
打包完成后,同上面操作一样,将当前版本的配置备份,将远程bundle上传。
6. 部分实现代码 打完整包 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 private void BuildAll ( ){ var target = EditorUserBuildSettings.activeBuildTarget; var configPath = Path.Join(Application.dataPath, "AddressableAssetsData" , _platformName); EditorUtils.TryDeleteDir(configPath); UpdateGroups(); EditorUtils.TryDeleteDir(_exportOutputPath); var outputPath = _exportOutputPath; if (target == BuildTarget.Android && !EditorUserBuildSettings.exportAsGoogleAndroidProject) { var extension = EditorUserBuildSettings.buildAppBundle ? "aab" : "apk" ; var filaName = $"{PlayerSettings.productName} _{PlayerSettings.bundleVersion} .{extension} " ; outputPath = Path.Join(_exportOutputPath, filaName); } BuildPipeline.BuildPlayer(_scenesBuild, outputPath, target, BuildOptions.None); BackupVersion(); EditorUtility.RevealInFinder(_exportOutputPath); }
打热更资源包 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 private void BuildPatch ( ){ if (_hotfixReliedVerIndex >= _versionPaths.Count) throw new ArgumentException("no replied on version found" ); var repliedVersionPath = _versionPaths[_hotfixReliedVerIndex]; if (!Directory.Exists(repliedVersionPath)) throw new ArgumentException($"{repliedVersionPath} not exist" ); var repliedConfigPath = Path.Join(repliedVersionPath, "AddressableAssetsData" ); if (!Directory.Exists(repliedConfigPath)) throw new ArgumentException($"{repliedConfigPath} not exist" ); var statePath = Path.Join(repliedConfigPath, $"{_platformName} /addressables_content_state.bin" ); if (!File.Exists(statePath)) throw new ArgumentException($"{statePath} not exist" ); var configPath = Path.Join(Application.dataPath, "AddressableAssetsData" ); EditorUtils.TryDeleteDir(configPath); EditorUtils.CopyDirectory(repliedConfigPath, configPath, false ); UpdateGroups(); var entries = ContentUpdateScript.GatherModifiedEntries(_addressable, statePath); var sb = new StringBuilder(); foreach (var entry in entries) { sb.AppendLine(entry.AssetPath); } var ok = EditorUtility.DisplayDialog("Assets Changed" , sb.ToString(), "OK" , "Cancel" ); if (ok) { var gn = $"ContentUpdate_{DateTime.Now:yyyyMMddhhmmss} " ; ContentUpdateScript.CreateContentUpdateGroup(_addressable, entries, gn); ContentUpdateScript.BuildContentUpdate(_addressable, statePath); var backupVersionPath = BackupVersion(); EditorUtility.RevealInFinder(backupVersionPath); } }
资源配置备份 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 private string BackupVersion ( ){ var profileName = _addressable.profileSettings.GetProfileName(_addressable.activeProfileId); var versionBackPath = Path.Join(_backupPath, profileName, PlayerSettings.bundleVersion); var configPath = Path.Join(Application.dataPath, "AddressableAssetsData" ); var backupPath = Path.Join(versionBackPath, "AddressableAssetsData" ); EditorUtils.CopyDirectory(configPath, backupPath, false ); var bundleBuildPath = Tools.GetAddressableRemoteBuildPath(); var bundleBackupPath = Path.Join(versionBackPath, "Bundles" ); EditorUtils.CopyDirectory(bundleBuildPath, bundleBackupPath, false ); if (_platformName == "Android" && !_isHotfix) { EditorUtils.CopyFiles(_exportOutputPath, versionBackPath, fn => fn.EndsWith(".apk" ) || fn.EndsWith(".aab" )); } var typePath = Path.Join(versionBackPath, "type.txt" ); if (_isHotfix) { var repliedVersion = Path.GetFileName(_versionPaths[_hotfixReliedVerIndex]); File.WriteAllText(typePath, $"Hotfix, Version Code: {GetVersionCode()} , Rely on {repliedVersion} " ); } else { File.WriteAllText(typePath, $"All, Version Code: {GetVersionCode()} " ); } return versionBackPath; }