使用Instruments进行iOS客户端性能测试
工具介绍
Instruments是苹果Mac电脑的编程工具Xcode下自带的一个应用程序,它的用处很多,苹果开发者官网有专门的英文文档,想方便看中文文档的,可以点击下载。
获取app的客户端性能,主要使用的是Instruments的“Activity Monitor”和“GPU Driver”这两个模版。点击“Library”,从中选取出这两个模版,选中其中一个模版后,可以在右边的属性面板勾选需要统计的数据项。可以查看的性能数据列表有CPU、Memory、GPU、Graph_mem、FPS等。
连接Mac电脑和iOS设备,第一次连接需要等一会,Instruments左上角连接设备信息变成黑色后就表示连接成功了。然后点击iOS设备名,就可以展开查看该设备上安装的app列表。
- 如果你的app打包的是debug版本的话,直接在app列表中选中它,点击红色按钮“Record”,Instruments就会自动启动app,并开始统计性能数据。
- 但如果你的app打包的是release版本,不是debug版本的话,在app未运行的情况下选中app,点击红色按钮“Record”会报错,提醒你不能通过Instruments直接启动app。这个时候最好的办法是先在iOS设备上启动app,然后再点击Instruments左上角的设备名,在运行中的程序列表中选中你的app程序,也同样可以开始统计性能数据。
需要注意的是“Activity Monitor”的页面信息每次刷新都只显示当前最新的数据,所以无法查看数据的变化过程。而“GPU Driver”每次刷新单独将数据写一行,这样就可以查看到所有的历史数据。而且Instruments不支持数据导出,所以如果想要将数据取出,绘制成图表的话,还需要做更多的事情。
使用atoma读取Instruments数据并记录成文本
atomac是一个Python库,主要是通过获取Instruments的界面UI对象,读取Instruments记录的性能数据,然后加以保存。安装的方式很简单,安装好Python后,在终端输入命令“sudo easy_install atomac”即可。一般mac默认是安装了Python的,本例中使用的是Python 2.7,可以在“终端”输入命令“python --version”来查看下Python的版本号。
除了atomac,还需要使用Xcode下的另一个工具“Accessibility Inspector”,它的主要作用是可以显示出界面控件列表,然后将控件名作为参数传给atomac进行操作。
- 使用Accessibility Inspector查看控件对象
从Xcode中启动Accessibility Inspector后,左上角选择要监控的Instruments程序,然后Accessibility Inspector界面下方就会将所有的UI控件树显示出来,如图:
上图用红色圈出来的,就是控件列表(考虑到系统版本不同,这里说明下这个图是在Mac OS 10.11.6版本)。双击控件名,就会在Instruments界面上用黄色底图标识出所点击的控件,当然也可以通过先点击Accessibility Inspector上的准心,再点击Instruments上的控件按钮,也能同步显示出控件名。
- 使用atomac操作界面控件
点击可以查看atomac的文档说明
文档里一开始的示例说明atomac有直接启动Instruments的方法:
atomac.launchAppByBundleId('com.apple.dt.Instruments')
但因为Instruments启动后,还需要从Library中选择模版,选中待测app,勾选属性面板里的显示数据等,每次测试的配置可能都不一样,所以暂时不直接用atomac进行启动和配置操作,主要是用它来执行反复获取界面数据的行为。
文档示例中,控制应用程序前就要先获取程序对象,前提是Instruments这个程序已经启动了:
import atomac
instrument = atomac.getAppRefByBundleId('com.apple.dt.Instruments')
这里的instrument存放的就是Instruments所有的UI控件列表了,如果要操作其中某个控件,就需要通过Accessibility Inspector查看到的控件名称,然后进行查找匹配后,才能调用。但每次操作前都要先查找,似乎有点麻烦,代码结构上也不太好看,所以我们从一开始,就将不同的控件类型进行分组,然后使用索引下标直接访问,示例代码如下:
import time
import atomac
# 初始化atomac对象的UI列表分组
def get_group(self, match = None):
return self._convenienceMatch('AXGroup','AXRoleDescription', match)
atomac.NativeUIElement.group = get_group
def get_splitgroup(self, match = None):
return self._convenienceMatch('AXSplitGroup','AXRoleDescription', match)
atomac.NativeUIElement.splitgroup = get_splitgroup
def get_outline(self, match = None):
return self._convenienceMatch('AXOutline','AXRoleDescription', match)
atomac.NativeUIElement.outline = get_outline
def get_row(self, match = None):
return self._convenienceMatch('AXRow','AXRoleDescription', match)
atomac.NativeUIElement.row = get_row
def get_scollarea(self, match = None):
return self._convenienceMatch('AXScrollArea','AXRoleDescription', match)
atomac.NativeUIElement.scollarea = get_scollarea
def get_table(self, match = None):
return self._convenienceMatch('AXTable','AXRoleDescription', match)
atomac.NativeUIElement.table = get_table
def get_statictext(self, match = None):
return self._convenienceMatch('AXStaticText','AXRoleDescription', match)
atomac.NativeUIElement.text = get_statictext
def get_toolbars(self, match = None):
return self._convenienceMatch('AXToolbar','AXRoleDescription', match)
atomac.NativeUIElement.toolbars = get_toolbars
def get_checkboxs(self, match = None):
return self._convenienceMatch('AXCheckBox','AXRoleDescription', match)
atomac.NativeUIElement.checkboxs = get_checkboxs
# 获取Instruments程序对象的UI列表
instrument = atomac.getAppRefByBundleId('com.apple.dt.Instruments')
# 点击Record按钮开始记录数据,再次点击停止记录
def kick_record():
## 通过UI分组索引获取到Record按钮
record_buttton = instrument.windows()[0].toolbars()[0].checkboxs()[0]
## 点击该按钮
record_buttton.Press()
# 点击不同Instruments控件,切换页面,从上至下的排列顺序:0-Avtivity Monitor,1-GPU Driver,只有切换页面后才能从当前显示的界面获取数据
def jump_page(index):
## 获取Instruments里加载的模版列表
page_group = instrument.windows()[0].group()[0].splitgroup()[0]
page_obj = page_group.group()[0].outline()[0].row()[index].table()[0].row()[1]
## 点击模版控件,需要点击两次,第一次是激活Instruments窗口,第二次是点击到模块控件切换对应的页面显示,且因为模版控件不是按钮,所以只能调用鼠标点击相应的坐标点
page_obj.clickMouseButtonLeft(page_obj.AXPosition)
time.sleep(0.1)
page_obj.clickMouseButtonLeft(page_obj.AXPosition)
# 获取Activity Monitor监控数据,需要传递app名称
def get_systeminfo(instrument, appname):
systeminfo = []
s_splitgroup = instrument.windows()[0].group()[0].splitgroup()[0]
datalist = s_splitgroup.splitgroup()[0].scollarea()[0].table()[0].row()
for i in datalist:
data = i.text()
if data[1].AXValue == appname:
for j in data:
systeminfo.append(j.AXValue)
return systeminfo
# 获取GPU Driver监控数据,不需要传递app名称
def get_graphinfo(instrument):
graphlist = []
g_splitgroup = instrument.windows()[0].group()[0].splitgroup()[0]
datalist = g_splitgroup.splitgroup()[0].scollarea()[0].table()[0].row()
for i in datalist:
data = i.text()
values = []
for j in data:
values.append(j.AXValue)
graphlist.append(values)
## 如果不是从0开始,则逆序
if datalist[0].text()[0].AXValue != '0':
graphlist.reverse()
return graphlist
# 每隔xsecond秒间隔收集一次Activity数据,共收集tsecond秒
def collect_systeminfo(sampletime = xsecond, ntimes = tsecond):
start_time = time.time()
if ntimes == -1:
ntimes = float("inf")
sysfile = open("system.log", "a")
jump_page(instrument, 0)
time.sleep(0.2)
now_time = time.time()
while now_time - start_time < ntimes:
try:
sysinfo = get_systeminfo(instrument, appname)
sysfile.write('\t'.join(sysinfo))
sysfile.write('\n')
now_time = time.time()
time.sleep(sampletime)
except Exception:
continue
sysfile.close()
# 一次性收集所有的GPU Driver数据
def collect_graphinfo():
graphfile = open("graph.log", "a")
jump_page(instrument, 1)
time.sleep(0.2)
grapinfolist = get_graphinfo(instrument)
for info in grapinfolist:
graphfile.write('\t'.join(info))
graphfile.write('\n')
graphfile.close()
# 主流程
if __name__ == "__main__":
## 开始记录数据
kick_record()
## 收集Activity数据,间隔1秒,总长30分钟
collect_systeminfo(1, 1800)
## 再次调用,停止记录数据
kick_record()
## 收集GPU Driver数据
collect_graphinfo()
以上代码,是一个比较简单可用的示例,可根据自己的需要进行调整。需要注意的是,在读取UI列表时,不同的Mac OS版本可能排版布局不一样,需要先尝试一下。上述例子是建立在Mac OS 10.11.6版本基础上的Xcode工具集。
绘图分析
统计了性能数据后,可以使用excel生成曲线图,但最好还是写一个通用的脚本,可以更加方便快捷地绘图,且能够重复使用。有关绘图脚本方法有很多,而且都比较简单,推荐使用百度的ECharts,这里就不再赘述绘图的脚本了,大致的数据展示效果如下:
分析方法之前也已经介绍过了,请参考客户端性能测试中数据分析的部分。另外Instruments的模版属性中也有很多可以查看的内容,可配合文档学习,比如可以查看Call Tree来判断函数调用的消耗,还有定位内存泄漏等。