Android卡顿检测(一)BlockCanary

卡顿检测是个至关大的话题,检测场景小到本机测试、自动化测试、本地监控,大到线上抽样采集上报。卡顿缘由也千差万别,跟CPU、内存、I/O可能都有关。本系列文章旨在经过一些经常使用的本地卡顿检测工具来定位卡顿缘由,并分析其底层实现原理。若是想自研一些APM工具这些原理必须掌握。html

卡顿分析工具概览.png

谈到卡顿首先想到的就是BlockCanary,它以其简单易用的特色被普遍用于检测全局的卡顿状况,咱们有必要首先了解一下它内部的原理。本篇先来看看BlockCanary项目传送门戳这里java

最新版本

com.github.markzhai:blockcanary-android:1.5.0
复制代码

BlockCanary原理解析

咱们知道Android Framework 不少业务都是经过消息机制完成的,包括UI绘制更新、四大组件生命周期、ANR检查等等。android

消息机制给咱们一个启发,咱们能够监测主线程消息处理的状况来追踪卡顿问题。以UI渲染为例,主线程Choreographer(Android 4.1及之后)每16ms请求一个vsync信号,当信号到来时触发doFrame操做,它内部又依次进行了input、Animation、Traversal过程(具体流程分析参考好文Android Choreographer 源码分析),而这些都是经过消息机制驱动的。git

BlockCanary检测的原理也是基于主线程消息的处理流程。既然要检测主线程消息处理状况,那先要清楚主线程Looper对象的建立。github

# -> ActivityThread
public static void main(String[] args) {
    ...

    Looper.prepareMainLooper();

    ActivityThread thread = new ActivityThread();
    thread.attach(false);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }

    if (false) {
        Looper.myLooper().setMessageLogging(new
                LogPrinter(Log.DEBUG, "ActivityThread"));
    }

    ...
    Looper.loop();

    throw new RuntimeException("Main thread loop unexpectedly exited");
}
复制代码

ActivityThread的main函数是Android程序的入口,它并非一个线程类,它运行在主线程中。能够看到经过prepareMainLooper和loop函数使主线程的looper跑起来了。shell

再看loop方法bash

# -> Looper.java
public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;
    ...

    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
        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);
        }

       	...

        msg.recycleUnchecked();
    }
}
复制代码

这里先留一个问题loop函数内部使用了死循环,主线程为何不会卡死?为何不会触发ANR?文末有参考文章。网络

dispatchMessage函数会对消息进行分发,并交由对应的runnable或handler处理,因此监控主线程的卡顿问题实际上就是监控dispatchMessage函数的耗时状况。app

能够看到在dispatchMessage先后各有一次logging的打印,而且调用println方法的logging对象还能够经过setMessageLogging方法设置,也就是说Looper内部自己就提供了hook点。ide

# -> Looper.java
public void setMessageLogging(@Nullable Printer printer) {
    mLogging = printer;
}
复制代码

咱们能够自定义一个Printer并复写其println函数来实现卡顿的监控。事实上,BlockCanary就是这么作的。监控到卡顿点后,dump函数调用堆栈并获取CPU运行状况,即可综合分析卡顿的缘由。

BlockCanary源码分析

来看看BlockCanary初始化的方法install和start。

# -> BlockCanary.java

/**
 * Install {@link BlockCanary}
 *
 * @param context            Application context
 * @param blockCanaryContext BlockCanary context
 * @return {@link BlockCanary}
 */
public static BlockCanary install(Context context, BlockCanaryContext blockCanaryContext) {
    BlockCanaryContext.init(context, blockCanaryContext);
    setEnabled(context, DisplayActivity.class, BlockCanaryContext.get().displayNotification());
    return get();
}
复制代码
# -> BlockCanary.java
public void start() {
    if (!mMonitorStarted) {
        mMonitorStarted = true;
        //设置自定义printer
        Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);
    }
}
复制代码

这里的mBlockCanaryCore.monitor就是LooperMonitor对象,它实现了Printer接口。 咱们重点看一下它的println方法。

# -> LooperMonitor.java
public void println(String x) {
    if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
        return;
    }
    if (!mPrintingStarted) {
        //dispatchMessage前一次打印进入这里
        mStartTimestamp = System.currentTimeMillis();
        mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
        mPrintingStarted = true;
        //开始dump信息
        startDump();
    } else {
        //dispatchMessage后一次打印进入这里
        final long endTime = System.currentTimeMillis();
        mPrintingStarted = false;
        //判断是否发生卡顿
        if (isBlock(endTime)) {
            //存储dump下来的信息并通知
            notifyBlockEvent(endTime);
        }
        //中止dump
        stopDump();
    }
}
复制代码

