TL; DR
Xcode Instruments允许在进行概要分析时由应用程序插入自定义标记(路标)。 通过明智地使用此功能,您可以在Xcode Instruments中快速识别标记的代码块,并轻松确定性能特征。 本文介绍了如何通过开发路标模块将此功能与Unity结合使用。
Unity已经附带了出色的Profiler。 使用集成的探查器,您可以轻松检查游戏中的性能问题,而无需实际将其部署在物理设备上。 您甚至可以将Unity Profiler连接到在设备上运行的应用程序。 这是巨大的帮助,您可以从此功能中获得有价值的信息,从而使您的游戏更加出色。 但是有时您甚至需要进一步挖掘(或者也许您很好奇引擎盖下的情况)。 可能是因为您要检查iOS级别上的确切内存分配,或者检查您的应用消耗了多少CPU。 然后,您需要一个本机的性能分析工具,例如Apple的Xcode Instruments,它可以告诉您有关应用程序性能指标的iOS观点。 Xcode Instruments Profiler随Xcode一起提供,可以免费使用。 您可以从Mac AppStore或从Apple的开发人员网站https://developer.apple.com下载。
Xcode Instruments是一个非常强大的工具,可让您决定要收集哪种性能指标类别(例如,CPU使用率,内存分配,系统调用等)。 每个类别都称为“ 乐器” ,可以从您的跟踪设置中添加或删除。
Xcode Instruments在对应用程序进行性能分析时会收集大量数据,这使您可以更详细地检查应用程序的行为。 例如,您可以获取应用程序中发生的每个分配(以及相应的取消分配)的本机调用堆栈。 您可以让Xcode Instruments跟踪每个系统调用以及在代码中触发的位置。
即使使用Unity将您的游戏通过Unity的IL2CPP工具从C#转换为C ++,符号名称仍会保留,即使您不太熟悉C ++,也不会因提供的C ++调用堆栈而迷失方向。
兴趣点和路标
您可以想象Xcode Instruments提供的大量数据很容易使您不知所措。 而且,由于要花费大量时间来查找所需的数据,因此可能会减少对应用程序进行配置的频率,并且这会对应用程序的性能和质量产生负面影响。
但是有帮助! 在WWDC 2016(第411节-深入的系统跟踪)上,Apple提到了其API中的一些小功能,这些功能可以帮助您像向导一样在应用程序的性能数据迷宫中导航(需要iOS 10+和Xcode 8+) ! 您可能已经知道某些乐器在时间线视图中插入标记(路标)以注释度量。
如果您的应用程序也可以做到这一点不是很好吗? 对于本文中的示例,我们利用可从Unity资产商店免费下载的Unity示例项目Endless Runner 。 在这个小游戏中,您控制一只猫在街上奔跑,收集物品并试图避开垃圾桶,狗等障碍物。
在第一个示例中,每次您与硬币(鱼骨)或障碍物碰撞时,我们都实现路标标记。 在下面的屏幕截图中,您可以看到运行带有用于CPU,分配和兴趣点的工具的工具。 兴趣点工具是用于在您的应用运行时显示所有自定义路标的容器。 现在,它显示了硬币碰撞和障碍物碰撞的路标事件。
使用Unity实施路标
sys / kdebug_signpost.h中提供的以下三个C函数将帮助我们在您的应用中添加路标:
int kdebug_signpost(uint32_t代码,
uintptr_t arg1,
uintptr_t arg2,
uintptr_t arg3,
uintptr_t arg4);
int kdebug_signpost_start(uint32_t代码,
uintptr_t arg1,
uintptr_t arg2,
uintptr_t arg3,
uintptr_t arg4);
int kdebug_signpost_end(uint32_t代码,
uintptr_t arg1,
uintptr_t arg2,
uintptr_t arg3,
uintptr_t arg4);
对于第一个示例,我们使用kdebug_signpost
,它将一个简单事件(点)插入到“兴趣点”时间轴中。 此方法接受一个code
参数和四个其他参数参数( arg1, arg2, arg3,arg4
)。 您的应用可以使用code
参数来区分不同的事件,每个事件使用不同的代码。 在我们的示例中, 硬币碰撞使用的值为0, 障碍碰撞使用的值为1。
为了使Xcode Instruments能够为每个代码显示相应的字符串描述,您可以通过选择File-> Recording Options ,然后在Options下拉菜单中选择Point of Interest来将代码映射为字符串描述(请参见左侧的屏幕截图)。 )。 通过在“ 路标代码名称”列表中添加字符串描述,Xcode Instruments将显示字符串描述而不是纯代码值。
我们还使用最后一个参数检查“ 颜色 ”设置,以便将不同的颜色应用于路标。 这意味着Xcode Instruments期望arg4
的值包含用于事件的颜色。
路标事件
为了能够在Unity中使用kdebug_signpost
方法,我们需要创建一个小的iOS插件。
因此,我们使用以下代码添加SignPost.mm :
#import
外部“ C”
{
int SignPostEvent(无符号int _code,
unsigned int _arg1,
unsigned int _arg2,
unsigned int _arg3,
unsigned int _arg4)
{
返回kdebug_signpost(_code,_arg1,_arg2,_arg3,_arg4);
}
}
这两个文件都应位于Plugins / iOS文件夹中,以便仅包含在iOS版本中。
为了能够在C#中使用此本机C函数,我们为其创建了一个包装器:
使用System.Runtime.InteropServices;
命名空间路标
{
公共类路标
{
#if UNITY_IOS &&!UNITY_EDITOR
[DllImport(“ __ Internal”)]
公共静态外部int SignPostEvent(uint _code,
uint _arg1,
uint _arg2,
uint _arg3,
uint _arg4);
#其他
公共静态int SignPostEvent(uint _code,
uint _arg1,
uint _arg2,
uint _arg3,
uint _arg4)
{
返回-1;
}
#万一
}
}
SignPost
类提供SignPost
函数的C#版本,从现在开始可以在您的C#项目中使用。 我们还添加了一个存根版本的SignPostEvent
。 这样,我们不需要将SignPosts.SignPostEvent
所有用法SignPosts.SignPostEvent
在#if UNITY_IOS && !UNITY_EDITOR
块内。 如果应用程序在其他平台上运行,则SignPosts.SignPostEvent
只会执行任何操作,甚至可能在优化阶段被编译器剥离。
为了使实现更易于使用,我们创建一个枚举来定义代码(称为类别),并将其作为第一个参数传递给SignPosts.SignPostEvent
。
命名空间路标
{
公共枚举SignPostCategory
{
CoinCollision = 0,
ObstacleCollision = 1
}
}
我们对颜色值也一样。 通过使用枚举类型而不是一堆硬编码值,API的用法变得更加清晰。
命名空间路标
{
公共枚举颜色:uint
{
蓝色= 0,
绿色= 1,
紫色= 2
橙色= 3,
红色= 4
}
}
现在,我们将所有这些组合到一个名为SignPostManager
的类中。 此类是用户的主要入口点。 稍后我们将看到该类的作用更大。
命名空间路标
{
公共静态类SignPostManager
{
#if UNITY_IOS &&!UNITY_EDITOR
公共静态无效SignSignEvent(SignPostCategory _category,
颜色_color,
uint _arg1 = 0,
uint _arg2 = 0,
uint _arg3 = 0)
{
SignPost.SignPostEvent((uint)_category,
_arg1,
_arg2,
_arg3,
(uint)_color);
}
#其他
公共静态无效SignSignEvent(SignPostCategory _category,
颜色_color,
uint _arg1 = 0,
uint _arg2 = 0,
uint _arg3 = 0)
{
}
#万一
}
}
通过将所有这些放在一起,在游戏中添加路标非常简单。 现在,只要猫碰到对撞机,便在示例项目中添加路标,如下所示:
受保护的void OnTriggerEnter(对撞机c)
{
如果(c.gameObject.layer == k_CoinsLayerIndex)
{
SignPostManager.SignPostEvent(SignPostCategory.CoinCollision,
SignPostManager.Color.Blue);
…
}
否则if(c.gameObject.layer == k_ObstacleLayerIndex)
{
SignPostManager.SignPostEvent(
SignPostCategory.ObstacleCollision,
SignPostManager.Color.Orange);
…
}
}
很简单,不是吗? 但是我们还没有完成。
路标间隔
除了路标事件,您还可以插入路标间隔。 路标间隔的附加值是它们包含一个隐式传递持续时间的开始和结束事件。 当您要跟踪应用程序中的特定操作需要花费多长时间以及它在何处开始和结束(包括调用堆栈)时,这非常有用。
在示例项目中,我们将添加一个时间间隔路标以异步加载资产。 在上面的屏幕截图中,您可以看到输出。 所有异步加载操作均显示为绿色的路标间隔标记。 在下部窗格中,您会看到所有发生的异步负载的列表,以及更多详细信息,例如持续时间,启动线程和调用堆栈(右面板)。
您可能已经注意到本文开头提到的两个功能kdebug_signpost_start
和kdebug_signpost_end
。 这两个功能实际上启动和停止特定的路标。 让我们添加缺少的代码。
首先,我们将两个方法添加到SignPost.mm中的iOS插件中:
#import
#import
外部“ C”
{
…
int SignPostStart(unsigned int _code,
unsigned int _arg1,
unsigned int _arg2,
unsigned int _arg3,
unsigned int _arg4)
{
返回kdebug_signpost_start(_code,
_arg1,
_arg2,
_arg3,
_arg4);
}
int SignPostEnd(unsigned int _code,
unsigned int _arg1,
unsigned int _arg2,
unsigned int _arg3,
unsigned int _arg4)
{
返回kdebug_signpost_end(_code,
_arg1,
_arg2,
_arg3,
_arg4);
}
}
现在,我们必须导入这些方法才能在C#代码中使用它们:
使用System.Runtime.InteropServices;
命名空间路标
{
公共类路标
{
#if UNITY_IOS &&!UNITY_EDITOR
...
[DllImport(“ __ Internal”)]
公共静态外部int SignPostStart(uint _code,
uint _arg1,
uint _arg2,
uint _arg3,
uint _arg4);
[DllImport(“ __ Internal”)]
公共静态外部int SignPostEnd(uint _code,
uint _arg1,
uint _arg2,
uint _arg3,
uint _arg4);
#其他
...
公共静态int SignPostStart(uint _code,
uint _arg1,
uint _arg2,
uint _arg3,
uint _arg4)
{
返回-1;
}
public static int SignPostEnd(uint _code,
uint _arg1,
uint _arg2,
uint _arg3,
uint _arg4)
{
返回-1;
}
#万一
}
}
现在的最后一步是将新方法集成到SignPostManager
类中。
使用System.Collections.Generic;
命名空间路标
{
公共静态类SignPostManager
{
私有静态只读uint s_invalidEventId = uint.MaxValue;
#if UNITY_IOS &&!UNITY_EDITOR
私有静态uint s_categoryCounter = 0;
私有静态Dictionary s_eventId2SignPostCategory = new Dictionary (32);
私有静态Dictionary s_eventId2SignPostColor = new Dictionary (32);
公共静态uint SignPostStart(SignPostCategory _category,
颜色_color,
uint _arg1 = 0,
uint _arg2 = 0)
{
var eventId = s_categoryCounter ++;
s_eventId2SignPostCategory [eventId] = _category;
s_eventId2SignPostColor [eventId] = _color;
SignPost.SignPostStart((uint)_category,
eventId,
_arg1,
_arg2,
(uint)_color);
返回eventId;
}
公共静态无效SignSignEnd(uint _eventId,
uint _arg1 = 0,
uint _arg2 = 0)
{
var category =(uint)s_eventId2SignPostCategory [_eventId];
var color =(uint)s_eventId2SignPostColor [_eventId];
SignPost.SignPostEnd(类别,
_eventId,
_arg1,
_arg2,
(uint)颜色);
s_eventId2SignPostCategory.Remove(_eventId);
s_eventId2SignPostColor.Remove(_eventId);
}
#其他
公共静态uint SignPostStart(SignPostCategory _category,
颜色_color,
uint _arg1 = 0,
uint _arg2 = 0)
{
返回s_invalidEventId;
}
公共静态无效SignSignEnd(uint _eventId,
uint _arg1 = 0,
uint _arg2 = 0)
{
}
#万一
}
}
现在,我们已经准备就绪,可以在游戏中添加路标间隔。 但是,让我们先详细了解一下代码。 与Signpost事件一样,您还需要为调用SignPostManager.SignPostStart
时开始的每个间隔指定一个类别和颜色。 然后,您需要存储返回值,因为该值是刚启动的Signpost的ID,以后您想通过SignPostManager.SignPostEnd
停止间隔时将需要该SignPostManager.SignPostEnd
。 但是Xcode Instruments如何知道我们要停止哪个路标间隔? 如上所述配置Xcode Instruments时,还需要检查选项Code和第一个参数 ,以设置匹配路标间隔为 。 这告诉Xcode Instruments,对于路标间隔, arg1
参数始终包含由应用程序提供的唯一标识符,用于标识特定路标间隔。 因此,每当您通过SignPostManager.SignPostStart
启动新的Signpost间隔时,就会在内部创建一个唯一的标识符并返回它。 这样,您可以并行使用同一类别的多个路标间隔,甚至可以在多个线程上使用。 但是请注意, SignPostManager
类的当前实现不是线程安全的,这很容易添加。
让我们看看如何将其集成到示例应用程序中:
公共类AssetBundleLoadAssetOperationFull
{
…
受保护的uint m_signPostId = 0;
public AssetBundleLoadAssetOperationFull(string bundleName,
字符串assetName,
System.Type类型)
{
…
m_signPostId = SignPostManager.SignPostStart(
SignPostCategory.LoadAssetAsync,
SignPostManager.Color.Green);
}
公共替代布尔IsDone()
{
…
if(m_Request!= null && m_Request.isDone)
SignPostManager.SignPostEnd(m_signPostId);
…
返回m_Request!= null && m_Request.isDone;
}
}
我们在AssetBundleLoadOperationFull
插入了路标处理代码,这是示例项目中异步资产加载的中心点。 在其构造函数中,我们调用SignPostManager.SignPostStart
并记住返回值,我们稍后将使用该返回值来完成Signpost间隔。 资产加载完成后,在AssetBundleLoadAssetOperationFull.IsDone
进行了检查,我们将其称为SignPostManager.SignPostEnd
并传递我们存储在m_signPostId
的值。
所以呢?
通过在游戏中明智且连续地添加路标(事件和间隔),您将从以下方面受益:
- 某些事件何时发生?
- 它们多久发生一次?
- 这些活动花了多少时间?
- 事件在源代码中的何处开始/停止?
- 是否与其他性能指标(例如CPU,分配等)相关?
- 比较多个Xcode运行,并查看事件以检查性能是否有所提高
如果Unity还添加对路标的支持并在其引擎的中心点触发它们,那就太好了。 这将使Unity的引擎行为更加透明,用户甚至可以更好地为Unity优化其应用程序。
使用的软件
- Xcode 9.1
- Unity 2017.2.0f3
参考文献
- WWDC 2016 —会话411 —深度的系统跟踪
- WWDC 2016 —会议418 —在仪器中使用时间分析器
- WWDC应用