Android 性能测试,跟 pc 性能测试一样分为客户端及服务器,但在客户端上的性能测试分为 2 类:
- 一类为 rom 版本的性能测试
- 一类为应用的性能测试
对于应用性能测试,包括很多测试项,如启动时间、内存、CPU、GPU、功耗、流量等。 但针对 rom 版本的性能测试,一般关注功耗。
先写下内存篇
内存的采集:
Android的内存的采集这边介绍三种方式:
1,通过Dumpsys 来取值
adb shell dumpsys meminfo
这里可以看到当前所有进程的内存信息!
如果你要看详细的内存:
adb shell dumpsys meminfo pakagename or Pid
看其中的Size 可以发现 Native Heap 和Dalvik Heap 占据了Heap Size
dalvik就是我们平常说的java堆,我们创建的对象是在这里面分配的。
对于内存的限制 这里纠正一下:是 dalvik heap不能超过最大限制,跟Native heap没有关系!
最大限制查看:
#查看单个应用程序最大内存限制 adb shell getprop|grep heapgrowthlimit
得到结果:
|[dalvik.vm.heapgrowthlimit]:
[96m]
这个96M是单个程序限制最大内存,而meminfo 里面的dalvik heap size 的最大值若果超出了96m 那就很可能会发生OOM
dalvik.vm.heapgrowthlimit和dalvik.vm.heapsize都是java虚拟机的最大内存限制,应用如果不想在dalvik heap达到heapgrowthlimit限制的时候出现OOM,需要在Manifest中的application标签中声明android:largeHeap=“true”,声明后应用dalvik heap 达到heapsize的时候才会出现OOM!
注:设备的不一样 最大内存限制也可能不一样
现在大多数手机 的android程序内存一般限制在96M以上甚至更高,也可能更低。
3,用/system/xbin/procrank工具 来取值很直观
adb shell procrank
VSS – Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)
RSS – Resident Set Size 实际使用物理内存(包含共享库占用的内存)
PSS – Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)
USS – Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)
USS 是针对某个进程开始有可疑内存泄露的情况, 是一个程序启动了会产生的虚拟内存,一旦这个程序进程杀掉就会释放!
3,使用ActivityManager的getMemoryInfo(ActivityManager.MemoryInfo outInfo)(这个方法是写一个简单的app去监控的时候用到的,轻便简单)
private
void
GetMemory
(
)
{
final
ActivityManager
activityManager
=
(
ActivityManager
)
getSystemService
(
ACTIVITY_SERVICE
);
ActivityManager
.
MemoryInfo
info
=
new
ActivityManager
.
MemoryInfo
();
activityManager
.
getMemoryInfo
(
info
);
Log
.
i
(
tag
,
"系统剩余内存:"
+(
info
.
availMem
>>
10
)+
"k"
);
Log
.
i
(
tag
,
"系统是否处于低内存运行:"
+
info
.
lowMemory
);
Log
.
i
(
tag
,
"当系统剩余内存低于"
+
info
.
threshold
+
"时就看成低内存运行"
);
}
availMem:表示系统剩余内存
lowMemory:它是boolean值,表示系统是否处于低内存运行
hreshold:它表示当系统剩余内存低于好多时就看成低内存运行
我用过以上三种最多,其实Top 也可以 还有很多方法都可以。
adb shell top
内存拿到后怎么去用呢?
这里我用的方法是用java封装Adb shell dumpsys meminfo再用字符串截取 打印的方式
public
static
String
GetMemory
(
String
packageName
)
throws
IOException
,
InterruptedException
{
String
str3
=
null
;
Runtime
runtime
=
Runtime
.
getRuntime
();
Process
proc
=
runtime
.
exec
(
"adb shell dumpsys meminfo "
+
packageName
);
try
{
if
(
proc
.
waitFor
()
!=
0
)
{
System
.
err
.
println
(
"exit value = "
+
proc
.
exitValue
());
}
BufferedReader
in
=
new
BufferedReader
(
new
InputStreamReader
(
proc
.
getInputStream
()));
StringBuffer
stringBuffer
=
new
StringBuffer
();
String
line
=
null
;
while
((
line
=
in
.
readLine
())
!=
null
)
{
stringBuffer
.
append
(
line
+
" "
);
}
String
str1
=
stringBuffer
.
toString
();
String
str2
=
str1
.
substring
(
str1
.
indexOf
(
"Objects"
)-
60
,
str1
.
indexOf
(
"Objects"
));
str3
=
str2
.
substring
(
0
,
10
);
str3
.
trim
();
}
catch
(
InterruptedException
e
)
{
System
.
err
.
println
(
e
);
}
finally
{
try
{
proc
.
destroy
();
}
catch
(
Exception
e2
)
{
}
}
return
str3
;
}
}
截取好之后呢 可以跟 其他的一些系统资源值拼在一起打印出来:
拿到这些值之后可以配合手工或自动化来做数据收集,你会看到有些步骤内存占用很高或者Cpu消耗也会较高,这样你就可以去check一下 关于这个步骤相关 的Code
关于Android 的Cpu占用率需要注意以下三种情况:
1.空闲状态下的应用CPU消耗情况 简单说这种情况呢就是说被测应用在系统资源非常空闲的情况下的占用率,比如只开一个被测应用
2.中等规格状态下的应用CPU消耗情况 简单说这种情况就是后台已经有几个应用在运行已经并且消耗了系统的一些资源的情况下进行测试。
3.满规格状态下的应用CPU消耗情况 这个就不要说了,你们懂得!
数据采集方案:
1.
adb shell dumpsys cpuinfo
这里可以看到所有进程的Cpu占用率
大家看第一个应用CPU占用率68%,这个过程是在用户(user)中花61%的时间,并在内核空间(kernel)花费7.1%的时间。
如果你想筛选出你自己的应用的话可以用下面这一段:
adb shell dumpsys cpuinfo |grep packagename
2.使用top命令:
进入Adb shell
adb shelltop -m 10 -s cpu
可查看占用cpu最高的前10个程序(-t 显示进程名称,-s 按指定行排序,-n 在退出前刷新几次,-d 刷新间隔,-m 显示最大数量)
如果你想筛选出你自己的应用的话可以用下面这一段:
adb shell top -n 1| grep PackageName
拿到这些数据怎么用
1,你可以从代码里面获取:
(dumpsys)
adb shell dumpsys cpuinfo
public
static
String
GetCpu
(
String
packageName
)
throws
IOException
{
String
str3
=
null
;
Runtime
runtime
=
Runtime
.
getRuntime
();
Process
proc
=
runtime
.
exec
(
"adb shell dumpsys cpuinfo $"
+
packageName
);
try
{
if
(
proc
.
waitFor
()
!=
0
)
{
System
.
err
.
println
(
"exit value = "
+
proc
.
exitValue
());
}
BufferedReader
in
=
new
BufferedReader
(
new
InputStreamReader
(
proc
.
getInputStream
()));
StringBuffer
stringBuffer
=
new
StringBuffer
();
String
line
=
null
;
while
((
line
=
in
.
readLine
())
!=
null
)
{
stringBuffer
.
append
(
line
+
" "
);
}
String
str1
=
stringBuffer
.
toString
();
String
str2
=
str1
.
substring
(
str1
.
indexOf
(
packageName
),
str1
.
indexOf
(
packageName
)+
28
);
str3
=
str2
.
substring
(
18
,
23
);
}
catch
(
InterruptedException
e
)
{
System
.
err
.
println
(
e
);
}
finally
{
try
{
proc
.
destroy
();
}
catch
(
Exception
e2
)
{
}
}
return
str3
;
}
}
(Top)
public
static
double
cpu
(
String
PackageName
)
throws
IOException
{
double
Cpu
=
0
;
try
{
Runtime
runtime
=
Runtime
.
getRuntime
();
Process
proc
=
runtime
.
exec
(
"adb shell top -n 1| grep "
+
PackageName
);
try
{
if
(
proc
.
waitFor
()
!=
0
)
{
System
.
err
.
println
(
"exit value = "
+
proc
.
exitValue
());
}
BufferedReader
in
=
new
BufferedReader
(
new
InputStreamReader
(
proc
.
getInputStream
()));
StringBuffer
stringBuffer
=
new
StringBuffer
();
String
line
=
null
;
while
((
line
=
in
.
readLine
())
!=
null
)
{
stringBuffer
.
append
(
line
+
" "
);
}
String
str1
=
stringBuffer
.
toString
();
String
str3
=
str1
.
substring
(
str1
.
indexOf
(
PackageName
)-
43
,
str1
.
indexOf
(
PackageName
));
String
cpu
=
str3
.
substring
(
0
,
4
);
cpu
=
cpu
.
trim
();
Cpu
=
Double
.
parseDouble
(
cpu
);
}
catch
(
InterruptedException
e
)
{
System
.
err
.
println
(
e
);
}
finally
{
try
{
proc
.
destroy
();
}
catch
(
Exception
e2
)
{
}
}
}
catch
(
Exception
StringIndexOutOfBoundsException
)
{
System
.
out
.
print
(
"请检查设备是否连接"
);
}
return
Cpu
;
}
2,直接 adb shell cat进去proc/cpuinfo/下面:
public
String
[]
getCpuInfo
(
)
{
String
str1
=
"/proc/cpuinfo"
;
String
str2
=
""
;
String
[]
cpuInfo
={
""
,
""
};
String
[]
arrayOfString
;
try
{
FileReader
fr
=
new
FileReader
(
str1
);
BufferedReader
localBufferedReader
=
new
BufferedReader
(
fr
,
8192
);
str2
=
localBufferedReader
.
readLine
();
arrayOfString
=
str2
.
split
(
"\\s+"
);
for
(
int
i
=
2
;
i
<
arrayOfString
.
length
;
i
++)
{
cpuInfo
[
0
]
=
cpuInfo
[
0
]
+
arrayOfString
[
i
]
+
" "
;
}
str2
=
localBufferedReader
.
readLine
();
arrayOfString
=
str2
.
split
(
"\\s+"
);
cpuInfo
[
1
]
+=
arrayOfString
[
2
];
localBufferedReader
.
close
();
}
catch
(
IOException
e
)
{
}
return
cpuInfo
;
}
取完你可以这么用》:
配合一些场景去采集数据:
这样可以看到每个步骤消耗的资源情况
然后汇总数据分析(最好多取几次求平均值):
流量篇
Android 2.2之前
对于Android2.2 的流量 版本以前的系统的流量信息都存放在 proc/net/dev(或者 proc/self/net/dev)文件下,读取文件然后对其进行解析就行了。读取某一个应用的流量,则读取proc/uid_stat/uid /tcp_rcv 文件进行解析(注:模拟器下不存在这个目录)。如需查看某个应用的流量信息,可以通过以下命令来实现:
adb
devices
列出所有设备
adb
-
s
设备名称
shell
进入对应的设备
cd
proc
进入设备的属性目录
cd
uid_stat
进入
user
id
状态目录,每个应用程序在安装的时候系统会为每个应用分配一个对应的
uid
ls
列出
uid_stat
目录下所有应用对应的
user
id
目录
cd
uid
进入对应应用的
uid
目录
ls
查看对应
uid
目录下的
tcp_rcv
和
tcp_snd
目录
cat
tcp_rcv
查看该应用接收的数据信息
cat
tcp_snd
查看该应用发送的数据信息
Android 2.2之后
我这里有两种办法:
第一种
通过PID下面的net/dev
先找到应用的PID
adb shell ps
这边拿到PID:21896 然后在去/proc目录下的PID/net/dev面可以看到:
adb shell cat /proc/
"+Pid+"/net/dev
"
这边的wlan0代表wifi 上传下载量标识! 上传下载量单位是字节可以/1024换算成KB
这里可以看到下载的字节数 、数据包 和 发送的字节数 、数据包
小技巧:wlan0这些值如何初始化0 很简单 你打开手机飞行模式再关掉就清0了
第二种
通过proc/net/xt_qtaguid/stats
在说第二种获取流量方法之前先给这边先给大家说下uid
uid的获取可以在对应的PID下面去查看status,里面会查到uid
adb shell cat /proc/<pid>/status
下面这个方法是通过PackageManager去取:
try
{
PackageManager
pm
=
getPackageManager
();
ApplicationInfo
ai
=
pm
.
getApplicationInfo
(
"PackageName"
,
PackageManager
.
GET_ACTIVITIES
);
Log
.
d
(
"!!"
,
"!!"
+
ai
.
uid
);
}
catch
(
NameNotFoundException
e
)
{
e
.
printStackTrace
();
}
拿到UID后呢继续:
adb shell cat /proc/net/xt_qtaguid/stats | grep uid
其中第6和8列为 rx_bytes(接收数据)和tx_bytes(传输数据)包含tcp,udp等所有网络流量传输的统计。
一个uid可能对应多个 进程,所以这有两行流量是累加的就求和就行。
用java去获取打印
我这边是用先获取PID然后调用!你可以把获取PID作为一个变量传到GetFlow里面来!
我这边只获取下载流量,你可以把上传下载的流量都获取出来!
//获取PID
public
static
String
PID
(
String
PackageName
)
throws
IOException
{
String
PID
=
null
;
Runtime
runtime
=
Runtime
.
getRuntime
();
Process
proc
=
runtime
.
exec
(
"adb shell ps |grep "
+
PackageName
);
try
{
if
(
proc
.
waitFor
()
!=
0
)
{
System
.
err
.
println
(
"exit value = "
+
proc
.
exitValue
());
}
BufferedReader
in
=
new
BufferedReader
(
new
InputStreamReader
(
proc
.
getInputStream
()));
StringBuffer
stringBuffer
=
new
StringBuffer
();
String
line
=
null
;
while
((
line
=
in
.
readLine
())
!=
null
)
{
stringBuffer
.
append
(
line
+
" "
);
}
String
str1
=
stringBuffer
.
toString
();
String
str2
=
str1
.
substring
(
str1
.
indexOf
(
" "
+
PackageName
)-
46
,
str1
.
indexOf
(
" "
+
PackageName
));
String
str3
=
str2
.
substring
(
0
,
7
);
str3
=
str3
.
trim
();
PID
=
str3
;
}
catch
(
InterruptedException
e
)
{
System
.
err
.
println
(
e
);
}
finally
{
try
{
proc
.
destroy
();
}
catch
(
Exception
e2
)
{
}
}
return
PID
;
}
//获取下载流量
public
static
double
GetFlow
(
String
PackageName
)
throws
IOException
{
double
FlowSize
=
0
;
String
Pid
=
PID
(
PackageName
);
try
{
Runtime
runtime
=
Runtime
.
getRuntime
();
Process
proc
=
runtime
.
exec
(
"adb shell cat /proc/"
+
Pid
+
"/net/dev"
);
try
{
if
(
proc
.
waitFor
()
!=
0
)
{
System
.
err
.
println
(
"exit value = "
+
proc
.
exitValue
());
}
BufferedReader
in
=
new
BufferedReader
(
new
InputStreamReader
(
proc
.
getInputStream
()));
StringBuffer
stringBuffer
=
new
StringBuffer
();
String
line
=
null
;
while
((
line
=
in
.
readLine
())
!=
null
)
{
stringBuffer
.
append
(
line
+
" "
);
}
String
str1
=
stringBuffer
.
toString
();
String
str2
=
str1
.
substring
(
str1
.
indexOf
(
"wlan0:"
),
str1
.
indexOf
(
"wlan0:"
)+
90
);
String
str4
=
str2
.
substring
(
7
,
16
);
str4
=
str4
.
trim
();
int
Flow
=
Integer
.
parseInt
(
str4
);
FlowSize
=
Flow
/
1024
;
}
catch
(
InterruptedException
e
)
{
System
.
err
.
println
(
e
);
}
finally
{
try
{
proc
.
destroy
();
}
catch
(
Exception
e2
)
{
}
}
}
catch
(
Exception
StringIndexOutOfBoundsException
)
{
}
return
FlowSize
;
}
获取每秒下载流量:
public
static
double
Flow
(
String
PackageName
)
throws
IOException
,
InterruptedException
{
double
Flow1
=
GetFlow
(
PackageName
);
Thread
.
sleep
(
1000
);
double
Flow
=
GetFlow
(
PackageName
)-
Flow1
;
//System.out.println(GetFlow()-Flow1);
return
Flow
;
}
场景设计
拿到流量值后在步骤前 将流量打印,再步骤完成后再打印一遍,再用步骤完成的流量值减去之前的流量值 得到这个步骤所消耗的流量!
场景案例:
拓展
下面的方法都是集成在Android 内部的方法:(仅供参考)
Android的TrafficStats类
前四个读取的/proc/net/dev里面的数据
Android
架构对流量的统计通过一个
TrafficStats
类可以直接获取
获取总接受流量
TrafficStats
.
getTotalRxBytes
()
,
获取总发送流量
TrafficStats
.
getTotalTxBytes
());
获取不包含
WIFI
的手机
GPRS
接收量
TrafficStats
.
getMobileRxBytes
());
获取不包含
Wifi
的手机
GPRS
发送量
TrafficStats
.
getMobileTxBytes
());
统计某一个进程的总接收量
TrafficStats
.
getUidRxBytes
(
Uid
));
统计某一个进程的总发送量
TrafficStats
.
getUidTxBytes
(
Uid
));
package
cn
.
sunzn
.
trafficmanger
;
import
android.app.Activity
;
import
android.net.TrafficStats
;
import
android.os.Bundle
;
import
android.view.Menu
;
public
class
MainActivity
extends
Activity
{
public
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
activity_main
);
/** 获取手机通过 2G/3G 接收的字节流量总数 */
TrafficStats
.
getMobileRxBytes
();
/** 获取手机通过 2G/3G 接收的数据包总数 */
TrafficStats
.
getMobileRxPackets
();
/** 获取手机通过 2G/3G 发出的字节流量总数 */
TrafficStats
.
getMobileTxBytes
();
/** 获取手机通过 2G/3G 发出的数据包总数 */
TrafficStats
.
getMobileTxPackets
();
/** 获取手机通过所有网络方式接收的字节流量总数(包括 wifi) */
TrafficStats
.
getTotalRxBytes
();
/** 获取手机通过所有网络方式接收的数据包总数(包括 wifi) */
TrafficStats
.
getTotalRxPackets
();
/** 获取手机通过所有网络方式发送的字节流量总数(包括 wifi) */
TrafficStats
.
getTotalTxBytes
();
/** 获取手机通过所有网络方式发送的数据包总数(包括 wifi) */
TrafficStats
.
getTotalTxPackets
();
/** 获取手机指定 UID 对应的应程序用通过所有网络方式接收的字节流量总数(包括 wifi) */
TrafficStats
.
getUidRxBytes
(
uid
);
/** 获取手机指定 UID 对应的应用程序通过所有网络方式发送的字节流量总数(包括 wifi) */
TrafficStats
.
getUidTxBytes
(
uid
);
}
public
boolean
onCreateOptionsMenu
(
Menu
menu
)
{
getMenuInflater
().
inflate
(
R
.
menu
.
activity_main
,
menu
);
return
true
;
}
}
启动时间
关于应用的启动时间的测试,分为三类:
1. 首次启动 --应用首次启动所花费的时间
2. 非首次启动 --应用非首次启动所花费的时间
3. 应用界面切换--应用界面内切换所花费的时间
那么如何来做启动时间的测试呢,一般我们分为2类,一类为使用软件来测试,一类为使用硬件来测试,首先我们说说软件测试的方法,可能大部分人都比较通晓使用 android 提供的 DisplayManager 来获取 activity 的启动时间吧,在这里我简单说下如何通过批处理来 DIY
- 通过日志过滤关键字 Displayed 来过滤所有 activity 所打印的,记录日志通过
adb logcat>/address/logcat.txt
然后使用 find “Displayed” /address/logcat.txt>/newaddress/fl.txt
- 通过 activity 名来过滤获取所测应用
find “ActivityName” /newaddress/fl.txt>/newaddress/last.txt
- 通过计算 activity 最后剩余的时间之和即可(这里可以使用 excel 表格自动相加也可以使用算法,我就不详细介绍了)
除了 DisplayManager 的打印时间方法后,还有通过关注 am 的启动时间及 DisplayManager 打印的结束时间,通过两者时间之间想减也能得到应用的启动时间,还有可以通过 PowerManager 来计算打印时间,在应用启动的时候,我们可以关注 ActivityManager-Launch 的变化来计算应用的启动时间,还有可以通过截图统计启动时间,对于自研应用,最效率的莫过于直接在程序中插入打印时间的节点了
说完了软件测试的方法,接下来我们聊聊硬件测试,这里我们可以使用高速相机或者手机,采用录像的方法把应用启动过程给录制下来,然后通过人工数帧或者程序数帧的方式计算启动时间
接下来我们聊聊大家不常关注的测试项-
功耗
功耗测试主要从以下几个方面入手进行测试
- 测试手机安装目标APK前后待机功耗无明显差异
- 常见使用场景中能够正常进入待机,待机电流在正常范围内.
- 长时间连续使用应用无异常耗电现象
功耗测试的方法分为两类,一类为软件测试,一类为硬件测试
我们先说说软件测试,这里我们会聊聊一些DIY的思路,软件测试一般分为2类,
接着说硬件测试,在这里我们一般使用万用表或者功耗仪进行测试,使用功耗仪测试的时候,需要制作假电池来进行的,有些不能拔插电池的手机还需要焊接才能进行功耗测试