卡顿、ANR、监控

卡顿原因

人眼能感觉到的帧率是每秒24帧,而屏幕每16毫秒会刷新一次,也就是每秒会刷新60次。当每秒刷新次数少于60次,即出现掉帧,则会感觉到卡顿。

关于屏幕刷新机制可以看绘制流程

卡顿原因主要有以下几点:

  1. 根据Handler消息分发机制,主线程所有操作都是存在于Loop的循环中,当某条Message执行耗时操作(即主线程存在耗时操作),就会导致消息队列中的Message消息被阻塞
  2. 消息队列中存在同步屏障,同步消息得不到执行。
    比如当点击一个按钮后,通过Handler post 执行一个动画(这是一个同步消息),如果此时存在同步屏障,则动画会等到同步屏障被移出后才会执行,就会感觉到卡顿
  3. View布局计算耗时(如层级太多),无法在16ms内计算完成,导致jank掉帧
  4. 内存占用过大、GC频繁、CPU占用过多都会导致卡顿,检测下是否有内存泄漏、大对象、对象频繁创建、io频繁等情况

ANR

ANR类型

  • InputDispatchTimeout:5s内无法响应用户输入事件(例如键盘输入, 触摸屏幕等).
  • BroadcastTimeout:比如前台广播在10s内未执行完成(后台广播60s)
  • ServiceTimeout:比如前台服务在20s内未执行完成(后台服务200s)
  • ContentProviderTimeout:内容提供者,在publish过超时10s;

原因

主线程(UI线程)里面做了太多的阻塞耗时操作

  1. 普通阻塞导致的ANR

  2. CPU满负荷

  3. 内存原因(OOM)

分析

ANR发生后会在data/anr下生成trace.txt

一般来讲直接先看tid=1的堆栈即对应主线程,因为ANR都是主线程执行超时导致

案例一:主线程Binder调用等待超时

image

很明显当时在做Binder通信,并没有waiting to lock等代表死锁的字样,那么说明这个案例即有可能是在等Binder对端响应,我们知道Binder通信对于发起方来说是默认是阻塞等待响应,只有有了返回结果后才会继续执行下去,当然了可以给接口设置oneway声明,这样的话binder请求就是异步请求,这里不多说

所以,如上这个案例中需要找到对端是哪个进程,这个进程当时在做什么,这时候就需要找到anr文件夹下另外一个文件binderinfo,这里需要找到与我们发起方进程1461通信的是哪个进程

image

可以看到是1666号这个进程,再回到trace中看下,这个进程当时在做什么

image

可以看到当时对端在做消息的读取,也就是说这里出了问题,很明显这里我们无法修改,我们这个问题在于主线程执行了Binder请求,对端迟迟未返回便很容易出现这个问题

解决:步中执行

案例二:主线程等待锁

关键字:waiting to lock

image

main thread在执行UploaderChimeraService的onDestroy方法时,需要lock 0x23f65d8b,但这个lock有被upload_periodic GCM Task 拿住,这个thread当前是在做连接网络的动作。从这段信息来看,很有可能与测试时手机连接的网络有关,当时连接的事google的网络,由于墙的原因,无法连接gms的相关server有关

还有一种情况就是死锁,即形成了头尾相连,互相等待的情况

解决:一般会尝试将锁改为超时锁,比如lock的trylock,超时会自动释放锁,从而避免一直持有锁的情况发生

案例三:卡在IO上

这种情况一般是和文件操作相关,判断是否是这种情况,可以看mainlog中搜索关键字"ANR in",看这段信息的最下边,比如下面的信息 ANRManager: 100% TOTAL: 2% user + 2.1% kernel + 95% iowait + 0.1% softirq 很明显,IO占比很高,这个时候就需要查看trace日志看当时的callstack,或者在这段ANR点往前看0~4s,看看当时做的什么文件操作

解决:对耗时文件操作采取异步操作

案例四:主线程有耗时的动作

这种情况是ANR类型问题里遇到最多的,比如网络访问,访问数据库之类的,都很容易造成主线程堵塞,

这里以访问数据库来说,这类型引起的ANR,一般来讲看当时的CPU使用情况会发现user占比较高,看trace中主线程当时的信息会发现会有一些比如query像ContentProvider这种数据库的动作。这种情况下,还可以去看eventlog或者mainlog,在ANR发生前后打印出来的信息,比如访问数据库这种,在eventlog中搜索"am_anr",然后看前后片段,会发现发生ANR的这个进程有很多数据库相关的信息,说明在发生ANR前后主线程一直在忙于访问数据库,这类型的问题常见于图库,联系人,彩短信应用。

