1 前言

关于TeamCity的安装教程以及Unity插件安装无需赘述,参考:
工作流篇(2): TeamCity,15分钟搭建U3d CI环境,自动测试、版本发布、部署

目前项目热更新方案是使用的HybridCLR;资源管理使用的是YooAsset. 整个构建过程是囊括了以上两个方案的流程。

2 版本控制

2.1 VCS checkout

目前项目分支结构为 v1/release、v1/hotfix。出包使用release分支,同时将release分支内容同步到hotfix分支,后续热更新往hotfix分支提交。需要出新包的时候就是新建v2/release分支。

起初是准备在TeamCity上面创建两个Build,分别监听hotfix和release分支 为了省一点硬盘空间 将两个Build的目录指向同一个,但两个不同Build启动的时候会将当前目录清空,重新拉取仓库内容,unity切换平台导入资源每次都会耗时2个小时,所以后来hotfix、release共用一个Build,构建步骤用条件进行区分


头三个checkout模式在两个不同的Build任务执行时都会将当前的目录重新checkout,最后作罢选择了两个流程共用一个Build。

2.2 Artifact

jenkins中每个step可以做一些成功或失败后的操作,TeamCity这边暂时没发现。所以release分支及hotfix分支的构建产物我直接在 Artifact paths中一起配置了,构建成功之后没收集到对应规则的产物会有警告,但不会终止当前构建。

例:

1
2
+:release/%build.number%.apk
+:Bundles/Android/%build.number% => %build.number%

3 构建流程

3.1 出包

流程整理如下图

出包期间会执行到HybridCLR与YooAsset的构建过程

3.1.1 流程大概

  • HybridCLR:

    • 出一次包:获取裁剪后的dll
    • 生成桥接函数(如果有新增)
    • 编译hotfix部分的dll
    • 重新出包
  • YooAsset的大概流程为:

    • 调用AssetBundleBuilder的Run方法就好了(需要提前给到一些参数的,例如:构建模式[增量、重构];资源版本号等)

3.1.2 配置步骤

  • HybridCLR

    • 编译DLL:CustomBuild.BuildDLL 
    • 编译HotFix工程:CustomBuild.BuildHotfixProject
    • 导出工程: CustomBuild.Build
  • YooAsset

    • CustomBuild.BuildInternal
  • 生成APK

    • Flaovr参考之前的jenkins出包
  • Git提交代码

    • TeamCity不支持push操作,所以需要自己写命令行提交
    • 在构建过程中手动push会触发配置的分支Triggers,所以先禁用Trigger,但build就变成了半自动

挖个坑:尝试使用TeamCity的Dependencies,让增量更新依赖上次的构建,这样就不用从git上拉取之前的版本内容了

Gradle任务中的自定义参数在Additional Gradle command line parameters中填写,需要注意的是参数前面需要追加一个“-P”不留空格。这样就可以将值传到AndroidStudio工程gradle.properties 中的配置字段了

