App性能监控

    技术2023-09-15  122

     

    目录

     

     

    Doraemonkit采集性能数据原理分析

    获取App启动耗时

    获取内存Memory大小

    获取CPU平均使用率

     

    获取FPS帧率

    卡顿检测

     

    检测UI层级

    流量监控

    客户端定制方案

    打通性能监控单项指标和体检功能

    改造体检功能

    新增持久化存储

    新增一个结果汇总的页面

    新增管理后台

    监控日志

     

    流量大小监控方案

    方案一,使用系统API

    方案二,使用OKHttp、HttpUrlConnection拦截器


    Doraemonkit采集性能数据原理分析

    获取App启动耗时

    使用ASM库插桩的方式,监听Application的声明周期回调 1. 在如下两个方法调用中分别记录一个时间戳,根据两个时间戳就可以计算出启动耗时。

    onMethodEnter  -> recodeObjectMethodCostStart

    onMethodExit -> recodeObjectMethodCostEnd

     

    获取内存Memory大小

    private float getMemoryData() { float mem = 0.0F; try { Debug.MemoryInfo memInfo = null; //28 为Android P if (Build.VERSION.SDK_INT > 28) { // 统计进程的内存信息 totalPss memInfo = new Debug.MemoryInfo(); Debug.getMemoryInfo(memInfo); } else { //As of Android Q, for regular apps this method will only return information about the memory info for the processes running as the caller's uid; // no other process memory info is available and will be zero. Also of Android Q the sample rate allowed by this API is significantly limited, if called faster the limit you will receive the same data as the previous call. Debug.MemoryInfo[] memInfos = mActivityManager.getProcessMemoryInfo(new int[]{Process.myPid()}); if (memInfos != null && memInfos.length > 0) { memInfo = memInfos[0]; } } int totalPss = memInfo.getTotalPss(); if (totalPss >= 0) { // Mem in MB mem = totalPss / 1024.0F; } } catch (Exception e) { e.printStackTrace(); } return mem; }

    获取CPU平均使用率

    Android 8.0及以上

    /** * 8.0以上获取cpu的方式 * * @return */ private float getCpuDataForO() { java.lang.Process process = null; try { process = Runtime.getRuntime().exec("top -n 1"); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; int cpuIndex = -1; while ((line = reader.readLine()) != null) { line = line.trim(); if (TextUtils.isEmpty(line)) { continue; } int tempIndex = getCPUIndex(line); if (tempIndex != -1) { cpuIndex = tempIndex; continue; } if (line.startsWith(String.valueOf(Process.myPid()))) { if (cpuIndex == -1) { continue; } String[] param = line.split("\\s+"); if (param.length <= cpuIndex) { continue; } String cpu = param[cpuIndex]; if (cpu.endsWith("%")) { cpu = cpu.substring(0, cpu.lastIndexOf("%")); } float rate = Float.parseFloat(cpu) / Runtime.getRuntime().availableProcessors(); return rate; } } } catch (IOException e) { e.printStackTrace(); } finally { if (process != null) { process.destroy(); } } return 0; }

    Android 8.0以下获取CPU平均使用率

    /** * 8.0一下获取cpu的方式 * * @return */ private float getCPUData() { long cpuTime; long appTime; float value = 0.0f; try { if (mProcStatFile == null || mAppStatFile == null) { mProcStatFile = new RandomAccessFile("/proc/stat", "r"); mAppStatFile = new RandomAccessFile("/proc/" + android.os.Process.myPid() + "/stat", "r"); } else { mProcStatFile.seek(0L); mAppStatFile.seek(0L); } String procStatString = mProcStatFile.readLine(); String appStatString = mAppStatFile.readLine(); String procStats[] = procStatString.split(" "); String appStats[] = appStatString.split(" "); cpuTime = Long.parseLong(procStats[2]) + Long.parseLong(procStats[3]) + Long.parseLong(procStats[4]) + Long.parseLong(procStats[5]) + Long.parseLong(procStats[6]) + Long.parseLong(procStats[7]) + Long.parseLong(procStats[8]); appTime = Long.parseLong(appStats[13]) + Long.parseLong(appStats[14]); if (mLastCpuTime == null && mLastAppCpuTime == null) { mLastCpuTime = cpuTime; mLastAppCpuTime = appTime; return value; } value = ((float) (appTime - mLastAppCpuTime) / (float) (cpuTime - mLastCpuTime)) * 100f; mLastCpuTime = cpuTime; mLastAppCpuTime = appTime; } catch (Exception e) { e.printStackTrace(); } return value; }

     

    获取FPS帧率

    帧率监控从API 16+开始支持

    监听Choreographer.FrameCallback#doFrame方法。系统每次绘制的时候都会回调doFrame方法,通过主线程handler每隔一秒种发一个runnable消息,消息中执行#doFrame,使用计时器累加刷新次数就可以计算出帧率。

     

    卡顿检测

    检测卡顿的原理是:

    监控主线程消息队列,消息队列处理消息时会打印日志,通过前后两次打印日志的间隔和阈值比较即可检测出是否出现卡顿。

    检测卡顿的原理官方解释是这样的:a log message will be written to <var>printer</var> at the beginning and ending of each message dispatch, identifying the target Handler and message contents.

     

    给主线程的消息队列设置日志打印类 Looper.getMainLooper().setMessageLogging(mMonitorCore)

     

    消息队列处理消息时,每隔300ms采集一次堆栈信息,子线程中采集采集堆栈信息方法,见下方代码块卡顿阈值:200ms,该值可以修改成符合实际情况的值(字段:BLOCK_THRESHOLD_MILLIS)信息卡顿通知,略卡顿列表最多收集50条数据,超过之后把index=0的挤掉 StringBuilder stringBuilder = new StringBuilder(); Thread thread = Looper.getMainLooper().getThread(); for (StackTraceElement stackTraceElement : thread.getStackTrace()) { stringBuilder .append(stackTraceElement.toString()) .append(SEPARATOR); }

     

    小结:其实消息的开始、结束日志一直在收集,只不过当检测出卡顿之后才把这条信息的日志当成卡顿日志记录下来。

     

    检测UI层级

    根据activity获取DecorView,递归遍历子view,直到子view是View类型返回

    递归的层级就是该子View的层级号layerNumber

    计算子View在屏幕上的位置

    public static Rect getViewRect(View view) { Rect rect = new Rect(); int[] locations = new int[2]; view.getLocationOnScreen(locations); rect.left = locations[0]; rect.top = locations[1]; if (!checkStatusBarVisible(view.getContext())) { rect.top -= UIUtils.getStatusBarHeight(); } rect.right = rect.left + view.getWidth(); rect.bottom = rect.top + view.getHeight(); return rect; }

    计算子view的id,但是我这里使用时发现view.getId一直是-1,这个问题待查

    /** * 要特别注意 返回的字段包含空格 做判断时一定要trim() * * @param view * @return */ public static String getIdText(View view) { final int id = view.getId(); StringBuilder out = new StringBuilder(); if (id != View.NO_ID) { final Resources r = view.getResources(); if (id > 0 && resourceHasPackage(id) && r != null) { try { String pkgname; switch (id & 0xff000000) { case 0x7f000000: pkgname = "app"; break; case 0x01000000: pkgname = "android"; break; default: pkgname = r.getResourcePackageName(id); break; } String typename = r.getResourceTypeName(id); String entryname = r.getResourceEntryName(id); out.append(" "); out.append(pkgname); out.append(":"); out.append(typename); out.append("/"); out.append(entryname); } catch (Resources.NotFoundException e) { e.printStackTrace(); } } } return TextUtils.isEmpty(out.toString()) ? "" : out.toString(); }

    子View绘制耗时,未实现。所以暂时无法计算出子View的绘制时间。

    不过层级结构应该是准的,我数了一下ActivityBookShelf有20层,和收集的数据匹配。

     

    流量监控

    使用OKHttp、HttpUrlConnection拦截器,见下文

     

    客户端定制方案

    打通性能监控单项指标和体检功能

    默认开启以下单项指标监控:

    流量监控

    数据共享

    弱化体检功能,废弃该功能,默认开启体检模式

    改造体检功能

    不需要手动开启,改成默认开启体检功能收集到的数据共享给性能监控单项模块使用

    新增持久化存储

    存储成文件或者数据库文件越大越好,收集更多的数据进行比较文件大小最多1G,再大了可能分析起来比较慢

    新增一个结果汇总的页面

    汇总展示各个单项指标的页面

    新增管理后台

    汇总各个性能数据,并展示增加详情展示信息定制性能标准,超过标准给与提示

     

    监控日志

    {"baseInfo":{"appName":"DoKitDemo","appVersion":"3.1.4","caseName":"给你","dokitVersion":"3.1.4","pId":"71d3d5ab5765e78c664fc3ac9cf33b34","phoneMode":"Pixel3a","platform":"Android","systemVersion":"10","testPerson":"33","time":"2020-07-16 18:20:40"},"data":{"appStart":{"costDetail":"{\"data\":[{\"costTime\":\"60ms\",\"functionName\":\"Application onCreate\"},{\"costTime\":\"1ms\",\"functionName\":\"Application attachBaseContext\"}],\"title\":\"App启动耗时\"}","costTime":61,"loadFunc":[]},"bigFile":[{"fileName":"2020-07-06_20-01-29_140.hprof","filePath":"/data/user/0/com.didichuxing.doraemondemo/files/leakcanary/2020-07-06_20-01-29_140.hprof","fileSize":"20978998"},{"fileName":"2020-07-06_20-02-57_026.hprof","filePath":"/data/user/0/com.didichuxing.doraemondemo/files/leakcanary/2020-07-06_20-02-57_026.hprof","fileSize":"21056617"}],"block":[],"cpu":[{"page":"com.didichuxing.doraemondemo.MainDebugActivity","pageKey":"com.didichuxing.doraemondemo.MainDebugActivity@853d38b","values":[{"time":"1594894772656","value":"6.5"},{"time":"1594894773432","value":"3.5"},{"time":"1594894774217","value":"5.5"},{"time":"1594894775017","value":"0.0"},{"time":"1594894775804","value":"0.0"},{"time":"1594894776592","value":"6.0"},{"time":"1594894777379","value":"4.5"},{"time":"1594894778165","value":"4.5"},{"time":"1594894778964","value":"5.5"},{"time":"1594894779753","value":"4.0"},{"time":"1594894780537","value":"5.5"},{"time":"1594894781323","value":"4.0"},{"time":"1594894782113","value":"3.8375"},{"time":"1594894782901","value":"0.0"},{"time":"1594894783687","value":"0.0"},{"time":"1594894784478","value":"4.5"},{"time":"1594894785265","value":"4.5"},{"time":"1594894786054","value":"4.5"},{"time":"1594894786840","value":"4.0"},{"time":"1594894787646","value":"0.0"},{"time":"1594894788432","value":"4.0"},{"time":"1594894789217","value":"4.0"},{"time":"1594894790021","value":"4.0"},{"time":"1594894790828","value":"0.5"},{"time":"1594894791628","value":"1.0"},{"time":"1594894792426","value":"0.5"},{"time":"1594894793212","value":"0.5"},{"time":"1594894793999","value":"1.0"},{"time":"1594894794798","value":"1.0"},{"time":"1594894795600","value":"0.5"},{"time":"1594894796410","value":"1.0"},{"time":"1594894797224","value":"0.0"},{"time":"1594894798039","value":"1.0"},{"time":"1594894798858","value":"0.95"},{"time":"1594894799661","value":"1.5"},{"time":"1594894800483","value":"0.95"},{"time":"1594894801281","value":"1.5"},{"time":"1594894802096","value":"4.0"},{"time":"1594894802913","value":"1.5"},{"time":"1594894803728","value":"1.0"}]},{"page":"com.didichuxing.doraemonkit.kit.core.UniversalActivity","pageKey":"com.didichuxing.doraemonkit.kit.core.UniversalActivity@afe3a23","values":[{"time":"1594894828584","value":"2.0"},{"time":"1594894829355","value":"1.0"},{"time":"1594894830123","value":"3.5"},{"time":"1594894830899","value":"3.0"},{"time":"1594894831703","value":"0.0"},{"time":"1594894832525","value":"1.5"},{"time":"1594894833308","value":"0.0"},{"time":"1594894834129","value":"1.0"},{"time":"1594894834906","value":"1.0"},{"time":"1594894835686","value":"0.5"},{"time":"1594894836465","value":"0.0"},{"time":"1594894837242","value":"1.5"},{"time":"1594894838057","value":"0.5"},{"time":"1594894838836","value":"1.0"},{"time":"1594894839629","value":"0.5"},{"time":"1594894840445","value":"1.0"}]}],"fps":[{"page":"com.didichuxing.doraemondemo.MainDebugActivity","pageKey":"com.didichuxing.doraemondemo.MainDebugActivity@853d38b","values":[{"time":"1594894772878","value":"36.0"},{"time":"1594894773895","value":"60.0"},{"time":"1594894774897","value":"60.0"},{"time":"1594894775899","value":"60.0"},{"time":"1594894776900","value":"60.0"},{"time":"1594894777903","value":"60.0"},{"time":"1594894778906","value":"60.0"},{"time":"1594894779910","value":"60.0"},{"time":"1594894780912","value":"60.0"},{"time":"1594894781914","value":"60.0"},{"time":"1594894782917","value":"60.0"},{"time":"1594894783919","value":"60.0"},{"time":"1594894784921","value":"60.0"},{"time":"1594894785924","value":"60.0"},{"time":"1594894786926","value":"60.0"},{"time":"1594894787928","value":"60.0"},{"time":"1594894788932","value":"60.0"},{"time":"1594894789934","value":"60.0"},{"time":"1594894790936","value":"60.0"},{"time":"1594894791940","value":"60.0"},{"time":"1594894792943","value":"60.0"},{"time":"1594894793949","value":"60.0"},{"time":"1594894794955","value":"60.0"},{"time":"1594894795959","value":"60.0"},{"time":"1594894796963","value":"60.0"},{"time":"1594894797966","value":"60.0"},{"time":"1594894798968","value":"60.0"},{"time":"1594894799972","value":"60.0"},{"time":"1594894800976","value":"60.0"},{"time":"1594894801979","value":"60.0"},{"time":"1594894802983","value":"60.0"},{"time":"1594894803986","value":"60.0"},{"time":"1594894804992","value":"60.0"},{"time":"1594894805998","value":"60.0"},{"time":"1594894807000","value":"60.0"},{"time":"1594894808009","value":"60.0"},{"time":"1594894809012","value":"60.0"},{"time":"1594894810017","value":"60.0"},{"time":"1594894811021","value":"60.0"},{"time":"1594894812026","value":"60.0"}]},{"page":"com.didichuxing.doraemonkit.kit.core.UniversalActivity","pageKey":"com.didichuxing.doraemonkit.kit.core.UniversalActivity@afe3a23","values":[{"time":"1594894830611","value":"2.0"},{"time":"1594894831617","value":"59.0"},{"time":"1594894832625","value":"60.0"},{"time":"1594894833627","value":"47.0"},{"time":"1594894834629","value":"57.0"},{"time":"1594894835632","value":"60.0"},{"time":"1594894836637","value":"59.0"},{"time":"1594894837643","value":"59.0"},{"time":"1594894838646","value":"60.0"},{"time":"1594894839649","value":"60.0"},{"time":"1594894840654","value":"60.0"}]}],"leak":[],"memory":[{"page":"com.didichuxing.doraemondemo.MainDebugActivity","pageKey":"com.didichuxing.doraemondemo.MainDebugActivity@853d38b","values":[{"time":"1594894772701","value":"46.74414"},{"time":"1594894773476","value":"48.335938"},{"time":"1594894774276","value":"45.541016"},{"time":"1594894775081","value":"45.79297"},{"time":"1594894775866","value":"46.0625"},{"time":"1594894776652","value":"46.333008"},{"time":"1594894777449","value":"46.984375"},{"time":"1594894778230","value":"47.257812"},{"time":"1594894779025","value":"47.439453"},{"time":"1594894779816","value":"47.70508"},{"time":"1594894780602","value":"46.13867"},{"time":"1594894781388","value":"46.38965"},{"time":"1594894782176","value":"46.651367"},{"time":"1594894782967","value":"46.901367"},{"time":"1594894783751","value":"47.160156"},{"time":"1594894784543","value":"47.262695"},{"time":"1594894785329","value":"47.536133"},{"time":"1594894786114","value":"47.822266"},{"time":"1594894786909","value":"48.083984"},{"time":"1594894787709","value":"46.22461"},{"time":"1594894788493","value":"46.45508"},{"time":"1594894789276","value":"46.685547"},{"time":"1594894790084","value":"46.916016"},{"time":"1594894790886","value":"47.103516"},{"time":"1594894791690","value":"47.33789"},{"time":"1594894792490","value":"47.564453"},{"time":"1594894793273","value":"47.77832"},{"time":"1594894794067","value":"47.969727"},{"time":"1594894794868","value":"45.512695"},{"time":"1594894795665","value":"45.74707"},{"time":"1594894796476","value":"45.960938"},{"time":"1594894797291","value":"46.183594"},{"time":"1594894798106","value":"46.410156"},{"time":"1594894798926","value":"46.625"},{"time":"1594894799726","value":"46.84375"},{"time":"1594894800550","value":"47.08203"},{"time":"1594894801345","value":"47.30078"},{"time":"1594894802160","value":"45.351562"},{"time":"1594894802977","value":"45.57422"},{"time":"1594894803795","value":"45.79297"}]},{"page":"com.didichuxing.doraemonkit.kit.core.UniversalActivity","pageKey":"com.didichuxing.doraemonkit.kit.core.UniversalActivity@afe3a23","values":[{"time":"1594894828626","value":"59.04883"},{"time":"1594894829397","value":"58.493164"},{"time":"1594894830168","value":"57.06836"},{"time":"1594894830951","value":"87.487305"},{"time":"1594894831767","value":"84.87402"},{"time":"1594894832596","value":"85.08496"},{"time":"1594894833366","value":"86.708984"},{"time":"1594894834198","value":"87.36133"},{"time":"1594894834957","value":"87.49902"},{"time":"1594894835743","value":"87.87402"},{"time":"1594894836522","value":"83.765625"},{"time":"1594894837313","value":"84.11133"},{"time":"1594894838110","value":"85.03906"},{"time":"1594894838886","value":"85.28613"},{"time":"1594894839681","value":"85.512695"},{"time":"1594894840517","value":"85.77832"}]}],"network":[{"page":"com.didichuxing.doraemondemo.MainDebugActivity","values":[{"code":"200","down":"0","method":"POST","time":"1594894772511","up":"364","url":"https://cc.map.qq.com/?desc_c"},{"code":"200","down":"1","method":"POST","time":"1594894772701","up":"402","url":"https://analytics.map.qq.com/tr?mllc"}]}],"pageLoad":[{"page":"com.didichuxing.doraemondemo.MainDebugActivity","time":"390","trace":"null -> MainDebugActivity"},{"page":"com.didichuxing.doraemondemo.SecondActivity","time":"108","trace":"MainDebugActivity -> SecondActivity"},{"page":"com.didichuxing.doraemondemo.MainDebugActivity","time":"85","trace":"MainDebugActivity -> MainDebugActivity"},{"page":"com.didichuxing.doraemonkit.kit.core.UniversalActivity","time":"2475","trace":"MainDebugActivity -> UniversalActivity"}],"subThreadUI":[],"uiLevel":[{"detail":"最大层级:9\n控件id: app:id/iv_fresco\n总绘制耗时:0.0ms\n绘制耗时最长控件:0.0ms\n绘制耗时最长控件id:no id\n","level":"9","page":"com.didichuxing.doraemondemo.MainDebugActivity"},{"detail":"最大层级:6\n控件id:\n总绘制耗时:0.0ms\n绘制耗时最长控件:0.0ms\n绘制耗时最长控件id:no id\n","level":"6","page":"com.didichuxing.doraemondemo.SecondActivity"},{"detail":"最大层级:9\n控件id: app:id/iv_fresco\n总绘制耗时:0.0ms\n绘制耗时最长控件:0.0ms\n绘制耗时最长控件id:no id\n","level":"9","page":"com.didichuxing.doraemondemo.MainDebugActivity"},{"detail":"最大层级:7\n控件id: app:id/icon\n总绘制耗时:0.0ms\n绘制耗时最长控件:0.0ms\n绘制耗时最长控件id:no id\n","level":"7","page":"com.didichuxing.doraemonkit.kit.core.UniversalActivity"}]}}

     

    流量大小监控方案

    方案一,使用系统API

    NetworkStatsManager#querySummary

    /** * Query network usage statistics summaries. Result filtered to include only uids belonging to * calling user. Result is aggregated over time, hence all buckets will have the same start and * end timestamps. Not aggregated over state, uid, metered, or roaming. This means buckets' * start and end timestamps are going to be the same as the 'startTime' and 'endTime' * parameters. State, uid, metered, and roaming are going to vary, and tag is going to be the * same. * * @param networkType As defined in {@link ConnectivityManager}, e.g. * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI} * etc. * @param subscriberId If applicable, the subscriber id of the network interface. * @param startTime Start of period. Defined in terms of "Unix time", see * {@link java.lang.System#currentTimeMillis}. * @param endTime End of period. Defined in terms of "Unix time", see * {@link java.lang.System#currentTimeMillis}. * @return Statistics object or null if permissions are insufficient or error happened during * statistics collection. */ public NetworkStats querySummary(int networkType, String subscriberId, long startTime, long endTime) throws SecurityException, RemoteException { NetworkTemplate template; try { template = createTemplate(networkType, subscriberId); } catch (IllegalArgumentException e) { if (DBG) Log.e(TAG, "Cannot create template", e); return null; } NetworkStats result; result = new NetworkStats(mContext, template, startTime, endTime); result.startSummaryEnumeration(); return result; }

    NetworkStats.Bucket#getRxBytes

    /** * Number of bytes received during the bucket's time interval. Statistics are measured at * the network layer, so they include both TCP and UDP usage. * @return Number of bytes. */ public long getRxBytes() { return mRxBytes; }

    方案二,使用OKHttp、HttpUrlConnection拦截器

    MockInterceptor#intercept,拿到chain就可以拿到所需的所有数据

    @Override public Response intercept(Chain chain) throws IOException { Request oldRequest = chain.request(); Response oldResponse = chain.proceed(oldRequest); ... }

     

     

     

    Processed: 0.009, SQL: 9