APP 性能测试的背景

  • 在任何软件的测试过程中,性能测试都是一个很重要的环节。我们一般所说的性能测试分为客户端及服务器端。针对客户端性能测试,尤其像游戏、视频类的软件,比如玩游戏不断卡顿、看视频电量消耗极快,都直接影响了用户体验。

  • 对于性能测试的需求,主要来源于行业的通用标准,竞品的参考数据,历史版本的测试数据,或是直接的客户反馈等。App性能测试指标主要包括:响应、内存、CPU、FPS、GPU渲染、耗电、耗流等。

  • APP性能测试的基本原理是在不同用户操作场景下通过监控APP的各项指标来识别和发现APP存在的代码质量问题并对程序进行修正和优化。

Android CPU占用

CPU使用率是性能测试是一项重要指标,CPU占用过高会使得设备运行程序出现卡顿与发热,甚至出现应用程序Crash,影响用户体验。在排除硬件环境的限制下,应用程序应该尽可能少的占用CPU。

CPU使用率原理

Android系统内核是基于Liunx,在Linux系统下CPU利用率分为用户态、系统态、空闲态,分别表示CPU处于用户态执行的时间,系统内核执行的时间,和空闲系统进程执行的时间。

平时所说的CPU利用率是指:CPU执行非系统空闲进程的时间 / CPU总的执行时间。那么这里所说的时间含义是什么呢?

  • HZ:Linux 核心每隔固定周期会发出timer interrupt (时钟中断),HZ是用来定义每一秒有几次时钟中断。例如HZ为1000,就代表每秒有1000次时钟中断。

  • Jiffies:在Linux的内核中,有一个全局变量:Jiffies。 Jiffies代表时间。它的单位随硬件平台的不同而不同。jiffies的单位就是 1/HZ。

  • Intel平台jiffies的单位是1/100秒,这就是系统所能分辨的最小时间间隔了。每个CPU时间片,Jiffies都要加1。

那么CPU利用率计算公式如下:
CPU使用率=(用户态Jiffies+系统态Jiffies)/总Jiffies

如果APP某场景进行操作时出现发烫、卡顿、ANR现象时,可以怀疑出现CPU问题,一般解决思路如下:
如果已经导致ANR, 则去logcat文件里面搜索"ANR in" ,以及adb pull拉取trace文件。
没有导致ANR则基于以上方法获取到的CPU占用率,如果某场景的CPU占用率走势异常、峰值存在异常、均值大于基线,则可以利用traceview查看分析Trace文件,或者使用Android studio里面的Android Monitor根据Monitor中的CPU可以看出目前CPU明细使用,由开发负责定位解决。

CPU测试方法

adb shell top方法

由于Android是基于Linux内核改造而成的操作系统,自然而然也能使用Linux的一些常用命令。比如我们可以使用top命令查看哪些进程是 CPU 的主要消耗者。
Top命令使用方法如下:

PS C:\Users\12446\Desktop> adb shell top --help
usage: top [-H] [-k FIELD,] [-o FIELD,] [-s SORT]

Show process activity in real time.

-H      Show threads   显示线程
-k      Fallback sort FIELDS (default -S,-%CPU,-ETIME,-PID)
-o      Show FIELDS (def PID,USER,PR,NI,VIRT,RES,SHR,S,%CPU,%MEM,TIME+,CMDLINE)
-O      Add FIELDS (replacing PR,NI,VIRT,RES,SHR,S from default)
-s      Sort by field number (1-X, default 9)

注意:由于Android 8.0以后Google的权限限制,再也拿不到进程CPU的实时占用率,只能拿到自己本身进程的Jiffies,而由于拿不到系统整体Jiffies的情况下,就没办法衡量CPU当前的消耗状况了,也没办法根据当前CPU状态实时做一些策略调整。
adb shell top -m 100 -n 1 -d 1 -s 9
-t 显示进程名称,-s 按指定行排序,-n 在退出前刷新几次,-d 刷新间隔,-m 显示最大数量
输入adb shell top命令可以看到如下所示数据

