对于用户来说我们的应用当中的很多性能问题比如内存占用高、流量消耗快等不容易被发现,但是卡顿却很容易被直观的感受到,对于开发者来说,卡顿问题又难以定位,那么它究竟难在哪里呢?
卡顿问题难点:
①、CPU Profiler
使用方式:
②、Systrace
使用方式:
Systrace优点
③、StrictMode
现在到之前的Demo中来实际使用一下,找到我们的Application类,新增一个方法initStrictMode():
private void initStrictMode(){
if (DEV_MODE) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectCustomSlowCalls() //API等级11,使用StrictMode.noteSlowCode
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()// or .detectAll() for all detectable problems
.penaltyLog() //在Logcat 中打印违规异常信息
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.setClassInstanceLimit(FeedBean.class, 1)
.detectLeakedClosableObjects() //API等级11
.penaltyLog()
.build());
}
}
首先在这里加了一个标记位DEV_MODE,也就是只在线下开发的时候才会走到这个方法。对于线程策略使用方式就是StrictMode.setThreadPolicy,然后就是一些配置比如磁盘的读取、写入、网络监控等,如果出现了违规情况我们使用的是penaltyLog()方法在日志中打印出违规信息,这里你也可以选择别的方式。对于虚拟机策略这里是配置需要检测出Sqlite对象的泄露,并且这里还设置某个类的实例数量是x,如果大于x它应该会被检测出不合规。
我在这里从Looper.java的loop()方法的源码中截取了一段代码,大家看下:
// This must be in a local variable, in case a UI event sets the logger
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
......
此处省略一大段代码
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
它在Message执行的前后都打印了一段日志并且是不同的,所以我们可以通过这个来判断Message处理的开始和结束的时机。
具体的实现原理:
下面我们在项目中实际使用一下:
首先在application中进行初始化:
//BlockCanary初始化
BlockCanary.install(this,new AppBlockCanaryContext()).start();
这里入参有一个AppBlockCanaryContext,这个是我们自定义BlockCanary配置的一些信息:
public class AppBlockCanaryContext extends BlockCanaryContext {
public String provideQualifier() {
return "unknown";
}
public String provideUid() {
return "uid";
}
public String provideNetworkType() {
return "unknown";
}
public int provideMonitorDuration() {
return -1;
}
//设置卡顿阈值为500ms
public int provideBlockThreshold() {
return 500;
}
public int provideDumpInterval() {
return provideBlockThreshold();
}
public String providePath() {
return "/blockcanary/";
}
public boolean displayNotification() {
return true;
}
public boolean zip(File[] src, File dest) {
return false;
}
public void upload(File zippedFile) {
throw new UnsupportedOperationException();
}
public List<String> concernPackages() {
return null;
}
public boolean filterNonConcernStack() {
return false;
}
public List<String> provideWhiteList() {
LinkedList<String> whiteList = new LinkedList<>();
whiteList.add("org.chromium");
return whiteList;
}
public boolean deleteFilesInWhiteList() {
return true;
}
public void onBlock(Context context, BlockInfo blockInfo) {
Log.i("jarchie","blockInfo "+blockInfo.toString());
}
}
然后在MainActivity中模拟一次卡顿,让当前线程休息2s,然后来看一下这个组件会不会通知我们:
try {
Thread.currentThread().sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
当我们把程序运行之后,会发现手机桌面上出现了一个Blocks的图标,这个玩意和之前我们使用LeakCanary的时候有点像哈,然后点进去果然发现了刚刚的Block信息,如下所示:

这里详细的打出了当前的CPU核心数、进程名、内存情况、block的堆栈信息等等,我们就可以根据这些堆栈找到对应哪个类的哪一行代码出现了问题,然后进行修改即可。
对于这种方案的总结如下:
这种方案网上有很多的使用资料,但是实际上它也是存在一定的问题的,自动检测方案的问题:

举个栗子:主线程在T1 T2时间段内发生了卡顿,卡顿检测方案获取卡顿堆栈的信息是T2时刻,但是实际情况可能是整个这一段时间之内某个函数的耗时过长导致的卡顿,捕获堆栈的时机此时该函数已经执行完成,所以在T2时刻捕获的堆栈信息并不能准确的反应现场情况。
自动检测方案优化

海量卡顿堆栈处理:高频卡顿上报量太大,会导致服务端有压力
ANR(Application Not Responding)是指应用程序未响应,Android 系统对于一些事件需要在一定时间范围内完成,如果超过预定时间未能得到有效响应或者响应时间过长,都会造成 ANR。
ANR 的产生需要满足三个条件:
为了降低因网络访问导致的 ANR,在 Android 4.0 之后强制规定访问网络必须在子线程处理,如果在主线程访问网络将会抛出 NetworkOnMainThreadException。
只要是耗时操作都可能会阻塞主线程,耗时操作要求放在子线程。
不同的场景产生 ANR 的方式也不同,在这里详细讲讲各种情况产生的场景。
| ANR 事件 | 超时时间 | 相应日志描述 |
|---|---|---|
| 点击事件(按键和触摸事件) | 5s 内没被处理 | Input event dispatching timed out |
| Service | 前台 Service 20s,后台 Service 200s 未完成启动 | Timeout executing service |
| BroadcastReceiver | 前台广播 10s,后台广播 60s,onReceive() 在规定时间内没处理完 | Timeout of broadcast Broadcast Record |
| ContentProvider | publish 在 10s 内没处理完 | Timeout publishing content providers |
需要注意的是,前台广播的 ANR 时间虽然是 10s 内 onReceive() 没有执行完就提示,这是在没有点击触摸事件导致 ANR 的前提下才是 10s,否则会先触发点击事件的 ANR,onReceive() 有可能执行不到 10s 就发生 ANR,所以不要在 onReceive() 处理耗时操作。
在实际项目中,大多数的 ANR 都是点击触摸事件超时导致,会超时的原因也主要由以下三个原因导致:
所以,我们想要得到为什么会出现 ANR,就必须对于原理了解清楚,且知道有多少情况会导致出现事件被拉长的问题。

在网上有很多分析 ANR 的文章都都将 ANR 触发过程理解为装炸弹和拆炸弹的过程,但说到本质上,系统内部对于 ANR 的触发流程其实也很简单,ANR 也是建立在主线程 Looper 机制上的,简单理解就是 先发送一个延时消息,然后在特定位置移除这个消息,如果消息没有被移除则证明整个流程出现问题,执行 ANR 处理。
触发 ANR 生成日志时,在不同的系统版本会有所不同,上图中是通过 ANRHelper 类处理 ANR 日志收集,在其他较低系统版本上是 AppErrors 类处理 ANR 日志收集。
当发生 ANR 时系统会在 /data/anr/ 目录额外生成一份 traces.txt 日志,方便我们可以了解到发生 ANR 时的基本信息和堆栈信息。
traces.txt 日志信息如下(以主线程为例):
// main 代表的主线程
// Native 是线程状态
// 下面的是堆栈信息
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 obj=0x73cff4c0 self=0xafa84400
| sysTid=5790 nice=0 cgrp=top-app sched=1073741825/1 handle=0xb2717534
| state=S schedstat=( 3240607247 80660807 2053 ) utm=283 stm=41 core=1 HZ=100
| stack=0xbe7c1000-0xbe7c3000 stackSize=8MB
| held mutexes=
kernel: (couldn't read /proc/self/task/5790/stack)
native: #00 pc 00048968 /system/lib/libc.so (__ioctl+8)
native: #01 pc 0001b0cf /system/lib/libc.so (ioctl+38)
native: #02 pc 0003cd25 /system/lib/libbinder.so (_ZN7android14IPCThreadState14talkWithDriverEb+168)
native: #03 pc 0003d737 /system/lib/libbinder.so (_ZN7android14IPCThreadState15waitForResponseEPNS_6ParcelEPi+238)
native: #04 pc 0003662d /system/lib/libbinder.so (_ZN7android8BpBinder8transactEjRKNS_6ParcelEPS1_j+36)
native: #05 pc 000999cf /system/lib/libandroid_runtime.so (???)
native: #06 pc 00607b09 /system/framework/arm/boot-framework.oat (Java_android_os_BinderProxy_transactNative__ILandroid_os_Parcel_2Landroid_os_Parcel_2I+140)
at android.os.BinderProxy.transactNative(Native method)
at android.os.BinderProxy.transact(Binder.java:930)
at android.view.IWindowSession$Stub$Proxy.remove(IWindowSession.java:924)
at android.view.ViewRootImpl.dispatchDetachedFromWindow(ViewRootImpl.java:3306)
at android.view.ViewRootImpl.doDie(ViewRootImpl.java:5961)
- locked <0x0ed5befa> (a android.view.ViewRootImpl)
at android.view.ViewRootImpl.die(ViewRootImpl.java:5938)
at android.view.WindowManagerGlobal.removeViewLocked(WindowManagerGlobal.java:459)
at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:397)
- locked <0x05ba7d9d> (a java.lang.Object)
at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:126)
at d.h.b.q.n.a$c.removeViewImmediate(SourceFile:1)
at android.widget.Toast$TN.handleHide(Toast.java:496)
at android.widget.Toast$TN$2.handleMessage(Toast.java:346)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6193)
at java.lang.reflect.Method.invoke!(Native method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
| 关键信息 | 描述 |
|---|---|
| main | main 标识是主线程。在 traces.txt 中如果是线程会命名为 “thread-x” 的格式,x 表示线程 id,逐步递增 |
| prio | 线程优先级,默认是 5 |
| tid | tid 不是线程的 id,是线程唯一标识 id |
| group | 线程组名称 |
| sCount | 该线程被挂起的次数 |
| dsCount | 线程被调试器挂起的次数 |
| obj | 对象地址 |
| self | 该线程 native 的地址 |
| sysTid | 线程号(主线程的线程号和进程号相同) |
| nice | 线程的调度优先级 |
| sched | 分别标志了线程的调度策略和优先级 |
| cgrp | 调度归属组 |
| handle | 线程处理函数的地址 |
| state | 调度状态 |
| schedstat | 从 /proc/[pid]/task/[tid]/schedstat 读出,三个值分别表示线程在 cpu 上执行的时间、线程的等待时间和线程执行的时间片长度,不支持这项信息的三个值都是 0 |
| utm | 线程用户态下使用的时间值(单位是 jiffies) |
| stm | 内核态下的调度时间值 |
| core | 最后执行这个线程的 cpu 核的序号 |
| 线程状态 | 说明 | 描述 |
|---|---|---|
| UNDEFINED = -1 | ||
| ZOMBIE = 0 | TERMINATED | 线程已经终止 |
| RUNNING = 1 | RUNNABLE or running now | 正常运行 |
| TIMED_WAIT = 2 | TIMED_WAITING Object.wait() | 等待,一般是调用 Object.wait(2000) |
| MONITOR = 3 | BLOCKED on a monitor | synchronized |
| WAIT = 4 | WAITING in Object.wait() | 调用 Object.wait() 或 LockSupport.park() 等待 |
| INITIALIZING = 5 | allocated, not yet running | 已经初始化线程,但是还没有进行 start |
| STARTING = 6 | started, not yet on thread list | 已经 start 但是没有 run |
| NATIVE = 7 | off in a JNI native method | native 线程出问题,有三种常见情况: 1、项目自己有 JNI 开发线程 2、有 IO 操作(IO 的本质是调用 Linux 内核的函数) 3、AQS 锁住了 |
| VMWAIT = 8 | waiting on a VM resource | 没有时间片 |
| SUSPENDED = 9 | suspended,usually by GC or debugger | 被 GC 挂起的(该情况发生的可能性不高) |
| Blocked | 死锁(查看 CPU usage 会发现几乎都是 0%,这也是死锁的体现) |
其中线程状态和堆栈信息是我们最关心的,它能够帮助我们快速定位到具体位置(堆栈信息有我们应用的函数调用的话)。
日志分析思路主要可以分为四个步骤:
在发生 ANR 的时候,系统会收集 ANR 相关的信息提供给开发者:
简单说就是我们至少需要两份文件:/data/anr/traces.txt 和 mainlog 日志。如果有 eventlog 能更快的定位到 ANR 的类型,当然 traces.txt 和 mainlog 也能分析得到。
traces.txt 文件通过命令 adb pull /data/anr/ 获取。
mainlog 日志需要在程序运行时就时刻记录 adb logcat -v time -b main > mainlog.log。
当我们拿到 traces.txt 文件时,主要找四个信息:发生 ANR 时的时间节点、主线程状态(在文件中搜索 main)、ANR 类型、事故点(堆栈信息中找到我们应用中的函数)。
分析发生 ANR 时进程中各个线程的堆栈,一般有几种情况:
/data/anr/traces.txt
// 发生 ANR 时的时间节点
----- pid 5790 at 2022-07-19 13:23:32 -----
// 主线程状态
"main" prio=5 tid=1 Waiting
// 事故点
// 不一定能找到我们应用的调用函数,因为抓 trace 的时候耗时操作可能已经执行完了,例如下面的堆栈
at android.os.BinderProxy.transactNative(Native method)
at android.os.BinderProxy.transact(Binder.java:930)
at android.view.IWindowSession$Stub$Proxy.remove(IWindowSession.java:924)
at android.view.ViewRootImpl.dispatchDetachedFromWindow(ViewRootImpl.java:3306)
at android.view.ViewRootImpl.doDie(ViewRootImpl.java:5961)
- locked <0x0ed5befa> (a android.view.ViewRootImpl)
at android.view.ViewRootImpl.die(ViewRootImpl.java:5938)
at android.view.WindowManagerGlobal.removeViewLocked(WindowManagerGlobal.java:459)
at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:397)
- locked <0x05ba7d9d> (a java.lang.Object)
at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:126)
at d.h.b.q.n.a$c.removeViewImmediate(SourceFile:1)
at android.widget.Toast$TN.handleHide(Toast.java:496)
at android.widget.Toast$TN$2.handleMessage(Toast.java:346)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6193)
at java.lang.reflect.Method.invoke!(Native method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
因为从上面的堆栈中其实不能分析到 ANR 类型,所以可以再借助 eventlog 或 mainlog 日志找到,可以在 mainlog 日志 搜索关键词 ANR in:
mainlog.log(对应的 adb logcat -v time -b main > mainlog.log)
07-19 13:23:38.029 2003 2016 E ActivityManager: ANR in com.example.demo (com.example.demo/.ui.login.LoginActivity)
07-19 13:23:38.029 2003 2016 E ActivityManager: PID: 5790
07-19 13:23:38.029 2003 2016 E ActivityManager: PSS: 42718 kB
07-19 13:23:38.029 2003 2016 E ActivityManager: Reason: Input dispatching timed out (Waiting to send key event because the focused window has not finished processing all of the input events that were previously delivered to it. Outbound queue length: 0. Wait queue length: 1.)
07-19 13:23:38.029 2003 2016 E ActivityManager: Load: 16.16 / 10.88 / 4.95
在 eventlog 日志 搜索关键词 am_anr:
07-19 13:22:29.166 2003 2016 I am_anr : [0,3392,com.example.demo,955792964,Input dispatching timed out (Waiting because no window has focus but there is a focused application that may eventually add a window when it finishes starting up.)]
在 mainlog 日志中根据 ANR 时间节点并 搜索关键词 CPU usage 查看发生 ANR 前后 CPU 的使用情况,从 CPU usage 信息中大概可以分析:
mainlog.log
07-19 13:23:38.029 2003 2016 E ActivityManager: CPU usage from 2068ms to -8489ms ago (2022-07-19 13:23:27.434 to 2022-07-19 13:23:37.991):
07-19 13:23:38.029 2003 2016 E ActivityManager: 30% 2003/system_server: 16% user + 14% kernel / faults: 7835 minor 541 major
07-19 13:23:38.029 2003 2016 E ActivityManager: 13% 5790/com.example.demo: 9.2% user + 3.9% kernel / faults: 11775 minor 141 major
07-19 13:23:38.029 2003 2016 E ActivityManager: 5.4% 247/mmcqd/0: 0% user + 5.4% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 5.4% 2591/com.xtc.i3launcher: 4.3% user + 1% kernel / faults: 4186 minor 276 major
07-19 13:23:38.029 2003 2016 E ActivityManager: 3.3% 36/kworker/u8:3: 0% user + 3.3% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 2.9% 410/audioserver: 1.7% user + 1.2% kernel / faults: 79 minor 3 major
07-19 13:23:38.029 2003 2016 E ActivityManager: 2.8% 5754/com.netease.xtc.cloudmusic: 2.8% user + 0% kernel / faults: 1954 minor 315 major
07-19 13:23:38.029 2003 2016 E ActivityManager: 2.3% 2457/com.android.phone: 1.6% user + 0.7% kernel / faults: 2226 minor 513 major
07-19 13:23:38.029 2003 2016 E ActivityManager: 2.2% 35/kworker/u8:2: 0% user + 2.2% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 2.2% 356/surfaceflinger: 0.9% user + 1.3% kernel / faults: 464 minor
07-19 13:23:38.029 2003 2016 E ActivityManager: 2.1% 110/kswapd0: 0% user + 2.1% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 2% 448/kworker/u8:8: 0% user + 2% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 1.9% 444/kworker/u8:7: 0% user + 1.9% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 1.9% 4683/kworker/u8:9: 0% user + 1.9% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 1.4% 4782/com.android.commands.monkey: 0.5% user + 0.8% kernel / faults: 1598 minor 3 major
07-19 13:23:38.029 2003 2016 E ActivityManager: 1.1% 299/logd: 0.5% user + 0.5% kernel / faults: 200 minor 115 major
07-19 13:23:38.029 2003 2016 E ActivityManager: 1.1% 3499/super_log: 0.2% user + 0.8% kernel / faults: 69 minor 1 major
07-19 13:23:38.029 2003 2016 E ActivityManager: 0.9% 7/rcu_preempt: 0% user + 0.9% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0.8% 2795/com.android.dialer: 0.7% user + 0% kernel / faults: 1270 minor 221 major
07-19 13:23:38.029 2003 2016 E ActivityManager: 0.4% 4696/mdss_fb0: 0% user + 0.4% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0.3% 12/ksoftirqd/1: 0% user + 0.3% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0.3% 20/ksoftirqd/3: 0% user + 0.3% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0.3% 258/core_ctl/0: 0% user + 0.3% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0.3% 2178/VosRXThread: 0% user + 0.3% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0.3% 2183/sdcard: 0% user + 0.3% kernel / faults: 42 minor 3 major
07-19 13:23:38.029 2003 2016 E ActivityManager: 0.2% 3/ksoftirqd/0: 0% user + 0.2% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0.2% 290/jbd2/mmcblk0p43: 0% user + 0.2% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0.1% 15/migration/2: 0% user + 0.1% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0.1% 16/ksoftirqd/2: 0% user + 0.1% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0.1% 19/migration/3: 0% user + 0.1% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0.1% 269/kworker/0:4: 0% user + 0.1% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0.1% 355/servicemanager: 0% user + 0% kernel / faults: 68 minor
07-19 13:23:38.029 2003 2016 E ActivityManager: 0% 1//init: 0% user + 0% kernel / faults: 6 minor 4 major
07-19 13:23:38.029 2003 2016 E ActivityManager: 0% 5/kworker/0:0H: 0% user + 0% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0% 8/rcu_sched: 0% user + 0% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0% 10/migration/0: 0% user + 0% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0% 11/migration/1: 0% user + 0% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0% 14/kworker/1:0H: 0% user + 0% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0% 135/mdp3_ppp: 0% user + 0% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0% 223/irq/114-5-0024: 0% user + 0% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0% 271/kworker/0:1H: 0% user + 0% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0% 276/kworker/3:3: 0% user + 0% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0% 309/debuggerd: 0% user + 0% kernel / faults: 237 minor 27 major
07-19 13:23:38.029 2003 2016 E ActivityManager: 0% 415/media.extractor: 0% user + 0% kernel / faults: 117 minor 66 major
07-19 13:23:38.029 2003 2016 E ActivityManager: 0% 419/netd: 0% user + 0% kernel / faults: 134 minor 2 major
07-19 13:23:38.029 2003 2016 E ActivityManager: 0% 2177/VosTXThread: 0% user + 0% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 0% 2713/com.xtc.i3launcher:push: 0% user + 0% kernel / faults: 955 minor 58 major
07-19 13:23:38.029 2003 2016 E ActivityManager: 0% 2767/perffeedbackd: 0% user + 0% kernel / faults: 77 minor
07-19 13:23:38.029 2003 2016 E ActivityManager: 0% 4546/kworker/1:3: 0% user + 0% kernel
07-19 13:23:38.029 2003 2016 E ActivityManager: 84% TOTAL: 14% user + 17% kernel + 51% iowait + 0.6% softirq
发生死锁时的 CPU 状态如下:

当上面分析了 CPU 状态后发现是非 CPU 问题时,就需要从内存 GC 分析,因为 GC 会触发 STW(Stop The World)导致线程执行时间被拉长。
// 这里只截取了一部分 GC 信息
// 如果还有其他信息,还需要分析如 GC 回收率等,下面的 GC 信息是正常的,这里仅用于展示
Total time waiting for GC to complete: 64.298ms
Total GC count: 30
Total GC time: 4.961s
Total blocking GC count: 1
Total blocking GC time: 149.286ms
1234567
其实 ANR 问题主要就是两类问题:
所以定位 ANR 总的来说就是:
以上的常规解决方案实际上只有一个核心,就是降低线程阻塞时间,将耗时操作放到子线程处理。
FileObserver 可以做到指定文件目录的监控功能,我们可以使用它实现监控 /data/anr 目录,当该目录有变动时即说明发生了 ANR。
public class ANRFileObserver extends FileObserver {
private static final String TAG = "ANRFileObserver";
public ANRFileObserver(String path) {
super(path);
}
@Override
public void onEvent(int event, @Nullable String path) {
switch (event) {
case FileObserver.ACCESS: // 文件被访问
Log.i(TAG, "ACCESS = " + path);
break;
case FileObserver.ATTRIB: // 文件属性被修改,如 chmod、chown、touch 等
Log.i(TAG, "ATTRIB = " + path);
break;
case FileObserver.CLOSE_NOWRITE: // 不可写文件被 close
Log.i(TAG, "CLOSE_NOWRITE = " + path);
break;
case FileObserver.CREATE: // 创建新文件
Log.i(TAG, "CREATE = " + path);
break;
case FileObserver.DELETE: // 文件被删除
Log.i(TAG, "DELETE = " + path);
break;
case FileObserver.DELETE_SELF: // 自删除,即一个可执行文件在执行时删除自己
Log.i(TAG, "DELETE_SELF = " + path);
break;
case FileObserver.MODIFY: // 文件被修改
Log.i(TAG, "MODIFY = " + path);
break;
case FileObserver.MOVE_SELF: // 自移动,即一个可执行文件在执行时移动自己
Log.i(TAG, "MOVE_SELF = " + path);
break;
case FileObserver.MOVED_FROM: // 文件被移走
Log.i(TAG, "MOVED_FROM = " + path);
break;
case FileObserver.MOVED_TO: // 文件被移动过来
Log.i(TAG, "MOVED_TO = " + path);
break;
case FileObserver.OPEN: // 文件被打开
Log.i(TAG, "OPEN = " + path);
break;
default:
Log.i(TAG, "event = " + event + ", path = " + path);
break;
}
}
}
ANRFileObserver fileObserver = new ANRFileObserver("/data/anr");
fileObserver.startWatching();
但是该监控方案也有弊端导致适配性不是很好:
Android 基于 Looper 的方案写了一个 WatchDog 监控,同样的也是通过 Handler post 消息检测时间,相关流程如下:

我们也可以参考 WatchDog 的源码和原理自定义一个监控 ANR 的 WatchDog,事件 ANR 是 5s 无响应,那就设定每 5s 从 Looper 插一条消息,如果 5s 后还没执行完成就说明出现了 ANR。具体代码如下:
public class ANRWatchDog extends Thread {
private static final String TAG = "ANRWatchDog";
private static final int TIMEOUT = 5000;
private static ANRWatchDog sWatchDog;
private final Handler mMainHandler = new Handler(Looper.getMainLooper());
private final ANRChecker mAnrChecker = new ANRChecker();
private ANRListener mAnrListener;
private ANRWatchDog() {
super("ANR-WatchDog-Thread");
}
public static ANRWatchDog getInstance() {
if (sWatchDog == null) {
sWatchDog = new ANRWatchDog();
}
return sWatchDog;
}
@Override
public void run() {
// 设置为后台线程
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
while (true) {
synchronized (this) {
// 开始计时,往主线程 Looper 插一条消息
mAnrChecker.schedule();
// 每 5s 循环一次
long waitTime = TIMEOUT;
long start = SystemClock.uptimeMillis();
while (waitTime > 0) { // 避免提前唤醒,保证 5s 检测一次
try {
wait(waitTime);
} catch (InterruptedException e) {
Log.w(TAG, e.toString());
}
waitTime = TIMEOUT - (SystemClock.uptimeMillis() - start);
}
// 没有超时 5s,继续循环
if (!mAnrChecker.isBlocked()) {
continue;
}
}
// 响应超过 5s 认为已经发生了 ANR,将堆栈信息打印出来
String stackTrace = getStackTraceInfo();
if (mAnrListener != null) {
mAnrListener.onAnrHappened(stackTrace);
}
mAnrListener = null;
break;
}
}
private String getStackTraceInfo() {
StringBuilder sb = new StringBuilder();
for (StackTraceElement element : Looper.getMainLooper().getThread().getStackTrace()) {
sb.append(element.toString()).append("\r\n");
}
return sb.toString();
}
private class ANRChecker implements Runnable {
private boolean completed;
private long startTime;
private long executeTime = SystemClock.uptimeMillis();
@Override
public void run() {
synchronized (ANRWatchDog.this) {
completed = true; // 执行完修改标志位
executeTime = SystemClock.uptimeMillis();
}
}
void schedule() {
completed = false;
startTime = SystemClock.uptimeMillis();
mMainHandler.postAtFrontOfQueue(this); // 往主线程 Looper 插入一条消息
}
// 如果标志位是 false 或响应时间超过 5s
boolean isBlocked() {
return !completed || executeTime - startTime >= TIMEOUT;
}
}
public ANRWatchDog setANRListener(ANRListener listener) {
mAnrListener = listener;
return this;
}
public interface ANRListener {
void onAnrHappened(String stackTrack);
}
}
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ANRWatchDog.getInstance().setANRListener(new ANRWatchDog.ANRListener() {
@Override
public void onAnrHappened(String stackTrack) {
Log.i("ANRWatchDog", "stack = " + stackTrack);
}
}).start();
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
// 模拟 anr
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
ANR 定位分析总结如下:
其实 ANR 发生的原因本质上只有三个:
应用界面秒开的实现方案:
这里来介绍一个开源方案:Lancet,它是一个轻量级的Android AOP框架:
下面我们来具体使用一下这个库,我们使用这个库来统计页面的onCreate()方法到onWindowsFocusChanged()方法之间的加载耗时情况:
①、添加依赖
这里大家可以参考github上的使用方式进行依赖的添加,主要是两个部分:工程的build.gradle和app module的build.gradle:
classpath 'me.ele:lancet-plugin:1.0.6' //工程的build.gradle
apply plugin: 'me.ele.lancet' //module的build.gradle
//lancet
compileOnly 'me.ele:lancet-base:1.0.6'
②、编写一个实体类,定义用于上述两个方法时间统计的成员变量:
public class ActivityLive {
public long mOnCreateTime;
public long mOnWindowsFocusChangedTime;
}
③、创建统计方法的工具类,在类中分别编写onCreate()和onWindowFocusChanged()方法,关于具体的注解的使用含义详见代码注释:
public class ActivityHooker {
public static ActivityLive mLive;
static {
mLive = new ActivityLive();
}
//@Insert:使用自己程序中自己的一些类需要添加,值这里就指定onCreate()方法,
//可配置项mayCreateSuper是当目标函数不存在的时候可以通过它来创建目标方法
//@TargetClass:框架知道要找的类是哪个,可配置项Scope.ALL:匹配value所指定的所有类的子类
@Insert(value = "onCreate",mayCreateSuper = true)
@TargetClass(value = "androidx.appcompat.app.AppCompatActivity", scope = Scope.ALL)
protected void onCreate(Bundle savedInstanceState) {
mLive.mOnCreateTime = System.currentTimeMillis();
Origin.callVoid(); //无返回值的调用
}
//注解含义同上面onCreate()
@Insert(value = "onWindowFocusChanged",mayCreateSuper = true)
@TargetClass(value = "androidx.appcompat.app.AppCompatActivity", scope = Scope.ALL)
public void onWindowFocusChanged(boolean hasFocus) {
mLive.mOnWindowsFocusChangedTime = System.currentTimeMillis();
Log.i("onWindowFocusChanged","---"+(mLive.mOnWindowsFocusChangedTime - mLive.mOnCreateTime));
Origin.callVoid();
}
}
下面运行程序来看下结果:

界面秒开监控纬度
对于一般的监控方案,它的监控指标只是一个大的范围,只是一个数据,比如:
如下代码所示,我首先在Activity的onCreate()方法中发送了一个msg,并且打印了一条日志

然后在列表展示的第一条同样打印一条日志:

最后输出的结果如下:

从执行结果来看,这个MSG是跑在Feed展示之前的,这个msg模拟的耗时是1s,此时用户看到界面的时间也就被往后延迟了1s。其实这个场景还是很常见的,因为我们可能由于某些业务需求在某个生命周期或者某个阶段及某些第三方的SDK中会做一些handler post的操作,这个操作很有可能会在列表展示之前被执行到,所以出现这种耗时的盲区,既普遍又不好排查。
耗时盲区监控难点
这种场景非常适合之前说过的一个工具,你能想到是什么吗?————答案是TraceView:
可行性方案:
在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
相信很多人在录制视频的时候都会遇到各种各样的问题,比如录制的视频没有声音。屏幕录制为什么没声音?今天小编就和大家分享一下如何录制音画同步视频的具体操作方法。如果你有录制的视频没有声音,你可以试试这个方法。 一、检查是否打开电脑系统声音相信很多小伙伴在录制视频后会发现录制的视频没有声音,屏幕录制为什么没声音?如果当时没有打开音频录制,则录制好的视频是没有声音的。因此,建议在录制前进行检查。屏幕上没有声音,很可能是因为你的电脑系统的声音被禁止了。您只需打开电脑系统的声音,即可录制音频和图画同步视频。操作方法:步骤1:点击电脑屏幕右下侧的“小喇叭”图案,在上方的选项中,选择“声音”。 步骤2:在“声
首先回顾一下拉格朗日定理的内容:函数f(x)是在闭区间[a,b]上连续、开区间(a,b)上可导的函数,那么至少存在一个,使得:通过这个表达式我们可以知道,f(x)是函数的主体,a和b可以看作是主体函数f(x)中所取的两个值。那么可以有, 也就意味着我们可以用来替换 这种替换可以用在求某些多项式差的极限中。方法: 外层函数f(x)是一致的,并且h(x)和g(x)是等价无穷小。此时,利用拉格朗日定理,将原式替换为 ,再进行求解,往往会省去复合函数求极限的很多麻烦。使用要注意:1.要先找到主体函数f(x),即外层函数必须相同。2.f(x)找到后,复合部分是等价无穷小。3.要满足作差的形式。如果是加
深度学习部署:Windows安装pycocotools报错解决方法1.pycocotools库的简介2.pycocotools安装的坑3.解决办法更多Ai资讯:公主号AiCharm本系列是作者在跑一些深度学习实例时,遇到的各种各样的问题及解决办法,希望能够帮助到大家。ERROR:Commanderroredoutwithexitstatus1:'D:\Anaconda3\python.exe'-u-c'importsys,setuptools,tokenize;sys.argv[0]='"'"'C:\\Users\\46653\\AppData\\Local\\Temp\\pip-instal
最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路
我有一个应用程序正在从Ruby迁移到JRuby(由于需要通过Java提供更好的Web服务安全支持)。我使用的gem之一是daemons创建后台作业。问题在于它使用fork+exec来创建后台进程,但这对JRuby来说是禁忌。那么-是否有用于创建后台作业的替代gem/wrapper?我目前的想法是只从shell脚本调用rake并让rake任务永远运行......提前致谢,克里斯。更新我们目前正在使用几个与Java线程相关的包装器,即https://github.com/jmettraux/rufus-scheduler和https://github.com/philostler/acts
原始问题Letd(n)bedefinedasthesumofproperdivisorsofn(numberslessthannwhichdivideevenlyinton).Ifd(a)=bandd(b)=a,whereab,thenaandbareanamicablepairandeachofaandbarecalledamicablenumbers.Forexample,theproperdivisorsof220are1,2,4,5,10,11,20,22,44,55and110;therefored(220)=284.Theproperdivisorsof284are1,2,
这个问题在这里已经有了答案:WhydoRubysettersneed"self."qualificationwithintheclass?(3个答案)关闭29天前。给定这段代码:classSomethingattr_accessor:my_variabledefinitialize@my_variable=0enddeffoomy_variable=my_variable+3endends=Something.news.foo我收到这个错误:test.rb:9:in`foo':undefinedmethod`+'fornil:NilClass(NoMethodError)fromtes
电脑启动出现显示器黑屏是一个相当常见的问题。如果您遇到了这个问题,不要惊慌,因为它有很多可能的原因,可以采取一些简单的措施来解决它。在本文中,小编将介绍下面4种常见的电脑启动后显示器黑屏的原因,排查这些原因,快速解决! 演示机型:联想Ideapad700-15ISK-ISE系统版本:Windows10一、显示器问题如果出现电脑启动后显示器黑屏的情况。那么首先您需要检查一下显示器是否正常工作。您可以通过更换另一个显示器或将当前显示器连接到另一台计算机来检查显示器是否存在问题。如果问题仍然存在,那么您可以排除显示器故障的可能性。 二、显卡问题如果您的电脑配备了独立显卡,那么显卡故障也可能是导致电脑