Android卡顿优化分析

  • Post author:
  • Post category:其他


本篇包含的主要内容如下所示:

  • 1、卡顿优化分析方法与工具

  • 2、自动化卡顿检测方案及优化

在某个 App 的时候,有时我们会看到某个 App 运行起来,即出现了卡现象,如何去定义发生了卡现象呢?马上来了解一下卡顿呢?

一、卡顿优化分析方法与工具

一、背景介绍

  • 看似简单的问题,但卡顿的问题很容易被感知。

  • 卡顿问题定位。

卡顿问题到底难在哪里呢?

  • 1、卡顿产生的原因是复杂复杂的,它涉及到代码、内存、绘制、IO、CPU等。

  • 2、 线上的问题是复现的,因为在线与当时的场景是强相关的,因为在线用户的卡IO空间不足,它影响了合金IO的性能,所以导致卡顿。针对这种情况,当时我们最好在卡顿的时候及时去记录用户发生卡顿的具体场景信息。

2、卡顿分析使用shell命令分析CPU之方法

造成卡顿的原因有很明显的变化,不过最初只是反应到CPU时间上。

CPU时间包含用户时间和系统时间。

  • 用户时间:应用程序代码所执行的用户时间。

  • 系统时间:内核架构系统调用所执行的时间包括I/O、锁、中断和其他系统调用所用的时间。

CPU的问题可以分为以下三类:


1、CPU资源占用使用

  • 效率太高:明明可以一次穿越的低谷却需要明去遍历,主要在寻找、试探、出现等算法。

  • 使用缓存:明明解密没有一次的图片还去重复解密。

  • 计算时使用的基本类型不正确:明明使用int就可以,却要长时间使用,这会导致CPU的资源多出4倍。


2、CPU资源争夺抢

  • 抢主线程的资源:这是Android 6000之前最常见的问题解决了,主线程的线程的严重程度就并且是否会出现在用户的卡问题上。

  • 视频抢夺CPU的资源:可以编解码解码消耗大量的CPU资源。我们对于视频解码的能力是由硬件,达不到要求的资源量。优化:1、解决非核心业务的消耗。2、优化自身的性能消耗,让CPU负载恢复为GPU负载,如使用renderscript来处理视频中的图像信息。

  • 大家,互相抢了:像在自定义的相册中,我找了20个会做的解码解码,没别的是抢图片的显示速度非常慢。这就是三个简单和尚水因此,我们需要按照系统核心数去控制线程数。


3、CPU资源低

为了保证启动、屏幕切换、音解码速度,我们这些场景,而为了保证引导去好好利用CPU。等。

1、了解CPU性能

我们可以通过CPU计算频率、核心参数、缓存等性能的好坏来评估CPU的执行能力、CPU执行能力的强弱也就是表现CPU执行能力和指令的性能、计算数据等。和执行的指令数的多少。

此外,最新的即能效用机构仅使用多级核架构(多级分层架构),以确保在正常运行时低负荷使用 CPU 时,可以正常使用。

另外,我们还可以通过shell命令直接查看手机的CPU核心数与频率等信息,如下所示:

// 先输入adb shell进入手机的shell环境
adb shell

// 获取 CPU 核心数,我的手机是8核
platina:/ $ cat /sys/devices/system/cpu/possible
0-7

// 获取第一个 CPU 的最大频率
platina:/ $ cat
/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq                         <
1843200

// 获取第二个CPU的最小频率
platina:/ $ cat
/sys/devices/system/cpu/cpu1/cpufreq/cpuinfo_min_freq                         <
633600

从 CPU 到 GPU 再到 AI 芯片(如专门为神经网络计算打造的 NPU(Neural network Processing Unit)),随着手机 CPU 整体性能的飞跃,医疗诊断、图像超清化等一些 AI 应用场景也可以在移动端更好地落地。我们可以充分利用移动端的计算能力来降低高昂的服务器成本。

此外,CPU 的不同性能,应用手机获得更好的支持,如线程池可以根据 CPU 核心来配备的数字设备,仅在较高频率或 NPU 的不同频率上去开启一些高级的人工智能功能。

2、读取/proc/stat与/proc/[PID]/stat文件计算并查看系统的CPU查询情况2

当应用出现卡顿问题之后,我们首先应该查看系统CPU的使用率。

首先,通过读取 /proc/[PID]/stat 获取应用进程的 CPU 时间,然后读取我们的 CPU 进程和 CPU 进程的时间间隔来预测其CPU使用率。

计算总的 CPU 使用率

1、接下来两个短的时间间隔显示,即需要两个CPU去读取/proc/stat文件的时间点,获取两个数据,如:

// 第一次采样
platina:/ $ cat /proc/stat
cpu  9931551 1082101 9002534 174463041 340947 1060438 1088978 0 0 0
cpu0 2244962 280573 2667000 22414199 99651 231869 439918 0 0 0
cpu1 2672378 421880 2943791 21540302 121818 236850 438733 0 0 0
cpu2 1648512 76856 1431036 25868789 46970 107094 52025 0 0 0
cpu3 1418757 41280 1397203 25772984 40292 110168 41667 0 0 0
cpu4 573203 79498 178263 19618235 9577 307949 10875 0 0 0
cpu5 522638 67978 155454 19684358 8793 19787 4603 0 0 0
cpu6 458438 64085 132252 19749439 8143 19942 98241 0 0 0
cpu7 392663 49951 97535 19814735 5703 26779 2916 0 0 0
intr...

// 第二次采样
platina:/ $ cat /proc/stat
cpu  9931673 1082113 9002679 174466561 340954 1060446 1088994 0 0 0
cpu0 2244999 280578 2667032 22414604 99653 231869 439918 0 0 0
cpu1 2672434 421881 2943861 21540606 121822 236855 438747 0 0 0
cpu2 1648525 76859 1431054 25869234 46971 107095 52026 0 0 0
cpu3 1418773 41283 1397228 25773412 40292 110170 41668 0 0 0
cpu4 573203 79498 178263 19618720 9577 307949 10875 0 0 0
cpu5 522638 67978 155454 19684842 8793 19787 4603 0 0 0
cpu6 458438 64085 132252 19749923 8143 19942 98241 0 0 0
cpu7 392663 49951 97535 19815220 5703 26779 2916 0 0 0
int...

因为我的手机是8核的,所以这里的cpu个数是8个,从cpu0到cpu7,第一行的cpu就是8个cpu的指标数据汇总,因为是要计算系统cpu的使用率,那当然应该以pu为基准了。两次cpu指标数据如下:

cpu1  9931551 1082101 9002534 174463041 340947 1060438 1088978 0 0 0
cpu2  9931673 1082113 9002679 174466561 340954 1060446 1088994 0 0 0

其各项指标如下:

CPU (user, nice, system, idle, iowait, irq, softirq, stealstolen, guest);

拿cpu1(9931551 1082101 9002534 174463041 340947 1060438 1088978 0 0 0)的意思,下面,我就来详细解释下数据的含义。

  • user(993):表示从系统启动到现在551的用户状态开始的运行不包含nice的进程。

  • nice(1082101) :表示从系统启动到现在的 CPU 时间。

  • system(900):表示从系统启动时间2到现在开始内核态的运行。

  • idle(174463041) :表示从系统开始除现在开始IO等待时间以外的其他等待时间。

  • iowait(340947):表示从系统启动到现在的IO等待时间。(从Linux V2.5.41开始)

  • irq(1060438):表示从系统启动到现在的终止时间。(从Linux V2.6.0-test4开始)

  • softirq(1088978):表示从系统启动到现在的软中断时间。(从Linux V2.6.0-test4开始)

  • stealstolen(0) :表示当在虚拟机运行时在其他操作系统中启动环境中所花费的时间。在Android系统下这个0。(从Linux V2.6.11)

  • guest(0) :表示当在Linux内核的控制下运行虚拟CPU所下为系统所花费的时间。在Android系统下此值为0。(从V2.6.24)


此外,这些计算量的计算结果, jiffies是内核中的一个数字变量,

jiffies, jiffies,jiffies,jiffies,jiffies,jiffies,jiffies,jiffies,jiffies,journalingsystems,because of the Festival’s Beats

的 Linux 系统内核这个值可能不同,通常在 1ms 到 10ms 之间

了解了/proc/stat命令下各项参数的含义之后,我们就可以由两个时间点的CPU数据时间计算得出cpu1和cpu2的活动,如下所示:

totalCPUTime = user + nice + system + idle + iowait + irq + softirq + stealstolen + guest 
cpu1 = 9931551 + 1082101 + 9002534 +  174463041 + 340947 + 1060438 + 1088978 + 0  + 0 + 0 = 196969590jiffies
cpu2 = 9931673 + 1082113 + 9002679 +  174466561 + 340954 + 1060446 + 1088994 + 0 + 0 + 0 = 196973420jiffies

因此可以得出总的 CPU 时间如下所示:

totalCPUTime = CPU2 – CPU1 = 3830jiffies

最后,我们就可以计算出系统CPU的使用率,如下所示:

// 先计算得到CPU的空闲时间
idleCPUTime = idle2 – idle1 = 3520jiffies
// 最后得到系统CPU的使用率
totalCPUUse = (totalCPUTime – idleCPUTime) / totalCPUTime = (3830 - 3520)/ 3830 = 8%

可以看到,前后左右的时间大概为8%,系统的CPU使用率是8%,如果CPU使用率一直在大于6%,则表示系统需要占用一段时间的状态,就可以了进一步分析时间和系统时间的比例,看看到底是系统占用了CPU还是应用占用了CPU。

3、使用top命令查看应用进程的CPU占用情况

另外,由于Android是基于Linux自然内核构建的操作系统,而然也能使用Linux的一些常用命令。我们使用top命令查看类进程是CPU的主要运行时间者。

1|platina:/ $ top
PID USER         PR  NI VIRT  RES  SHR S[%CPU] %MEM     TIME+ ARGS
12700 u0_a945      10 -10 4.3G 122M  67M S 15.6   2.1   1:06.41 json.chao.com.w+
753 system       RT   0  90M 1.1M 1.0M S 13.6   0.0 127:47.73 android.hardwar+
2064 system       18  -2 4.6G 309M 215M S 12.3   5.4 978:15.18 system_server
22142 u0_a163      20   0 2.0G  97M  41M S 10.3   1.6   2:22.99 com.tencent.mob+
2293 system       20   0 4.7G 250M  87M S  8.6   4.3 353:15.77 com.android.sys+

从以上-W最上面我们的1个Android应用程序占用了5个Awesome的CPU。此外,这里的命令下面列出了最常用的%,如下所示:

// 排除0%的进程信息
adb shell top | grep -v '0% S'

