iOS 测试 iOS 自动化性能采集

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

前言

对于 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 条回复 时间 点赞
梦梦GO 回复

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

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

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

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

codeskyblue 回复

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

哪里可以上课?

匿名 #23 · 2019年01月29日
xinxi 回复

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

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

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

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

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

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

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

xinxi 回复

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

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

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

david 回复

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

xinxi 回复

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

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

匿名 #12 · 2018年11月06日

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

大佬,求科普

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

颜如玉 回复

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

剪烛 回复

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

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

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

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

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

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

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

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

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