主线已经清楚,咱们先大体看一下BlockCanary运行的核心流程把握全局。

官方流程图
再来看startDump和stopDump

# -> LooperMonitor.java
private void startDump() {
    if (null != BlockCanaryInternals.getInstance().stackSampler) {
        BlockCanaryInternals.getInstance().stackSampler.start();
    }

    if (null != BlockCanaryInternals.getInstance().cpuSampler) {
        BlockCanaryInternals.getInstance().cpuSampler.start();
    }
}

private void stopDump() {
    if (null != BlockCanaryInternals.getInstance().stackSampler) {
        BlockCanaryInternals.getInstance().stackSampler.stop();
    }

    if (null != BlockCanaryInternals.getInstance().cpuSampler) {
        BlockCanaryInternals.getInstance().cpuSampler.stop();
    }
}
复制代码

可见内部有一个调用堆栈采样器和cpu采样器。 这里有一点须要注意:采样开始的时间点为0.8*卡顿阈值。为何不在卡顿阈值那个点采样呢?这里实际上是一种容错处理。 假设当前函数调用及实际耗时状况以下,卡顿阈值设置为220。

fun foo () {
    a()//函数耗时200
    b()//函数耗时20
    c()//函数耗时10
}
复制代码

可见致使卡顿的罪魁祸首应该是函数a,但若是在卡顿阈值220才开始dump调用堆栈,有可能捕获到的卡顿堆栈为foo() -> b()或c(),设置0.8倍的预采样点就是为了下降这种状况出现的概率。咱们悲观的认为当前已超过80%卡顿阈值的函数就是致使卡顿的主因。

回到采样流程来,首先看stackSampler是如何采样的。

# -> StackSampler.java
protected void doSample() {
    StringBuilder stringBuilder = new StringBuilder();

    for (StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) {
        stringBuilder
                .append(stackTraceElement.toString())
                .append(BlockInfo.SEPARATOR);
    }

    synchronized (sStackMap) {
        if (sStackMap.size() == mMaxEntryCount && mMaxEntryCount > 0) {
            sStackMap.remove(sStackMap.keySet().iterator().next());
        }
        sStackMap.put(System.currentTimeMillis(), stringBuilder.toString());
    }
}
复制代码

很简单,就是获取当前线程的堆栈信息,并保存在一个LinkedHashMap对象sStackMap中。

再来看cpuSampler的处理

# -> CpuSampler
@Override
protected void doSample() {
    BufferedReader cpuReader = null;
    BufferedReader pidReader = null;

    try {
        cpuReader = new BufferedReader(new InputStreamReader(
                new FileInputStream("/proc/stat")), BUFFER_SIZE);
        String cpuRate = cpuReader.readLine();
        if (cpuRate == null) {
            cpuRate = "";
        }

        if (mPid == 0) {
            mPid = android.os.Process.myPid();
        }
        pidReader = new BufferedReader(new InputStreamReader(
                new FileInputStream("/proc/" + mPid + "/stat")), BUFFER_SIZE);
        String pidCpuRate = pidReader.readLine();
        if (pidCpuRate == null) {
            pidCpuRate = "";
        }

        parse(cpuRate, pidCpuRate);
    } catch (Throwable throwable) {
        Log.e(TAG, "doSample: ", throwable);
    } finally {
        //release resource
        ...
    }
}
复制代码

这里是依据Linux系统cpu的统计方式,Linux系统会将cpu信息和当前进程信息分别存放在/proc/stat和/proc/pid/stat文件中,具体统计原理参看Linux平台Cpu使用率的计算

经过CPU的使用状况能够大体了解系统的运行状况,CPU若是处于高负载状态,多是在作CPU密集型计算。若是CPU负载正常,可能处于IO密集状态。

当信息都采集完成后咱们回到主线代码。

# -> LooperMonitor
@Override
public void println(String x) {
    if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
        return;
    }
    if (!mPrintingStarted) {
        mStartTimestamp = System.currentTimeMillis();
        mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
        mPrintingStarted = true;
        startDump();
    } else {
        final long endTime = System.currentTimeMillis();
        mPrintingStarted = false;
        if (isBlock(endTime)) {
            notifyBlockEvent(endTime);
        }
        stopDump();
    }
}