// 只打印1次按CPU排序的TOP 10的进程信息
adb shell top -m 10 -s cpu -n 1
|platina:/ $ top -d 1|grep json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M S 13.8   2.2   1:04.46 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M S 19.0   2.2   1:04.51 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M S 15.0   2.2   1:04.70 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M S  9.0   2.2   1:04.85 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M S 26.0   2.2   1:04.94 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M S  9.0   2.2   1:05.20 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M R 17.0   2.2   1:05.29 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M S 20.0   2.2   1:05.46 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M S  9.0   2.2   1:05.66 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M R 21.0   2.2   1:05.75 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M S 14.0   2.2   1:05.96 json.chao.com.w+

4、PS软件

除了顶级命令可以比较全面地查看整个 CPU 信息之外,如果我们要查看当前指定进程的时间已经在系统总占用时间的所有和其他占的状态信息的话,可以使用 ps 命令,常用的 ps 命令如下所示:

// 查看指定进程的状态信息
platina:/ $ ps -p 31333
USER           PID  PPID     VSZ    RSS WCHAN            ADDR S NAME
u0_a945      31333  1277 4521308 127460 0                   0 S json.chao.com.w+

// 查看指定进程已经消耗的CPU时间占系统总时间的百分比
platina:/ $ ps -o PCPU -p 31333
%CPU
10.8

其中输出参数的含义如下所示:

  • USER:用户名

  • PID:进程ID

  • PPID:父进程ID

  • VSZ:虚拟内存大小(1k为单位)

  • RSS:常驻内存大小(正在使用的页面)

  • WCHAN:进程在内核态中的运行时间

  • 指令指针:指针指针

  • NAME:进程名

最后的输出参数是当前进程的状态,表示有10种可能的状态显示:

R (running) S (sleeping) D (device I/O) T (stopped)  t (traced)
Z (zombie)  X (deader)   x (dead)       K (wakekill) W (waking)

可以看到,我们当前主是休眠的状态。

5、dumpsys cpuinfo

使用dumpsys命令获取的信息从ctop命令得到的信息要精炼,如下所示:

platina:/ $ dumpsys cpuinfo
Load: 1.92 / 1.59 / 0.97
CPU usage from 45482ms to 25373ms ago (2020-02-04 17:00:37.666 to 2020-02-04 17:00:57.775):
33% 2060/system_server: 22% user + 10% kernel / faults: 8152 minor 6 major
17% 2292/com.android.systemui: 12% user + 4.7% kernel / faults: 21636 minor 3 major
14% 750/android.hardware.sensors@1.0-service: 4.4% user + 10% kernel
6.1% 778/surfaceflinger: 3.3% user + 2.7% kernel / faults: 128 minor
3.3% 2598/com.miui.home: 2.8% user + 0.4% kernel / faults: 7655 minor 11 major
2.2% 2914/cnss_diag: 1.6% user + 0.6% kernel
1.9% 745/android.hardware.graphics.composer@2.1-service: 1.4% user + 0.5% kernel / faults: 5 minor
1.7% 4525/kworker/u16:6: 0% user + 1.7% kernel
1.6% 748/android.hardware.memtrack@1.0-service: 0.6% user + 0.9% kernel
1.4% 4551/kworker/u16:14: 0% user + 1.4% kernel
1.4% 31333/json.chao.com.wanandroid: 0.9% user + 0.4% kernel / faults: 3995 minor 22 major
1.1% 6670/kworker/u16:0: 0% user + 1.1% kernel
0.9% 448/mmc-cmdqd/0: 0% user + 0.9% kernel
0.7% 95/system: 0% user + 0.7% kernel
0.6% 4512/mdss_fb0: 0% user + 0.6% kernel
0.6% 7393/com.android.incallui: 0.6% user + 0% kernel / faults: 2272 minor
0.6% 594/logd: 0.4% user + 0.1% kernel / faults: 38 minor 3 major
0.5% 3108/com.xiaomi.xmsf: 0.2% user + 0.2% kernel / faults: 1812 minor
0.5% 4526/kworker/u16:9: 0% user + 0.5% kernel
0.5% 4621/com.gotokeep.keep: 0.3% user + 0.1% kernel / faults: 55 minor
0.5% 354/irq/267-NVT-ts: 0% user + 0.5% kernel
0.5% 2572/com.android.phone: 0.3% user + 0.1% kernel / faults: 323 minor
0.5% 4554/kworker/u16:15: 0% user + 0.5% kernel
0.4% 290/kgsl_worker_thr: 0% user + 0.4% kernel
0.3% 2933/irq/61-1008000.: 0% user + 0.3% kernel
0.3% 3932/com.tencent.mm: 0.2% user + 0% kernel / faults: 647 minor 1 major
0.3% 4550/kworker/u16:13: 0% user + 0.3% kernel
0.3% 744/android.hardware.graphics.allocator@2.0-service: 0% user + 0.3% kernel / faults: 48 minor
0.3% 8906/com.tencent.mm:appbrand0: 0.2% user + 0% kernel / faults: 45 minor
0.2% 79/smem_native_rpm: 0% user + 0.2% kernel
0.2% 759/vendor.qti.hardware.perf@1.0-service: 0% user + 0.2% kernel / faults: 46 minor
0.2% 3197/com.miui.powerkeeper: 0% user + 0.1% kernel / faults: 141 minor
0.2% 4489/kworker/1:1: 0% user + 0.2% kernel
0.2% 595/servicemanager: 0% user + 0.2% kernel
0.2% 754/android.hardware.wifi@1.0-service: 0.1% user + 0% kernel
0.2% 1258/jbd2/dm-2-8: 0% user + 0.2% kernel
0.2% 5800/com.eg.android.AlipayGphone: 0.1% user + 0% kernel / faults: 48 minor
0.2% 21590/iptables-restore: 0% user + 0.1% kernel / faults: 563 minor
0.2% 21592/ip6tables-restore: 0% user + 0.1% kernel / faults: 647 minor
0.1% 3/ksoftirqd/0: 0% user + 0.1% kernel
0.1% 442/cfinteractive: 0% user + 0.1% kernel
0.1% 568/ueventd: 0% user + 0% kernel
0.1% 1295/netd: 0% user + 0.1% kernel / faults: 250 minor
0.1% 3002/com.miui.securitycenter.remote: 0.1% user + 0% kernel / faults: 818 minor 1 major
0.1% 20555/com.eg.android.AlipayGphone:push: 0% user + 0% kernel / faults: 20 minor
0.1% 7/rcu_preempt: 0% user + 0.1% kernel
0.1% 15/ksoftirqd/1: 0% user + 0.1% kernel
0.1% 76/lpass_smem_glin: 0% user + 0.1% kernel
0.1% 1299/rild: 0.1% user + 0% kernel / faults: 12 minor
0.1% 1448/android.process.acore: 0.1% user + 0% kernel / faults: 1719 minor
0% 4419/com.google.android.webview:s: 0% user + 0% kernel / faults: 602 minor
0% 20465/com.miui.hybrid: 0% user + 0% kernel / faults: 1575 minor
0% 10/rcuop/0: 0% user + 0% kernel
0% 75/smem_native_lpa: 0% user + 0% kernel
0% 90/kcompactd0: 0% user + 0% kernel
0% 1508/msm_irqbalance: 0% user + 0% kernel
0% 1745/cds_mc_thread: 0% user + 0% kernel
0% 2899/charge_logger: 0% user + 0% kernel
0% 3612/com.tencent.mm:tools: 0% user + 0% kernel / faults: 29 minor
0% 4203/kworker/0:0: 0% user + 0% kernel
0% 7377/com.android.server.telecom:ui: 0% user + 0% kernel / faults: 1083 minor
0% 32113/com.tencent.mobileqq: 0% user + 0% kernel / faults: 49 minor
0% 8/rcu_sched: 0% user + 0% kernel
0% 22/ksoftirqd/2: 0% user + 0% kernel
0% 25/rcuop/2: 0% user + 0% kernel
0% 29/ksoftirqd/3: 0% user + 0% kernel
0% 39/rcuop/4: 0% user + 0% kernel
0% 53/rcuop/6: 0% user + 0% kernel
0% 487/irq/715-ima-rdy: 0% user + 0% kernel
0% 749/android.hardware.power@1.0-service: 0% user + 0% kernel
0% 764/healthd: 0% user + 0% kernel / faults: 2 minor
0% 845/wlan_logging_th: 0% user + 0% kernel
0% 860/mm-pp-dpps: 0% user + 0% kernel
0% 1297/wificond: 0% user + 0% kernel / faults: 12 minor
 0% 1309/com.miui.weather2: 0% user + 0% kernel / faults: 729 minor 23 major
