我的理解,进入一个activity开始 一直到首屏页面被渲染出来也就是用户可见的状态。这个时间固然是越短越好。这个时间越长, activity的白屏时间就越长,这对于不少低端的手机用户来讲是不可忍受的,用户体验极差。java
答:先说结论,此测量activity首屏渲染时间的方法为错误。 下面从多个维度来证实这个方案的错误。android
首先咱们看onResume的函数注释:git
注意看这个地方红线标注的单词是指的“交互”这个意思,也就是说,执行到onresume这个方法的时候是指的用户能够交互了, 并无说能够看到东西了,也没说ui绘制完毕了。有人问 “能够交互“”难道不是在“能够看见“”以后么,你没看到怎么交互呢? 其实这个说法是错误的,若是你的代码写的很烂,手机又不好的话,其实activity在白屏的时候 页面还没渲染出来你就能够点返回 键进行返回了。这个返回的动做 就是能够交互的状态,可是白屏表明着界面还没绘制完毕。这一点你用MONKEY跑自动化测试的时候 能够明显看到。github
能够看到用命令行启动一个acitivty的时候 下面也是有时间输出的。这个时间通常都会认为至关接近咱们想要的activity的启动时间了。咱们注意看一下 一样的一条命令, 咱们第一次启动这个activity远远比后面几回时间要长。缘由就是第一次加载一个activity的时候 不少图片类的资源 文字资源 xml等等信息都是第一次load到内存里,因此比较耗时,后面由于加载过一次因此内存有一些缓存之类的东西因此后面几回时间会比较快(要知道io操做是至关耗时的,直接从内存加载固然快不少)。shell
咱们在源码里搜索一下这段输出的日志关键字,最终定位到这段日志是在activityrecord这个类的这个方法里输出的。数据库
你们能够看一下,这个totalTime 的定义,当前时间 减去 开始运行的时间。能够得出一个结论这个时间已经很是接近 咱们想要的时间了。咱们的界面绘制时间必定是小于这个总时间的。 有兴趣的同窗能够跟踪一下这个mLaunStartTime 究竟是在哪里被谁赋值。我这里篇幅所限就不过多论述。缓存
能够给点提示activitystack的这个方法被调用的时候赋值的。bash
方案B的时间虽然能够接近咱们想要的结果,可是毕竟这是命令行才能使用,还得有root权限,非root权限的手机你是没法 执行这个命令的,这让咱们想统计activity的启动时间带来了困难。必定要找到一个能够从代码层面输出界面绘制时间的方法。微信
都知道activity的管理者真正是activitythread,因此咱们直接找这个类的源码看看。这个方法过长了,咱们先放主要的片断网络
首先咱们看第一张图,这里明显的调用了,resume这个方法的回调,可是下面第二张图能够看到里面有个decorView 而且这个decorView 正在被vm add进去,都知道decorView的子view 有个xml布局里面有个framelayout是咱们acitivity的rootview,就是那个id为content的layout。能够看出来 这里onResume方法调用就在这个addview 前面了,因此再次证实方案a是多么不靠谱,你acitivity的界面都没add进去呢 怎么可能绘制结束?
这里可能有些绕,可是只要记住activity的层级关系便可:
一个Activity包含了一个Window,这个Window实际上是一个PhoneWindow,在PhoneWindow中包含了DecorView,变量名称为mDecor,mDecor有一个子View,这个子View的布局方式根据设定的主题来肯定,在这个子View的xml布局中包含了一个FrameLayout元素,这个FrameLayout元素的id为content,这个content对应于PhoneWindow中的mContentParent变量,用户自定义的布局做为mContentParent的子View存在,通常状况下mContentParnet只有一个子View,若是在Activity调用addView方式其实是给PhoneWindow中的mContentParent添加子View,因为mContentParent是一个FrameLayout,所以新的子view会覆盖经过setContentView添加的子view。
继续跟:
一直跟,跟到这里:
这里咱们new 出了ViewRootImpl对象, 咱们知道这个对象就是android view的根对象了,负责view绘制的measure, layout, draw的巨长的方法 performTraversals就是这个类的,咱们继续看setView方法 这里面最重要的就是调用了requestLayout 这个方法
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
//这个方法其实不难理解,看名字本身翻译下就知道就是遍历作一些事情的意思(至因而什么事固然是ui绘制啊)
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//Choreographer 负责帧率刷新的一个类,之后会讲到他。暂时理解成相似于往ui线程post了一个消息就能够了
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
//mTraversalRunnable 就是这个类的对象
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
//这个方法应该很敏感,颇有名的一个方法 就不分析他了 太长了,超出篇幅。
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
复制代码
分析到这里,应该能够稍微理一理activity绘制的一个大概流程:
1.activitythread 调用handleresumeactivity方法 也就是 先回调onresume方法 2.scheduleTraversals post了一个TraversalRunnable消息。 3.post的这个消息作了一件事 调用了绘制ui的核心方法performTraversals。
这个流程也再次验证了方案a 利用oncreate和onresume时间差的不靠谱
方案C 是一个接近靠谱的方法。在阐述这个方法以前,咱们先用一张图回归一下Handler Looper和MessageQueue这个东西。
简单来讲一下这三者之间的关系: Handler经过sendMessage将消息投递给MessageQueue,Looper经过消息循环(loop)不断的从MessageQueue中取出消息,而后消息被Handler的dispatchMessage分发到handleMessage方法消费掉。
而后咱们看一个特殊的源码,来自于MessageQueue:
注意看他的注释:
其实意思就是说,若是咱们looper里的消息都处理完了,那么就会回调这个接口,若是这个方法返回false,那么回调这一次之后就会把这个idleHandler给干掉,若是返回true,那么消息处理完毕就继续调用这个iderHandler接口的queueidle方法。
so:咱们的正确方案C 就呼之欲出了:
简单来讲,在大部分低端手机中,咱们老是但愿用户进入一个新页面的时候能尽快看到这个页面想要展现的内容,尤为在弱网环境 或者大量数据须要从网络中获取时,咱们老是但愿界面能先展现一些固定的结构,甚至基本要素。而后等对应的接口回来之后再进去 填充数据,不然页面白白的区域显示时间过长,体验不佳(这点头条新浪微博微信等作的尤为出色)
cpu的时间片老是固定的,硬件所限,为了让ui线程尽快的处理完毕,咱们老是但愿这一段时间内尽量的只有ui线程在跑, 这样ui线程获取的时间片更多,执行速度起来就会很快,若是你一开始就在oncreate方法里作了太多的诸如网络操做, io操做,数据库操做,那必然的是ui线程获取cpu时间变少,速度变慢。
咱们来看这样一段程序:
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.tv);
Log.v("wuyue", "textView height==" + textView.getWidth());
}
@Override
protected void onResume() {
super.onResume();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
Log.v("wuyue", "textView height2==" + textView.getWidth());
return false;
}
});
}
}
复制代码
很显然,第一种在oncreate方法里获取tv的高度确定获取不到由于这会还没绘制结束呢。 第二种就能够拿到了,缘由前面已经说过了。很少讲。
日志也反应了咱们的正确性。
那么有没有更好的方法来证实这个是正确的呢?
能够用android studio的 method trace来看方法的执行轨迹,ddms的 method profiling也能够。这2个工具在这里很少介绍了。 是查卡顿的很重要的方法,各位自行百度谷歌使用方法便可。
前面讲述的是activity的启动优化,实际上,咱们更但愿实时的知道咱们app运行的具体状况,好比滑动的时候到底有没有卡顿? 若是有卡顿发生,怎么知道大概在哪里出现了问题以便咱们迅速定位到问题代码?
这个命令你们都很熟悉,可获取最新128帧的绘制信息,详细包括每一帧绘制的Draw,Process,Execute三个过程的耗时,若是这三个时间总和超过16.6ms即认为是发生了卡顿。 可是咱们不可能每次到一个页面都去手动执行如下这个命令,太麻烦了,并且 不一样的手机还要屡次打这个命令,线上实际生产版本也没办法让用户来打这个命令获取结果,因此实际上这个方法并不使用。 仍是须要在代码层面下功夫
ui线程绑定的looper的loop方法 无限循环跑这段代码,执行dispatch方法,注意这个方法的先后都有logging的输出。 那么这2个logging输出的时间差 是否是就能够认为这是咱们执行ui线程的时间吗?这个时间长不就表明了ui线程有卡顿现象么?
同时咱们到 这个me.mLogging还能够经过public的set方法来设置。
package com.suning.mobile.ebuy;
import android.os.Looper;
import android.util.Printer;
public class CustomPrinterForGetBlockInfo {
public static void start() {
Looper.getMainLooper().setMessageLogging(new Printer() {
//日志输出有不少种格式,咱们这里只捕获ui线程中dispatch上下文的日志信息
//因此这里定义了2个key值,注意不一样的手机这2个key值可能不同,有须要的话这里要作机型适配,
//不然部分手机这里可能抓取不到日志信息
private static final String START = ">>>>> Dispatching";
private static final String END = "<<<<< Finished";
@Override
public void println(String x) {
//这里的思路就是若是发如今打印dispatch方法的 start信息,
//那么咱们就在 “时间戳” 以后 post一个runnable
if (x.startsWith(START)) {
LogMonitor.getInstance().startMonitor();
}
//由于咱们start 不是当即start runnable 而是在“时间戳” 以后 那么若是在这个时间戳以内
//dispacth方法执行完毕之后的END到来,那么就会remove掉这个runnable
//因此 这里就知道 若是dispatch方法执行时间在时间戳以内 那么咱们就认为这个ui没卡顿,不输出任何卡顿信息
//不然就输出卡顿信息 这里卡顿信息主要用StackTraceElement 来输出
if (x.startsWith(END)) {
LogMonitor.getInstance().removeMonitor();
}
}
});
}
}
复制代码
package com.suning.mobile.ebuy;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.util.Log;
public class LogMonitor {
private static LogMonitor sInstance = new LogMonitor();
//HandlerThread 这个其实就是一个thread,只不过相对于普通的thread 他对外暴露了一个looper而已。方便
//咱们和handler配合使用
private HandlerThread mLogThread = new HandlerThread("BLOCKINFO");
private Handler mIoHandler;
//这个时间戳的值,一般设置成不超过1000,你能够调低这个数值来优化你的代码。数值越低 暴露的信息就越多
private static final long TIME_BLOCK = 1000L;
private LogMonitor() {
mLogThread.start();
mIoHandler = new Handler(mLogThread.getLooper());
}
private static Runnable mLogRunnable = new Runnable() {
@Override
public void run() {
StringBuilder sb = new StringBuilder();
//把ui线程的block的堆栈信息都打印出来 方便咱们定位问题
StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
for (StackTraceElement s : stackTrace) {
sb.append(s.toString() + "\n");
}
Log.e("BLOCK", sb.toString());
}
};
public static LogMonitor getInstance() {
return sInstance;
}
public void startMonitor() {
//在time以后 再启动这个runnable 若是在这个time以前调用了removeMonitor 方法,那这个runnable确定就没法执行了
mIoHandler.postDelayed(mLogRunnable, TIME_BLOCK);
}
public void removeMonitor() {
mIoHandler.removeCallbacks(mLogRunnable);
}
}
复制代码
基本上就能够了。能够知足咱们的卡顿统计需求。
前面咱们分析actiivty页面绘制的时候提到过Choreographer这个类。其实这个类网上资料超多,你们能够自行搜索一下, 这个类的 Choreographer.getInstance().postFrameCallback(this); 是能够统计到帧率的。实时的,很方便。 经过这个咱们也能够检测到卡顿现象,和上面的方法其实效果差很少,惟一要注意的,大多数blog的isMonitor 其实都不可用,缘由是
注意看这个函数是个hide函数,压根没办法给咱们app使用到的。编译是不可能编译经过的。 这里给出正确的写法,其他代码我就很少复述了其实都差很少。搜搜均可以搜到。
public boolean isMonitor() {
//网上流传的方法多数是这个,可是这个是错的,由于hasCallbacks 是一个hide函数 你压根调用不了的,只能反射调用
//return mIoHandler.hasCallbacks(mLogRunnable);
try {
//经过详细地类名获取到指定的类
Class<?> handlerClass = Class.forName("android.os.Handler");
//经过方法名,传入参数获取指定方法
java.lang.reflect.Method method = handlerClass.getMethod("hasCallbacks", Runnable.class);
Boolean ret = (Boolean) method.invoke(mIoHandler, mLogRunnable);
return ret;
} catch (Exception e) {
}
return false;
}
复制代码
说了这么多,其实本篇文章核心思想就2点,统计activity启动时间,尽量缩小页面白屏的时间。 统计卡顿的上下文环境,方便咱们定位代码问题便于优化。大致的分析问题和解决问题的思路都在这里了。 有兴趣的同窗能够自行拓展思路,写出一个个库方便使用。可是核心思想应该就是上述内容。 固然不想重复造轮子的同窗也可使用开源库。在这里我推荐2个我的认为比较好的: