前言
之前我所负责的项目使用的是购买的成品安卓设备,所以一直没有什么问题。
不久之前,老板决定不再购买成品设备,而是自己设计制作安卓硬件设备。
但是更换硬件之后,运行同一个 APP 的同一个版本会出现卡顿的现象。
并且开机时间越长该现象越明显,当开机时间达到一定时间后,甚至卡顿到完全没法使用。
而且卡顿时不仅是我们的 APP 卡顿,而是整个系统都在卡顿,这显然是散热有问题。
但是老板可不会听我们的所谓"显然",凡事都需要拿出证据。
于是就有了这篇文章的内容。
基础知识
监控手机的运行性能使用多种方式都可以实现。
例如,可以使用 Android Studio 的 Profile 工具直接录制。
也可以下载其他第三方 APP 来监控记录。
但是,由于我们这里的背景是需要查看造成系统卡顿的原因,所以可想而知,到后期时整个系统运行有多吃力,此时还挂着第三方 APP 的话,大概率会被强杀进程,或者索性直接卡死(别问我怎么知道的)。
而使用 Profile 的话,问题在于本身这个工具在数据量大时就非常卡,而我们需要做的监控是少则需要录制一天时间的,到时估计录制出来的文件都打不开了(别问我怎么知道的x2)。
所以,我们这里选择直接使用 ADB 运行 shell 指令获取需要的数据,然后保存下来。
因此,在开始之前,我们需要了解一些基础知识,相信 ADB 的使用作为安卓开发的大伙都不会陌生吧。
我们这里就简单介绍几个接下来可能会用到的 ADB 常用知识。
获取当前连接的设备:
adb devices
返回:
text
List of devices attached
78cb57bd device
其中前面的 "78cb57bd" 为 transport id ,在我们同时连接多个设备时可以使用 -t transport id 指定发送指令的设备。
在设备上执行 shell 指令
adb shell command
其中的 command 即为需要执行的 shell 指令,例如: 在设备上执行 ls
指令:
adb shell ls
但是我比较懒,不想每次都打这么长的指令,那我们就可以通过:
adb sehll
进入到设备的终端 shell 中,此时执行 shell 指令就不需要加上 adb shell
前缀了。
注意:接下来的指令其实都不是 adb 指令了,而是 shell 指令。
获取当前设备已安装的应用包名
pm list packages
获取当前运行的进程列表
ps -e -o PID,NAME
这个主要是用来获取应用的 pid 。
其中 ps 表示获取当前运行的进程,-e
表示输出所有进程,-o
表示指定输出内容,这里我们指定只输出 pid 和名称(即一般应用的包名)。
需要的指令
获取内存信息
可以使用指令 dumpsys meminfo
输出完整的内存信息:
text
cas:/ $ dumpsys meminfo
Applications Memory Usage (in Kilobytes):
Uptime: 5209932538 Realtime: 7380312852
Total RSS by process:
643,928K: system (pid 1633)
343,276K: com.android.systemui (pid 3552)
325,780K: com.tencent.mobileqq (pid 1402 / activities)
298,708K: com.tencent.mm (pid 4045 / activities)
269,648K: com.douban.frodo (pid 6511)
245,488K: com.android.phone (pid 3548)
242,148K: com.miui.home (pid 23433 / activities)
231,992K: com.miui.securitycenter.remote (pid 32725)
221,004K: tv.danmaku.bili (pid 5776)
199,776K: com.sohu.inputmethod.sogou (pid 15661)
168,496K: com.tencent.wework (pid 26035 / activities)
141,364K: com.miui.aod (pid 31856)
134,662K: surfaceflinger (pid 1253)
130,180K: com.mi.health:device (pid 5775)
130,156K: com.tencent.mm:push (pid 22102)
121,716K: com.google.android.gms.persistent (pid 13831)
121,482K: com.equationl.starryskywallpaper (pid 26623)
114,552K: com.android.settings (pid 4751)
109,164K: com.douban.frodo:pushservice (pid 6836)
104,212K: com.tencent.mobileqq:MSF (pid 3095)
104,160K: com.google.android.gms (pid 13826)
103,244K: tv.danmaku.bili:download (pid 26771)
102,764K: tv.danmaku.bili:pushservice (pid 26747)
96,304K: com.android.vending (pid 32161)
94,752K: com.miui.personalassistant (pid 16045)
94,608K: com.tencent.wework:wxa_container0 (pid 26374)
93,236K: com.xiaomi.market (pid 16530)
91,472K: com.android.bluetooth (pid 25257)
90,828K: com.xingin.xhs:longlink (pid 2909)
89,672K: com.android.calendar (pid 22772 / activities)
88,108K: com.miui.voiceassist (pid 11476)
85,172K: system:ui (pid 26808)
82,280K: com.miui.cloudservice (pid 6997)
80,744K: com.miui.phrase (pid 6978)
79,200K: com.miui.powerkeeper (pid 8331)
78,188K: com.miui.miwallpaper (pid 23645)
77,948K: com.miui.analytics (pid 8904)
77,816K: com.google.android.gms.unstable (pid 11157)
75,996K: com.miui.touchassistant:float (pid 17509)
73,892K: com.xiaomi.misettings (pid 5333)
73,524K: tv.danmaku.bili:ijkservice (pid 29513)
72,384K: com.miui.systemAdSolution (pid 10661)
72,016K: com.android.mms (pid 27473)
71,964K: com.xiaomi.aiasst.service (pid 28497)
69,500K: com.google.android.wearable.app.cn:background (pid 21870)
69,172K: com.xiaomi.metoknlp (pid 31268)
67,188K: com.google.android.webview:webview_service (pid 6603)
67,048K: com.xiaomi.xmsf (pid 30709)
66,048K: com.miui.yellowpage (pid 3714)
65,820K: com.lbe.security.miui (pid 23573)
65,684K: com.miui.misound (pid 20012)
65,512K: com.miui.mishare.connectivity (pid 3230)
64,484K: com.google.android.gms.persistent (pid 9804)
64,480K: com.xiaomi.bluetooth (pid 14657)
63,724K: com.miui.screenrecorder (pid 3447)
63,148K: com.xiaomi.account (pid 16742)
62,560K: com.xiaomi.gnss.polaris:remote (pid 3996)
62,360K: com.miui.daemon (pid 5664)
//.....................
但是输出的内容太多,反而不好使,所以我们需要稍微过滤一下需要的内容。
例如获取某个应用的内存信息可以使用 dumpsys meminfo pkg | pid
,其中的 pkg | pid
可以使用应用包名也可以使用 pid,例如获取微信的内存占用:
text
cas:/ $ dumpsys meminfo com.tencent.mm
Applications Memory Usage (in Kilobytes):
Uptime: 5210015496 Realtime: 7380395810
** MEMINFO in pid 4045 [com.tencent.mm] **
Pss Private Private SwapPss Rss Heap Heap Heap
Total Dirty Clean Dirty Total Size Alloc Free
------ ------ ------ ------ ------ ------ ------ ------
Native Heap 28441 28396 0 148021 29284 202460 184218 18241
Dalvik Heap 66381 66340 4 5605 67456 75619 67427 8192
Dalvik Other 11010 10296 4 1160 12436
Stack 3068 3068 0 8488 3076
Ashmem 65 48 0 0 848
Gfx dev 10672 10672 0 0 10672
Other dev 18 0 16 0 372
.so mmap 9843 2440 5972 12073 24296
.jar mmap 1006 0 8 0 35676
.apk mmap 17376 0 11736 0 23880
.ttf mmap 1615 0 0 0 19096
.dex mmap 6768 32 6632 64 7488
.oat mmap 225 0 0 0 12964
.art mmap 20532 20020 168 6204 30516
Other mmap 596 24 524 0 1072
GL mtrack 384 384 0 0 384
Unknown 6007 6004 0 40216 6272
TOTAL 405838 147724 25064 221831 285788 278079 251645 26433
App Summary
Pss(KB) Rss(KB)
------ ------
Java Heap: 86528 97972
Native Heap: 28396 29284
Code: 26864 124880
Stack: 3068 3076
Graphics: 11056 11056
Private Other: 16876
System: 233050
Unknown: 19520
TOTAL PSS: 405838 TOTAL RSS: 285788 TOTAL SWAP PSS: 221831
Objects
Views: 2955 ViewRootImpl: 1
AppContexts: 12 Activities: 1
Assets: 33 AssetManagers: 0
Local Binders: 298 Proxy Binders: 621
Parcel memory: 410 Parcel count: 326
Death Recipients: 496 OpenSSL Sockets: 0
WebViews: 0
SQL
MEMORY_USED: 642
PAGECACHE_OVERFLOW: 226 MALLOC_SIZE: 46
DATABASES
pgsz dbsz Lookaside(b) cache Dbname
4 96 55 66/85/25 /data/user/0/com.tencent.mm/no_backup/androidx.work.workdb
4 8 0/0/0 (attached) temp
4 96 40 3/16/4 /data/user/0/com.tencent.mm/no_backup/androidx.work.workdb (2)
4 92 46 3703/27/4 /data/user/0/com.tencent.mm/databases/Scheduler.db
4 108 124 27/31/16 /data/user/0/com.tencent.mm/databases/google_app_measurement.db
cas:/ $
可以看到,输出内容少了很多,但是还是不够精简 ,我们需要的只是这个应用占用的总内存而已。
而在上述输出中,我们可以看到几种不同的内存占用,他们的含义分别如下:
- VSS - Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)
- RSS - Resident Set Size 实际使用物理内存(包含共享库占用的内存)
- PSS - Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)
- USS - Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)
我们一般需要查看的是 PSS 值,因此我们可以将上述指令加上过滤操作:
dumpsys meminfo com.tencent.mm | grep "TOTAL PSS"
返回如下:
text
cas:/ $ dumpsys meminfo com.tencent.mm | grep "TOTAL PSS"
TOTAL PSS: 404910 TOTAL RSS: 287560 TOTAL SWAP PSS: 219203
cas:/ $
其中在指令后面添加的 grep
表示过滤内容,后面跟着的内容即表示需要查找的内容,支持正则,也支持字符串匹配,在这里的意思是仅过滤出包含 "TOTAL PSS" 的这一行输出。
而两个指令之间的 |
符号是管道连接符,表示连接两个指令。
最后,还可以通过以下两种不同的方式获取到当前手机的总内存占用:
dumpsys meminfo | grep -n "RAM"
或 procrank | grep "RAM"
。
从指令不难看出,它们分别是从不同的指令中过滤出我们需要的总内存信息,至于它们原本的输出是什么,感兴趣的可以自己把参数和过滤去掉执行一下看看。
后面的指令我们就不一一解释这个过滤操作了。
最后的最后,附加几个查看当前系统的 APP 可用最大内存配置的方式:
- 单个应用的最大内存限制
getprop | grep heapgrowthlimit
- 应用启动后分配的初始内存
getprop|grep dalvik.vm.heapstartsize
- 单个java虚拟机的最大内存限制
getprop|grep dalvik.vm.heapsize
CPU 当前占用信息
获取 CPU 的占用信息依旧是有两种方式。
方式一:
top -n 5 | grep "com.tencent.mm"
其中 -n 5
表示指定执行 5 次,如果不指定次数则会实时刷新当前的 CPU 占用情况。
返回数据:
text
cas:/ $ top -n 5 | grep "com.tencent.mm"
7491 shell 20 0 2.0G 3.6M 2.9M S 0.0 0.0 0:00.01 grep com.tencent.mm
4045 u0_a276 20 0 129G 91M 2.1M S 0.6 1.1 10:38.22 com.tencent.mm
需要注意的是,第一行数据不是微信的占用数据啊,这个是我们刚才执行的这个 shell 的占用信息。
第二行才是微信的信息,其中的 "S 0.6" 即微信的占用率,这里表示为 0.6% 。
第二种方式:
dumpsys cpuinfo | grep "com.tencent.mm"
返回数据:
text
cas:/ $ dumpsys cpuinfo | grep "com.tencent.mm"
0.6% 4045/com.tencent.mm: 0.4% user + 0.1% kernel / faults: 122 minor 18 major
0.2% 22102/com.tencent.mm:push: 0.1% user + 0% kernel / faults: 47 minor 1 major
cas:/ $
第一列数据即微信的总占用率,后面则是它的详细数据。
CPU 温度
我们可以通过读取 /sys/class/thermal/thermal_zone*/temp
文件获取到各个传感器的温度值,即:
cat /sys/class/thermal/thermal_zone*/temp
返回数据:
text
cas:/ $ cat /sys/class/thermal/thermal_zone*/temp
35400
36200
35800
35000
36200
35800
35800
36200
35200
35200
35600
36000
35800
35600
36000
35600
34500
36400
36400
37000
36600
36000
36600
36200
36200
36200
37000
35400
35400
35400
35800
35000
36200
36200
36600
35800
35600
35600
36000
35600
35600
36000
34900
33000
35000
37400
// ...............
可以看到,返回了很多数据,那么哪个才是我们需要的数据呢?
我们可以通过读取 /sys/class/thermal/thermal_zone*/type
文件获取到每一行对应的是什么的传感器,即:
cat /sys/class/thermal/thermal_zone*/type
返回数据:
text
1|cas:/ $ cat /sys/class/thermal/thermal_zone*/type
aoss0-usr
cpu-0-0-usr
cpu-1-3-usr
cpu-1-4-usr
cpu-1-5-usr
cpu-1-6-usr
cpu-1-7-usr
gpuss-0-usr
aoss-1-usr
cwlan-usr
video-usr
ddr-usr
cpu-0-1-usr
q6-hvx-usr
camera-usr
cmpss-usr
npu-usr
gpuss-1-usr
gpuss-max-step
apc-0-max-step
apc-1-max-step
pop-mem-step
cpu-0-0-step
cpu-0-2-usr
cpu-0-1-step
// ........
如果不想要这么多数据,只是想要某个位置的数据,那么也不需要用过滤,直接把上面文件路径中的 *
换为相应的序号(即行号)即可,例如获取 cpu-0-0-usr 的温度:
cat /sys/class/thermal/thermal_zone1/temp
电池温度
如果你觉得从上面的一堆数据中找出电池的温度数据太麻烦了,那么你可以使用另外一个单独的指令获取到电池的温度数据:
dumpsys battery | grep temperature
CPU 当前频率
获取 CPU 的当前频率依然是有两种方式。
方式一:
cat /sys/devices/system/cpu/cpuX/cpufreq/scaling_cur_freq
其中的 cpuX 为 cpu 的核心序号,例如想获取序号为 0 的核心的运行频率可以写成:
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq
返回数据:
text
cas:/ $ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq
1804800
需要注意的是这种方式返回的不是硬件当前实际的运行频率,而是调度程序发送给硬件,让硬件应该以这个频率来运行,一般来说,此时硬件确实是以这个频率运行的,但是有时候硬件可能会有点"任性",就是不按这个频率运行,此时我们就需要另外一个指令:
cat /sys/devices/system/cpu/cpuX/cpufreq/cpuinfo_cur_freq
其中的 cpuX 依旧为 cpu 的核心序号。
现在返回的就是当前硬件实际的运行频率了,但是很不幸,在高版本安卓上已经不允许读取这个文件了,想要读取的话必须拥有 root 权限。
编写监控脚本
现在,我们已经知道了获取所需数据的 shell 指令了,是时候来编写一个脚本实时获取需要的数据了:
shell
echo "watcher running..."
log_path="/sdcard/watcher.log"
echo "start watch state at $(date)\n" >> $log_path
while true
do
sleep 1
gpu_temp=$(cat /sys/class/thermal/thermal_zone1/temp)
msg="$(date), GPU Temp: $gpu_temp\n"
echo "$msg"
echo "$msg" >> $log_path
cpu_temp=$(cat /sys/class/thermal/thermal_zone0/temp)
cpu_0=$(cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_cur_freq)
cpu_1=$(cat /sys/devices/system/cpu/cpu1/cpufreq/cpuinfo_cur_freq)
cpu_2=$(cat /sys/devices/system/cpu/cpu2/cpufreq/cpuinfo_cur_freq)
cpu_3=$(cat /sys/devices/system/cpu/cpu3/cpufreq/cpuinfo_cur_freq)
msg="CPU Temp: $cpu_temp, CPU0: $cpu_0, CPU1: $cpu_1, CPU2: $cpu_2, CPU3: $cpu_3, \n"
echo "$msg"
echo "$msg" >> $log_path
cpu_usage_info=$(top -n 1 | grep "com.xxxx.yyyy")
msg="CPU Usage: $cpu_usage_info"
echo "$msg"
echo "$msg" >> $log_path
# 这个速度非常慢,如果对记录速度有要求的话,最好不要加这个
mem_info_total=$(procrank | grep "RAM")
mem_info_current=$(dumpsys meminfo com.xxxx.yyyy | grep -n "TOTAL PSS")
msg="Mem Info, Total: $mem_info_total, Current: $mem_info_current\n"
echo "$msg"
echo "$msg" >> $log_path
done
上面这个脚本非常的简单,就是开启一个 while 循环,然后每隔 1s 读取一次各项数据,并将其写入 /sdcard/watcher.log
文件中。
我们将以上脚本内容保存为任意文件,例如: watcher.sh
,然后放到设备的任意位置,例如 /sdcard/watcher.sh
。
在 shell 中执行 sh /sdcard/watcher.sh
即可,运行后我们只需要正常使用我们的设备,这个脚本会在后台默默的帮我们把数据都记录下来的。
PS:上面这个脚本因为只是自己使用,所以各项数据筛选做的很粗糙,筛了一堆没用的数据进来,各位大佬使用的时候最好根据自己需求重新筛选一下数据。
结尾
在开着这个脚本跑了一天之后,最终发现,随着设备的开机,温度一直在上升,上升到某个值后似乎撞到了温度墙,就开始在这个温度范围内波动,同时 CPU 的各个核心运行频率开始大幅的降低。除此之外,其他各项数值均未见异常。
换句话说,这证明了造成卡顿的原因确实是因为散热不行导致 CPU 降频,最终导致系统卡顿。
在这铁一般的证据面前,老板终于"放过了"我,转而去找硬件工程师去了。哈哈哈哈。