0% 1542/rild: 0% user + 0% kernel / faults: 3 minor
0% 2915/tcpdump: 0% user + 0% kernel / faults: 6 minor
0% 2974/com.tencent.mobileqq:MSF: 0% user + 0% kernel / faults: 121 minor
0% 3044/com.miui.contentcatcher: 0% user + 0% kernel / faults: 315 minor
0% 3057/com.miui.dmregservice: 0% user + 0% kernel / faults: 332 minor
0% 3095/com.xiaomi.mircs: 0% user + 0% kernel
0% 3115/com.xiaomi.finddevice: 0% user + 0% kernel / faults: 270 minor 3 major
0% 3513/com.xiaomi.metoknlp: 0% user + 0% kernel / faults: 136 minor
0% 3603/com.tencent.mm:toolsmp: 0% user + 0% kernel / faults: 35 minor
0% 4527/kworker/u16:11: 0% user + 0% kernel
0% 4841/com.gotokeep.keep:xg_service_v4: 0% user + 0% kernel / faults: 275 minor
0% 5064/com.sohu.inputmethod.sogou.xiaomi: 0% user + 0% kernel / faults: 102 minor
0% 5257/kworker/0:1: 0% user + 0% kernel
0% 5839/com.tencent.mm:push: 0% user + 0% kernel / faults: 98 minor
0% 6644/kworker/3:2: 0% user + 0% kernel
0% 6657/com.miui.wmsvc: 0% user + 0% kernel / faults: 52 minor
0% 6945/com.xiaomi.account:accountservice: 0% user + 0% kernel / faults: 1 minor
0% 9387/com.tencent.mm:appbrand1: 0% user + 0% kernel / faults: 27 minor
13% TOTAL: 6.8% user + 5.3% kernel + 0.2% iowait + 0.3% irq + 0.4% softirq

当天的信息表明,第一行显示的负载是(平均值):负载:1.92 / 159 /

0.92 / 159 / 0.92 / 0.92 / 157 从今天开始只需一个核心的数字,而只是为了降低数字的数字表示,或者说这里有更好的机器。这里需要加载一个核心是,问题的核心数字为 8,所以每一个核心数字都需要注意CPU的Load为0.24 / 0.20 / 0.12,如果Load超过1,则表示出现了问题。

