iOS 测试 iOS 自动化性能采集

xinxi · 2018年09月08日 · 最后由 不贱不散 回复于 2020年03月02日 · 7285 次阅读
本帖已被设为精华帖!

前言

对于 iOS 总体生态是比较封闭的,相比 Android 没有像 adb 这种可以查看内存、cpu 的命令.在日常做性能测试,需要借助 xcode 中 instruments 查看内存、cpu 等数据.

但是借助 instruments 比较麻烦、又不能提供命令行.在持续集成中,很难时时的监控 app 的性能指标.并且现在 app 发版一般是 2 周左右,留给做专项测试的时间更少了,那么做核心场景性能测试,肯定是来不及的.

所以需要借助一些自动化工具来减轻手工采集性能指标的工作量.

性能采集项

app 中基本性能采集项,内存、cpu、fps、电量等,因为自动化采集中手机设备是插着电脑充电的,所以不能采集电量数据.

已有工具

  • instruments 是官方提供的,不能做到自动化采集
  • 腾讯 gt,需要在 app 中集成 sdk,有一定的接入成本
  • 第三 sdk,类似腾讯 gt 需要在 app 集成,可能会有数据泄漏风险

脚本开发

上述的已有工具都不满足,在持续集成中做到自动化采集性能数据,期望的性能测试工具有一下几点:

  • 方便接入
  • 可生成性能报告
  • 可持续化
  • 数据收集精准

所以基于这几点,需要自己开发一套性能采集脚本.

使用官方提供的 api 做性能采集

获取内存、cpu 等

#import <mach/mach.h>

/**
 *  获取内存
 */
- (NSString *)get_memory {
    int64_t memoryUsageInByte = 0;
    task_vm_info_data_t vmInfo;
    mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
    kern_return_t kernelReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
    if(kernelReturn == KERN_SUCCESS) {
        memoryUsageInByte = (int64_t) vmInfo.phys_footprint;
        NSLog(@"Memory in use (in bytes): %lld", memoryUsageInByte);
    } else {
        NSLog(@"Error with task_info(): %s", mach_error_string(kernelReturn));
    }

    double mem = memoryUsageInByte / (1024.0 * 1024.0);
    NSString *memtostring ;
    memtostring = [NSString stringWithFormat:@"%.1lf",mem];

    return memtostring;
}


/**
 * 获取cpu
 */
- (NSString *) get_cpu{
    kern_return_t kr;
    task_info_data_t tinfo;
    mach_msg_type_number_t task_info_count;

    task_info_count = TASK_INFO_MAX;
    kr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)tinfo, &task_info_count);
    if (kr != KERN_SUCCESS) {
        return [ NSString stringWithFormat: @"%f" ,-1];
    }

    task_basic_info_t      basic_info;
    thread_array_t         thread_list;
    mach_msg_type_number_t thread_count;

    thread_info_data_t     thinfo;
    mach_msg_type_number_t thread_info_count;

    thread_basic_info_t basic_info_th;
    uint32_t stat_thread = 0; // Mach threads

    basic_info = (task_basic_info_t)tinfo;

    // get threads in the task
    kr = task_threads(mach_task_self(), &thread_list, &thread_count);
    if (kr != KERN_SUCCESS) {
        return [ NSString stringWithFormat: @"%f" ,-1];
    }
    if (thread_count > 0)
        stat_thread += thread_count;

    long tot_sec = 0;
    long tot_usec = 0;
    float tot_cpu = 0;
    int j;

    for (j = 0; j < thread_count; j++)
    {
        thread_info_count = THREAD_INFO_MAX;
        kr = thread_info(thread_list[j], THREAD_BASIC_INFO,
                         (thread_info_t)thinfo, &thread_info_count);
        if (kr != KERN_SUCCESS) {
            tot_cpu = -1;
            //return -1;
        }

        basic_info_th = (thread_basic_info_t)thinfo;

        if (!(basic_info_th->flags & TH_FLAGS_IDLE)) {
            tot_sec = tot_sec + basic_info_th->user_time.seconds + basic_info_th->system_time.seconds;
            tot_usec = tot_usec + basic_info_th->user_time.microseconds + basic_info_th->system_time.microseconds;
            tot_cpu = tot_cpu + basic_info_th->cpu_usage / (float)TH_USAGE_SCALE * 100.0;
        }

    } // for each thread

    kr = vm_deallocate(mach_task_self(), (vm_offset_t)thread_list, thread_count * sizeof(thread_t));
    assert(kr == KERN_SUCCESS);

    NSString *tostring = nil ;
    tostring = [ NSString stringWithFormat: @"%.1f" ,tot_cpu];
    NSLog (@"performance  cpu:%@",tostring);

    return tostring;
}