解决:一般考虑的是异步解决,异步解决并不是简单的new一个线程,要根据业务场景以及频率来决定,Android常见的异步AsyncTask, IntentService, 线程池(官方四种或自定义), new thread等,一般来说不建议直接new thread

案例五:binder线程池被占满

系统对每个process最多分配15个binder线程,这个是谷歌的设计

如果另一个process发送太多重复binder请求,那么就会导致接收端binder线程被占满,从而处理不了其它的binder请求

这时候请求端发起的请求就会阻塞等待了(未设置异步请求的前提下),这本身就是系统的一个限制,如果应用未按照系统的要求来实现对应逻辑,那么就会造成问题。

而系统端是不会(也不建议)通过修改系统行为来兼容应用逻辑,否则更容易造成其它根据系统需求正常编写的应用反而出现不可预料的问题。

判断Binder是否用完,可以在trace中搜索关键字"binder_f",如果搜索到则表示已经用完,然后就要找log其他地方看是谁一直在消耗binder或者是有死锁发生

解决:解决的思路就是降低极短时间内大量Binder请求的发生,修复的手法是发送BInder请求的函数中做时间差过滤,限定在500ms内最多执行一次

案例六:NullPointerException导致ANR

ANR前出现频繁NE,NE所在的进程与ANR的进程有交互,在解决了NE后,ANR也不复存在,对于这类在ANR前有JE或者NE,一般思路是先解决JE或NE,因为JE/NE发生时会去dump一大堆异常信息,本身也会加重CPU loading,修改完异常后再来看ANR是否还存在,如果还存在,那么就看trace 堆栈,如果不存在,则可以基本判定是JE或NE导致

案例七:只存在于Monkey测试下

有些问题是只有在Monkey环境下才能跑出来,平时的user版本用户使用是不会出现的,这种问题的话就没有改动的意义。

比如下面这个例子:

ActivityManager: Not finishing activity because controller resumed

03-18 07:25:50.901 810 870 I am_anr : [0,25443,android.process.media,1086897733,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.)]

发生这个ANR的原因是Contoller将resume的操作给拦截了, 导致Focus不过去, 从而导致ANR,User版本不会有Contoller, 所以不会出现这个 ANR. 所以这个 ANR 可以忽略.

触发流程

如下引用该文:理解Android ANR的触发原理 分别记录了由ServiceBroadcastReceiverContentProvider、Input系统造成的ANR。

触发ANR的过程可分为三个步骤: 埋炸弹, 拆炸弹, 引爆炸弹

Service

Service Timeout是位于”ActivityManager”线程中的AMS.MainHandler收到SERVICE_TIMEOUT_MSG消息时触发。

对于Service有两类:

  • 对于前台服务,则超时为SERVICE_TIMEOUT = 20s;
  • 对于后台服务,则超时为SERVICE_BACKGROUND_TIMEOUT = 200s

由变量ProcessRecord.execServicesFg来决定是否前台启动

埋炸弹

当发起Service启动请求时,通过Handler发起SERVICE_TIMEOUT_MSG延迟消息

// ActiveServices
private final void realStartServiceLocked(ServiceRecord r, ProcessRecord app, boolean execInFg) throws RemoteException {
    ...
    // 发送delay消息(SERVICE_TIMEOUT_MSG)
    bumpServiceExecutingLocked(r, execInFg, "create");
    try {
        ...
        // 创建服务,最终执行服务的onCreate()方法
        app.thread.scheduleCreateService(r, r.serviceInfo,
                mAm.compatibilityInfoForPackageLocked(r.serviceInfo.applicationInfo),
                app.repProcState);
    } catch (DeadObjectException e) {
        mAm.appDiedLocked(app);
        throw e;
    } finally {
        ...
    }
}

// ActiveServices
private final void bumpServiceExecutingLocked(ServiceRecord r, boolean fg, String why) {
    ... 
    scheduleServiceTimeoutLocked(r.app);
}

// ActiveServices
void scheduleServiceTimeoutLocked(ProcessRecord proc) {
    if (proc.executingServices.size() == 0 || proc.thread == null) {
        return;
    }
    long now = SystemClock.uptimeMillis();
    Message msg = mAm.mHandler.obtainMessage(
            ActivityManagerService.SERVICE_TIMEOUT_MSG);
    msg.obj = proc;

    //当超时后仍没有remove该SERVICE_TIMEOUT_MSG消息,则执行service Timeout流程
    mAm.mHandler.sendMessageAtTime(msg,
        proc.execServicesFg ? (now+SERVICE_TIMEOUT) : (now+ SERVICE_BACKGROUND_TIMEOUT));
}

