一.从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 blockif (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;}@Overridepublic Context context() {return mContext;}@Overridepublic SmConfig config() {return new SmConfig(mLongBlockThreshold, mShortBlockThreshold, mDumpInterval);}
}

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

  @Overridepublic synchronized void install(SmContext config) {if (mInstalled) {L.d("sm already installed, ignore.");return;}
//将安装的标志置为truethis.mInstalled = true;
//实例化SmCore对象,这个方法是流畅度的逻辑实现主类this.mBlockCore = new SmCore(config.context(), config.config());
//用来LooperMonitor中调用的拦截器this.mBlockCore.addBlockInterceptor(new BlockInterceptor() {@Overridepublic void onStart(Context context) {}@Overridepublic void onStop(Context context) {}@WorkerThread@Overridepublic void onShortBlock(Context context, long blockTimeMillis) {produce(new BlockInfo(new ShortBlockInfo(blockTimeMillis)));}@WorkerThread@Overridepublic 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() {@Overridepublic void onEventStart(long startTime) {startDump();}@Overridepublic void onEventEnd(long endTime) {stopDump();}@Overridepublic 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() {@Overridepublic 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>() {@Overridepublic MemoryInfo call() throws Exception {return new MemoryInfo(MemoryUtil.getAppHeapInfo(), MemoryUtil.getAppPssInfo(mContext), MemoryUtil.getRamInfo(mContext));}}).subscribe(new Consumer<MemoryInfo>() {@Overridepublic 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接口要实现的方法,主要用于打印:

  @Overridepublic 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实现类这几个方法的实现:

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

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

  @Overridepublic 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() {@Overridepublic 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>() {@Overridepublic MemoryInfo call() throws Exception {return new MemoryInfo(MemoryUtil.getAppHeapInfo(), MemoryUtil.getAppPssInfo(mContext), MemoryUtil.getRamInfo(mContext));}}).subscribe(new Consumer<MemoryInfo>() {@Overridepublic 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() {@Overridepublic void run() {doSample();if (mShouldSample.get()) {HandlerThreadFactory.getDoDumpThreadHandler().postDelayed(mRunnable, mSampleInterval);}}};

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

 @Overrideprotected 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 0cpu0 1022597 63462 141826 83528451 366530 9362 15386 0cpu1 1009407 39185 96518 83602282 391909 5796 2492 0intr 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 0ctxt 236095529btime 1195210746processes 401389procs_running 1procs_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:deadppid=6723 父进程IDpgid=6873 线程组号sid=6723 c该任务所在的会话组IDtty_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 该任务在用户态运行的时间,单位为jiffiesstime=1 该任务在核心态运行的时间,单位为jiffiescutime=0 累计的该任务的所有的waited-for进程曾经在用户态运行的时间,单位为jiffiescstime=0 累计的该任务的所有的waited-for进程曾经在核心态运行的时间,单位为jiffiespriority=25 任务的动态优先级nice=0 任务的静态优先级num_threads=3 该任务所在的线程组里线程的个数it_real_value=0 由于计时间隔导致的下一个 SIGALRM 发送进程的时延,以 jiffy 为单位.start_time=5882654 该任务启动的时间,单位为jiffiesvsize=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()方法里面的:

 @Overrideprotected 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的原理是一样的,实现方法是很巧妙的,希望大家通过这篇文章有所收获,同时也能熟练获取和应用这些性能数据。

+qq群:853967238。获取以上高清技术思维图,以及相关技术的免费视频学习资料。

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

  1. iOS性能优化系列篇之“列表流畅度优化”工具篇

    这一篇文章是iOS性能优化系列文章的的第二篇,主要内容是关于列表流畅度的优化.在具体内容的阐述过程中会结合性能优化的总体原则进行分析,所以建议大家在阅读这篇文章前先阅读一下上一篇文章:iOS性能优化系 ...

  2. 前端项目性能优化(全面解析)

    前言 为什么要进行Web性能优化? 流量搜索转换率用户体验Amazon 发现每100ms延迟导致1%的销量损失 寻找性能瓶颈 了解性能指标 - 多快才算快 利用测量工具和APIs 优化问题,重新测量( ...

  3. 52条 SQL 语句性能优化策略全面解析,你知道多少种?建议收藏!

    点击上方 "编程技术圈"关注, 星标或置顶一起成长 后台回复"大礼包"有惊喜礼包! 每日英文 Don't always in the memories of t ...

  4. 最新IP数据库 存储优化 查询性能优化 每秒解析上千万

    高性能IP数据库格式详解 每秒解析1000多万ip  qqzeng-ip-ultimate.dat 3.0版 编码:UTF8     字节序:Little-Endian 返回规范字段(如:亚洲|中国| ...

  5. Android性能优化全方面解析

    目的 公司的新需求终于解决完了,离测试和发布还有段时间,第一次体验了下没需求没bug的感觉,真是舒爽~然后翻了翻有什么可以学的.无意翻到了Android后期发展的五大趋势.一.性能优化.二.高级UI. ...

  6. sql 行政区划关联查询优化_最新IP数据库 存储优化 查询性能优化 每秒解析上千万...

    高性能IP数据库格式详解 每秒解析1000多万ip  qqzeng-ip-ultimate.dat 3.0版编码:UTF8字节序:Little-Endian 返回规范字段(如:亚洲|中国|香港|九龙| ...

  7. android10系统打王者掉帧,再也不怕掉帧卡顿 教你如何自己优化《王者荣耀》流畅度 - 非凡软件站...

    最近有许多玩家给小编说,王者荣耀更新到某个版本后,游戏出现了卡顿掉帧情况,这其实是游戏自己"优化"所导致.对于不同机型的手机,,<王者荣耀>会使用不同的图形API.不同 ...

  8. 如何用 GPU 硬件层加速优化 Android 系统的游戏流畅度

    作为一款VR实时操作游戏App,我们需要根据重力感应系统,实时监控手机的角度,并渲染出相应位置的VR图像,因此在不同 Android 设备之间,由于使用的芯片组和不同架构的GPU,游戏性能会因此受到影 ...

  9. 如何用 GPU硬件层加速优化Android系统的游戏流畅度—应用性能管理

    作为一款VR实时操作游戏App,我们需要根据重力感应系统,实时监控手机的角度,并渲染出相应位置的VR图像,因此在不同 Android 设备之间,由于使用的芯片组和不同架构的GPU,游戏性能会因此受到影 ...

最新文章

  1. WPF 中动态创建和删除控件
  2. 2021/6/18~19 每天学习python 30分钟 -了解python - python的基本变量概述
  3. “==“和equals的区别是什么(史上最全总结、最靠谱)
  4. VMware虚拟机网络模式详解 NAT模式
  5. MFC控件随窗口大小变化原理及实现
  6. 使用ueditor实现多图片上传案例
  7. (转)Redis上踩过的一些坑-美团
  8. 自己封装的ASP.NET的SQLITE数据库的操作类
  9. JavaScript实现省市选择功能,jQuery,Json
  10. 待熟悉的工具或API清单列表
  11. 别在网上乱找代码了,找了一段代码突然爆了!!!
  12. 7-9 字符串字母大小写转换 (15 point(s))
  13. 十二、React脚手架
  14. ASO优化选词:三种方法教你精准定位关键词
  15. 亚马逊云EC2助力5G产品测试
  16. 多线程-day-09CAS原理
  17. matlab无限长一维原子链,固体物理 03-03一维双原子链
  18. 成都拓嘉辰丰:拼多多子账号建立的方法?
  19. java8 GC日志分析
  20. Scala可变长度参数和:_*使用

热门文章

  1. CNBC对话投资人:什么事物可以代币化?代币的上限在哪里?
  2. 图像像素数和分辨率的区别
  3. luogu P1803线段覆盖
  4. DTMF双音多频系统信号输入和检测识别算法matlab仿真
  5. 寻求长期合作工程师做兼职STM32 51 8S FPGA 图像处理
  6. 黑马程序员,黑马论坛----黑马.Net8期,就业率98.61%,平均薪水5722元!
  7. 浅析std::string的append方法
  8. 本地计算机无法启动windowstime,win10系统windowstime服务无法启动的设置方案
  9. mysql workbench改密码_在MySQL Workbench8.0中,忘记MySQL root密码的情况下修改密码
  10. 【办公】解决京瓷打印机总是出现烦人的“在手送纸盘中装纸 彩色纸”的问题