另外,系统CPU占用是system_server,资源的wanandroid应用程序当中最多只占用了

1.4%的CPU资源,有0.9%是用户态所占用的时间,0.4%是内核态所占用的时间。最后,我们可以看到系统总占用的CPU时间是13%,这个值是根据

前面所有的值加起来/系统CPU数

的处理的,也是

104% / 8 = 13%

它们能够帮助我们有效地评估、并且树顿这两种情况,同时还能以这种方式来分析地优化应用程序发生率。的卡顿。

卡顿率

就像深入探索Android稳定性优化一文中讲到的UV、PV崩溃率,卡顿有它的UV、PV卡顿率,UV就是唯一的访问者,同样的一台手机客户端也可以指一个访问者,00:00-24:00 相同的客户端只计算一次。而PV即Page View,即页面浏览量或点击量。所以UV、PV卡顿率的定义即为如下所示:

// UV 卡顿率可以评估卡顿的影响范围
UV 卡顿率 = 发生过卡顿 UV / 开启卡顿采集 UV
// PV 卡顿率评估卡顿的严重程度
PV 卡顿率 = 发生过卡顿 PV / 启动采集 PV

因为卡顿的规则是类似的,所以一般情况下,问题上报的方式都应该是每个用户都来使用的。如果有一个用户可以在一天内使用,如果有任何记录,都可以与用户一起使用。

卡顿树

由于可以看到当时的具体情况,可以看到实时场景的性能。在大应用上出现非常多所以大的问题时,大应用上出现的卡顿值如3个一样,就像我们每天一样,就可以抑制各种不同的情况,我们每天都可以按不同的比例来聚合各种不同的大小。类的卡顿信息就很能从卡顿上看到它去解决卡顿问题最多的地方,以便于我们能够使用我们最顶级的卡顿问题获取最大的能量。优化效果的目的。

3、卡顿优化工具

1、CPU Profiler回顾

CPU Profiler 的用户还可以去深入探索 Android 启动速度中的详细分析过去,如果对 CPU Profiler 不是很复杂的话,看看文章。

下面来简单来回顾一下CPU Profiler。

优势:
  • 图形的形式展示执行时间、调用栈等。

  • 信息全面,包含所有问题。

劣势:

运行时,广泛的范围,可以改变我们的范围。

使用方式:
Debug.startMethodTracing("");
// 需要检测的代码片段
...
Debug.stopMethodTracing();

最终生成的生成文件在sd卡:Android/data/packagename/files。

2、Systrace回顾

systrace 也使用了 Linux 的 ftrace 调试(trace 是用来了解 Linux 内核内部运行情况的),相当于在系统的关键代码工具的位置上安装了一些性能指标,一些 ftrace 的性能指标,给了一些调试的性能。在 ftrace 的上封装跟踪,并增加了更多的直接前端,比如 Graphics、Activity Manager、Dalvik VM、System Server等等。文章中已经详细分析过了,如果对 Systrace 还不是很的话可以去看看这篇文章。

下面我们来简单回顾一下Systrace。

作用:

监控和跟踪API调用、线程报告运行情况,生成HTML。

建议:

API 18以上使用,推荐使用TraceCompat。

使用方式:

使用python命令执行脚本,背面添加铭牌,如下所示:

python systrace.py -t 10 [other-options] [categories]
优势:
  • 1、轻量级,轻量级。

  • 2、它能够准确地感知CPU的特性。

  • 3、根据我们的应用程序显示的比较具体的建议,它会告诉我们应用程序的具体情况,或者说GC比较好。

编译时给我们每一个函数通过的来实现自动插线下增加应用程序关注的过滤过滤器,需要通过这个短函数以通过性能提高的方式,还可以(最后一点黑)程序的调用方式,包括应用程序的线程调用,例如配置线程的函数调用,例如配置、线程锁,消耗整个应用程序的函数。时等。

的考虑,如果不是要在线上使用这个环境,最好只是对环境的表现。虽然只是简单的插桩方案,但影响很大,建议在线中使用。

此外,您需要分析原生函数的调用,请使用 Android 5.0 新增的Simpleperf性能分析工具,它利用了 CPU 的性能监控(PMU)提供的硬件性能事件。使用 Simpleperf 可以看到所有的原生代码的单元在类,直接调用类,比较支持类库对有帮助分析问题的类 Android 3.2 3.2 也很简单。(API Level 26+)),这方便了原生代码的调试。

3、严格模式

Strict 2.3引入了一个运行检测类型的人员,它的模式被称为严苛模式,是Android提供的时时机制,可以帮助开发人员使用工具代码中的一些不规范的问题。对于我们的项目使用检测自动,可能比较有可能会出现一些问题我们的配置提出了相应的反应。

StrictMode 是非常强大的,但是它可能因为对而不是发射掉它。

1、针对策略

线程策略的内容,是一些自定义的查询、查询等操作网络。

2、虚拟机策略

虚拟机策略的检测内容如下:

  • 活动漏

  • Sqlite对象泄漏

  • 检测实例数量

严格模式实战

如果要在应用中使用 StrictMode,只需要在应用程序中的创建方法中对 StrictMode 进行统一配置,代码如下所示:

private void initStrictMode() {
    // 1、设置Debug标志位,仅仅在线下环境才使用StrictMode
    if (DEV_MODE) {
        // 2、设置线程策略
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                .detectCustomSlowCalls() //API等级11,使用StrictMode.noteSlowCode
                .detectDiskReads()
                .detectDiskWrites()
                .detectNetwork()// or .detectAll() for all detectable problems
                .penaltyLog() //在Logcat 中打印违规异常信息
//              .penaltyDialog()//也可以直接跳出警报dialog
//              .penaltyDeath()//或者直接崩溃
                .build());
        // 3、设置虚拟机策略
        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                .detectLeakedSqlLiteObjects()
                // 给NewsItem对象的实例数量限制为1
                .setClassInstanceLimit(NewsItem.class, 1)
                .detectLeakedClosableObjects() //API等级11
                .penaltyLog()
                .build());
    }
}

注意使用“StrictMode”关键字过滤出结果的日志。

4、简介

Profilo 是一个用于收集应用程序生产版本的性能跟踪的 Android 库。

从Profilo 来说,它集成了一个trace 功能,ftrace 所有的性能数据通过排查数据,将文件写入到特定的跟踪,Profilo 使用了特定的操作,PLT Hook 拦截了写入,以选择部分有趣的事件执行的分析这样的系统跟踪事件我们所有的都可以到达,例如组件生命、锁定时间、类监控、GC时间等。pid|activityStart”,我们无法明确知道该事件的具体是由哪个 Activity 来创建的。

此外,能够快速获取 Java 资源。具体的当信号处理原理到当前为信号后,它获取正在执行的线程,通过线程对象就可以拿到当前线程的 ManagedStack,ManagedStack 的链表,它的 ShadowFrame 或 QuickFrame当前,指针,先与内部内部有关。

8be4e946725245fc0691c30f78e371a3.png

Profilo,可以处理同时运行的,我们还可以帮它做检查,并且可以同时运行的同时可以发射不计的功能。目前Profilo 8.0和Android 9.0的基本功能是不支持的。内部使用的大量黑科技手段,持续使用等功能,建议开启部分用户功能来开启该等功能。

简介项目地址

Systrace主要是根据Linux的ftrace机制来实现的,而ftrace的作用是帮助我们了解Linux内核的运行时行为,以便进行故障或性能分析。ftrace的整体架构如下所示:

e449f9ea17d08c1ae862b5b5dbe2041c.png

由上图管理,信息用于保存Ftrace有不同的跟踪部分,一个是框架中另外一个跟踪的功能,它们是在环形缓冲区中的一个跟踪框架,由 framework 负责管理。Framework 使用 debugfs 系统在 /debugfs 下建立跟踪目录,并提供了跟踪的控制文件。

下面,我们这里建议使用 PLTHook 技术来获取 Atrace 的日志的一个项目。

1、使用profilo的PLTHook来hook libc.so的write和__write_chk方法

使用PLTHook技术来获取Atrace的日志-项目地址

运行项目后,我们点击打开Atrace日志,然后就可以在Logcat中看到如下的原生层日志信息:

2020-02-05 10:58:00.873 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ===============install systrace hoook==================
2020-02-05 10:58:00.879 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|inflate
2020-02-05 10:58:00.880 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|LinearLayout
2020-02-05 10:58:00.881 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.882 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|TextView
2020-02-05 10:58:00.884 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.885 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.888 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|notifyFramePending
2020-02-05 10:58:00.888 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.889 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|Choreographer#doFrame
2020-02-05 10:58:00.889 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|input
2020-02-05 10:58:00.889 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.889 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|traversal
2020-02-05 10:58:00.889 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|draw
2020-02-05 10:58:00.890 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|Record View#draw()
2020-02-05 10:58:00.891 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|DrawFrame
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|syncFrameState
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|prepareTree
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query
2020-02-05 10:58:00.891 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|setBuffersDimensions
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|dequeueBuffer
2020-02-05 10:58:00.894 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|importBuffer
2020-02-05 10:58:00.894 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|HIDL::IMapper::importBuffer::passthrough
2020-02-05 10:58:00.894 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.894 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.894 13052-13058/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|Compiling
2020-02-05 10:58:00.894 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.894 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query

需要注意的是,日志中的 Bbegin,也就是时间开始的时间,而 E 代表 End,即事件结束的时间,并且,B|Event 和 E|Event 是对出现的,这样我们就通过该事件的结束时间,例如可以显示使用事件的开始使用日志,我们在中间时间可以获取到 TextView 的draw。

此外,在下面这个项目里展示了如何使用PLTHook 技术来获取资源。

2、使用PLTH技术来获取线程的烹饪技巧

使用 PLTHook 来获取地址创建专业的技术-项目

运行项目后,我们的线程钩子,然后点击新建线程点击按钮。

2020-02-05 13:47:59.006 20159-20159/com.dodola.thread E/HOOOOOOOOK: stack:com.dodola.thread.ThreadHook.getStack(ThreadHook.java:16)
com.dodola.thread.MainActivity$2.onClick(MainActivity.java:40)
android.view.View.performClick(View.java:6311)
android.view.View$PerformClick.run(View.java:24833)
android.os.Handler.handleCallback(Handler.java:794)
android.os.Handler.dispatchMessage(Handler.java:99)
android.os.Looper.loop(Looper.java:173)
android.app.ActivityThread.main(ActivityThread.java:6653)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:821)
2020-02-05 13:47:59.007 20159-20339/com.dodola.thread E/HOOOOOOOOK: thread name:Thread-2
2020-02-05 13:47:59.008 20159-20339/com.dodola.thread E/HOOOOOOOOK: thread id:1057
2020-02-05 13:47:59.009 20159-20339/com.dodola.thread E/HOOOOOOOOK: stack:com.dodola.thread.ThreadHook.getStack(ThreadHook.java:16)
com.dodola.thread.MainActivity$2$1.run(MainActivity.java:38)
2020-02-05 13:47:59.011 20159-20340/com.dodola.thread E/HOOOOOOOOK: inner thread name:Thread-3
2020-02-05 13:47:59.012 20159-20340/com.dodola.thread E/HOOOOOOOOK: inner thread id:1058

