Xamarin.Android实时获取Top指标的字符串分配优化问题
优化Xamarin.Android实时Top指标采集的方案
你遇到的字符串分配持续增长和CPU占用过高的问题,本质是因为原方案依赖top进程的输出解析,频繁产生字符串对象,且进程调度带来额外开销。下面给你几个针对性的优化方向,从减少内存分配和降低CPU负载两个维度入手:
一、优化top命令的输出与解析逻辑(轻量改进)
如果暂时不想切换到/proc文件读取,先从现有流程优化:
1. 预编译正则并减少不必要的字符串操作
原代码中每次循环都创建正则匹配、调用Where+ToList,会产生大量临时对象。可以:
- 提前编译正则表达式(仅在方法内创建一次)
- 使用
StringSplitOptions.RemoveEmptyEntries替代Where+ToList,直接获取非空数组
优化后的代码片段:
public IObservable<GlobalMetric> GetCpuLive(CancellationToken? cancellationToken = null) { var pid = Process.MyPid(); // 预编译正则,仅创建一次 var pidRegex = new Regex($"{pid}\\s+.*", RegexOptions.Compiled); var p = Java.Lang.Runtime.GetRuntime().Exec($"top -b -p {pid} -d 1 -o %CPU,%MEM -n 0"); var streamReader = new Java.IO.InputStreamReader(p.InputStream); var token = cancellationToken ?? CancellationToken.None; token.Register(() => { p.Destroy(); streamReader?.Close(); }); return Observable.Create<GlobalMetric>(async (observer, innerToken) => { using (var reader = new Java.IO.BufferedReader(streamReader)) { string line; while (!innerToken.IsCancellationRequested && (line = await reader.ReadLineAsync()) != null) { var match = pidRegex.Match(line); if (match.Success) { // 用StringSplitOptions直接去掉空元素,避免额外枚举和列表分配 var splitted = match.Value.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries); if (splitted.Length >= 10) { // 确保数组长度足够 double.TryParse(splitted[8], out var cpu); double.TryParse(splitted[9], out var memory); observer.OnNext(new GlobalMetric() { Cpu = cpu, Memory = memory }); } } } } await p.WaitForAsync(); }); }
2. 精简top输出内容
修改top命令参数,只输出需要的列,减少每行字符串长度:
- 添加
-b(批处理模式,避免终端控制字符) - 用
-o %CPU,%MEM指定仅输出CPU和内存列 - 用
-n 0表示持续输出,替代原-m 15(不需要限制进程数量,因为已经指定了-p)
新命令:top -b -p {pid} -d 1 -o %CPU,%MEM -n 0
二、切换到/proc文件直接读取(推荐,更低开销)
top命令本质也是读取/proc虚拟文件系统的内容,直接读取这些文件可以避免启动额外进程,大幅降低CPU负载和字符串分配:
1. 读取CPU使用率
通过/proc/[pid]/stat和/proc/stat计算进程CPU占比(需要记录上一次的累计值,计算差值):
private class CpuState { public long ProcessTotal { get; set; } public long SystemTotal { get; set; } } private CpuState _lastCpuState; private double GetProcessCpuUsage(int pid) { // 读取进程累计CPU时间 using (var procStatReader = new StreamReader($"/proc/{pid}/stat")) { var statLine = procStatReader.ReadLine(); if (string.IsNullOrEmpty(statLine)) return 0; var statParts = statLine.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries); if (statParts.Length < 17) return 0; long utime = long.Parse(statParts[13]); // 用户态时间 long stime = long.Parse(statParts[14]); // 内核态时间 long cutime = long.Parse(statParts[15]); // 子进程用户态时间 long cstime = long.Parse(statParts[16]); // 子进程内核态时间 long processTotal = utime + stime + cutime + cstime; // 读取系统总CPU时间 using (var systemStatReader = new StreamReader("/proc/stat")) { var systemLine = systemStatReader.ReadLine(); if (string.IsNullOrEmpty(systemLine)) return 0; var systemParts = systemLine.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries); long systemTotal = 0; for (int i = 1; i < systemParts.Length; i++) { systemTotal += long.Parse(systemParts[i]); } // 首次读取初始化状态 if (_lastCpuState == null) { _lastCpuState = new CpuState { ProcessTotal = processTotal, SystemTotal = systemTotal }; return 0; } // 计算差值占比 var processDelta = processTotal - _lastCpuState.ProcessTotal; var systemDelta = systemTotal - _lastCpuState.SystemTotal; var cpuUsage = systemDelta == 0 ? 0 : (processDelta / (double)systemDelta) * 100; // 更新状态 _lastCpuState.ProcessTotal = processTotal; _lastCpuState.SystemTotal = systemTotal; return cpuUsage; } } }
2. 读取内存使用率
通过/proc/[pid]/status读取VmRSS字段(物理内存占用,单位KB):
private double GetProcessMemoryUsage(int pid) { using (var statusReader = new StreamReader($"/proc/{pid}/status")) { string line; while ((line = statusReader.ReadLine()) != null) { if (line.StartsWith("VmRSS:")) { var parts = line.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 2 && long.TryParse(parts[1], out var rssKb)) { // 转换成MB或者直接返回KB,根据你的需求调整 return rssKb / 1024.0; } break; } } return 0; } }
3. 结合Observable.Interval定时采集
用Observable.Interval替代top的持续输出,定时调用上述方法:
public IObservable<GlobalMetric> GetCpuLive(CancellationToken? cancellationToken = null) { var pid = Process.MyPid(); var token = cancellationToken ?? CancellationToken.None; return Observable.Interval(TimeSpan.FromSeconds(1), TaskPoolScheduler.Default) .Select(_ => { var cpu = GetProcessCpuUsage(pid); var memory = GetProcessMemoryUsage(pid); return new GlobalMetric { Cpu = cpu, Memory = memory }; }) .TakeUntil(_ => token.IsCancellationRequested); }
三、进一步减少内存分配
- 将
GlobalMetric改为struct(如果不需要继承或引用类型特性),避免堆分配 - 复用字符串拆分的数组(比如提前创建足够大的数组,用
Split的重载指定数组大小) - 避免在循环内创建临时对象,比如把正则、拆分选项等提前定义为静态常量
内容的提问来源于stack exchange,提问作者ʃʈɑɲ
相关产品推荐
相关产品推荐