//判断是否发生了卡顿
private boolean isBlock(long endTime) {
    return endTime - mStartTimestamp > mBlockThresholdMillis;
}

private void notifyBlockEvent(final long endTime) {
    final long startTime = mStartTimestamp;
    final long startThreadTime = mStartThreadTimestamp;
    final long endThreadTime = SystemClock.currentThreadTimeMillis();
    //通知写日志线程记录日志
    HandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() {
        @Override
        public void run() {
           mBlockListener.onBlockEvent(startTime, endTime, startThreadTime, endThreadTime);
        }
    });
}
复制代码

这里须要注意的是对于threadTime的统计,它经过函数SystemClock.currentThreadTimeMillis()获取,它反映的是线程处于running状态下的时间,这里须要一张Thread运行状态图。

线程状态.png

因此好比经过调用thread.sleep方式致使卡顿时并不会统计到threadTime中的。也就是说threadTime反映的是线程真正运行的时间,中间好比锁的获取、cpu的调度及其余非running状态等状况不计算在内。

onBlockEvent的实如今BlockCanary建立之初。

public BlockCanaryInternals() {
    stackSampler = new StackSampler(
            Looper.getMainLooper().getThread(),
            sContext.provideDumpInterval());
    cpuSampler = new CpuSampler(sContext.provideDumpInterval());

    setMonitor(new LooperMonitor(new LooperMonitor.BlockListener() {

        @Override
        public void onBlockEvent(long realTimeStart, long realTimeEnd,
                                 long threadTimeStart, long threadTimeEnd) {
            // Get recent thread-stack entries and cpu usage
            ArrayList<String> threadStackEntries = stackSampler
                    .getThreadStackEntries(realTimeStart, realTimeEnd);
            if (!threadStackEntries.isEmpty()) {
                BlockInfo blockInfo = BlockInfo.newInstance()
                        .setMainThreadTimeCost(realTimeStart, realTimeEnd, threadTimeStart, threadTimeEnd)
                        .setCpuBusyFlag(cpuSampler.isCpuBusy(realTimeStart, realTimeEnd))
                        .setRecentCpuRate(cpuSampler.getCpuRateInfo())
                        .setThreadStackEntries(threadStackEntries)
                        .flushString();
                //写入文件系统
                LogWriter.save(blockInfo.toString());

                if (mInterceptorChain.size() != 0) {
                    for (BlockInterceptor interceptor : mInterceptorChain) {
                      //回调观察者,发送通知
                      interceptor.onBlock(getContext().provideContext(), blockInfo);
                    }
                }
            }
        }
    }, getContext().provideBlockThreshold(), getContext().stopWhenDebugging()));

    LogWriter.cleanObsolete();
}
复制代码

mInterceptorChain目前注册了两个回调,一个是DisplayService,它收到block消息会发送通知。另外一个是BlockCanaryContext,咱们能够经过自定义BlockCanaryContext并复写onBlock方法作额外的处理,好比上报网络。

# -> BlockCanaryContext
/**
 * Block interceptor, developer may provide their own actions.
 */
@Override
public void onBlock(Context context, BlockInfo blockInfo) {

}
复制代码

接下来就能够经过通知消息查看卡顿的具体信息。电视端若是屏蔽了通知栏,可在应用列表中找到入口,若是应用列表入口也被系统屏蔽,可直接使用adb命令打开。

adb shell am start <packageName>/com.github.moduth.blockcanary.ui.DisplayActivity
复制代码

BlockCanary的不足

  • 全局性,只能在初始化以后使用,初始化以前的卡顿问题没法分析,好比Application的attachBaseContext函数。这一点只能经过系通通计工具(Traceview/Systrace)或手动插桩。
  • 准确性,因为其使用0.8倍的卡顿阈值做为采样点,仍可能出现不能准确识别卡顿函数的状况。
  • 卡顿阈值把控,手动设置的卡顿阈值是全局的,但对于某个重要场景咱们的要求可能更为严苛,这样就须要在不一样的业务场景设置不一样的卡顿阈值。
  • 细粒度的函数耗时评估,BlockCanary只能告诉咱们当前的卡顿函数是哪一个,但不能准确的告知到底卡顿了多久,这对于卡顿优化来讲是更为精细的指标(Hugo就能够优雅的解决这个问题)。

参考文章

相关文章
相关标签/搜索