使用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来判断函数调用的消耗,还有定位内存泄漏等。

results matching ""

    No results matching ""