获取页面 vc

上边收集了内存和 cpu,还需要在收集数据的同时和页面对应上.这样就清楚了是当前页面的内存和 cpu 情况.

/**
 *获取当前vc
 */
- (UIViewController *) get_vc {
    UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
    __weak typeof(self) weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        if ([keyWindow.rootViewController isKindOfClass:[UITabBarController class]]) {
            UITabBarController *tab = (UITabBarController *)keyWindow.rootViewController;
            UINavigationController *nav = tab.childViewControllers[tab.selectedIndex];
            DDContainerController *content = [nav topViewController];
            weakSelf.vc = [content contentViewController];
        }
    });
    return self.vc;
}

获取设备信息

/*
 *获取设备名称
 */
- (NSString *) get_devicesName {
    NSString *devicesName = [UIDevice currentDevice].name; //设备名称
    NSLog(@"performance   devicesName:%@", devicesName);
    return devicesName;

}

/*
 *获取系统版本
 */
- (NSString *) get_systemVersion{
    NSString *systemVersion = [UIDevice currentDevice].systemVersion; //系统版本
    NSLog(@"performance   version:%@", systemVersion);
    return systemVersion;
}

/*
 *获取设备idf
 */
- (NSString *) get_idf {
    NSString *idf = [UIDevice currentDevice].identifierForVendor.UUIDString;
    NSLog(@"performance   idf:%@", idf);
    return idf;

}

数据拼接

最终要把内存、cpu 等数据拼接成字典的形式,方便输出查看

输出log日志的数据格式

{
    "cpu": "0.4",
    "fps": "60 FPS",
    "version": "11.2",
    "appname": "xxxxxx",
    "battery": "-100.0",
    "appversion": "5.0.4",
    "time": "2018-09-07 11:45:24",
    "memory": "141.9",
    "devicesName": "xxxxxx",
    "vcClass": "DDAlreadPaidTabListVC",
    "idf": "8863F83E-70CB-43D5-B6C7-EAB85F3A2AAD"
}

开启子线程采集

开一个子线程定时采集数据

/*
 * 性能采集子线程
 */

- (void) performancethread {
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"performance   ======get performance======");

        [self get_fps];

        while (true) {
            DDPerformanceModel *model = [DDPerformanceModel new];
            model.time=[self get_time];
            model.appname=[self get_appname];
            model.appversion=[self get_appversion];
            model.idf =[self get_idf];
            model.devicesName =[self get_devicesName];
            model.version = [self get_systemVersion ];
            model.vcClass = NSStringFromClass([self get_vc].class);
            model.memory = [self get_memory];
            model.battery = [self get_battery];
            model.cpu = [self get_cpu];
            model.fps = self.percount;

            NSString *json = [model modelToJSONString];

//            printf(" getperformance    %s\r\n", [json UTF8String]);
            NSLog(@"getperformance model  %@", json);
            sleep(5);
        }
    }];
    [thread start];

    NSLog(@"performance   ======continue mainblock======");
}


初始化性能采集

AppDelegate.m文件中didFinishLaunchingWithOptions方法中用户各种初始化操作,可以在第一行初始化性能采集,
这样app启动以后就可以定时采集数据

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    [[getperformance new] performancethread];//获取性能数据

    }

性能采集日志存储

一般来说日志存储都是写入到本地 log 日志,然后读取.但是有两个问题

  • 需要读写文件代码,对于不熟悉 oc 的人来说比较难
  • 因为是定时采集,文件 IO 操作频繁

所以不考虑存储本地 log 日志的方式,可以在代码中打印出数据,通过截获当前设备运行的日志获取数据.

模拟器可以使用xcrun simctl命令获取当前设备运行日志,
真机用libimobiledevice获取日志

xcrun simctl spawn booted log stream --level=debug | grep getperformance

输出log日志的数据格式,这块做了json美化,每歌几秒在控制台就打印一次

{
    "cpu": "0.4",
    "fps": "60 FPS",
    "version": "11.2",
    "appname": "xxxxxx",
    "battery": "-100.0",
    "appversion": "5.0.4",
    "time": "2018-09-07 11:45:24",
    "memory": "141.9",
    "devicesName": "xxxxxx",
    "vcClass": "DDAlreadPaidTabListVC",
    "idf": "8863F83E-70CB-43D5-B6C7-EAB85F3A2AAD"
}

如果获取多次数据可以使用shell脚本把命令放到后台,定时写入到logpath中
nohup xcrun simctl spawn booted log stream --level=debug >${logpath} &

代码插入到工程中

因为在持续集成中,每次打取的代码都是不带性能测试代码,这些代码是单独写到文件中.在编译项目前,用 shell 把代码插入到工程中,这样打出来的包才能有采集性能数据功能.

scriptrootpath=${2}
AddFiles=${2}"/GetPerformance/performancefiles"
localDDPerformanceModelh=${scriptrootpath}"/GetPerformance/performancefiles/DDPerformanceModel.h"
localDDPerformanceModelm=${scriptrootpath}"/GetPerformance/performancefiles/DDPerformanceModel.m"
localgetperformanceh=${scriptrootpath}"/GetPerformance/performancefiles/getperformance.h"
localgetperformancem=${scriptrootpath}"/GetPerformance/performancefiles/getperformance.m"

addfiles(){

    echo "删除${projectaddpath}中的原性能采集文件"

    rm -rf ${DDPerformanceModelh}
    rm -rf ${DDPerformanceModelm}
    rm -rf ${getperformanceh}
    rm -rf ${getperformancem}

    echo "复制文件到${projectaddpath}路径"

    cp  ${localDDPerformanceModelh} ${projectaddpath}
    cp  ${localDDPerformanceModelm} ${projectaddpath}
    cp  ${localgetperformanceh} ${projectaddpath}
    cp  ${localgetperformancem} ${projectaddpath}

}

性能数据绘制

在手工和自动化使用插入性能测试代码的 app,如果截获性能数据后,可以对数据做性能数据绘制.

用 Higcharts 或者 echarts 绘制性能走势图

如何在持续集成中使用

monkey 和 UI 自动化中使用,最终会发送一份性能报告.

Demo 代码

已经把性能代码脱了主项目,可在 Demo 代码中编译,github 地址:https://github.com/xinxi1990/iOSPerformanceTest

最后

虽然 iOS 生态封闭,但是对于开发者和测试者还是有一些空间可以利用的.

iOS 测试一直都是一个难点,难懂的 oc 语法和 iOS 整体框架.如果你开始慢慢接触 iOS,会发现 iOS 测试也并不是那么难,需要一点耐心和一点专心而已.

共收到 26 条回复 时间 点赞

楼主写的很好,很详细。关于 monkey 的性能有点想不通,monkey 既然是随机的那获取的性能的意义体现在什么地方?

codeskyblue 回复

这个 monkey 类似 app 的自动遍历吧,通过配置一些策略可以让 monkey 更精准到达页面,目前 monkey 还有优化中,大致可以通过 monkey 产生的性能数据,看出当前 app 的性能高峰值.

思寒_seveniruby 将本帖设为了精华贴 09月08日 15:50

