You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,提问作者ʃʈɑɲ

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 06:49:56