Android系统性能监控最全面分析与实践(二)

1,763 阅读9分钟

续上一篇《Android系统性能监控最全面分析与实践(一)》,本文主要分析内存、网速、磁盘读写速度,go on!!

4、内存占用获取

4.1 当前内存使用情况

内存使用情况也是老生常谈的问题了,网上有很多资料,在此简单重温一遍。

方案一:cat /proc/meminfo

/proc/meminfo展示了系统内存使用情况,其中有两个关键指标:

  1. MemTotal:代表系统总内存大小,单位是KB
  2. MemFree:代表系统当前可用内存大小,单位是KB 在apk执行cat指令读取该文件可以获取到目标数值,无论是效率还是适用性都很高。

image.png

方案二:dumpsys meminfo

dumpsys meminfo + [pkg]通常用来查看指定进程包名的内存使用情况,如果不加包名,可以得到当前内存总量和可使用内存量,其中:

  • Total RAM:内存总量
  • Free RAM:可使用内存量

image.png

然而dumpsys meminfo耗时达到秒级,耗时较久,效率不高。

方案三:free

使用free命令可以查看系统当前内存使用情况,其中有两个关键数据:

  • total:总内存大小
  • used:已使用的大小

image.png

虽然也满足需求,但本方案和方案一和二,都是通过执行shell指令获取一串数据,因此需要对数据加以解析才能得到目标数据,在方案实现上稍微麻烦一点。

方案四:MemoryInfo 获取

ActivityManager服务是对Activity管理、运行时功能管理和运行时数据结构的封装,进程、应用程序、服务、任务信息等。通过 ActivityManager 获取到的 MemoryInfo 是整个设备的内存信息,主要包括:

  • totalMem:内核可访问的总内存。
  • availMem:系统上的可用内存。
  • lowMemory:如果系统认为自己当前处于低内存状况,则设置为True。
  • threshold:系统认为内存较低并开始杀死后台服务和其他非外部进程的可用阈值。
val memoryInfo = ActivityManager.MemoryInfo()
mActivityManager.getMemoryInfo(memoryInfo)
totalMemory = memoryInfo.totalMem
availMemroy = memoryInfo.availMem

该方案是针对应用层获取系统内存信息的API函数,在应用层获取内存信息,推荐使用该方案。

结论

获取内存使用情况的方法效率实现难度推荐
dumpsys meminfo
cat /proc/meminfo
free
MemoryInfo 获取

4.2 内存使用率TOP5进程获取

前文获取了实时CPU使用率最高的5个进程,那么实时占用内存最高的5个进程也有必要获取。在获取指标之前,我们先来了解几个名词:

  • RSS:(Resident Set Size)实际使用物理内存(包含共享库占用的内存)。RSS可能具有误导性,因为它包括了该进程与其他进程共享的实际物理内存使用量,例如,对于共享库,其往往只加载到内存中一次,而不管有多少进程使用它。因此RSS不能准确地表示单个进程的内存使用情况。
  • PSS:(Proportional Set Size)实际使用的物理内存(比例分配共享库占用的内存),比如2个进程使用10KB的共享库,那么每个进程算5KB内存占用到PSS中。这种方式表示进程的内存使用情况较准确,但当只有一个进程使用共享库时,其情况和RSS一模一样。
  • VSS:(Virtual Set Size) 虚拟耗用内存(包含共享库占用的内存).表示一个进程可访问的全部内存地址空间的大小。这个大小包括了进程已经申请但尚未使用的内存空间。在实际中很少用这种方式来表示进程占用内存的情况,用它来表示单个进程的内存使用情况是不准确的。
  • USS: (Unique Set Size) 进程独自占用的物理内存(不包含共享库占用的内存),表示一个进程本身占用的内存空间大小,不包含其它任何成分,这是表示进程内存大小的最好方式!

方案一:top指令

又是强大的top指令,可以查到每个进程占用的内存率,我们在man手册查看top指令,可以这样的描述:“ %MEM -- Memory Usage (RES):A task's currently used share of available physical memory.”,符合我们的需求。如果是一般情况下,推荐使用top指令获取进程相关数据,但用于性能监控程序,top指令还是太耗时了。

方案二:procrank指令

procrank是一个统计内存使用的神器,可以获取上述VSS,PSS,PSS和USS的详细参数,是一个分析内存使用非常有用的工具。然而计算出如此详细的参数自然需要较长的时间,因此在这里并不是最优解。

 PID       Vss      Rss      Pss      Uss     Swap    PSwap    USwap    ZSwap  cmdline
 1894  8866976K  262320K  119897K  109052K    3336K      58K       0K      11K  com.launcher
 3479  7979016K  251528K  118276K  109140K   65728K    7437K       0K    1474K  com.guide
  945  8659104K  278584K  117064K  101728K   41800K    6061K      12K    1201K  system_server
 2482  8185712K  255492K  107944K   93516K    3536K      62K       0K      12K  com.speech
 2387  5820232K  159392K   64362K   60268K    4416K      79K       0K      15K  com.sitting
 ...

方案三:dumpsys meminfo

dumpsys meminfo可以按PSS降序排序获取当前系统所有进程的内存占用情况,也包含进程名和对应pid,很适合需求,然而dumpsys meminfo耗时达到秒级,还是太耗时了。目前为止的三种方案耗时都是秒级,而且都是通过shell指令获取,其数据量较大,分析时较为麻烦。

Applications Memory Usage (in Kilobytes):
Uptime: 8646482 Realtime: 8646482

Total PSS by process:
    263,269K: com.guide (pid 3479)
    193,979K: system (pid 945)
    166,109K: com.launcher (pid 1894 / activities)
    145,046K: com.speech (pid 2482)
    105,911K: surfaceflinger (pid 489)
     92,802K: com.settings (pid 2349 / activities)
    ...

方案四:ps指令

ps指令相信很多人对它又熟悉又陌生,我们经常用ps查看进程信息,不出所料ps也可以查看进程占用内存的情况。在man手册查询ps指令,有如下描述:

“%MEM :ratio of the process's resident set size  to the physical memory on the machine, expressed as a percentage.”。

也就是通过计算得到每个进程的RSS对应的百分比,我们再根据%MEM指标对所有进程进行降序排序,就可以获得内存使用率TOP5进程,指令如下:

ps -eo CMD,%MEM --sort=-%MEM 

打印内容如下:

CMD              %MEM
com.speech        4.6
com.launcher      4.2
system_server     3.8
main              2.7
...

通过测试,执行一遍ps指令耗时是毫秒级别,远优于上述方案1-3,同时对数据的解析也相对简单,虽然不是以USS排序,综合考虑推荐使用ps指令来获取内存使用率TOP5进程。

结论

内存使用率TOP5进程获取的方法效率实现难度推荐
top指令
procrank指令
dumpsys meminfo
ps指令取

5、网速获取

Android 在应用层提供了流量统计的工具类android.net.TrafficStats,这个统计包括字节数的发送和接收,网络包的发送和接等接口,为了获得接收和发送的字节量可以直接调用以下接口:

  • getTotalRxBytes():获取总的接收字节数
  • getTotalTxBytes():获取总的发送字节数 通过算计较短的时间间隔内接收/发送的字节数增量,也就可以获取到接收/发送的速度啦。方法比较通用,在此提供一个demo供参考。
val rcvTrafficPre = TrafficStats.getTotalRxBytes()
val trsTrafficPre = TrafficStats.getTotalTxBytes()
Timer().schedule(object : TimerTask() {
    @RequiresApi(api = Build.VERSION_CODES.N)
    override fun run() {
        val rcvTrafficCur = TrafficStats.getTotalRxBytes()
        val trsTrafficCur = TrafficStats.getTotalTxBytes()
        val trafficSpeedUp = trsTrafficCur - trsTrafficPre
        val trafficSpeedDown = rcvTrafficCur - rcvTrafficPre

        val msg = mHandler.obtainMessage()
        msg.what = HANDLER_MSG_WRITE_NET_SPEED
        trafficSpeedStr = "$trafficSpeedUp,$trafficSpeedDown"
        msg.obj = trafficSpeedStr
        mHandler.sendMessage(msg)
    }
}, 1000)

6、磁盘读写速度获取

我们都知道,目前市面上的安卓设备,长期所使用后系统流畅度会越来越卡顿,这是因为长期使用会产生的磁盘碎片,影响了磁盘的读写性能。因此监控一台设备的磁盘读写速度是有所必要的。

关于磁盘读写速度,目前通过读取/proc/vmstat文件,获取到以下两个指标:

  1. pgpgin:从启动到现在,从硬盘读取到物理内存的内存页数
  2. pgpgout:从启动到现在,从物理内存写入硬盘的内存页数 计算方法: 获取10秒内pgpgin的增量,把pgpgin的增量再除以10得到每秒的平均增量,即为平均每秒把数据从硬盘读到物理内存的数据量。pgpgout同理,可以得出平均每秒把数据从物理内存写到硬盘的数据量。 PS:执行getconf PAGESIZE 结果为4096, 即一页=4096字节=4KB. 以下提供获取pgpgin和pgpgout的demo:
val diskInfoResult: ShellCommandUtils.CommandResult =
    ShellCommandUtils.executeCommand("cat /proc/vmstat |grep pgpg")
if (diskInfoResult.result == 0) {
    val line: String = diskInfoResult.successMsg
    val value = line.split("\n")
    val pgpgin = value[0].split(" ")
    val pgpgout = value[1].split(" ")
    Log.d(TAG, "pgpgin = $pgpgin")
    Log.d(TAG, "pgpgout = $pgpgout")
    return DiskStatInfo(pgpgin = pgpgin[1].toLong(), pgpgout = pgpgout[1].toLong())
} 

7、总结与展望

本文对CPU使用率、CPU使用率TOP5的进程、内存、内存占用TOP5进程、网络速度、磁盘速度这6个常见的系统性能指标的获取方式进行分析和优化,最终开发出一套Android系统通用性能监控工具。可灵活配置各项指标的获取和上报时间间隔,将收集到的数据保存在本地,等达到设定的上报时间将所有数据压缩后上报到后台。最终通过这些性能监控数据,找到自身性能短板,针对性的优化用户体验。

为更加全面地监控Android系统性能,下一步可考虑增加以下其他Android系统性能监控指标:

  • CPU和内存的交互速度
  • 系统温度
  • 耗电量
  • fps(用户可见的每秒显示帧数)
  • 开机速度
  • GPU负载
  • 磁盘存储情况