Tasks: 124 total,   1 running, 123 sleeping,   0 stopped,   0 zombie
Mem:   3566392k total,   759068k used,  2807324k free,    15100k buffers
Swap:        0k total,        0k used,        0k free,   512064k cached
400%cpu   0%user   0%nice   0%sys 400%idle   0%iow   0%irq   0%sirq   0%host
  PID USER     PR  NI VIRT  RES  SHR S[%CPU] %MEM     TIME+ ARGS
 2744 u0_a40   20   0 1.2G 106M  78M S  0.3   3.0   0:02.82 com.alipay.hulu
 3305 root     20   0 6.8M 3.4M 2.8M R  0.0   0.0   0:00.15 top
 2948 root     20   0 2.5M 496K 464K S  0.0   0.0   0:00.00 su --daemon
 2829 u0_a12   20   0 881M  60M  40M S  0.0   1.7   0:00.06 com.android.onetimei
 2804 u0_a3    20   0 882M  59M  39M S  0.0   1.7   0:00.16 com.android.carrierc
 2798 u0_a11   20   0 882M  58M  38M S  0.0   1.6   0:00.10 com.android.managedp
 2760 u0_a2    20   0 884M  68M  47M S  0.0   1.9   0:00.24 com.android.provider
 2722 u0_a1    20   0 890M  82M  60M S  0.0   2.3   0:00.51 android.process.acor
 2627 u0_a39   20   0 889M  63M  42M S  0.0   1.8   0:00.14 com.google.android.w
 2555 u0_a37   20   0 882M  61M  41M S  0.0   1.7   0:00.08 com.android.printspo
 2539 camerase 20   0  40M 7.7M 6.7M S  0.0   0.2   0:00.16 cameraserver
 2497 system   10 -10 962M 165M 126M S  0.0   4.7   0:03.44 com.vphone.launcher
 2483 system   20   0 882M  60M  40M S  0.0   1.7   0:00.06 com.android.keychain
 2471 u0_a8    20   0 890M  80M  57M S  0.0   2.2   0:01.90 android.process.medi
 2429 u0_a9    20   0 882M  60M  39M S  0.0   1.7   0:00.03 android.ext.services
 2312 system   20   0 901M  72M  50M S  0.0   2.0   0:00.12 com.android.settings
 2309 wifi     20   0  10M 4.8M 4.3M S  0.0   0.1   0:00.10 wpa_supplicant -ieth
 2298 radio    20   0 900M  89M  65M S  0.0   2.5   0:00.41 com.android.phone
 2251 media_rw 20   0 9.1M 3.3M 2.3M S  0.0   0.0   0:00.76 sdcard -u 1023 -g 10
 2240 u0_a19   20   0 965M 149M 104M S  0.0   4.2   0:02.43 com.android.systemui

解释:

Tasks: 552 total,   1 running, 510 sleeping,   0 stopped,   0 zombie  
任务(进程) 系统现在共有552个进程,其中处于运行中的有1个,510个在休眠(sleep),stoped状态的有0个,zombie状态(僵尸)的有0个。
 
Mem:   5849960k total,  4014628k used,  1835332k free,     5756k buffers    
内存状态: 物理内存总量 (5.6G)  使用中的内存总量  空闲内存总量 缓存的内存量
1TB=1024GB ,1GB=1024MB ,1MB=1024KB ,1KB=1024字节。
 
Swap:  2293756k total,  1039804k used,  1253952k free,   918600k cached  
swap交换分区: 交换区总量  使用的交换区总量  空闲交换区总量  缓冲的交换区总量
 
如果出于习惯去计算可用内存数,这里有个近似的计算公式:
Mem的free + Mem的buffers + Swap的cached
按这个公式此台服务器的可用内存:1835332k + 5756k + 918600k = 2759688k(约2.6G)
 
800%cpu  13%user   0%nice  31%sys 756%idle   0%iow   0%irq   0%sirq   0%host    
cpu状态  
800%cpu -- CPU总量
13%user -- 用户空间占用CPU的百分比。
0%nice -- 改变过优先级的进程占用CPU的百分比
31%sys -- 内核空间占用CPU的百分比
756%idle -- 空闲CPU百分比
0%iow  --  IO等待占用CPU的百分比
0%irq -- 硬中断(Hardware IRQ)占用CPU的百分比
0%sirq --  软中断(Software Interrupts)占用CPU的百分比
0%host -- 
 
