性能优化(11)-AndroidGodEye解析之流畅度(sm)

主目录见:Android高级进阶知识(这是总目录索引)
框架源码:AndroidGodEye

 讲这篇文章之前,这里推荐一篇非常优秀的博文[Android应用性能评测调优],文章比较长,大家可以细细品味,同时今天的文章跟之前[BlockCanary原理分析]里的原理都是一样的,但是为什么还要讲这一篇呢?主要还是为了讲解这个框架的完整度,如果看过前面那篇文章的可以跳过这篇。

我们评测一个应用的流畅度是大量数据和时间积累的结果,但是第一步就是你首先要有数据,不然再好的评测都是白瞎,我们今天就从第一步开始说起。

一.从Looper开始说起

 如果要详细的讲解可以参考Handler,MessageQueue,与Looper三者关系分析,Looper是给线程提供处理消息能力的类,在Android Framework启动的时候,就会创建一个Main Looper即主线程对应的Looper,Looper中会维护一个MessageQueue,负责接收Handler发送过来的消息,MessageQueue是个消息队列,它是顺序取消息的,只有取完一个任务才会接着取另外一个任务,所以一旦主线程的前一个任务耗时特别多,UI就会有卡顿的感觉,我们知道我们在Looper#looper()方法中就会开始消息的循环工作,所以我们来看看这个方法:

  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;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        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
//这里可以看到,Looper里面有mLogging对象调用println用来打印日志
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            final long traceTag = me.mTraceTag;
            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }
            try {
                msg.target.dispatchMessage(msg);
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
//这里是消息处理完成的日志打印
            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                ................       
            }

            msg.recycleUnchecked();
        }
    }

首先看到在msg.target.dispatchMessage(msg)方法前面会有一个日志打印:

  final Printer logging = me.mLogging;
  if (logging != null) {
         logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
  }

在处理完消息的时候,又会有一个结束的日志打印:

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

所以我们想到,如果我们能在消息执行前后做些事情,我们是否就知道消息的耗时等一些信息,当前的日志打印Printer是系统自带的LogPrinter,主要是通过Looper中的setMessageLogging()方法进行设置:

public void setMessageLogging(@Nullable Printer printer) {
        mLogging = printer;
    }

我们可以通过实现Printer接口,然后调用这个方法进行设置日志打印类。

二.从AndroidGodEye的Sm#install()开始

我们前面在AndroidGodEye解析之帧率(fps)说过这个框架的基本使用了,使用起来不难,我们今天讲流畅度,那么我们就从模块android-godeye中的sm包中的Sm#install()开发讲:

 public synchronized void install(Context context) {
        install(new SmContextImpl(context, 2000, 500, 800));
    }

我们看到这里new了一个SmContextImpl对象,这个框架的Context是配置实现类,我们看下:

public class SmContextImpl implements SmContext {
    private static final int LONG_BLOCK_TIME = 2000;
    private static final int SHORT_BLOCK_TIME = 500;
    //800ms dump一次
    private static final int DUMP_INTERVAL = 800;
    private Context mContext;
    //长卡顿阀值
    public int mLongBlockThreshold;
    //短卡顿阀值
    public int mShortBlockThreshold;
    //dump信息的间隔
    public int mDumpInterval;

    public SmContextImpl(Context context, int longBlockThreshold, int shortBlockThreshold, int dumpInterval) {
        mContext = context.getApplicationContext();
        this.mLongBlockThreshold = longBlockThreshold <= 0 ? LONG_BLOCK_TIME : longBlockThreshold;
        this.mShortBlockThreshold = shortBlockThreshold <= 0 ? SHORT_BLOCK_TIME : shortBlockThreshold;
        this.mDumpInterval = dumpInterval <= 0 ? DUMP_INTERVAL : dumpInterval;
    }

    @Override
    public Context context() {
        return mContext;
    }

    @Override
    public SmConfig config() {
        return new SmConfig(mLongBlockThreshold, mShortBlockThreshold, mDumpInterval);
    }
}