拆炸弹

当service启动完成,则移除服务超时消息SERVICE_TIMEOUT_MSG。

// ActivityThread
private void handleCreateService(CreateServiceData data) {
        ...
        java.lang.ClassLoader cl = packageInfo.getClassLoader();
        Service service = (Service) cl.loadClass(data.info.name).newInstance();
        ...

        try {
            //创建ContextImpl对象
            ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
            context.setOuterContext(service);
            //创建Application对象
            Application app = packageInfo.makeApplication(false, mInstrumentation);
            service.attach(context, this, data.info.name, data.token, app,
                    ActivityManagerNative.getDefault());
            //调用服务onCreate()方法
            service.onCreate();

            //拆除炸弹引线
            ActivityManagerNative.getDefault().serviceDoneExecuting(
                    data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
        } catch (Exception e) {
            ...
        }
}

private void serviceDoneExecutingLocked(ServiceRecord r, boolean inDestroying, boolean finishing) {
    ...
    if (r.executeNesting <= 0) {
        if (r.app != null) {
            r.app.execServicesFg = false;
            r.app.executingServices.remove(r);
            if (r.app.executingServices.size() == 0) {
                // 当前服务所在进程中没有正在执行的service
                mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_TIMEOUT_MSG, r.app);
        ...
    }
    ...
}

引爆炸弹

service.onCreate()执行超时,导致SERVICE_TIMEOUT_MSG消息没有被移除,则会触发ANR

// ActivityManagerService
final class MainHandler extends Handler {
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case SERVICE_TIMEOUT_MSG: {
                ...
                mServices.serviceTimeout((ProcessRecord)msg.obj);
            } break;
            ...
        }
        ...
    }
}

// ActiveServices
void serviceTimeout(ProcessRecord proc) {
    ... 
    if (anrMessage != null) {
        // 当存在timeout的service,则执行ProcessRecord.appNotResponding
        proc.appNotResponding(proc, null, null, false, anrMessage);
    }
}

小结:

  • 埋炸弹:当发起Service启动请求时,通过Handler发起SERVICE_TIMEOUT_MSG延迟消息

  • 拆炸弹:当service启动完成,则移除服务超时消息SERVICE_TIMEOUT_MSG

  • 引爆炸弹:service.onCreate()执行超时,导致SERVICE_TIMEOUT_MSG消息没有被移除,则会触发ANR

BroadcastReceiver

BroadcastReceiver Timeout是位于”ActivityManager”线程中的BroadcastQueue.BroadcastHandler收到BROADCAST_TIMEOUT_MSG消息时触发。

对于广播队列有两个: foreground队列和background队列:

  • 对于前台广播,则超时为BROADCAST_FG_TIMEOUT = 10s;
  • 对于后台广播,则超时为BROADCAST_BG_TIMEOUT = 60s

broadcast跟service超时机制大抵相同,但有一个非常隐蔽的技能点,那就是通过静态注册的广播超时会受SharedPreferences(简称SP)的影响,只有XML静态注册的广播超时检测过程会考虑是否有SP尚未完成,动态广播并不受其影响。

小结

  • 埋炸弹:当该广播所有的接收者处理前,通过Handler发起BROADCAST_TIMEOUT_MSG延迟消息

  • 拆炸弹:当广播处理完成,则移除超时消息BROADCAST_TIMEOUT_MSG

  • 引爆炸弹:广播处理超时,导致BROADCAST_TIMEOUT_MSG消息没有被移除,则会触发ANR

ContentProvider

ContentProvider Timeout是位于”ActivityManager”线程中的AMS.MainHandler收到CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG消息时触发。

ContentProvider 超时为CONTENT_PROVIDER_PUBLISH_TIMEOUT = 10s

小结:

  • 埋炸弹:进程创建后,通过Handler发起CONTENT_PROVIDER_PUBLISH_TIMEOUT延迟消息

  • 拆炸弹:当provider成功publish之后,则移除服务超时消息CONTENT_PROVIDER_PUBLISH_TIMEOUT

  • 引爆炸弹:publish超时,导致CONTENT_PROVIDER_PUBLISH_TIMEOUT消息没有被移除,则会触发ANR

总结

当出现ANR时,都是调用到AMS.appNotResponding()方法,详细过程见文章理解Android ANR的信息收集过程. 这里介绍的provider例外.