[7m  
 
PID — 进程id
USER — 进程所有者
PR — 进程优先级
NI — nice值。负值表示高优先级,正值表示低优先级
VIRT — 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES
RES — 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA
SHR — 共享内存大小,单位kb
S — 进程状态。D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程
%CPU — 上次更新到现在的CPU时间占用百分比
%MEM — 进程使用的物理内存百分比
TIME+ — 进程使用的CPU时间总计,单位1/100秒
COMMAND — 进程名称(命令名/命令行)

按cpu排序

adb shell top -s 9

如果使用adb shell top -m 10 -s cpu会报下面错误: top: not integer: cpu
想到adb top --help命令
我的adb shell top -s命令只支持0-X数字,默认9,猜测分别指代

0  ,1   ,3 ,4 ,5   ,6  ,7  ,8,9   ,10  ,11   ,12
PID,USER,PR,NI,VIRT,RES,SHR,S,%CPU,%MEM,TIME+,CMDLINE

其中,9刚好代表的是cpu

adb shell cat /proc/<进程ID>/stat命令

当使用adb shell cat /proc/<进程ID>/stat命令时,您可以获取特定进程的详细CPU统计信息。以下是关于该命令的详细介绍:

  1. 进程ID(PID):第一个字段是进程的唯一标识符,它可以用来区分不同的进程。

  2. 可执行文件名:第二个字段是进程的可执行文件名。

  3. 进程状态:第三个字段表示进程当前的状态。常见的一些状态包括:

    • R(Running):正在运行。

    • S(Sleeping):睡眠状态。

    • D(Disk sleep):在等待磁盘I/O完成时处于睡眠状态。

    • Z(Zombie):僵尸进程,已经终止但尚未被父进程回收的进程。

    • T(Stopped):停止状态,例如收到了SIGSTOP信号。

  4. 父进程ID(PPID):第四个字段是父进程的进程ID。

  5. 进程组ID(PGID):第五个字段是进程所属的进程组ID。

  6. 会话ID(SID):第六个字段是进程所属的会话ID。

  7. 控制终端:第七个字段是与进程关联的控制终端。

  8. 进程组ID(Tgid):第八个字段是线程组ID,通常与进程ID相同。

  9. 用户态CPU时间和内核态CPU时间:第十三个字段是进程在用户态和内核态下的CPU时间。它们以时钟滴答为单位(即jiffies)表示。

  10. 其他统计信息:在后续字段中,还可能包含其他与进程性能和状态相关的统计信息。

要从adb shell cat /proc/<进程ID>/stat命令的输出中提取CPU相关的详细信息,您可以使用文本处理工具(如awk、sed等)来过滤和清洗数据,只保留CPU相关的字段。以下是一个基本的示例:

adb shell cat /proc/<进程ID>/stat | awk '{print "用户态CPU时间:"$14"\n内核态CPU时间:"$15}'

该命令使用了awk工具,通过指定“$14”和“$15”来获取第14个和第15个字段,即进程在用户态和内核态下的CPU时间。输出将以易于阅读的格式显示这些值。

如果您需要获取其他CPU相关的统计信息,例如进程的CPU使用率、平均负载等,请在awk命令中添加其他字段的索引号。例如:

adb shell cat /proc/<进程ID>/stat | awk '{print "用户态CPU时间:"$14"\n内核态CPU时间:"$15"\nCPU使用率:"($14+$15)/($14+$15+$16+$17)*100"%"}'

该命令不仅打印了用户态和内核态CPU时间,还计算了进程的CPU使用率,并将其以百分比的形式输出。

请注意,每个Android版本中/proc/<进程ID>/stat文件的格式可能略有不同,因此具体的字段索引号可能会有所变化。您需要根据实际情况调整命令中字段的索引号,以正确提取您需要的CPU统计信息。

SoloPi测试应用CPU

image-1654945368746
性能数据查看与记录

  1. 勾选性能项,Soloπ会展示对应的性能指标
    image-1654945387593

在进入应用前,Soloπ会显示全局指标,进入应用后,Soloπ会显示应用最上层进程的相关性能指标。
CPU、内存指标为进程维度,响应耗时、帧率、网络为应用维度,电池为全局指标。
具体性能指标描述请参考后文性能指标一段。
  1. 点击悬浮窗中的应用标题,进入目标应用,切换为进程维度数据
    image-1654945398964

  2. 点击开始按钮(绿色三角),进行性能数据录制,可进行相关操作
    image-1654945409493

  3. 结束录制,点击终止按钮(红色圆形),Soloπ会提示录制数据存放的位置,数据保存在/sdcard/solopi/records/XXX目录下
    image-1654945420721

录制数据为csv格式,包含三列,第一列为Unix时间戳,第二列为数据数值,第三列为额外字段
  1. 点击悬浮窗右上角关闭键(红色"X")可关闭悬浮窗,需要手动回到性能测试页面,可在录制数据查看一项中查看之前录制过的数据。
    image-1654945438837

  2. 在录制项筛选中,可以选择之前录制的各项数据进行观看,内存与CPU会记录在顶层出现过的进程的数据。如果发生了进程切换,会记录前后10次数据。
    image-1654945449279

  3. 当录制项过多时,可以点击右上角删除图标,对旧数据进行删除。
    image-1654945458220

  4. 环境加压
    Soloπ提供了环境加压的功能,可以提供CPU与内存加压
    由于android系统的调度,CPU与内存加压均存在一定限制,当应用处于后台状态时,CPU加压占比会遭到一定程度的限制,而内存加压过大可能导致Soloπ被系统进行回收

自动化获取性能数据

前面我们使用adb命令获取CPU数据,但是如果想批量获取性能数据,使用命令一个个查询会非常的不方便,我们可以使用Python自动化代码来自动获取性能数据,代码实现如下:


#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2022/6/11 21:23
# @Author  : shisuiyi
# @File    : autoApp_cup_meminfo .py
# @Software: win10 Tensorflow1.13.1 python3.9
import csv
from matplotlib import pyplot as plt
import os
import time



class Monitoring(object):
    def __init__(self, count, pkg):
        self.pkg = pkg  # 包名
        self.counter = count  # 统计次数
        self.cpudata = [("timestamp", "cpustatus")]  # cpu性能数据

    def getCurrentTime(self):
        '''获取当前的时间戳'''

        currentTime = time.strftime("%H:%M:%S", time.localtime())
        return currentTime


    def getCurrentDate(self):
        '''获取当前日期'''
        datetime = time.strftime("%Y-%m-%d %H_%M_%S")
        return datetime


    def monitoring_cpu(self):
        '''cpu监控'''
        result = os.popen(" adb shell top -m 100 -n 1  -d 1 -s 9 | findstr " + str(self.pkg))  # 获取cpu性能指标数据
        res = result.readline().split(" ")  # 根据返回数据进行分割
        print(res)

       if res == ['']:  # 返回数据为空时处理
            print('no data')
        else:
            print(res)
           if 'S' in res:
                index = res.index('S')
            else:
                index = res.index('R')
            cpuvalue = res[index + 1]  # 获取cpu数据
            if cpuvalue == '':
                cpuvalue = '0'
            currenttime = self.getCurrentTime()

            print("current time is:" + currenttime)
            print("cpu used is:" + cpuvalue)
            self.cpudata.append([currenttime, cpuvalue])

    def get_cpu_datas(self):
        '''连续获取cpu性能数据'''
        while self.counter > 0:
            self.monitoring_cpu()
            self.counter = self.counter - 1
            time.sleep(2)

    def saveDataToCSV(self, data_type):
        '''
        存储性能测试数据
        :param data_type:
        :return:
        '''
        now = self.getCurrentDate()
        if data_type == 'cpu':
            csvnmame = './cpustatus_' + now + '.csv'
            csvfile = open(csvnmame, 'w', encoding='utf8', newline='')
            writer = csv.writer(csvfile)
            writer.writerows(self.cpudata)
            csvfile.close()
            return csvnmame
        else:
            print('data_type error!')

    def drawing(self,csvnmame):
        # 加载CSV文件
        file = csvnmame

        with open(file) as f:
            # 加载CSV模块阅读器
            reader = csv.reader(f)

            # 读取第一行表头
            header_row = next(reader)

            # 依次读取每行,并保存日期,cpu数据
            dates, cpudatas = [], []
            for row in reader:
                current_date = row[0]
                cpudata = float(row[1])
                dates.append(current_date)
                cpudatas.append(cpudata)

        # 图像分辨率,尺寸设置
        fig = plt.figure(dpi=128, figsize=(10, 6))

        # 标题设置
        plt.title('CpuStatus for date')

        # X轴标签设置,自动更新格式
        plt.xlabel(header_row[0])
        fig.autofmt_xdate()

        # Y轴标签和坐标范围设置
        plt.ylabel(header_row[1])
        plt.ylim(0, 100)

        # 刻度设置
        plt.tick_params(axis='both', which='both', labelsize=4)

        # 根据数据画折线图
        plt.plot(dates, cpudatas, linewidth=3, c='blue', alpha=0.5)
        # 图像显示
        plt.savefig((f'{file}')+'.png')
        plt.show()

if __name__ == '__main__':
    m = Monitoring(20, 'com.netease')
    m.get_cpu_datas()
    name = m.saveDataToCSV('cpu')
    m.drawing(name)

内存使用率

从操作系统的角度来说,内存就是一块数据存储区域,是可被操作系统调度的资源。在多任务(进程)的操作系统中,内存管理尤为重要,操作系统需要为每一个进程合理的分配内存资源。所以可以从操作系统对内存分配和回收两方面来理解内存管理机制。
分配机制:为每一个任务(进程)分配一个合理大小的内存块,保证每一个进程能够正常的运行,同时确保进程不会占用太多的内存。
回收机制:当系统内存不足的时候,需要有一个合理的回收再分配机制,以保证新的进程可以正常运行。

内存分类
在Linux里面,一个进程占用的内存有不同种说法,有四种形式:

  • VSS- Virtual Set Size 虚拟耗用内存

  • RSS- Resident Set Size实际使用物理内存

  • PSS- Proportional Set Size 按比例使用的物理内存

  • USS- Unique Set Size 进程独自占用的物理内存
    VSS
    VSS是单个进程全部可访问的地址空间,其大小可能包括还尚未在内存中驻留的部分。对于确定单个进程实际内存使用大小,VSS用处不大。
    RSS
    RSS是单个进程实际占用的内存大小,RSS不太准确的地方在于它包括该进程所使用共享库全部内存大小。对于一个共享库,可能被多个进程使用,实际该共享库只会被装入内存一次。
    PSS
    PSS不同于RSS,它只是按比例包含其所使用的共享库大小。PSS相对于RSS计算共享库内存大小是按比例的。例如:3个进程使用同一个占用30个内存页的共享库。对于三个进程中的任何一个,PSS 将只包括10个内存页。 PSS 是一个非常有用的数字,因为系统中全部进程以整体的方式被统计, 对于系统中的整体内存使用是比较准确的统计。
    USS
    USS是单个进程私有的内存大小,即该进程独占的内存部分。USS揭示了运行一个特定进程在的真实内存增量大小。如果进程终止,USS就是实际被返还给系统的内存大小。
    说明:
    一般来说内存占用大小有如下规律:VSS >= RSS >= PSS >= USS
    实际在统计查看某个进程内存占用情况的时候,看PSS是比较客观的

Android 内存测试

获取设备内存信息
在Linux操作系统中,/proc是一个位于内存中的伪文件系统(in-memory pseudo-file system)。该目录下保存的不是真正的文件和目录,而是一些运行时信息,如系统内存、磁盘io、设备挂载信息和硬件配置信息等。
使用命令adb shell cat /proc/meminfo查看设备的整体内存使用情况。

PS C:\Users\12446\Desktop> adb shell cat /proc/meminfo
MemTotal:        3566392 kB
MemFree:         2198376 kB
MemAvailable:    2979152 kB
Buffers:           86852 kB
Cached:           783012 kB
SwapCached:            0 kB
Active:           741664 kB
Inactive:         565176 kB
Active(anon):     439240 kB
Inactive(anon):    27952 kB
Active(file):     302424 kB
Inactive(file):   537224 kB
Unevictable:         256 kB
Mlocked:             256 kB
HighTotal:       3219400 kB
HighFree:        2071780 kB
LowTotal:         346992 kB
LowFree:          126596 kB
SwapTotal:             0 kB
SwapFree:              0 kB
Dirty:                40 kB
Writeback:             0 kB
AnonPages:        437168 kB
Mapped:           369552 kB
Shmem:             30232 kB
Slab:              37780 kB
SReclaimable:      14588 kB
SUnreclaim:        23192 kB
KernelStack:        6352 kB
PageTables:         9328 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:     1783196 kB
Committed_AS:   16437568 kB
VmallocTotal:     122880 kB
VmallocUsed:       81660 kB
VmallocChunk:      21228 kB
DirectMap4k:       16376 kB
DirectMap4M:      368640 kB
PS C:\Users\12446\Desktop>

部分参数含义如下:
MemTotal: 表示可供系统支配的内存,系统从开机到加载完成,操作系统内核要保留一些内存,最后剩下可供系统支配的内存就是MemTotal。

  • MemFree:表示系统尚未使用的内存。

  • MemAvailable:应用程序可用内存大小。

系统中有些内存虽然已被使用但是可以回收的,比如cache可以回收,
所以MemFree不能代表全部可用的内存,这部分可回收的内存加上MemFree才是系统可用的内存,
即:MemAvailable≈MemFree+系统回收内存,它是内核使用特定的算法计算出来的,是一个估计值。
它与MemFree的关键区别点在于,MemFree是说的系统层面,MemAvailable是说的应用程序层面。
  • Cached: 缓冲区内存大小。

  • Buffers: 缓存区内存大小。

获取指定包的内存占用情况

我们可以使用adb命令来测试指定进程包名的内存使用详细情况,命令格式如下:
adb shell dumpsys meminfo [pkg or pid]
命令执行之后如下所示

C:\Users\12446>adb shell dumpsys meminfo  3397
Applications Memory Usage (in Kilobytes):
Uptime: 4356982 Realtime: 4356982

** MEMINFO in pid 3397 [com.netease.cloudmusic] **
                   Pss  Private  Private     Swap     Heap     Heap     Heap
                 Total    Dirty    Clean    Dirty     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------
  Native Heap        0        0        0        0    38912    33641     5270
  Dalvik Heap    24831    24700        0        0    29009    21757     7252
 Dalvik Other     1258     1256        0        0
        Stack      912      912        0        0
       Ashmem      132      132        0        0
    Other dev       16        0       12        0
     .so mmap     7699     1532     1252        0
    .apk mmap     7234      124       16        0
    .ttf mmap      287        0       88        0
    .dex mmap    16189        8     7644        0
    .oat mmap     5072        0      384        0
    .art mmap     4638     4228        0        0
   Other mmap     3156        4      448        0
      Unknown    28135    28064        0        0
        TOTAL    99559    60960     9844        0    67921    55398    12522
      1,484K: sdcard (pid 2250)
 App Summary: drmserver (pid 1881)
                       Pss(KB)
                        ------
           Java Heap:    2892883)
         Native Heap:        09)
                Code:    11048
               Stack:      912 1803)
            Graphics:        082)
       Private Other:    299160)
              System:    28755
        546K: lmkd (pid 1873)
               TOTAL:    99559      TOTAL SWAP (KB):        0
        484K: servicemanager (pid 1874)
 Objects335K: debuggerd (pid 1744)
               Views:      435         ViewRootImpl:        1
         AppContexts:        6           Activities:        1
              Assets:        3        AssetManagers:        5
       Local Binders:       63        Proxy Binders:       35
       Parcel memory:       19         Parcel count:       76
    Death Recipients:        3      OpenSSL Sockets:        0
            WebViews:        0(pid 1884)
          4,904K: mediaserver (pid 1887)
 SQL      4,662K: media.extractor (pid 1886)
         MEMORY_USED:      851(pid 1879)
  PAGECACHE_OVERFLOW:      281          MALLOC_SIZE:       62
          2,103K: cameraserver (pid 2518)
 DATABASES2,094K: mediadrmserver (pid 1885)
      pgsz     dbsz   Lookaside(b)          cache  Dbname
         4      236            375       18/37/19  /data/user/0/com.netease.cloudmusic/databases/cloudmusic.db        1,526K: logd (pid 1737)
         4       24             29         1/16/2  /data/user/0/com.netease.cloudmusic/databases/web_cache_info.db    1,484K: sdcard (pid 2250)
         4       20             81    1217/868/25  /data/user/0/com.netease.cloudmusic/databases/netease_apm.db       1,331K: adbd (pid 1800)
         4       24             20         1/16/2  /storage/emulated/0/netease/utils/wu.dat
         4       48             88       52/41/21  /data/user/0/com.netease.cloudmusic/databases/mobidroid.sqlite       896K: healthd (pid 1869)
         4       52             52         2/17/3  /data/user/0/com.netease.cloudmusic/databases/transfer.db
            734K: gatekeeperd (pid 1803)
 Asset Allocationsinstalld (pid 1882)
    zip:/data/app/com.netease.cloudmusic-1/base.apk:/resources.arsc: 1331K
            560K: su (pid 2911)
C:\Users\12446>K: lmkd (pid 1873)
            536K: dumpsys (pid 4525)
            484K: servicemanager (pid 1874)
            335K: debuggerd (pid 1744)
            324K: ueventd (pid 1141)
            264K: debuggerd:signaller (pid 1751)
     82,177K: System
         82,177K: system (pid 2172)
     76,506K: Persistent
         58,958K: com.android.systemui (pid 2241)
         17,548K: com.android.phone (pid 2298)
     55,859K: Foreground
         41,956K: com.android.gallery3d (pid 4347 / activities)
         13,903K: android.process.media (pid 2565)
      4,963K: Visible
          4,963K: android.ext.services (pid 2427)
     41,784K: Perceptible
         41,784K: com.alipay.hulu (pid 2719 / activities)
     35,770K: A Services
         35,770K: com.netease.cloudmusic:play (pid 3473)
     48,034K: Home
         48,034K: com.vphone.launcher (pid 2522 / activities)
    139,669K: B Services
        100,601K: com.netease.cloudmusic (pid 3397 / activities)
         39,068K: com.netease.cloudmusic:browser (pid 3443)
    123,454K: Cached
         38,824K: com.netease.cloudmusic:cmMP1 (pid 3454)
         29,869K: com.android.systemui:screenshot (pid 4274)
         12,131K: android.process.acore (pid 2465)
         11,031K: com.android.settings (pid 2311)
          6,724K: com.android.providers.calendar (pid 2793)
          5,566K: com.google.android.webview:webview_service (pid 2638)
          5,483K: com.android.printspooler (pid 2506)
          4,908K: com.android.managedprovisioning (pid 2814)
          4,560K: com.android.carrierconfig (pid 2829)
          4,358K: com.android.onetimeinitializer (pid 2861)

