使用 adb 执行 shell 脚本持续监控安卓手机运行性能

前言

之前我所负责的项目使用的是购买的成品安卓设备,所以一直没有什么问题。

不久之前,老板决定不再购买成品设备,而是自己设计制作安卓硬件设备。

但是更换硬件之后,运行同一个 APP 的同一个版本会出现卡顿的现象。

并且开机时间越长该现象越明显,当开机时间达到一定时间后,甚至卡顿到完全没法使用。

而且卡顿时不仅是我们的 APP 卡顿,而是整个系统都在卡顿,这显然是散热有问题。

但是老板可不会听我们的所谓“显然”,凡事都需要拿出证据。

于是就有了这篇文章的内容。

基础知识

监控手机的运行性能使用多种方式都可以实现。

例如,可以使用 Android Studio 的 Profile 工具直接录制。

也可以下载其他第三方 APP 来监控记录。

但是,由于我们这里的背景是需要查看造成系统卡顿的原因,所以可想而知,到后期时整个系统运行有多吃力,此时还挂着第三方 APP 的话,大概率会被强杀进程,或者索性直接卡死(别问我怎么知道的)。

而使用 Profile 的话,问题在于本身这个工具在数据量大时就非常卡,而我们需要做的监控是少则需要录制一天时间的,到时估计录制出来的文件都打不开了(别问我怎么知道的x2)。

所以,我们这里选择直接使用 ADB 运行 shell 指令获取需要的数据,然后保存下来。

因此,在开始之前,我们需要了解一些基础知识,相信 ADB 的使用作为安卓开发的大伙都不会陌生吧。

我们这里就简单介绍几个接下来可能会用到的 ADB 常用知识。

获取当前连接的设备:

adb devices

返回:

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 输出完整的内存信息:

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,例如获取微信的内存占用:

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"

返回如下:

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 占用情况。

返回数据:

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"

返回数据:

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

返回数据:

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

返回数据:

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

返回数据:

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 指令了,是时候来编写一个脚本实时获取需要的数据了:

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 降频,最终导致系统卡顿。

在这铁一般的证据面前,老板终于“放过了”我,转而去找硬件工程师去了。哈哈哈哈。