instruments 也是可以自动化执行的,下次上课的时候我给你说下。

希望楼主可以讲讲如何运用这些数据,怎么从数据提取出性能问题,或者优化点

棒棒哒,这样流量统计也可以加上了

楼主您好,我现在 app 已经实现了 UI 自动化,根据您的描述,我是不是在打包前,把性能测试代码插入到工程中,然后跑自动化,结束后就会生成一份性能报告呢,谢谢

剪烛 回复

嗯 后续会再写一个性能测试的帖子,会根据一些实际的工作项目

颜如玉 回复

demo 代码里边有需要的性能文件代码,需要在打包前用 shell 脚本自动化插入到被测代码中.
关于性能报告需要用一个自己写一个服务.
我再整理下脚本代码,最终做的形态是执行 shell 脚本插入被测代码和生成性能报告,写好的话我会放到 git 上的

楼主您好,实际测试的时候我如何运行你写的这些脚本,需要嵌入到被测 app 的工程里吗?我想配合 python 的 UI 自动化测试一起提取性能数据该如何实现

大佬,求科普

匿名 #12 · 2018年11月06日

楼主你好,你的内存拿到的是 App 的内存对比 Instruments 是否一致呢?你的 CPU 拿到的是 System CPU 还是 App 的 CPU?

设备整体 cpu,测试前可以把其他 app 都干掉

xinxi 回复

话说这种嵌到工程里的测试代码,对主线程的影响这么评估。
内存,CPU 拿整机的话,应该差的有点多吧。

david 回复

用了一段时间,也没发生过什么崩溃现象,现在就是个参考值,最准的工具还是 xcode 自带工具比较准, 类似 appium 最新版本可以使用方法采集性能数据,然后导出查看

xinxi #16 · 2018年11月10日 Author
david 回复

我刚才撸了一遍代码,内存和 cpu 是使用,和 xcode 中 debug 面板是一样的,也就是当前 app 的内存和 cpu

xinxi 回复

嗯,我搜了一下,这个 phys_footprint 貌似是可以。
还有一个疑问啊,这种嵌入式的东东,不好做竞品分析啊。我们之前做 iOS 的竞品分析,都是用 insturment 跑,跑完拿到 log,再放到我们写的一个 project 里面解析,便宜又大碗。但是 instrument 拿到的内存貌似是 resident mem,感觉不是很有参考价值。。。这块卡了好久,没有好的解决方案。

xinxi #18 · 2018年11月15日 Author
david 回复

iOS 的确工具比较,你可以试试腾讯 gt,不过也是嵌入式,如果有 iOS 开发能力的话,可以自己写成 sdk 上报到自己的平台

匿名 #19 · 2018年11月17日
xinxi 回复

经过测试,楼主的是 App 的 CPU 不是系统 CPU

求 instruments 自动化收集贴,毕竟和开发独立的测试 拿不到源码 😭

simple 专栏文章:[精华帖] 社区历年精华帖分类归总 中提及了此贴 12月13日 14:44
simple [精彩盘点] TesterHome 社区 2018 年 度精华帖 中提及了此贴 01月07日 12:08
匿名 #23 · 2019年01月29日
xinxi 回复

楼主 ,脚本您上传到 Git 上了吗

哪里可以上课?

codeskyblue 回复

我也在思考 monkey 的性能数据要用来做什么呢?

大佬求问 instruments 怎么自动化执行。。

Instruments 命令行实现自动化:
https://www.jianshu.com/p/c39939e88079
亲测可用

作者,你好,我运行您 Git 上的代码时,控制台有性能信息输出,但是我使用 fastMonkey 对 UIcatalog 进行 monkey 测试,控制台中只有 Monkey 日志,那您说的 “monkey 和 UI 自动化中使用,最终会发送一份性能报告.”,这个性能报告在哪呢?,还有,您的这个代码是 OC,fastMonkey 是 swift,这个语言不统一,应该怎么解决呢?

梦梦GO 回复

请问找到 instruments 自动化的方案了吗,

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册