Total PSS by category:
    190,269K: Unknown
    100,012K: .dex mmap
     89,950K: .so mmap
     88,107K: Dalvik
     60,984K: .oat mmap
     44,825K: .art mmap
     39,863K: .apk mmap
     24,186K: Other mmap
     23,744K: Ashmem
     12,216K: Dalvik Other
      9,871K: .ttf mmap
      8,552K: Stack
        533K: Other dev
        160K: .jar mmap
         32K: Native
          0K: Cursor
          0K: Gfx dev
          0K: EGL mtrack
          0K: GL mtrack
          0K: Other mtrack

Total RAM: 3,566,392K (status normal)
 Free RAM: 2,930,922K (  123,454K cached pss +   310,708K cached kernel + 2,496,760K free)
 Used RAM:   689,694K (  569,850K used pss +   119,844K kernel)
 Lost RAM:   -54,224K
   Tuning: 128 (large 192), oom   184,320K, restore limit    61,440K (high-end-gfx)

重点关注参数
一般情况下横轴仅需关注Pss Total和Private Dirty列。 Private Dirty表示进程独占内存。

  • Native Heap:Native代码分配的内存。native进程采用C/C++实现,不包含dalvik实例的linux进程,/system/bin/目录下面的程序文件运行后都是以native进程形式存在的.

  • Dalvik Heap:Java对象分配的占据内存
    其他参数

  • Dalvik Other:类数据结构和索引占据内存。

  • Stack:栈内存

  • Ashmem:不以dalvik- 开头的内存区域,匿名共享内存用来提供共享内存通过分配一个多个进程可以共享的带名称的内存块。匿名共享内存(Anonymous Shared Memory-Ashmem。Android匿名共享内存是基于Linux共享内存的,都是在tmpfs文件系统上新建文件,并将其映射到不同的进程空间,从而达到共享内存的目的,只是Android在Linux的基础上进行了改造,并借助Binder+fd文件描述符实现了共享内存的传递。

  • Other dev:内部driver占用的内存

  • .so mmap C库代码占用的内存

  • .jar mmap java文件代码占用的内存

  • .apk mmap apk代码占用的内存

  • .ttf mmap ttf文件代码占用的内存

  • .dex mmap dex文件代码占用内存。类函数的代码和常量占用的内存,dex mmap是映射classex.dex文件,Dalvik虚拟机从dex文件加载类信息和字符串常量等。Dex文件有索引区和Data区

  • Other mmap 其它文件占用的内存

重点关注如下几个字段:

  • Native/Dalvik 的 Heap 信息
    具体在上面的第一行和第二行,它分别给出的是JNI层和Java层的内存分配情况,如果发现这个值一直增长,则代表程序可能出现了内存泄漏。

  • Total 的 PSS 信息
    这个值就是你的应用真正占据的内存大小,通过这个信息,你可以轻松判别手机中哪些程序占内存比较大了。

内存泄漏与溢出

内存泄漏(Memory Leak)和内存溢出(Memory Overflow)是两种与内存管理相关的问题,但含义和表现方式略有不同。

内存泄漏(Memory Leak):

指在程序运行过程中,动态分配的内存没有正确释放或回收,导致这些内存无法被再利用。如果反复发生内存泄漏,最终会导致系统可用内存耗尽,从而影响系统的正常运行。

内存泄漏通常发生在以下情况下:

  • 动态分配的内存没有被及时释放。

  • 指针或引用被错误地赋值或传递,导致无法访问到原本应该释放的内存。

  • 数据结构中存在循环引用,导致无法被垃圾回收机制识别并释放。

内存溢出(Memory Overflow):

指程序在申请内存空间时,超出了系统所能提供的可用内存大小。当程序试图分配更多的内存空间时,由于没有足够的内存可供分配,就会引发内存溢出错误。

内存溢出通常发生在以下情况下:

  • 递归调用导致栈空间溢出,也称为栈溢出。

  • 动态分配的堆内存超过了系统的物理内存限制。

  • 由于程序错误或恶意攻击,向缓冲区写入超出其容量的数据。

启动耗时

应用启动是整个App工序的第一道流程。对于开发者,一般需要在应用启动过程中进行初始化工作,启动页的UI展示。而对于用户来说,启动速度的快慢则极大地影响了使用体验,并且间接地影响了用户的留存率。

应用的启动方式
冷启动:当启动应用时,后台没有该应用的进程,这时系统会首先会创建一个新的进程分配给该应用,这种启动方式就是冷启动。

热启动:当启动应用时,后台已有该应用的进程,这种启动方式叫热启动。

一般测试过程中更多的关注冷启动时间。

adb 命令方式获取

通过命令adb shell am start -W [PackageName]/[PackageName.MainActivity]获取启动时间。

C:\Users\12446> adb shell am start -W com.lemon.lemonban/.activity.MainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.lemon.lemonban/.activity.MainActivity }
Status: ok
LaunchState: COLD
Activity: com.lemon.lemonban/.activity.MainActivity
TotalTime: 677
WaitTime: 679
Complete