伪代码部分

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
48
49
50
51
52
53
54
55
56
57
58
59
60
public class CustomBuild
{
///<summary>
///BuildDll伪代码
///</summary>
public static void BuildDLL()
{
string[] args = System.Environment.GetCommandLineArgs();//获取命令行参数
var outDir = System.Environment.CurrentDirectory + "/../output/" + buildTarget.ToString();
//...do something
string[] scenes = new[] { "Assets/Main.unity" };
PlayerSettings.SetScriptingBackend(buildTargetGroup, ScriptingImplementation.IL2CPP);
BuildPipeline.BuildPlayer(scenes, outDir, buildTarget, BuildOptions.CompressWithLz4);
}

///<summary>
///HotFix工程编译伪代码
///</summary>
public static void BuildHotfixProject(BuildTarget target)
{
//HybridCLR编译DLL
HybridCLR.Editor.CompileDllCommand.CompileDll(target);

//复制DLL到资源目录
string dllPath = $"{HybridCLR.Editor.SettingsUtil.GetHotFixDllsOutputDirByTarget(target)}/{dll}";
var myPath = "xxx/xxx/xxx.bytes";
File.Copy(dllPath, myPath, true);
//dll加密
//EncryptStaticData();
}

///<summary>
///资源构建伪代码
///</summary>
public static void BuildInternal()
{
string[] args = System.Environment.GetCommandLineArgs();
//获取目标平台参数:args[i]=="-buildTarget"
//获取资源版本号参数: args[i] == "-buildVersion"
//获取构建模式:args[i]=="-buildType"(EBuildMode.ForceRebuild、EBuildMode.IncrementalBuild)

// YooAsset构建参数
string defaultOutputRoot = AssetBundleBuilderHelper.GetDefaultOutputRoot();
BuildParameters buildParameters = new BuildParameters();
buildParameters.VerifyBuildingResult = true;
buildParameters.OutputRoot = defaultOutputRoot;
buildParameters.BuildTarget = buildTarget;
buildParameters.BuildVersion = buildVersion;
buildParameters.CompressOption = ECompressOption.LZ4;
//注意:
//不追加后缀默认导出的的Android项目会因为unityStreamingAssets配置项中内容过长导致build失败
//1.追加后缀
//2.unityStreamingAssets内删除所有"YooAsset/哈希"内容
buildParameters.AppendFileExtension = true;
buildParameters.BuildMode = buildType;

// 执行构建
AssetBundleBuilder builder = new AssetBundleBuilder();
builder.Run(buildParameters);
}

TeamCity中有Unity插件 所以只需要在Execute method处填上对应的方法即可,其他追加的参数填在Command line arguments处。

在jenkins中使用命令行操作

1
"%unity%" -quit -batchmode -logFile log/${UNITY_LOG_BUILD_DLL} -projectPath ../unity -executeMethod CustomBuild.BuildDLL ${BUILD_TARGET} 

3.2 资源更新


资源更新部分其实就是把打包过程中YooAsset部分的构建模式换为增量,以及给一个版本号即可,YooAsset的增量更新每次都是把所有文件打出来了,为了节省上传时间及本地磁盘占用 我在YooAsset的任务中追加了一个剔除重复资源的BuildTask,每次打包的资源与首包进行对比。

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
48
49
50
51
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

namespace YooAsset.Editor
{
[TaskAttribute("删除重复资源")]
public class TaskDeleteDuplicate : IBuildTask
{
void IBuildTask.Run(BuildContext context)
{
var buildParameters = context.GetContextObject<AssetBundleBuilder.BuildParametersContext>();
var buildMode = buildParameters.Parameters.BuildMode;

if (buildMode == EBuildMode.IncrementalBuild && buildParameters.Parameters.DeleteDuplicate)
{
ComparePatch(buildParameters);
}
}
private void ComparePatch(AssetBundleBuilder.BuildParametersContext buildParameters)
{
string packageDirectory = buildParameters.GetPackageDirectory();

PatchManifest patchManifest1 = AssetBundleBuilderHelper.LoadPatchManifestFile(buildParameters.PipelineOutputDirectory, 1);
PatchManifest patchManifest2 = AssetBundleBuilderHelper.LoadPatchManifestFile(buildParameters.PipelineOutputDirectory, buildParameters.Parameters.BuildVersion);

//删除重复资源
foreach (var patchBundle2 in patchManifest2.BundleList)
{
if (patchManifest1.Bundles.TryGetValue(patchBundle2.BundleName, out PatchBundle patchBundle1))
{
if (patchBundle2.Hash == patchBundle1.Hash)
{
string delPath = $"{packageDirectory}/{patchBundle2.Hash}";
File.Delete(delPath);
}
else
{
Debug.Log($"变动文件:{patchBundle2.BundleName}");
}
}
else
{
Debug.Log($"新增文件:{patchBundle2.BundleName}");
}
}
Debug.Log("重复资源删除完成!");
}
}
}

先结束