Hook Pro 开发了这一篇关于性能的文章与 C/C++、NDK 开发的部分不做详细讲解,如对 NDK 的同学的可以期待下我后面,等Android-NDK 系列系列优化文章全部更新完毕,开始系统学习NDK相关的开发知识,预计之后去。

二、自动化卡顿检测方案及优化

1、为什么需要自动化卡顿检测方案?

  • 1 Sysr工具仅适用于线下分析等系统。

  • 2、线上及测试环境需要自动化的卡顿检测方案来定位卡顿,同时,更重要的是,能够记录卡顿发生时的场景。

2、卡顿检测方案原理

它的原理是Android的消息处理机制,一个线程不管有多少处理程序,有一个Looper存在,通过主线程的执行Looper.loop()方法。而在Looper函数中,它有它的执行方法。主处理这个对象在处理前面的被调用。)进行监控。

卡顿检测方案的具体实现步骤

首先,我们看下Looper用于执行消息循环的loop()方法,关键代码如下所示:

/**
 * Run the message queue in this thread. Be sure to call
 * {@link #quit()} to end the loop.
 */
public static void loop() {

    ...

    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }

        // This must be in a local variable, in case a UI event sets the logger
        final Printer logging = me.mLogging;
        if (logging != null) {
            // 1
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }

        ...

        try {
             // 2 
             msg.target.dispatchMessage(msg);
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }

        ...

        if (logging != null) {
            // 3
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }

在 Looper 的 loop() 方法中,在其执行的每个消息(注释 2 处)的前后都由记录进行了一次输出。可以看到,在执行打印前是输出的”>>>>> Dispatching to “,在执行消息后是输出的”<<<<< 完成到“,他们打印的消息不是的,我们就可以这样来判断执行消息的前后一样。

所以,具体的实现可以归纳为以下步骤:

  • 1、首先,我们需要Looper.getMainLooper().setMessageLogging()打印输出之后使用我们自己设置的打印机实现类去记录。这样,在每条消息执行之前和调用我们设置的这个打印机实现类。

  • 2、我们匹配到”>>>>>调度到“之后,我们就可以执行一行代码也就是在指定的时间,如果有值去去,我们在子线程执行一个任务,这个任务获取当前主线程的显示以及当前的一些情况信息,例如:内存大小、电脑、网络状态等。

  • 3、如果匹配到了”<<<<<Finished to”,那么说明在指定的执行值之内,消息就被完成了,说明此时产生我们的卡顿,就可以将这个子取消任务取消。

3、Android性能监视器

它是一个非监控式监控的性能组件,可以通过监控通知我们的监控信息。它的原理就是刚刚透露到的监控监控的实现原理。

下面通过一个简单的例子来接我们讲解一下它的使用。

首先,需要在moudles的build.gradle下配置它的依赖,如下所示:

// release:项目中实现了线上监控体系的时候去使用
api 'com.github.markzhai:blockcanary-android:1.5.0'

// 仅在debug包启用BlockCanary进行卡顿监控和提示的话,可以这么用
debugApi 'com.github.markzhai:blockcanary-android:1.5.0'
releaseApi 'com.github.markzhai:blockcanary-no-op:1.5.0'

在Application的onCreate方法中开启某个卡顿监控:

// 注意在主进程初始化调用
BlockCanary.install(this, new AppBlockCanaryContext()).start();

最后,继承BlockCanaryContext类去实现自己的监控配置时间类:

public class AppBlockCanaryContext extends BlockCanaryContext {
    // 实现各种上下文,包括应用标识符,用户uid,网络类型,卡顿判断阙值,Log保存位置等等

    /**
    * 提供应用的标识符
    *
    * @return 标识符能够在安装的时候被指定,建议为 version + flavor.
    */
    public String provideQualifier() {
        return "unknown";
    }

    /**
    * 提供用户uid,以便在上报时能够将对应的
    * 用户信息上报至服务器 
    *
    * @return user id
    */
    public String provideUid() {
        return "uid";
    }

    /**
    * 提供当前的网络类型
    *
    * @return {@link String} like 2G, 3G, 4G, wifi, etc.
    */
    public String provideNetworkType() {
        return "unknown";
    }

    /**
    * 配置监控的时间区间,超过这个时间区间    ,BlockCanary将会停止, use
    * with {@code BlockCanary}'s isMonitorDurationEnd
    *
    * @return monitor last duration (in hour)
    */
    public int provideMonitorDuration() {
        return -1;
    }

    /**
    * 指定判定为卡顿的阈值threshold (in millis),  
    * 你可以根据不同设备的性能去指定不同的阈值
    *
    * @return threshold in mills
    */
    public int provideBlockThreshold() {
        return 1000;
    }