这里面涉及到三个时间,ThisTime、TotalTime 和 WaitTime。

  • ThisTime 表示启动一连串 Activity 的最后一个 Activity 的启动耗时。

  • TotalTime 表示新应用启动的耗时,包括新进程的启动和 Activity 的启动,但不包括前一个应用 Activity pause 的耗时。也就是说,开发者一般只要关心 TotalTime 即可,这个时间才是自己应用真正启动的耗时。

  • WaitTime 就是总的耗时,包括前一个应用 Activity pause的时间和新应用启动的时间。

Solopi

响应耗时计算工具
Soloπ响应耗时计算工具,通过录屏分帧的方式自动识别起始点和结束点,精确计算耗时。
响应耗时计算

特性

  • 模拟用户视觉,计算结果更贴近用户体验

  • 自动记录点击起始点,自动识别屏幕变化结束点

  • 通过OpenCV进行图像识别

  • 支持原生应用、H5、游戏

  • 支持启动耗时、页面跳转耗时、列表滑动耗时、动画耗时

使用方法

  1. 进入Soloπ性能测试页面,点击启动耗时计算。
    4b8ffac8f335125fc8bc0d30a2e3626

  2. 建议默认参数无需修改,点击启动按钮,会弹出录制浮窗。
    image-1655024364030

  3. 切换至待测应用,并进入操作路径的起始页面。
    如果不需要关注细微的动画,请将二值对比差异设置为2%
    image-1655024412537

  4. 点击开始录制(按钮会变为结束录制),然后在被测应用界面执行操作
    image-1655024431756

  5. 当页面停止变化后,点击结束录制。几秒后会显示测试结果。
    image-1655024455494

