1. GameObject 的 SpawnPool 应支持“移出屏幕”功能
GameObject
(比如特效)可能会被频繁的在 使用中
、不使用
的状态间切换。我们的 SpawnPool
不应过快地把 刚刚不使用
的 GameObject
立刻 Deactivate
掉,否则会引起不必要的 Deactivate/Activate
的性能消耗。应有一个 从热变冷
的过程: 刚刚不使用
只是移出屏幕;只有 不使用一段时间
的 GameObject
,才会得以 Deactivate
。可能的实现方式如下:
/// On each timer, we try to make parts of "hot" items to be "cool" by deactivating them.
internal void OnTimer()
{
if(teleportCache.items.Count > 0) {
if(Time.realtimeSinceStartup - teleportCache.lastSpawnTime
< SpawnPool.TeleportThenDeactivateDuration &&
teleportCache.items.Count <= 3) {
this.MoreLogInfo("this prefab is recently spawned, \
and the teleport cache is not too large, \
we don't deactivate these remaining items");
}
else {
int deactivateCount = Mathf.Max(1, teleportCache.items.Count / 3);
SpawnIdentity oneId;
while(deactivateCount > 0) {
--deactivateCount;
oneId = teleportCache.items.Pop();
if(null != oneId) {
this.MoreLogInfo("Deactivate from teleportCache:" + oneId);
oneId.gameObject.MoreSetActive(false, this);
oneId.gameObject.transform.SetParent(null, true);
deactivateCache.Push(oneId);
SpawnPoolProfiler.AddDeactivateCount(oneId.gameObject);
}
}
}
}
}
2. Transform 的孩子不应过多
当 Transform 包含不该有的孩子 Transform 或其他组件时,为该 Transform 进行 position
、rotation
赋值,会引起消耗,特别是包含粒子系统的时候。
对 Transform 进行 rotation 赋值时,由于其孩子包含粒子系统所产生的消耗 但考虑到切换 Transform 的 parent 本身也会有消耗,因此,我们对此也应有 从热变冷
的过程: 刚刚不使用
依然保留在父亲 Transform 里;只有 不使用一段时间
的 GameObject,才从父亲 Transform 移出。
3. 应减少粒子系统的 Play() 的调用次数
每次调用 ParticleSystem.Play()
都会有消耗,如果粒子系统本身没有明显 前摇
阶段,应先检查 ParticleSystem.isPlaying
,例子如下:
ParticleSystem ps;
for (int i = 0; i < num; ++i) {
//m_particleSystemLst[i].Stop();
ps = m_particleSystemLst[i];
/// CAUTION! WE SHOULD CHECK isPlaying before calling Play()!
/// OR, IT WILL AFFECT PERFORMANCE!
if(!ps.isPlaying) {
ps.Play();
}
}
4. 应减少每帧 Material.GetXX()/Material.SetXX() 的次数
每次调用 Material.GetXX()
或 Material.SetXX()
都会有消耗,应减少调用该 API 的频率。比如使用 C# 对象变量来记录 Material
的变量状态,从而规避 Material.GetXX()
;在 Shader
里把多个 uniform half
变量合并为 uniform half 4
,从而把 4 个 Material.SetXX()
调用合并为 1 个 Material.SetXX()
。
5. 应使用支持 Conditional 的日志输出机制
简单使用 Debug.Log(ToString() + "hello " + "world");
,其实参会造成 CPU 消耗及 GC 。使用支持 Conditional
的日志输出机制,则无此问题,只需在构建时,取消对应的编译参数即可。
/// MoreDebug.cs,带Conditional条件编译的日志输出机制
[Conditional("MORE_DEBUG_INFO")]
public static void MoreLogInfo(this object caller) { DoMoreLog(MoreLogLevel.Info, false, caller); }
/// 用户代码.cs,调用方简单正常调用即可。正式构建时,取消MORE_DEBUG_INFO编译参数。
this.MoreLogInfo("writerSize=" + writer.Position, "channelId=" + channelId);
6. 依然需要减少 GetComponent() 的频率
即使在 Unity5.5 中,GetComponent()
会有一定的 GC 产生,有少量的 CPU 消耗。如有可能,我们依然需要规避冗余的 GetComponent()
。另,自 Unity5 起,Unity 已就 .transform
进行了 cache,我们不需再为 .transform
担心,见 《UNITY 5: API CHANGES & AUTOMATIC SCRIPT UPDATING》 最后一段。
7. 应减少 UnityEngine.Object 的 null 比较
因为 Unity overwrite 掉了 Object.Equals()
,《CUSTOM == OPERATOR, SHOULD WE KEEP IT?》 也说过 unityEngineObject==null
事实上和 GetComponent()
的消耗类似,都涉及到Engine层面的机制调用,所以 UnityEngine.Object
的 null 比较,都会有少许的性能消耗。对于基础功能、调用栈叶子节点逻辑、高频功能,我们应少 null 比较,使用 assertion
来处理。只有在调用栈根节点逻辑,有必要的时候,才进行 null 比较。
上面 C# 代码对应的 IL2CPP
代码
而且,从代码质量来看,无脑的 null 保护也是不值得推崇的,因为其将错误隐藏到了更偏离错误根源的逻辑。理论上,当错误发生了,应尽早报错,从而帮助开发者能更快速地定位错误根源。所以,多用assertion
,少用 null 保护,无论是对代码质量,还是代码性能,都是不错的实践。
8. 应减少不必要的 Transform.position/rotation 等访问
每次访问 Transform.position/rotation
都有相应的消耗。应能 cache 就 cache 其返回结果。
9. 应尽量为类或函数声明为 sealed
IL2CPP
就 sealed
的类或函数会有优化,变虚函数调用为直接函数调用。详见 《IL2CPP OPTIMIZATIONS: DEVIRTUALIZATION》 。
10. C#/CPP interop 时,不需为 blittable 的变量声明为 MarshalAs
某些数值类型,托管代码和原生代码的二进制表达方式一致,这些称为 blittable
数值类型。blittable
数值类型在 interop
时为高效的简单内存拷贝,故应值得推崇。C# 中的 blittable 数值类型为 byte
、int
、float
等,但注意不包括常用的 bool
、string
。仅有 blittable 数值类型组成的数组或 struct
,也为 blittable。
blittable 的变量不应声明 MarshalAs
。 比如下面代码,
[DllImport(ApolloCommon.PluginName, CallingConvention = CallingConvention.Cdecl)]
private static extern ApolloResult apollo_connector_readUdpData(UInt64 objId, /*[MarshalAs(UnmanagedType.LPArray)]*/ byte[] buff, ref int size);
注释前后的 IL2CPP
代码如下图,右侧明显避免了 marhal
的产生。