Timeout时长

  • 对于前台服务,则超时为SERVICE_TIMEOUT = 20s;

  • 对于后台服务,则超时为SERVICE_BACKGROUND_TIMEOUT = 200s

  • 对于前台广播,则超时为BROADCAST_FG_TIMEOUT = 10s;

  • 对于后台广播,则超时为BROADCAST_BG_TIMEOUT = 60s;

  • ContentProvider超时为CONTENT_PROVIDER_PUBLISH_TIMEOUT = 10s;

超时检测

Service超时检测机制:

  • 超过一定时间没有执行完相应操作来触发移除延时消息,则会触发anr;

BroadcastReceiver超时检测机制:

  • 有序广播的总执行时间超过 2* receiver个数 * timeout时长,则会触发anr;
  • 有序广播的某一个receiver执行过程超过 timeout时长,则会触发anr;

另外:

  • 对于Service, Broadcast, Input发生ANR之后,最终都会调用AMS.appNotResponding;
  • 对于provider,在其进程启动时publish过程可能会出现ANR, 则会直接杀进程以及清理相应信息,而不会弹出ANR的对话框. appNotRespondingViaProvider()过程会走appNotResponding(), 这个就不介绍了,很少使用,由用户自定义超时时间.

关于input的ANR触发过程,见Input系统—ANR原理分析

卡顿监控

替换 Looper 的 Printer

  • 计算Looper两次获取消息的时间差,如果时间太长(比如3s)就说明Handler处理时间过长,直接把堆栈信息打印出来,就可以定位到耗时代码。
  • println 方法参数涉及到字符串拼接,考虑性能问题,所以这种方式只推荐在Debug模式下使用。基于此原理的开源库代表是:BlockCanary
// Looper
public void setMessageLogging(@Nullable Printer printer) {
    mLogging = printer;
}

public static void loop() {
    ...
    for (;;) {
            // 读取消息
        Message msg = queue.next();
        if (msg == null) {
           return;
        }
        final Printer logging = me.mLogging;
        if (logging != null) {
                // 处理消息前回调一次
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }
        ...
        // 处理消息
        msg.target.dispatchMessage(msg);
        ...
        if (logging != null) {
                // 处理消息后回调一次
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }
   }
}

基于消息队列监控卡顿

1.执行消息前,通过HandlerThread的Handler发送一个延迟消息到子线程,时间为定义的阈值
2.执行这条消息后,移除消息
3.如果消息耗时,延迟消息未移出并得到执行,获取主线程的堆栈信息

这个方案存在问题:

当某条消息执行如ABC三个方法,而A为耗时的方法,但是获取到的堆栈可能为B或者C,

而BlockCanary不存在此问题

BlockCanary原理

  1. 执行消息前,开启循环,每300ms获取一次堆栈,并保存下来
  2. 执行完消息,停止循环,获取耗时的堆栈

插入空消息到消息队列

  • 通过一个监控线程,每隔1秒向主线程消息队列的头部插入一条空消息。
  • 假设1秒后这个消息并没有被主线程消费掉,说明阻塞消息运行的时间在0~1秒之间。换句话说,如果我们需要监控3秒卡顿,那在第4次轮询中,头部消息依然没有被消费的话,就可以确定主线程出现了一次3秒以上的卡顿。

插桩

编译过程插桩(例如使用AspectJ),在方法入口和出口加入耗时监控的代码。

插桩前

public void test(){
    doSomething();
}

插桩后

public void test(){
    long startTime = System.currentTimeMillis();
    doSomething();
    long methodTime = System.currentTimeMillis() - startTime;//计算方法耗时
}

缺点:

  1. 无法监控系统方法
  2. apk体积会增大

需要注意:

  1. 过滤简单的方法
  2. 只需要监控主线程执行的方法

Profiler

Traceview 已弃用,应使用 CPU Profiler 来执行以下操作:检查通过使用 Debug 类对应用进行插桩检测而捕获的 .trace 文件、记录新方法跟踪信息、保存 .trace 文件以及检查应用进程的实时 CPU 使用情况。

cpu profile是Android Profiler里面的一个功能

CPU profiler性能分析工具介绍和使用详解

FPS检测

gpu呈现模式分析

打开开发者模式,选择gpu呈现模式分析。可以在屏幕上看到竖条,代表每一帧所耗费的时间,不同的颜色代表在绘制的不同阶段的耗时,可以据此判断哪些阶段存在问题。横向的绿线代表16.67ms的基线,只有耗费的时长小于16.67ms才不会出现丢帧的情况。

image

Choreographer

基于Choreographer,通过Choreographer.postFrameCallBack获取每一帧的回调,可以计算出每一秒的帧数FPS

  • Choreographer是一个ThreadLocal变量,UI线程里可以任务是一个单例,接受VSync信号进行界面的渲染
  • 界面不发生改变时,VSync信号也会传递