注意事项

  • 结束页存在轮播图?

    • 请在进入结果页后,在轮播图变化前点击结束录制

  • 测试结果异常:过大,过小,或为负数?

    • 本次结果失效,请重新测算。结果以多次测算后稳定的结果为准。

  • 结束录制后,一直阻塞在计算中?

    • 请关闭浮窗,退出Soloπ,重新打开耗时计算工具进行测算。

流畅度测试

帧率&刷新频率

首先需要了解到两个概念:

刷新频率(Refresh Rate)

代表屏幕在一秒内刷新操作的次数,这取决于硬件的固定参数,例如60HZ。

帧率(Frame Rate)

代表GPU在一秒内绘制操作的帧数,例如常见的24fps,60fps,单位是fps(每秒帧数),很多游戏里面也会有这个指标。

对于连续绘制的应用(游戏、视频)我们可以选用fps指标。

步骤如下:

  • 将手机通过USB线连接之PC端,开发者选项中USB调试打开,保证adb devices能够检测到设备

  • 再次进入到开发者选项中,找到GPU呈现模式分析

  • 选择“在adb shell dumpsys gfxinfo中”

  • 启动测试App应用,测试对应的场景,结束后在命令行输入adb shell dumpsys gfxinfo 应用包名

Draw+Prepare+Process+Execute=完整显示一帧的时间