我们看到这个类实现比较简单,这里存储了dump信息间隔,还有长卡顿阈值和短卡顿阈值,待会会使用到这些配置。接着我们来看install()的具体方法实现:

  @Override
    public synchronized void install(SmContext config) {
        if (mInstalled) {
            L.d("sm already installed, ignore.");
            return;
        }
//将安装的标志置为true
        this.mInstalled = true;
//实例化SmCore对象,这个方法是流畅度的逻辑实现主类
        this.mBlockCore = new SmCore(config.context(), config.config());
//用来LooperMonitor中调用的拦截器
        this.mBlockCore.addBlockInterceptor(new BlockInterceptor() {
            @Override
            public void onStart(Context context) {
            }

            @Override
            public void onStop(Context context) {
            }

            @WorkerThread
            @Override
            public void onShortBlock(Context context, long blockTimeMillis) {
                produce(new BlockInfo(new ShortBlockInfo(blockTimeMillis)));
            }

            @WorkerThread
            @Override
            public void onLongBlock(Context context, LongBlockInfo blockInfo) {
                produce(new BlockInfo(blockInfo));
            }
        });
//这个方法主要是将LooperMonitor设置进Looper中,即调用了setMessageLogging方法
        mBlockCore.install();
        L.d("sm installed");
    }

我们看到这个方法主要是实例化SmCore类,然后往SmCore类添加拦截器,最后设置LooperMonitor给Main Looper的日志属性。我们接着看SmCore的构造函数:

 public SmCore(final Context context, SmConfig smConfig) {
        this.mContext = context;
        this.mSmConfig = smConfig;
//实例化堆栈采集器
        this.stackSampler = new StackSampler(
                Looper.getMainLooper().getThread(), this.mSmConfig.dumpInterval);
//实例化cpu信息采集器
        this.cpuSampler = new CpuSampler(this.mSmConfig.dumpInterval);
//实例化日志打印器Printer的实现类
        this.mMonitor = new LooperMonitor(new LooperMonitor.BlockListener() {

            @Override
            public void onEventStart(long startTime) {
                startDump();
            }

            @Override
            public void onEventEnd(long endTime) {
                stopDump();
            }

            @Override
            public void onBlockEvent(final long blockTimeMillis, final long threadBlockTimeMillis, final boolean longBlock, final long eventStartTimeMilliis, final long eventEndTimeMillis, long longBlockThresholdMillis, long shortBlockThresholdMillis) {
                HandlerThreadFactory.getObtainDumpThreadHandler().post(new Runnable() {
                    @Override
                    public void run() {
                        if (!longBlock) {//短卡顿
                            if (!mInterceptorChain.isEmpty()) {
                                for (BlockInterceptor interceptor : mInterceptorChain) {
                                    interceptor.onShortBlock(context, blockTimeMillis);
                                }
                            }
                            return;
                        }
                        //如果是长卡顿,那么需要记录很多信息
                        final boolean cpuBusy = cpuSampler.isCpuBusy(eventStartTimeMilliis, eventEndTimeMillis);
                        //这里短卡顿基本是dump不到数据的,因为dump延时一般都会比短卡顿时间久
                        final List<CpuInfo> cpuInfos = cpuSampler.getCpuRateInfo(eventStartTimeMilliis, eventEndTimeMillis);
                        final Map<Long, List<StackTraceElement>> threadStackEntries = stackSampler.getThreadStackEntries(eventStartTimeMilliis, eventEndTimeMillis);
                        Observable.fromCallable(new Callable<MemoryInfo>() {
                            @Override
                            public MemoryInfo call() throws Exception {
                                return new MemoryInfo(MemoryUtil.getAppHeapInfo(), MemoryUtil.getAppPssInfo(mContext), MemoryUtil.getRamInfo(mContext));
                            }
                        }).subscribe(new Consumer<MemoryInfo>() {
                            @Override
                            public void accept(MemoryInfo memoryInfo) throws Exception {
                                LongBlockInfo blockBaseinfo = LongBlockInfo.create(eventStartTimeMilliis, eventEndTimeMillis, threadBlockTimeMillis,
                                        blockTimeMillis, cpuBusy, cpuInfos, threadStackEntries, memoryInfo);
                                if (!mInterceptorChain.isEmpty()) {
                                    for (BlockInterceptor interceptor : mInterceptorChain) {
                                        interceptor.onLongBlock(context, blockBaseinfo);
                                    }
                                }
                            }
                        });
                    }
                });
            }
        }, this.mSmConfig.longBlockThreshold, this.mSmConfig.shortBlockThreshold);
    }

方法比较长,我们一个一个来,首先我们先来讲类LooperMonitor,实例化LooperMonitor的时候,我们传进去BlockListener接口的实现,这个接口主要是如下几个方法:

 public interface BlockListener {
        void onEventStart(long startTime);

        void onEventEnd(long endTime);

        /**
         * 卡顿事件
         *
         * @param eventStartTimeMilliis     事件开始时间
         * @param eventEndTimeMillis        事件结束时间
         * @param blockTimeMillis           卡顿时间(事件处理时间)
         * @param threadBlockTimeMillis     事件真实消耗时间
         * @param longBlockThresholdMillis  长卡顿阀值标准
         * @param shortBlockThresholdMillis 短卡顿阀值标准
         */
        void onBlockEvent(long blockTimeMillis, long threadBlockTimeMillis, boolean longBlock,
                          long eventStartTimeMilliis, long eventEndTimeMillis, long longBlockThresholdMillis,
                          long shortBlockThresholdMillis);
    }

这几个方法主要是会在LooperMonitor类中的println()方法中会进行调用,这个方法是实现Printer接口要实现的方法,主要用于打印:

  @Override
    public void println(String x) {
        if (!mEventStart) {// 事件开始
            mThisEventStartTime = System.currentTimeMillis();
            mThisEventStartThreadTime = SystemClock.currentThreadTimeMillis();
            mEventStart = true;
            mBlockListener.onEventStart(mThisEventStartTime);
        } else {// 事件结束
            final long thisEventEndTime = System.currentTimeMillis();
            final long thisEventThreadEndTime = SystemClock.currentThreadTimeMillis();
            mEventStart = false;

            long eventCostTime = thisEventEndTime - mThisEventStartTime;
            long eventCostThreadTime = thisEventThreadEndTime - mThisEventStartThreadTime;
            if (eventCostTime >= mLongBlockThresholdMillis) {
                mBlockListener.onBlockEvent(eventCostTime, eventCostThreadTime, true, mThisEventStartTime,
                        thisEventEndTime, mLongBlockThresholdMillis, mShortBlockThresholdMillis);
            } else if (eventCostTime >= mShortBlockThresholdMillis) {
                mBlockListener.onBlockEvent(eventCostTime, eventCostThreadTime, false, mThisEventStartTime,
                        thisEventEndTime, mLongBlockThresholdMillis, mShortBlockThresholdMillis);
            }
            mBlockListener.onEventEnd(thisEventEndTime);
        }
    }

我们看到这个方法在第一个判断if (!mEventStart)中首先判断事件是否开始,如果开始则记录当前的开始时间,然后调用设置进来的BlockListener的实现类的onEventStart()方法,接着在事件结束时候,会记录结束的时间,同时计算用时,然后跟设置的长卡顿阈值和短卡顿阈值进行比较,如果有卡顿现象,则调用onBlockEvent()方法,并且把相关参数设置进去。那么我们就看看BlockListener实现类这几个方法的实现:

  @Override
 public void onEventStart(long startTime) {
          startDump();
 }
 @Override
 public void onEventEnd(long endTime) {
         stopDump();
 }

我们看到事件开始和结束分别调用了开始dump和结束dump,这两个里面主要是启动堆栈信息采集和Cpu信息采集,等会会来讲,我们先看onBlockEvent()方法:

  @Override
            public void onBlockEvent(final long blockTimeMillis, final long threadBlockTimeMillis, final boolean longBlock, final long eventStartTimeMilliis, final long eventEndTimeMillis, long longBlockThresholdMillis, long shortBlockThresholdMillis) {
                HandlerThreadFactory.getObtainDumpThreadHandler().post(new Runnable() {
                    @Override
                    public void run() {
                        if (!longBlock) {//短卡顿
                            if (!mInterceptorChain.isEmpty()) {
                                for (BlockInterceptor interceptor : mInterceptorChain) {
                                    interceptor.onShortBlock(context, blockTimeMillis);
                                }
                            }
                            return;
                        }
                        //如果是长卡顿,那么需要记录很多信息
                        final boolean cpuBusy = cpuSampler.isCpuBusy(eventStartTimeMilliis, eventEndTimeMillis);
                        //这里短卡顿基本是dump不到数据的,因为dump延时一般都会比短卡顿时间久
                        final List<CpuInfo> cpuInfos = cpuSampler.getCpuRateInfo(eventStartTimeMilliis, eventEndTimeMillis);
                        final Map<Long, List<StackTraceElement>> threadStackEntries = stackSampler.getThreadStackEntries(eventStartTimeMilliis, eventEndTimeMillis);
                        Observable.fromCallable(new Callable<MemoryInfo>() {
                            @Override
                            public MemoryInfo call() throws Exception {
                                return new MemoryInfo(MemoryUtil.getAppHeapInfo(), MemoryUtil.getAppPssInfo(mContext), MemoryUtil.getRamInfo(mContext));
                            }
                        }).subscribe(new Consumer<MemoryInfo>() {
                            @Override
                            public void accept(MemoryInfo memoryInfo) throws Exception {
                                LongBlockInfo blockBaseinfo = LongBlockInfo.create(eventStartTimeMilliis, eventEndTimeMillis, threadBlockTimeMillis,
                                        blockTimeMillis, cpuBusy, cpuInfos, threadStackEntries, memoryInfo);
                                if (!mInterceptorChain.isEmpty()) {
                                    for (BlockInterceptor interceptor : mInterceptorChain) {
                                        interceptor.onLongBlock(context, blockBaseinfo);
                                    }
                                }
                            }
                        });
                    }
                });
            }
        }, this.mSmConfig.longBlockThreshold, this.mSmConfig.shortBlockThreshold);

这个方法主要是处理卡顿事件的,首先方法判断是否是短卡顿,如果是的话就调用拦截器的onShortBlock()方法,如果是长卡顿,则需要获取Cpu和内存的一些信息,然后最后调用onLongBlock()方法,那么现在我们就可以来看看cpu和内存一些信息是怎么采集的。

三.Cpu startDump()

启动dump操作的是在onEventStart中调用startDump()方法:

 private void startDump() {
        if (null != stackSampler) {
            stackSampler.start();
        }
        if (null != cpuSampler) {
            cpuSampler.start();
        }
    }

我们看到,这里面会调用采集器的start()方法,这个方法是在父类中实现的:

public void start() {
        if (mShouldSample.get()) {
            return;
        }
        mShouldSample.set(true);

        HandlerThreadFactory.getDoDumpThreadHandler().removeCallbacks(mRunnable);
        HandlerThreadFactory.getDoDumpThreadHandler().postDelayed(mRunnable,
                Sm.core().getSampleDelay());
    }

我们看到这个方法主要是发送了一条Handler消息,如果你不懂得Handler的相关机制,可以去看看前面的文章,这样我们程序会执行mRunnablerun方法:

    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            doSample();

            if (mShouldSample.get()) {
                HandlerThreadFactory.getDoDumpThreadHandler()
                        .postDelayed(mRunnable, mSampleInterval);
            }
        }
    };

我们看到这个方法里面会调用doSample()方法,并且会开启定时发送消息即定时采集。我们来看看doSample()方法:

 @Override
    protected void doSample() {
        /**
         * cpu信息采集必须要有两次执行才能出结果,否则为空
         * 也就是说,如果认为block时间是1000ms,开始采集的延时时间为800ms,sampl时间间隔为300ms,如果实际运行中
         * block的时间 >1000ms && < 1400ms (delayTime + 2*intervalMillis),那么是采集不到cpu数据的
         */
        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 = "";
            }
            /**
             * 从系统启动开始,花在各种处理上的apu时间
             */
            parse(cpuRate, pidCpuRate);
        } catch (Throwable throwable) {
        } finally {
            IoUtil.closeSilently(cpuReader);
            IoUtil.closeSilently(pidReader);
        }
    }

这个方法主要是获取CPU时间片使用情况的:
1)获取系统CPU时间片读取proc/stat,文件的内容如下:

    cpu 2032004 102648 238344 167130733 758440 15159 17878 0
    cpu0 1022597 63462 141826 83528451 366530 9362 15386 0
    cpu1 1009407 39185 96518 83602282 391909 5796 2492 0
    intr 303194010 212852371 3 0 0 11 0 0 2 1 1 0 0 3 0 11097365 0 72615114 6628960 0 179 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    ctxt 236095529
    btime 1195210746
    processes 401389
    procs_running 1
    procs_blocked 0

第一行各个字段的含义:

    user (14624) 从系统启动开始累计到当前时刻,处于用户态的运行时间,不包含 nice值为负进程。 
    nice (771) 从系统启动开始累计到当前时刻,nice值为负的进程所占用的CPU时间 
    system (8484) 从系统启动开始累计到当前时刻,处于核心态的运行时间 
    idle (283052) 从系统启动开始累计到当前时刻,除IO等待时间以外的其它等待时间 
    iowait (0) 从系统启动开始累计到当前时刻,IO等待时间(since 2.5.41) 
    irq (0) 从系统启动开始累计到当前时刻,硬中断时间(since 2.6.0-test4) 
    softirq (62) 从系统启动开始累计到当前时刻,软中断时间(since 2.6.0-test4) 

总的cpu时间totalCpuTime = user + nice + system + idle + iowait + irq + softirq。
2)获取进程和线程的CPU时间片
获取进程CPU时间片使用情况:读取proc/pid/stat,获取线程CPU时间片使用情况:读取proc/pid/task/tid/stat,这两个文件的内容相同,如下:

6873 (a.out) R 6723 6873 6723 34819 6873 8388608 77 0 0 0 41958 31 0 0 25 0 3 0 5882654 1409024 56 4294967295 134512640 134513720 3215579040 0 2097798 0 0 0 0 0 0 0 17 0 0 0

各个字段的含义:

    pid=6873 进程(包括轻量级进程,即线程)号
    comm=a.out 应用程序或命令的名字
    task_state=R 任务的状态,R:runnign, S:sleeping (TASK_INTERRUPTIBLE), D:disk sleep (TASK_UNINTERRUPTIBLE), T: stopped, T:tracing stop,Z:zombie, X:dead
    ppid=6723 父进程ID
    pgid=6873 线程组号
    sid=6723 c该任务所在的会话组ID
    tty_nr=34819(pts/3) 该任务的tty终端的设备号,INT(34817/256)=主设备号,(34817-主设备号)=次设备号
    tty_pgrp=6873 终端的进程组号,当前运行在该任务所在终端的前台任务(包括shell 应用程序)的PID。
    task->flags=8388608 进程标志位,查看该任务的特性
    min_flt=77 该任务不需要从硬盘拷数据而发生的缺页(次缺页)的次数
    cmin_flt=0 累计的该任务的所有的waited-for进程曾经发生的次缺页的次数目
    maj_flt=0 该任务需要从硬盘拷数据而发生的缺页(主缺页)的次数
    cmaj_flt=0 累计的该任务的所有的waited-for进程曾经发生的主缺页的次数目
    utime=1587 该任务在用户态运行的时间,单位为jiffies
    stime=1 该任务在核心态运行的时间,单位为jiffies
    cutime=0 累计的该任务的所有的waited-for进程曾经在用户态运行的时间,单位为jiffies
    cstime=0 累计的该任务的所有的waited-for进程曾经在核心态运行的时间,单位为jiffies
    priority=25 任务的动态优先级
    nice=0 任务的静态优先级
    num_threads=3 该任务所在的线程组里线程的个数
    it_real_value=0 由于计时间隔导致的下一个 SIGALRM 发送进程的时延,以 jiffy 为单位.
    start_time=5882654 该任务启动的时间,单位为jiffies
    vsize=1409024(page) 该任务的虚拟地址空间大小
    rss=56(page) 该任务当前驻留物理地址空间的大小
    rlim=4294967295(bytes) 该任务能驻留物理地址空间的最大值
    start_code=134512640 该任务在虚拟地址空间的代码段的起始地址
    end_code=134513720 该任务在虚拟地址空间的代码段的结束地址
    start_stack=3215579040 该任务在虚拟地址空间的栈的结束地址
    kstkesp=0 esp(32 位堆栈指针) 的当前值, 与在进程的内核堆栈页得到的一致.
    kstkeip=2097798 指向将要执行的指令的指针, EIP(32 位指令指针)的当前值.
    pendingsig=0 待处理信号的位图,记录发送给进程的普通信号
    block_sig=0 阻塞信号的位图
    sigign=0 忽略的信号的位图
    sigcatch=082985 被俘获的信号的位图
    wchan=0 如果该进程是睡眠状态,该值给出调度的调用点
    nswap 被swapped的页数,当前没用
    cnswap 所有子进程被swapped的页数的和,当前没用
    exit_signal=17 该进程结束时,向父进程所发送的信号
    task_cpu(task)=0 运行在哪个CPU上
    task_rt_priority=0 实时进程的相对优先级别
    task_policy=0 进程的调度策略,0=非实时进程,1=FIFO实时进程;2=RR实时进程

进程的总Cpu时间processCpuTime = utime + stime + cutime + cstime
线程的总Cpu时间threadCpuTime = utime + stime + cutime + cstime
上面的资料主要是对应的方法里面的parse()方法调用:

 private void parse(String cpuRate, String pidCpuRate) {
        String[] cpuInfoArray = cpuRate.split(" ");
        if (cpuInfoArray.length < 9) {
            return;
        }
        long user = Long.parseLong(cpuInfoArray[2]);
        long nice = Long.parseLong(cpuInfoArray[3]);
        long system = Long.parseLong(cpuInfoArray[4]);
        long idle = Long.parseLong(cpuInfoArray[5]);
        long ioWait = Long.parseLong(cpuInfoArray[6]);
        long total = user + nice + system + idle + ioWait
                + Long.parseLong(cpuInfoArray[7])
                + Long.parseLong(cpuInfoArray[8]);
        String[] pidCpuInfoList = pidCpuRate.split(" ");
        if (pidCpuInfoList.length < 17) {
            return;
        }

        long appCpuTime = Long.parseLong(pidCpuInfoList[13])
                + Long.parseLong(pidCpuInfoList[14])
                + Long.parseLong(pidCpuInfoList[15])
                + Long.parseLong(pidCpuInfoList[16]);
        if (mTotalLast != 0) {
            long idleTime = idle - mIdleLast;
            long totalTime = total - mTotalLast;
            /**
             * 一个sample时间段内
             * 总的cpu使用率
             * app的cpu使用率
             * 用户进程cpu使用率
             * 系统进程cpu使用率
             * io等待时间占比
             */
            CpuInfo cpuInfo = new CpuInfo((totalTime - idleTime) * 100L / totalTime, (appCpuTime - mAppCpuTimeLast) *
                    100L / totalTime,
                    (user - mUserLast) * 100L / totalTime, (system - mSystemLast) * 100L / totalTime, (ioWait -
                    mIoWaitLast) * 100L / totalTime);

            synchronized (mCpuInfoEntries) {
                mCpuInfoEntries.put(System.currentTimeMillis(), cpuInfo);
                if (mCpuInfoEntries.size() > MAX_ENTRY_COUNT) {
                    for (Map.Entry<Long, CpuInfo> entry : mCpuInfoEntries.entrySet()) {
                        Long key = entry.getKey();
                        mCpuInfoEntries.remove(key);
                        break;
                    }
                }
            }
        }
        mUserLast = user;
        mSystemLast = system;
        mIdleLast = idle;
        mIoWaitLast = ioWait;
        mTotalLast = total;

        mAppCpuTimeLast = appCpuTime;
    }

借鉴BlockCanary原理分析的文章总结Cpu参数的作用:

  • 采集当前cpu的使用率,如果cpu使用率太高,可能会导致cpu处理来不及,所以函数执行到一半可能暂时挂起,等待cpu重新调度
  • 采集当前cpu是否繁忙而处理不过来,道理如上,cpu繁忙会导致函数执行一半倍挂起,需要等到下一次cpu调度后重新继续执行
  • 当前app的cpu占用率
  • 用户使用情况,系统使用情况
  • %ioWait:首先 %iowait 升高并不能证明等待I/O的进程数量增多了,也不能证明等待I/O的总时间增加了;
      1)例如,在CPU繁忙期间发生的I/O,无论IO是多还是少,%iowait都不会变;当CPU繁忙程度下降时,有一部分IO落入CPU空闲时间段内,导致%iowait升高。
      2)再比如,IO的并发度低,%iowait就高;IO的并发度高,%iowait可能就比较低。
    可见%iowait是一个非常模糊的指标,如果看到 %iowait 升高,还需检查I/O量有没有明显增加,avserv/avwait/avque等指标有没有明显增大,应用有没有感觉变慢,如果都没有,就没什么好担心的。

四.StackSampler doSample

同样地,方法堆栈信息的采集也是在StackSampler#doSample()方法里面的:

 @Override
    protected void doSample() {
        synchronized (sStackMap) {
            if (sStackMap.size() == mMaxEntryCount && mMaxEntryCount > 0) {
                sStackMap.remove(sStackMap.keySet().iterator().next());
            }
            sStackMap.put(System.currentTimeMillis(), mCurrentThread.getStackTrace());
        }
    }

同样借鉴BlockCanary原理分析中的说明:
mCurrentThread就是主线程对象,0.8 * mSampleInterval(卡顿时长阀值)后的去获取线程的堆栈信息并保存到sStackMap中,这里的意思是,我们认为方法执行超过mSampleInterval就表示卡顿,当方法执行时间已经到了mSampleInterval的0.8倍的时候还没执行完,那么这时候就开始采集方法执行堆栈信息了,如果方法在0.9 * mSampleInterval的时候执行完成,那么不会警告卡顿,但是如果方法执行耗时超过mSampleInterval,那就把0.8 * mSampleInterval这个时间点的堆栈信息认为是造成耗时原因的堆栈信息,而且,这里只要方法还没执行完,就会间隔mSampleInterval去再次获取函数执行堆栈信息并保存,这里之所以遥在0.8 * mSampleInterval的时候就去获取堆栈信息时为了获取到准确的堆栈信息,因为既然函数耗时已经达到0.8 * mSampleInterval了,并且函数还没执行结束,那么很大概率上会导致卡顿了,所以提前获取函数执行堆栈保证获取到造成卡顿的函数调用堆栈的正确性,后面又不断间隔mSampleInterval去获取函数执行堆栈式要获取到更多完整的堆栈信息,当方法执行完成后就会停止获取函数执行堆栈了,所有的函数执行堆栈信息最多存100条,也就是最多有100个函数调用堆栈,以当前的时间戳作为key,当监测到卡顿的时候,要把之前保存在sStackMap的函数堆栈信息展示通知出来,通过时间戳就能取到。

总结:到这里,流畅度的获取也就完成了,跟BlockCanary的原理是一样的,实现方法是很巧妙的,希望大家通过这篇文章有所收获,同时也能熟练获取和应用这些性能数据。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容