Window.OnFrameMetricsAvailableListener

OnFrameMetricsAvailableListener是Android在api 24版本加入的强大新功能,通过设置OnFrameMetricsAvailableListener回调可以获得每一帧每一个阶段的耗时,以及总的耗时,并且可以获得当前帧是否是第一次绘制,android官方认为第一帧由于存在大量初始化的代码,其第一帧的数据不应记在FPS内。

线上卡顿监控

Matrix

GitHub

Matrix 原理分析

mmap

JVMTI

原理总结

Looper(FrameTracer)
  1. 通过反射获取主线程Looper中的Printer(originPrinter),并替换成自己的Printer
Printer originPrinter = ReflectUtils.get(looper.getClass(), "mLogging", looper);
looper.setMessageLogging(new LooperPrinter(originPrinter));
  1. 通过Printer获取到每一个Message的开始及结束调用时机,以及耗时
  2. 计算帧率
Choreographer(FrameTracer)
  1. 反射获取Choreographer中CALLBACK_INPUT、CALLBACK_ANIMATIO、CALLBACK_TRAVERSAL(绘制)三种类型回调的队列CallbackQueue,并注册三种类型的回调到队列的头部

  2. 由于回调在头部,可以监听并计算出前两种类型回调的执行耗时

  3. 配合Looper的Printer方案,通过Message的结束时机,可以计算出第三种类型的执行耗时

注:VSync信号回调是通过Handler发送同步屏障+异步消息的机制执行的

  1. 计算帧率
慢方法(EvilMethodTracer)
  1. ASM埋点,记录方法耗时及调用栈,通过一个long数组记录是否是进入方法及方法id及时间戳

注:通过一个映射表记录,记录方法名与方法id的对应关系,可以节省日志大小

  1. 通过Looper及Choreographer监听主线程Message或者每一帧的耗时,如果大于设定阙值(默认700ms),则通过long计算出方法调用栈及耗时,并进行裁切
  2. 通过mmap机制保存日志
冷启动(StartupTracer)
  1. 继承Application,实现onCreate方法;在ASM埋点中,会在onCreate方法中插入埋点,此时记录应用启动开始时间
  2. 反射获取ActivityThread中的mH(Handler),反射替换mH中的callback,在回调中的方法之后,记录应用启动结束时间(Application启动时间)

注:根据应用启动流程,应用启动时,AMS会通过s应用进程的ApplicationThread Binder对象通知应用进程创建Application,ApplicationThread通过mH(Handler)将消息发送到主线程创建Application,所以在mH中的callback中,即可获取到创建完Application之后的时间

  1. 通过步骤1、2,即可计算出Application启动时间

  2. ASM埋点中,如果Class为Activity,检查是否有实现onWindowFocusChanged方法,没有则插入onWindowFocusChanged方法,并插入回调方法,通知Matrix,即为Activity启动完成时机

  3. 通过步骤1、4,即可计算出冷启动到第一屏的启动时间

热启动(StartupTracer)
  1. 通过Application.registerActivityLifecycleCallbacks注册Activity生命周期回调,监听onCreate时机
  2. 热启动时间则为onWindowFocusChanged的时间-onCreate时间

ANR(LooperAnrTracer)

  1. 通过Looper,在每一个Message的执行前,通过Handler发送一个5s的延时消息,在Message执行之后,将延时消息移除;当延时消息得到处理,则认定为发生了ANR,输出内存占用信息及方法调用栈耗时等信息
  2. 步骤1的延时时间设为2s,则认为发生卡顿
JVMTI(JVM tool interface)
  • JVMTI位于jpda 最底层,是Java 虚拟机所提供的native编程接口。 JVMTI可以提供性能分析、debug、内存管理、线程分析等功能。
  • VMTI是一套本地编程接口,因此使用JVMTI,需要与c/c++ 以及JNI打交道。事实上,开发时一般采用建立一个Agent的方式来使用JVMTI,Agent使用jvmti函数,设置一些回调函数,并从Java虚拟机中得到当前的运行态信息,并作出自己的判断, 最后还可能操作虚拟机的运行态。把Agent编译成一个动态链接库之后,可以再Java程序启动时来加载它(比如IDE调试时使用的libjdwp.so 就是采用这种方式),当然也可以通过Attach方式,中途加入(比如jmap, jps,jstack 等等)。

  • 通过JVMTI可以实现对象创建,GC,ANR,OOM等的监控
  • Android 9.0 加入了JVMTI的支持

你可能感兴趣的:(卡顿、ANR、监控)