主目录见: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 cpuInfos = cpuSampler.getCpuRateInfo(eventStartTimeMilliis, eventEndTimeMillis);
final Map> threadStackEntries = stackSampler.getThreadStackEntries(eventStartTimeMilliis, eventEndTimeMillis);
Observable.fromCallable(new Callable() {
@Override
public MemoryInfo call() throws Exception {
return new MemoryInfo(MemoryUtil.getAppHeapInfo(), MemoryUtil.getAppPssInfo(mContext), MemoryUtil.getRamInfo(mContext));
}
}).subscribe(new Consumer() {
@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 cpuInfos = cpuSampler.getCpuRateInfo(eventStartTimeMilliis, eventEndTimeMillis);
final Map> threadStackEntries = stackSampler.getThreadStackEntries(eventStartTimeMilliis, eventEndTimeMillis);
Observable.fromCallable(new Callable() {
@Override
public MemoryInfo call() throws Exception {
return new MemoryInfo(MemoryUtil.getAppHeapInfo(), MemoryUtil.getAppPssInfo(mContext), MemoryUtil.getRamInfo(mContext));
}
}).subscribe(new Consumer() {
@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的相关机制,可以去看看前面的文章,这样我们程序会执行mRunnable
的run
方法:
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 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的原理是一样的,实现方法是很巧妙的,希望大家通过这篇文章有所收获,同时也能熟练获取和应用这些性能数据。