这个时间需要小于16.67ms才能保证不丢帧

fps数据获取:

FPS(Frames Per Second,每秒帧数)

计算总数据的行数(跳过第一行)

frameCount=rowNum(总行数)

计算每帧的渲染时间

renderTime=Draw+Prepare+Process+Execute

当渲染时间renderTime大于16.67ms,该帧渲染超时,算一次丢帧,需要用掉额外的Vsync个数为(多需要的同步信号):
fps计算公式为

fps=frameCount*60 / frameCount+vsyncOverNum

弱网测试

弱网测试背景

弱网测试主要就是对带宽、丢包、延时等进行模拟弱网环境,属于健壮性测试的内容。

目前移动端用户所处的网络环境并非为完全流畅的WiFi环境,并且在WiFi环境下也会有网络波动。

在实时性要求非常高的场景,容易伤害用户体验,因此,为了避免用户体验不友好造成用户流失,弱网测试显得尤为重要。所以在我们测试的时候,进行必要的弱网测试还是有必要的。

弱网测试要点

弱网测试可分为弱网功能测试、网络切换测试、断网测试等,并且在不同网络环境测试的同时密切关注用户体验。

弱网下功能测试

关注页面数据加载时间即接口发出请求到数据返回响应时间,是否有友好加载提示如loading动画or进度条,在客户端设置超时时间内响应正常展示页面数据及使用功能,超出超时时间后显示异常友好提示即超时机制,以及根据场景判断超时后是否进行重连请求机制等。

进行网络切换

操作时,关注网络切换中正好处于数据加载状态,是否会导致App crash or ANR。

断网状态下

    页面展示考虑三种加载情况,

  1.     初始化从零加载应显示异常提示页面

  2.     页面加载部分数据后断网,已加载数据是否正常展示,未加载部分是否与异常提示UI设计保持一致;

  3.     已加载所有数据后断网重新刷新页面是正常展示还是覆盖已有数据(取决开发加载策略)。

在已有本地数据存储的情况下,查看断网重连后能否正常使用功能以及传参数据正确性。

在弱网前提下,我们既要关注网络问题,策略,还要保证是否有兜底方案,不能引发崩溃等问题,是否可以给用户合理的提示,让用户知道不是APP出现的问题,而是网络的问题,给用户最佳的体验。

弱网测试工具

 在测试过程中,除了在实际场景如电梯、地下车库、地铁等环境进行模拟测试外,还可以借助第三方工具来进行网络模拟测试。

Charles

Fiddler

   Rules-customer rules打开自定义脚本编辑器,找到如下图代码,更改带宽延迟设置

 iOS自带开发者工具Network Link Conditioner

        因为是苹果自带的网速模拟工具,需要在Xcode环境下激活手机设置中才会显示开发者选项

Facebook开源工具ATC

测试WI-FI的路由器的网络限速功能

现在很多路由器都可以限制对应的链接设备的网速,可以根据需要对设备进行限速

不同网络测试环境设置参考如下图: