欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

2章 性能平台GodEye源码分析-数据模块

程序员文章站 2022-07-02 23:21:27
2. 数据模块2.1 CpuCpu的获取有两种方式通过读取/proc/stat文件获取,但高版本的Android已经不给权限读取通过adb命令top -n 1,获取当前所有进程列表的cpu使用情况,再过滤当前进程源码分析1、启动Cpu的监控Cpu的监控通过启动定时器,按xml配置的时间进行采集,通过CpuUsage.getCpuInfo()获取Cpu信息public class CpuEngine implements Engine { private Producer

2. 数据模块

2.1 Cpu

2章 性能平台GodEye源码分析-数据模块

Cpu的获取有两种方式

  1. 通过读取/proc/stat文件获取,但高版本的Android已经不给权限读取
  2. 通过adb命令top -n 1,获取当前所有进程列表的cpu使用情况,再过滤当前进程

源码分析

1、启动Cpu的监控

Cpu的监控通过启动定时器,按xml配置的时间进行采集,通过CpuUsage.getCpuInfo()获取Cpu信息

public class CpuEngine implements Engine {
    private Producer<CpuInfo> mProducer;
    private long mIntervalMillis;
    private CompositeDisposable mCompositeDisposable;

    public CpuEngine(Producer<CpuInfo> producer, long intervalMillis) {
        mProducer = producer;
        mIntervalMillis = intervalMillis;
        mCompositeDisposable = new CompositeDisposable();
    }

    @Override
    public void work() {
        mCompositeDisposable.add(Observable.interval(mIntervalMillis, TimeUnit.MILLISECONDS)
                .subscribeOn(ThreadUtil.computationScheduler())
                .observeOn(ThreadUtil.computationScheduler())
                .map(new Function<Long, CpuInfo>() {
                    @Override
                    public CpuInfo apply(Long aLong) throws Exception {
                        ThreadUtil.ensureWorkThread("CpuEngine apply");
                        return CpuUsage.getCpuInfo();
                    }
                })
                .filter(new Predicate<CpuInfo>() {
                            @Override
                            public boolean test(CpuInfo cpuInfo) throws Exception {
                                return CpuInfo.INVALID != cpuInfo;
                            }
                        }
                )
                .subscribe(new Consumer<CpuInfo>() {
                    @Override
                    public void accept(CpuInfo food) throws Exception {
                        ThreadUtil.ensureWorkThread("CpuEngine accept");
                        mProducer.produce(food);
                    }
                }));
    }

    @Override
    public void shutdown() {
        mCompositeDisposable.dispose();
    }
}

2、Cpu文件采集

初始化阶段获取是否拥有/proc/stat/proc/ + pid + /stat文件的读取权限

public class CpuUsage {
    static {
        isReadFromFile = cpuFileUsability();
    }
    
    private static boolean cpuFileUsability() {
        File stat = new File("/proc/stat");
        if (!stat.exists() || !stat.canRead()) {
            return false;
        }
        int pid = android.os.Process.myPid();
        File statPid = new File("/proc/" + pid + "/stat");
        if (!statPid.exists() || !statPid.canRead()) {
            return false;
        }
        return true;
    }
}

如果有权限则通过文件读取

  • getCpuRateOfDevice()对应获取设备的Cpu占用率,体现在/proc/stat文件
  • getCpuRateOfApp()对应获取App的Cpu占用率,体现在/proc/ + pid + /stat文件
public static CpuInfo getCpuInfo() {
    if (isReadFromFile) {
        if (sLastCpuSnapshot == null) {
            //1、获取首次Cpu快照
            sLastCpuSnapshot = parse(getCpuRateOfDevice(), getCpuRateOfApp());
            return CpuInfo.INVALID;
        } else {
            //2、第二次开始正常采集
            CpuSnapshot current = parse(getCpuRateOfDevice(), getCpuRateOfApp());
            float totalTime = (current.total - sLastCpuSnapshot.total) * 1.0f;
            if (totalTime <= 0) {
                L.e("totalTime must greater than 0");
                return CpuInfo.INVALID;
            }
            //3、通过差值计算出Cpu占用率情况
            long idleTime = current.idle - sLastCpuSnapshot.idle;
            double totalRatio = (totalTime - idleTime) / totalTime;
            double appRatio = (current.app - sLastCpuSnapshot.app) / totalTime;
            double userRatio = (current.user - sLastCpuSnapshot.user) / totalTime;
            double systemRatio = (current.system - sLastCpuSnapshot.system) / totalTime;
            double ioWaitRatio = (current.ioWait - sLastCpuSnapshot.ioWait) / totalTime;
            return new CpuInfo(filterCpuRatio(totalRatio), filterCpuRatio(appRatio), filterCpuRatio(userRatio), filterCpuRatio(systemRatio), filterCpuRatio(ioWaitRatio));
        }
    } else {
        return getCpuInfoFromShell();
    }
}

private static String getCpuRateOfDevice() {
    BufferedReader cpuReader = null;
    try {
        cpuReader = new BufferedReader(new InputStreamReader(
                new FileInputStream("/proc/stat")), BUFFER_SIZE);
        String cpuRate = cpuReader.readLine();
        if (cpuRate == null) {
            return "";
        }
        return cpuRate.trim();
    } catch (Throwable e) {
        return "";
    } finally {
        IoUtil.closeSilently(cpuReader);
    }
}

private static String getCpuRateOfApp() {
    BufferedReader pidReader = null;
    try {
        int pid = android.os.Process.myPid();
        pidReader = new BufferedReader(new InputStreamReader(
                new FileInputStream("/proc/" + pid + "/stat")), BUFFER_SIZE);
        String pidCpuRate = pidReader.readLine();
        if (pidCpuRate == null) {
            return "";
        }
        return pidCpuRate;
    } catch (Throwable throwable) {
        return "";
    } finally {
        IoUtil.closeSilently(pidReader);
    }
}

从proc文件中可以获取Cpu的时间片,包含多个进程Cpu使用情况。你会看到代码先获取首次Cpu的快照,通过采集两次时间片数据,获取进程CPU占用率,CPU占用率 = (Cpu使用时间2-Cpu使用时间T1)/(系统总时间2-系统总时间1)的时间片比值,相当于相对时间内,Cpu使用时间占据总时间的占比就是CPU占用率

/proc/stat文件

$ adb shell cat /proc/stat

//CPU指标:user,nice, system, idle, iowait, irq, softirq
cpu  4412950 601238 1778465 7121326 13345 411594 242073 0 0 0
cpu0 1353604 129916 544580 4805215 12287 140100 103310 0 0 0
cpu1 1792083 131733 628762 207535 547 129677 101769 0 0 0
cpu2 734610 172925 338288 236550 278 74416 21146 0 0 0
cpu3 480023 134831 237948 239156 197 61648 11430 0 0 0
cpu4 18583 14926 10303 406774 14 815 352 0 0 0
cpu5 16366 12440 8877 408038 8 4257 252 0 0 0
cpu6 9708 2679 5428 408075 5 407 161 0 0 0
cpu7 7973 1788 4279 409983 9 274 3653 0 0 0
intr ...
ctxt 239514543
btime 1605087699
processes 58405
procs_running 1
procs_blocked 0
softirq 42587145 1026 11042966 22389 2020257 1526512 1026 8516904 12031928 0 7424137

从文件表示的字段意思,取cpu第一排的第2-8的数据作为cpu的基准

  • user(4412950):用户态时间
  • nice(601238):用户态时间(低优先级,nice>0)
  • system(1778465):处于核心态的运行时间
  • idle(7121326):除IO等待时间以外的其它等待时间
  • iowait(13345):I/O等待时间
  • irq(411594):硬中断时间
  • softirq(242073):软中断时间

/proc/ + pid + /stat文件

$ adb shell cat /proc/21749/stat

21749 (com.tencent.mm) S 979 979 0 0 -1 1077952832 55150 0 25 0 549 156 0 0 20 0 65 0 6468487 4870205440 43654 184467440
73709551615 1 1 0 0 0 0 4608 4097 1073775868 0 0 0 17 2 0 0 71 0 0 0 0 0 0 0 0 0 0

从文件表示的字段意思,取第13-16的数据作为pid应用的Cpu使用总时间

  • 13.utime(549):该进程处于用户态的时间
  • 14.stime(156):该进程处于内核态的时间
  • 15.cutime(0):当前进程等待子进程的用户态的时间
  • 16.cstime(0):当前进程等待子进程的内核态的时间

知道各个指标后通过代码即可读取他们各自的指标

@VisibleForTesting
static CpuSnapshot parse(String cpuRate, String pidCpuRate) {
    String[] cpuInfoArray = cpuRate.split("\\s+");
    if (cpuInfoArray.length < 9) {
        throw new IllegalStateException("cpu info array size must great than 9");
    }
    long user = Long.parseLong(cpuInfoArray[2]);
    long nice = Long.parseLong(cpuInfoArray[3]);
    long system = Long.parseLong(cpuInfoArray[4]);
    long idle = Long.parseLong(cpuInfoArray[5]);
    long ioWait = Long.parseLong(cpuInfoArray[6]);
    long total = user + nice + system + idle + ioWait
            + Long.parseLong(cpuInfoArray[7])
            + Long.parseLong(cpuInfoArray[8]);
    String[] pidCpuInfoList = pidCpuRate.split(" ");
    if (pidCpuInfoList.length < 17) {
        throw new IllegalStateException("pid cpu info array size must great than 17");
    }
    long appCpuTime = Long.parseLong(pidCpuInfoList[13])
            + Long.parseLong(pidCpuInfoList[14])
            + Long.parseLong(pidCpuInfoList[15])
            + Long.parseLong(pidCpuInfoList[16]);
    return new CpuSnapshot(user, system, idle, ioWait, total, appCpuTime);
}

3、Cpu命令采集

通过adb命令top -n 1,获取当前所有进程列表的cpu使用情况

OP4679:/ $ top -n 1
[H[J[?25l[H[JTasks: 761 total,   4 running, 756 sleeping,   0 stopped,   1 zombie
  Mem:      7.4G total,      7.2G used,      168M free,      295M buffers
 Swap:      2.5G total,      1.1G used,      1.3G free,      3.6G cached
800%cpu 116%user   0%nice 106%sys 555%idle   0%iow  16%irq   6%sirq   0%host
[7m  PID USER         PR  NI VIRT  RES  SHR S[%CPU] %MEM     TIME+ ARGS
    [0m
[mm24732 u0_a424       6 -14 2.5G 327M 242M R 83.8   4.2  10:43.94 com.hensen.mobile
 1074 system       -2  -8 515M  17M  12M S 35.4   0.2 309:54.36 surfaceflinger
[mm26215 shell        20   0  29M 3.7M 2.7M R 25.8   0.0   0:00.07 top -n 1
  960 system       -3  -4 175M 6.0M 4.5M S 22.5   0.0  97:14.27 android.hardware.graphics.composer@2.3-service
24996 u0_i9125     10 -10 1.6G  91M  72M S  6.4   1.1   0:36.92 com.google.android.webview:sandboxed_process0:org.chrom+

  554 logd         30  10 262M  79M 2.2M S  6.4   1.0  55:27.92 logd
  102 root         20   0    0    0    0 S  6.4   0.0   4:18.83 [system]
25908 shell        20   0 123M 5.2M 4.2M S  3.2   0.0   0:09.17 adbd --root_seclabel=u:r:su:s0
25522 root          0 -20    0    0    0 S  3.2   0.0   0:01.43 [kworker/u17:3]
25399 u0_a424      20   0 2.4G 223M 5.4M S  3.2   2.9   0:27.68 app_d
25354 u0_a424      20   0 1.9G  71M  71M S  3.2   0.9   0:14.26 com.duowan.mobile:Daemon
20704 root         20   0    0    0    0 S  3.2   0.0   0:10.92 [kworker/u16:17]
[mm20703 root         20   0    0    0    0 R  3.2   0.0   0:09.43 [kworker/u16:16]
[mm20584 root         20   0    0    0    0 R  3.2   0.0   0:05.29 [kworker/u16:9]
15688 u0_a424      12  -8 1.9G 104M 104M S  3.2   1.3   3:04.35 com.duowan.mobile:yyPushService
15381 u0_a424      12  -8 1.9G 103M 103M S  3.2   1.3   2:17.64 com.duowan.mobile:sync
 4915 root         RT   0    0    0    0 S  3.2   0.0   9:50.10 [sugov:6]
 3988 system       20   0 7.1G  86M  86M S  3.2   1.1  25:45.51 com.coloros.persist.system
 1807 system       18  -2  10G 260M 260M S  3.2   3.4 272:51.65 system_server
  998 system       20   0  86M 2.9M 2.5M S  3.2   0.0  14:17.59 vendor.qti.hardware.display.allocator@1.0-service
  973 system       20   0 349M 3.5M 2.7M S  3.2   0.0  34:24.58 android.hardware.sensors@1.0-service
  918 root         20   0    0    0    0 S  3.2   0.0   5:03.29 [lpass_IPCRTR]
  246 root         -3   0    0    0    0 S  3.2   0.0  19:05.06 [kgsl_worker_thr]
  239 root         RT   0    0    0    0 S  3.2   0.0  28:49.59 [crtc_commit:102]
26213 shell        20   0  27M 2.7M 2.1M S  0.0   0.0   0:00.02 sh -
26187 root         20   0    0    0    0 S  0.0   0.0   0:00.00 [kworker/3:1]
26166 root          0 -20    0    0    0 S  0.0   0.0   0:00.06 [kworker/2:2H]
26163 root         20   0    0    0    0 S  0.0   0.0   0:00.00 [kworker/2:1]
26159 root         20   0    0    0    0 S  0.0   0.0   0:00.00 [kworker/0:2]
26151 root          0 -20    0    0    0 S  0.0   0.0   0:00.09 [kworker/0:1H]
26123 root         20   0    0    0    0 S  0.0   0.0   0:00.31 [kworker/1:1]
26121 root          0 -20    0    0    0 S  0.0   0.0   0:00.28 [kworker/u17:0]
26071 root         20   0    0    0    0 S  0.0   0.0   0:00.14 [kworker/0:0]
26046 root          0 -20    0    0    0 S  0.0   0.0   0:00.16 [kworker/1:0H]
26035 root          0 -20    0    0    0 S  0.0   0.0   0:00.08 [kworker/3:0H]
25926 shell        20   0  28M 2.7M 2.2M S  0.0   0.0   0:05.48 logcat -v long -v epoch
25878 root         20   0    0    0    0 S  0.0   0.0   0:00.00 [kworker/5:0]
25748 system       20   0 5.1G  70M  52M S  0.0   0.9   0:00.16 com.coloros.securepay
25727 u0_a424      12  -8 1.9G 110M 105M S  0.0   1.4   0:13.01 com.duowan.mobile:RemoteBackgroundProcess
25706 u0_a33       20   0 5.1G  77M  58M S  0.0   1.0   0:00.31 android.process.media
25673 root         20   0    0    0    0 S  0.0   0.0   0:00.00 [kworker/4:1]
25665 root          0 -20    0    0    0 S  0.0   0.0   0:00.19 [kworker/0:2H]
25664 root          0 -20    0    0    0 S  0.0   0.0   0:00.08 [kworker/2:0H]
25654 root         20   0    0    0    0 S  0.0   0.0   0:00.01 [kworker/1:0]
[?25h[0m[1000;1H[K[?25h[?25h[0m[1000;1H[KOP4679:/ $ 00 [kworker/2:0]

代码启动命令是Runtime.getRuntime().exec("top -n 1")

private static CpuInfo getCpuInfoFromShell() {
    java.lang.Process process = null;
    CpuInfo cpuInfo = new CpuInfo();
    try {
        process = Runtime.getRuntime().exec("top -n 1");
        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        String line;
        int cpuIndex = -1;
        Map<String, Float> cpuDevice = null;
        while ((line = reader.readLine()) != null) {
            line = line.trim();
            if (TextUtils.isEmpty(line)) {
                continue;
            }
            //1、当前设备的Cpu使用占用率
            Map<String, Float> tempCpuDevice = parseCpuRateOfDeviceAndTotalByShell(line);
            if (tempCpuDevice != null) {
                // 800%cpu   7%user   3%nice  28%sys 755%idle   0%iow   3%irq   3%sirq   0%host
                cpuDevice = tempCpuDevice;
                Float total = cpuDevice.get("cpu");
                Float user = cpuDevice.get("user");
                Float sys = cpuDevice.get("sys");
                Float idle = cpuDevice.get("idle");
                Float iow = cpuDevice.get("iow");
                if (total != null && total > 0 && iow != null && iow >= 0) {
                    cpuInfo.ioWaitRatio = iow / total;
                }
                if (total != null && total > 0 && sys != null && sys >= 0) {
                    cpuInfo.sysCpuRatio = sys / total;
                }
                if (total != null && total > 0 && idle != null && idle >= 0) {
                    cpuInfo.totalUseRatio = (total - idle) / total;
                }
                if (total != null && total > 0 && user != null && user >= 0) {
                    cpuInfo.userCpuRatio = user / total;
                }
                continue;
            }
            //2、当前App的Cpu使用占用率
            int tempIndex = parseCPUIndex(line);
            if (tempIndex != -1) {
                // PID USER         PR  NI VIRT  RES  SHR S[%CPU] %MEM     TIME+ ARGS
                cpuIndex = tempIndex;
                continue;
            }
            float tempAppRatio = parseCpuRateOfAppByShell(line, cpuIndex, cpuDevice == null ? null : cpuDevice.get("cpu"));
            if (tempAppRatio != -1) {
                // 816 system       -2  -8 2.1G  27M  22M S  6.8   0.3  38:04.22 surfaceflinger
                cpuInfo.appCpuRatio = tempAppRatio;
                return cpuInfo;
            }
        }
    } catch (IOException e) {
        L.e(e);
    } finally {
        if (process != null) {
            process.destroy();
        }
    }
    return cpuInfo;
}
  • 当前设备的Cpu使用占用率

通过adb命令我们查看需要解析的数据,可以发现在中间有段800%cpu 116%user 0%nice 106%sys 555%idle 0%iow 16%irq 6%sirq 0%host。这句就是我们当前设备的Cpu使用占用率,接下来就是正则匹配,将解析出来的数据,放到Map<String, Float> tempCpuDevice上,再从Map中取出对应的值

  • 当前App的Cpu使用占用率

首先要解析出第五句,表示当前Cpu的Index位置,具体在[7m PID USER PR NI VIRT RES SHR S[%CPU] %MEM TIME+ ARGS,通过匹配获取CPU字样的index

@VisibleForTesting
static int parseCPUIndex(String line) {
    if (line.contains("CPU")) {
        String[] titles = line.split("\\s+");
        for (int i = 0; i < titles.length; i++) {
            if (titles[i].contains("CPU")) {
                return i;
            }
        }
    }
    return -1;
}

接着要解析第七句,表示当前Cpu的Index位置的具体Cpu的值,具体在[mm24732 u0_a424 6 -14 2.5G 327M 242M R 83.8 4.2 10:43.94 com.hensen.mobile,通过正则获取Cpu当前的占用值83.8,当然这个值要和Cpu的总值进行计算得出Cpu占比率

@VisibleForTesting
static float parseCpuRateOfAppByShell(String line, int cpuIndex, Float cpuTotal) {
    if (line.startsWith(String.valueOf(android.os.Process.myPid()))) {
        if (cpuIndex == -1) {
            throw new IllegalStateException("parseCpuRateOfAppByShell but cpuIndex == -1:" + line);
        }
        String[] param = line.split("\\s+");
        if (param.length <= cpuIndex) {
            throw new IllegalStateException("parseCpuRateOfAppByShell but param.length <= cpuIndex:" + line);
        }
        String cpu = param[cpuIndex];
        if (cpu.endsWith("%")) {
            cpu = cpu.substring(0, cpu.lastIndexOf("%"));
        }
        if (cpuTotal == null || cpuTotal <= 0) {
            throw new IllegalStateException("parseCpuRateOfAppByShell but cpuTotal == null || cpuTotal <= 0:" + line);
        }
        try {
            return Float.parseFloat(cpu) / cpuTotal;
        } catch (Throwable e) {
            throw new IllegalStateException("parseCpuRateOfAppByShell but " + e + ":" + line);
        }
    }
    return -1;
}

4、总结

其实,Cpu占用率的获取要么从文件中去获取,要么从命令去获取,通过系统给的一些关键值,我们人为解析出来,再通过计算得出来Cpu的使用率,很大程度上,Cpu的值的对错跟系统给的值准确性有关

2.2 Battery

2章 性能平台GodEye源码分析-数据模块

Battery的获取有以下几种方式

  • 通过系统电量变化的广播进行采集(GodEye采取这种)
  • 通过命令adb shell dumpsys battery定时采集(网上方案)
  • 通过BatteryManager定时采集(网上方案)

源码分析

1、启动Battery的监控

启动的过程其实就是注册广播的过程

public class BatteryEngine implements Engine {

    ......
    
    /**
     * 任意线程
     * 不保证线程安全,由外部保证
     */
    @Override
    public void work() {
        if (mBatteryChangeReceiver == null) {
            mBatteryChangeReceiver = new BatteryChangeReceiver();
            mBatteryChangeReceiver.setBatteryInfoProducer(mProducer);
            mContext.registerReceiver(mBatteryChangeReceiver, BatteryIntentFilterHolder.BATTERY_INTENT_FILTER);
        }
    }

    private static final class BatteryIntentFilterHolder {
        private static final IntentFilter BATTERY_INTENT_FILTER = new IntentFilter();

        static {
            BATTERY_INTENT_FILTER.addAction(Intent.ACTION_BATTERY_CHANGED);
            BATTERY_INTENT_FILTER.addAction(Intent.ACTION_BATTERY_LOW);
            BATTERY_INTENT_FILTER.addAction(Intent.ACTION_BATTERY_OKAY);
        }
    }
}

2、采集Battery信息

Battery的采集是通过广播传递过来的值,按照Api给定的类型,获取对应的电量状态、电池剩余容量、电池最大值、连接的电源插座、电压、温度、电池类型等信息

public class BatteryChangeReceiver extends BroadcastReceiver {
    private Producer<BatteryInfo> mBatteryInfoProducer;

    public void setBatteryInfoProducer(Producer<BatteryInfo> batteryInfoProducer) {
        mBatteryInfoProducer = batteryInfoProducer;
    }

    @Override
    public void onReceive(Context context, final Intent batteryInfoIntent) {
        ThreadUtil.computationScheduler().scheduleDirect(new Runnable() {
            @Override
            public void run() {
                try {
                    BatteryInfo batteryInfo = new BatteryInfo();
                    batteryInfo.status = batteryInfoIntent.getIntExtra(BatteryManager.EXTRA_STATUS, BatteryManager
                            .BATTERY_STATUS_UNKNOWN);
                    batteryInfo.health = batteryInfoIntent.getIntExtra(BatteryManager.EXTRA_HEALTH, BatteryManager
                            .BATTERY_HEALTH_UNKNOWN);
                    batteryInfo.present = batteryInfoIntent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false);
                    batteryInfo.level = batteryInfoIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
                    batteryInfo.scale = batteryInfoIntent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
                    batteryInfo.plugged = batteryInfoIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0);
                    batteryInfo.voltage = batteryInfoIntent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0);
                    batteryInfo.temperature = batteryInfoIntent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0);
                    batteryInfo.technology = batteryInfoIntent.getStringExtra(BatteryManager.EXTRA_TECHNOLOGY);
                    mBatteryInfoProducer.produce(batteryInfo);
                } catch (Throwable e) {
                    L.e(String.valueOf(e));
                }
            }
        });

    }
}

3、总结

电量的获取比较简单,代码量相对较少,数据的准确也是依赖于系统的准确性

2.3 Fps

2章 性能平台GodEye源码分析-数据模块

Fps指的是帧率的采集,默认指的是60帧每秒,1帧需要16ms

源码分析

1、启动Fps的监控

Fps的监控通过启动定时器,按xml配置的时间进行采集,通过mFpsMonitor.start()启动Fps的采集,通过mFpsMonitor.exportThenReset()获取Fps信息

public class FpsEngine implements Engine {

    private Producer<FpsInfo> mProducer;
    private long mIntervalMillis;
    private CompositeDisposable mCompositeDisposable;
    private FpsMonitor mFpsMonitor;
    private final int mSystemRate;

    public FpsEngine(Context context, Producer<FpsInfo> producer, long intervalMillis) {
        mProducer = producer;
        mIntervalMillis = intervalMillis;
        mSystemRate = getRefreshRate(context);
        mFpsMonitor = new FpsMonitor();
        mCompositeDisposable = new CompositeDisposable();
    }
    
    @Override
    public void work() {
        ThreadUtil.ensureMainThread("FpsEngine work");
        mFpsMonitor.start();
        mCompositeDisposable.add(Observable.interval(mIntervalMillis, TimeUnit.MILLISECONDS)
                .observeOn(ThreadUtil.computationScheduler())
                .subscribeOn(ThreadUtil.computationScheduler())
                .subscribe(aLong -> {
                    ThreadUtil.ensureWorkThread("FpsEngine accept");
                    if (!AndroidDebug.isDebugging()) {// if debugging, then ignore
                        int fps = mFpsMonitor.exportThenReset();
                        mProducer.produce(new FpsInfo(fps, mSystemRate));
                    }
                }));
    }

    
}

2、采集Fps信息

Fps采集FpsInfo上报2个值

  • 当前正在运行的fps(计算出来的)
  • 系统的fps(通过Api获取)

当前正在运行的fps(计算出来的)

当前正在运行的fps主要是通过FpsMonitor来监控当前的值

public class FpsMonitor implements Choreographer.FrameCallback {
    private Choreographer mChoreographer;
    private int mCurrentFrameCount;
    private long mStartFrameTimeNanos;
    private long mCurrentFrameTimeNanos;

    @UiThread
    public void start() {
        mChoreographer = ChoreographerInjecor.getChoreographerProvider().getChoreographer();
        mCurrentFrameCount = 0;
        mCurrentFrameTimeNanos = mStartFrameTimeNanos;
        mChoreographer.postFrameCallback(this);
    }

    @UiThread
    public void stop() {
        mChoreographer.removeFrameCallback(this);
        mChoreographer = null;
        mCurrentFrameCount = 0;
        mStartFrameTimeNanos = 0;
        mCurrentFrameTimeNanos = 0;
    }

    // callback every 16ms
    @Override
    public void doFrame(long frameTimeNanos) {
        mCurrentFrameCount++;
        if (mStartFrameTimeNanos == 0) {
            mStartFrameTimeNanos = frameTimeNanos;
        }
        mCurrentFrameTimeNanos = frameTimeNanos;
        mChoreographer.postFrameCallback(this);
    }

    /**
     * 导出当前平均fps(从上次导出开始)
     *
     * @return
     */
    @UiThread
    int exportThenReset() {
        if (mCurrentFrameCount < 1 || mCurrentFrameTimeNanos < mStartFrameTimeNanos) {
            return -1;
        }
        double fps = (mCurrentFrameCount - 1) * 1000000000.0 / (mCurrentFrameTimeNanos - mStartFrameTimeNanos);
        mStartFrameTimeNanos = 0;
        mCurrentFrameTimeNanos = 0;
        mCurrentFrameCount = 0;
        return (int) Math.round(fps);
    }
}

通过mFpsMonitor.start()启动Choreographer对象的postFrameCallback,其代码逻辑

@UiThread
public void start() {
    mChoreographer = ChoreographerInjecor.getChoreographerProvider().getChoreographer();
    ......
    mChoreographer.postFrameCallback(this);
}

// callback every 16ms
@Override
public void doFrame(long frameTimeNanos) {
    ......
    mChoreographer.postFrameCallback(this);
}
  1. mChoreographer.postFrameCallback(this)增加FrameCallback到系统的Choreographer对象
  2. 当系统的Vsync 信号下发时,会回调新增的FrameCallbackdoFrame(long frameTimeNanos),参数frameTimeNanos表示当前的帧的时间戳,单位是毫微秒,换算成秒公式为1秒=1000*1000*1000=1000000000毫微秒
  3. 在回调调用户新增的FrameCallbackdoFrame(long frameTimeNanos)时,重新mChoreographer.postFrameCallback(this),如此循环让系统一直回调当前的帧时间

当前的逻辑从mChoreographer.postFrameCallback(this)->doFrame(long frameTimeNanos)->mChoreographer.postFrameCallback(this)过程中,由于我们采用了定时器(默认2s)调用exportThenReset()计算fps,那么在这个过程中,2s时间内我们做了哪些事情,注意是在我们定时器的2s时间内

// callback every 16ms
@Override
public void doFrame(long frameTimeNanos) {
    mCurrentFrameCount++;
    if (mStartFrameTimeNanos == 0) {
        mStartFrameTimeNanos = frameTimeNanos;
    }
    mCurrentFrameTimeNanos = frameTimeNanos;
    mChoreographer.postFrameCallback(this);
}
  1. 在每帧16ms的回调中,将帧数累加
  2. 在每帧16ms的回调中,标记开始帧时间(只标记一次)
  3. 在每帧16ms的回调中,标记结束帧时间(每次都标记)

当定时器2s触发exportThenReset()时,其计算公式如下,由于fps概念=帧每秒,相当于需要将毫微秒进行单位转换,公式为帧数/s = 帧数*1000000000.0/毫微秒

@UiThread
int exportThenReset() {
    ......
    double fps = (mCurrentFrameCount - 1) * 1000000000.0 / (mCurrentFrameTimeNanos - mStartFrameTimeNanos);
    ......
    return (int) Math.round(fps);
}

这里要注意的点

  1. 采集时长不得小于16ms,如果小于Vsync 信号时长,那么每次采集到都不到1帧数据,数据样本为0
  2. 采集时长默认2s左右,这个值因人而异,如果不卡顿的情况下fps计算出来的值都在60,那么在卡顿的时候,这里就相当于2s内的卡顿平均,达不到实效性
  3. 极端情况,在采集的2s时间段内,前面8帧1.28s正常不发生卡顿,后面发生了1.5s的卡顿,那么就存在1.5-(2-1.28)=0.78s被平均到下一帧的fps上
  4. 总而言之,这个值不宜过长也不宜过短

系统的fps(通过Api获取)

系统的fps获取简单,代码如下

private static int getRefreshRate(Context context) {
    Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
    return Math.round(display.getRefreshRate());
}

3、总结

Fps的获取方式有很多种方案,目前源码的这种方法缺乏实效性,在业务量巨大的应用下,需要实时性的fps才比较适用,但作者这种实现方式也是其中的一种方案,值得借鉴。代码看起来都挺少,初次理解可能会比较困难,但是理解之后,就比较简单了

2.4 Traffic

2章 性能平台GodEye源码分析-数据模块

Traffic指的是流量的获取,主要是通过系统提供的TrafficStats获取

源码分析

1、启动Traffic的监控

Traffic的监控通过启动定时器,按xml配置的时间进行采集,通过create()获取Traffic信息

public class TrafficEngine {
    private Producer<TrafficInfo> mProducer;
    private long mIntervalMillis;
    private long mSampleMillis;
    private CompositeDisposable mCompositeDisposable;

    public TrafficEngine(Producer<TrafficInfo> producer, long intervalMillis, long sampleMillis) {
        mProducer = producer;
        mIntervalMillis = intervalMillis;
        mSampleMillis = sampleMillis;
        mCompositeDisposable = new CompositeDisposable();
    }

    public void work() {
        mCompositeDisposable.add(Observable.interval(mIntervalMillis, TimeUnit.MILLISECONDS)
                .subscribeOn(ThreadUtil.computationScheduler())
                .observeOn(ThreadUtil.computationScheduler())
                .concatMap(new Function<Long, ObservableSource<TrafficInfo>>() {
                    @Override
                    public ObservableSource<TrafficInfo> apply(Long aLong) throws Exception {
                        ThreadUtil.ensureWorkThread("TrafficEngine apply");
                        return create();
                    }
                }).subscribe(new Consumer<TrafficInfo>() {
                    @Override
                    public void accept(TrafficInfo food) throws Exception {
                        ThreadUtil.ensureWorkThread("TrafficEngine accept");
                        mProducer.produce(food);
                    }
                }));
    }

    public Observable<TrafficInfo> create() {
        final TrafficSnapshot start = TrafficSnapshot.snapshot();
        return Observable.timer(mSampleMillis, TimeUnit.MILLISECONDS).map(new Function<Long, TrafficInfo>() {
            @Override
            public TrafficInfo apply(Long aLong) throws Exception {
                TrafficSnapshot endTrafficSnapshot = TrafficSnapshot.snapshot();
                TrafficInfo trafficInfo = new TrafficInfo();
                trafficInfo.rxTotalRate = (endTrafficSnapshot.rxTotalKB - start.rxTotalKB) * 1000 / mSampleMillis;
                trafficInfo.txTotalRate = (endTrafficSnapshot.txTotalKB - start.txTotalKB) * 1000 / mSampleMillis;
                trafficInfo.rxUidRate = (endTrafficSnapshot.rxUidKB - start.rxUidKB) * 1000 / mSampleMillis;
                trafficInfo.txUidRate = (endTrafficSnapshot.txUidKB - start.txUidKB) * 1000 / mSampleMillis;
                return trafficInfo;
            }
        });
    }
}

2、采集Traffic信息

通过create()获取指定采集时间内mSampleMillis,前后两次的流量值的均值,最终获取的流量单位为kb/s

public Observable<TrafficInfo> create() {
    final TrafficSnapshot start = TrafficSnapshot.snapshot();
    return Observable.timer(mSampleMillis, TimeUnit.MILLISECONDS).map(new Function<Long, TrafficInfo>() {
        @Override
        public TrafficInfo apply(Long aLong) throws Exception {
            TrafficSnapshot endTrafficSnapshot = TrafficSnapshot.snapshot();
            TrafficInfo trafficInfo = new TrafficInfo();
            trafficInfo.rxTotalRate = (endTrafficSnapshot.rxTotalKB - start.rxTotalKB) * 1000 / mSampleMillis;
            trafficInfo.txTotalRate = (endTrafficSnapshot.txTotalKB - start.txTotalKB) * 1000 / mSampleMillis;
            trafficInfo.rxUidRate = (endTrafficSnapshot.rxUidKB - start.rxUidKB) * 1000 / mSampleMillis;
            trafficInfo.txUidRate = (endTrafficSnapshot.txUidKB - start.txUidKB) * 1000 / mSampleMillis;
            return trafficInfo;
        }
    });
}

流量快照信息通过TrafficStats系统提供的Api直接获取

/**
 * 流量快照
 * 还有其他一些比如区分Wi-Fi和移动流量的数据没有包含进来
 * 单位kb
 * Created by kysonchao on 2017/5/22.
 */
@Keep
public class TrafficSnapshot implements Serializable {
    //下行总字节数
    public float rxTotalKB;
    //上行总字节数
    public float txTotalKB;
    //下行应用总字节数
    public float rxUidKB;
    //上行应用总字节数
    public float txUidKB;

    @WorkerThread
    public static TrafficSnapshot snapshot() {
        TrafficSnapshot snapshot = new TrafficSnapshot();
        snapshot.rxTotalKB = TrafficStats.getTotalRxBytes() / 1024f;
        snapshot.txTotalKB = TrafficStats.getTotalTxBytes() / 1024f;
        snapshot.rxUidKB = TrafficStats.getUidRxBytes(android.os.Process.myUid()) / 1024f;
        snapshot.txUidKB = TrafficStats.getUidTxBytes(android.os.Process.myUid()) / 1024f;
        return snapshot;
    }
}

这里要注意的是

  • 定时器默认是2s采集一次,而流量快照默认是1s输出一次,两者都需要时间采集
  • 如果要让流量正常输出,那么就必须遵从,定时器的时长>流量快照输出的时长

3、总结

流量的采集比较简单,通过测试后,系统的api返回的数值准确性可以保证

2.5 StartUp

2章 性能平台GodEye源码分析-数据模块

StartUp指的是获取系统启动首页的时间

源码分析

StartUp指的是启动速度,侵入性较大,需要在启动的Activity上增加代码,作者通过例子也告诉我们大概的计算过程

1、启动StartUp的监控

StartUp的启动并没有做任何事情,只是初始化变量,通过StartupInfo发送出去

public class Startup extends ProduceableSubject<StartupInfo> implements Install<StartupConfig> {

    private StartupConfig mConfig;

    @Override
    public synchronized boolean install(StartupConfig config) {
        if (config == null) {
            throw new IllegalArgumentException("Startup module install fail because config is null.");
        }
        if (mConfig != null) {
            L.d("Startup already installed, ignore.");
            return true;
        }
        mConfig = config;
        L.d("Startup installed.");
        return true;
    }

    @Override
    public synchronized void uninstall() {
        if (mConfig == null) {
            L.d("Startup already uninstalled, ignore.");
            return;
        }
        mConfig = null;
        L.d("Startup uninstall.");
    }

    @Override
    public synchronized boolean isInstalled() {
        return mConfig != null;
    }

    @Override
    public StartupConfig config() {
        return mConfig;
    }

    @Override
    public void produce(StartupInfo data) {
        if (mConfig == null) {
            L.d("Startup is not installed, produce data fail.");
            return;
        }
        super.produce(data);
    }

    @Override
    protected Subject<StartupInfo> createSubject() {
        return BehaviorSubject.create();
    }
}

StartupInfo表示启动速度的信息,其中startupType可以表示冷启动和热启动,而startupTime表示启动时间

public class StartupInfo implements Serializable {
    @Retention(RetentionPolicy.SOURCE)
    @StringDef({StartUpType.COLD, StartUpType.HOT})
    public @interface StartUpType {
        public static final String COLD = "cold";
        public static final String HOT = "hot";
    }

    public @StartUpType
    String startupType;
    public long startupTime;

    public StartupInfo(@StartUpType String startupType, long startupTime) {
        this.startupType = startupType;
        this.startupTime = startupTime;
    }

    @Override
    public String toString() {
        return "StartupInfo{" +
                "startupType='" + startupType + '\'' +
                ", startupTime=" + startupTime +
                '}';
    }
}

2、采集StartUp信息

在作者的例子中发现整个采集的调用链

  • 在Application上打点StartupTracer.get().onApplicationCreate();
public class SampleApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        StartupTracer.get().onApplicationCreate();
        ......
    }
}
  • 在闪屏SplashActivity上打点StartupTracer.get().onSplashCreate();
public class SplashActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_splash);
        StartupTracer.get().onSplashCreate();
    }
}
  • 在首页Main2Activity进行统计StartupTracer.get().onHomeCreate(this);
public class Main2Activity extends AppCompatActivity implements InstallFragment.OnInstallModuleChangeListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        ......
        StartupTracer.get().onHomeCreate(this);
    }
}

整个调用链都通过工具类StartupTracer,用来记录并统计启动的过程,这里的统计涉及到两个启动类型

  • 热启动:从Application->Splash->Main的过程,耗时=Main-Application
  • 冷启动:从Splash->Main的过程,耗时=Main-Splash
public class StartupTracer {
    public interface OnStartupEndCallback {
        public void onStartupEnd();
    }

    private long mApplicationStartTime;
    private long mSplashStartTime;

    private StartupTracer() {
    }

    private static class InstanceHolder {
        private static final StartupTracer sInstance = new StartupTracer();
    }

    public static StartupTracer get() {
        return InstanceHolder.sInstance;
    }

    public void onApplicationCreate() {
        mApplicationStartTime = System.currentTimeMillis();
    }

    public void onSplashCreate() {
        mSplashStartTime = System.currentTimeMillis();
    }

    public void onHomeCreate(Activity activity) {
        if (Looper.myLooper() != Looper.getMainLooper()) {
            throw new IllegalStateException("call onHomeCreate ui thread!");
        }
        activity.getWindow().getDecorView().post(() -> new Handler().post(() -> {
            try {
                GodEyeHelper.onAppStartEnd(generateStartupInfo(System.currentTimeMillis()));
            } catch (UninstallException e) {
                e.printStackTrace();
            }
        }));
    }

    private StartupInfo generateStartupInfo(long homeEndTime) {
        if (isCodeStart(homeEndTime)) {
            return new StartupInfo(StartupInfo.StartUpType.COLD, homeEndTime - mApplicationStartTime);
        } else if (isHotStart(homeEndTime)) {
            return new StartupInfo(StartupInfo.StartUpType.HOT, homeEndTime - mSplashStartTime);
        } else {
            return null;
        }
    }

    private boolean isCodeStart(long homeEndTime) {
        return mApplicationStartTime > 0 && homeEndTime > mSplashStartTime && mSplashStartTime > mApplicationStartTime;
    }

    private boolean isHotStart(long homeEndTime) {
        return mApplicationStartTime <= 0 && mSplashStartTime > 0 && homeEndTime > mSplashStartTime;
    }
}

3、总结

启动耗时的统计对代码侵入性较大,可以考虑配置的方式,监听Activity生命周期,通过指定的Activity回调作为统计耗时的起点和终点

2.6 Thread

2章 性能平台GodEye源码分析-数据模块

Thread指的是获取当前App上所有正在执行的线程,且线程本身有Api可以获取当前线程的堆栈

源码分析

1、启动Thread的监控

Thread的监控通过启动定时器,按xml配置的时间进行采集,通过dump()获取Thread信息

class ThreadEngine implements Engine {
    private Producer<List<ThreadInfo>> mProducer;
    private long mIntervalMillis;
    private CompositeDisposable mCompositeDisposable;
    private @NonNull
    ThreadFilter mThreadFilter;
    private @NonNull
    ThreadTagger mThreadTagger;

    ThreadEngine(Producer<List<ThreadInfo>> producer, ThreadConfig config) {
        ThreadFilter threadFilter = new ExcludeSystemThreadFilter();
        try {
            threadFilter = (ThreadFilter) Class.forName(config.threadFilter).newInstance();
        } catch (Throwable e) {
            L.e(String.format("Thread install warning, can not find ThreadFilter class %s, use ExcludeSystemThreadFilter, error: %s", config.threadFilter, e));
        }
        ThreadTagger threadTagger = new DefaultThreadTagger();
        try {
            threadTagger = (ThreadTagger) Class.forName(config.threadTagger).newInstance();
        } catch (Throwable e) {
            L.e(String.format("Thread install warning, can not find ThreadTagger class %s, use DefaultThreadTagger, error: %s", config.threadTagger, e));
        }
        mProducer = producer;
        mIntervalMillis = config.intervalMillis;
        mThreadFilter = threadFilter;
        mThreadTagger = threadTagger;
        mCompositeDisposable = new CompositeDisposable();
    }

    @Override
    public void work() {
        mCompositeDisposable.add(Observable.interval(mIntervalMillis, TimeUnit.MILLISECONDS).map(aLong -> {
            ThreadUtil.ensureWorkThread("ThreadEngine apply");
            return dump(mThreadFilter, mThreadTagger);
        }).subscribeOn(ThreadUtil.computationScheduler())
                .observeOn(ThreadUtil.computationScheduler())
                .subscribe(food -> {
                    ThreadUtil.ensureWorkThread("ThreadEngine accept");
                    mProducer.produce(food);
                }));
    }
}

2、采集Thread信息

获取线程信息

  1. 通过获取当前线程的线程组
  2. 获取线程组的最*的线程组
  3. 复制当前最*的线程组到数组中
  4. 复制中如果超过当前的数组容量,则扩容后继续复制

其中有两个参数

  • threadFilter:通过xml配置实例化的类,表示想过滤的线程
  • threadTagger:通过xml配置实例化的类,表示可以设置线程的tag,用作Web显示
/**
 * dump当前所有线程
 *
 * @param threadFilter
 * @return
 */
@VisibleForTesting
static List<ThreadInfo> dump(@NonNull ThreadFilter threadFilter, @NonNull ThreadTagger threadTagger) {
    //1、通过获取当前线程的线程组
    ThreadGroup rootGroup = Thread.currentThread().getThreadGroup();
    if (rootGroup == null) {
        return new ArrayList<>();
    }
    ThreadGroup parentGroup;
    //2、获取线程组的最*的线程组
    while ((parentGroup = rootGroup.getParent()) != null) {
        rootGroup = parentGroup;
    }
    Thread[] threads = new Thread[rootGroup.activeCount()];
    //3、复制当前最*的线程组到数组中
    while (rootGroup.enumerate(threads, true) >= threads.length) {
        //4、复制中如果超过当前的数组容量,则扩容后继续复制
        threads = new Thread[threads.length * 2];
    }
    List<ThreadInfo> threadList = new ArrayList<>();
    Set<Long> threadIds = new HashSet<>();
    for (Thread thread : threads) {
        if (thread != null && !threadIds.contains(thread.getId()) && threadFilter.filter(thread)) {
            threadIds.add(thread.getId());
            ThreadInfo threadInfo = new ThreadInfo(thread);
            threadInfo.threadTag = threadTagger.tag(thread, threadInfo);
            threadList.add(threadInfo);
        }
    }
    return threadList;
}

ThreadInfo构造函数中,会获取当前的线程的堆栈信息thread.getStackTrace()

public ThreadInfo(Thread thread) {
    this.id = thread.getId();
    this.name = thread.getName();
    this.state = String.valueOf(thread.getState());
    this.deamon = thread.isDaemon();
    this.priority = thread.getPriority();
    this.isAlive = thread.isAlive();
    this.isInterrupted = thread.isInterrupted();
    this.stackTraceElements = StacktraceUtil.getStackTraceOfThread(thread);
    this.parent = new ThreadGroup(thread.getThreadGroup() == null ? "" : thread.getThreadGroup().getName());
}

public static List<String> getStackTraceOfThread(Thread thread) {
    StackTraceElement[] stackTraceElements = thread.getStackTrace();
    return stackTraceToStringArray(Arrays.asList(stackTraceElements));
}

public static List<String> stackTraceToStringArray(List<StackTraceElement> stackTraceElements) {
    List<String> stackList = new ArrayList<>();
    for (StackTraceElement traceElement : stackTraceElements) {
        stackList.add(String.valueOf(traceElement));
    }
    return stackList;
}

3、总结

线程的获取比较简单,代码量相对较少,获取的过程中有涉及到获取堆栈信息,对性能会有些消耗

本文地址:https://blog.csdn.net/qq_30379689/article/details/111870007