    /**
    * 设置线程堆栈dump的间隔, 当阻塞发生的时候使用, BlockCanary 将会根据
    * 当前的循环周期在主线程去dump堆栈信息
    * <p>
    * 由于依赖于Looper的实现机制, 真实的dump周期 
    * 将会比设定的dump间隔要长(尤其是当CPU很繁忙的时候).
    * </p>
    *
    * @return dump interval (in millis)
    */
    public int provideDumpInterval() {
        return provideBlockThreshold();
    }

    /**
    * 保存log的路径, 比如 "/blockcanary/", 如果权限允许的话,
    * 会保存在本地sd卡中
    *
    * @return path of log files
    */
    public String providePath() {
        return "/blockcanary/";
    }

    /**
    * 是否需要通知去通知用户发生阻塞
    *
    * @return true if need, else if not need.
    */
    public boolean displayNotification() {
        return true;
    }

    /**
    * 用于将多个文件压缩为一个.zip文件
    *
    * @param src  files before compress
    * @param dest files compressed
    * @return true if compression is successful
    */
    public boolean zip(File[] src, File dest) {
        return false;
    }

    /**
    * 用于将已经被压缩好的.zip log文件上传至
    * APM后台
    *
    * @param zippedFile zipped file
    */
    public void upload(File zippedFile) {
        throw new UnsupportedOperationException();
    }

    /**
    * 用于设定包名, 默认使用进程名,
    *
    * @return null if simply concern only package with process name.
    */
    public List<String> concernPackages() {
        return null;
    }

    /**
    * 使用 @{code concernPackages}方法指定过滤的堆栈信息 
    *
    * @return true if filter, false it not.
    */
    public boolean filterNonConcernStack() {
        return false;
    }

    /**
    * 指定一个白名单, 在白名单的条目将不会出现在展示阻塞信息的UI中
    *
    * @return return null if you don't need white-list filter.
    */
    public List<String> provideWhiteList() {
        LinkedList<String> whiteList = new LinkedList<>();
        whiteList.add("org.chromium");
        return whiteList;
    }

    /**
    * 使用白名单的时候,是否去删除堆栈在白名单中的文件
    *
    * @return true if delete, false it not.
    */
    public boolean deleteFilesInWhiteList() {
        return true;
    }

    /**
    * 阻塞拦截器, 我们可以指定发生阻塞时应该做的工作
    */
    public void onBlock(Context context, BlockInfo blockInfo) {

    }
}

看到,在上面的配置中,我们指定了卡顿的大约在 1000ms 左右。

try {
    Thread.sleep(3000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

然后,我们运行项目,打开应用程序,自动打开类似 LeakCanary 之类的卡信息。

除了发生卡顿时,Canary 提供的对于卡顿的可视化大屏幕分析和测试人员直接查看顿原因之外最大的作用还是在线上环境自动化猴子测试其进行或者阻塞,的收集和分析的程度。,可以从以下两个纬度来进行:

  • 卡顿时间。

  • 已知异血出现的卡顿次数来进行示范和归类。

BlockCanary 的优势

  • 非侵入式。

  • 方便,代码能够轻松到达代码的行。

这种自动检测卡顿的方案有什么问题吗?

但卡顿的时间范围内,应用可能会发生问题下面我们先看看下面的一张示意图:

63d29114bb49b039dcf6b0bca7f928e2.png

主线程在T1到T2的时间段内发生事件,检测方案获取卡顿时的信息可能是T2,实际上发生卡顿的时刻是在区域内函数的某个时刻就在如此长的时间,同样在我们实现了真正的卡顿的时刻,其顿时的时机已经实现了,所以卡顿的2个时刻能够实现真正的突破。问题出来的信息只是一个表象,自己的藏身抵达。

进行,我们如何针对这种情况进行优化呢?

我们可以还原这些信息,我们可以在这里获取这些,而我们因为这里有一个,这样的卡顿就可以根据现场的情况来获取卡顿的信息。我们完全知道卡顿时间信息的产生原因,它的函数调用时间长了。,看看下面的卡顿检测结果我们比较下面:

a8feaa96fa9488dde4cacfdf99b7caf8.png

根据指示,可以实现为出成绩后的具体步骤:

  • 1、监控首先,我们会通过startMonitor方法对这个过程进行。

  • 2、,我们就开始采集到这里,或者我们方法。

  • 3、然后,将我们追踪到的周边信息。

  • 4、最后,在合适的时机上报给我们的服务器。

通过,我们就可以知道在整个卡周期内,是什么时候执行的比较之方法。

卡顿分析下的另一次海量问题的处理量又是我们在承受着巨大的压力,而如何处理服务端对这种压力的处理方式来了。

在出现我们应该关注的情况下,我们可能会发现我们在大卡顿的情况下,而这个世界各地的地方应该是我们对一个地方的信息。这类资源需要处理的资源很重,需要处理的资源。地卡顿的原因。

4、小结

在本节中,我们学习了自动化问题卡顿检测的原理,然后,我们使用这种方案进行了实战,最后,我介绍了这种方案的优化思路。

三、总结

在本篇文章中,我们主要对卡顿优化方法

的相关知识进行了全面的讲解,这里再简单总结一下本篇文章与工具的相关分析、主题分析:

  • 1、卡顿优化分析方法与工具介绍、卡顿分析使用shell命令分析CPU背景、卡顿优化工具。

  • 2、自动化卡顿检测方案及优化:卡检测方案原理、AndroidPerformanceMonitor实战及优化。