//2、时间倒退了,可能是由于改了系统时间,此时就重新申请vsync信号(一般不会走这里)
if (frameTimeNanos < mLastFrameTimeNanos) {
if (DEBUG_JANK) {
Log.d(TAG, "Frame time appears to be going backwards. May be due to a "
- “previously skipped frame. Waiting for next vsync.”);
}
//这里申请下一次vsync信号,流程跟上面分析一样了。
scheduleVsyncLocked();
return;
}
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
mFrameScheduled = false;
mLastFrameTimeNanos = frameTimeNanos;
}
//3 能绘制的话,就走到下面
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, “Choreographer#doFrame”);
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
}
}
分析:
1. 计算收到vsync信号到doFrame被调用的时间差,vsync信号间隔是16毫秒一次,大于16毫秒就是掉帧了,如果超过30帧(默认30),就打印log提示开发者检查主线程是否有耗时操作。
2. 如果时间发生倒退,可能是修改了系统时间,就不绘制,而是重新注册下一次vsync信号 3. 正常情况下会走到 doCallbacks 里去,callbackType 按顺序是Choreographer.CALLBACK_INPUT、Choreographer.CALLBACK_ANIMATION、Choreographer.CALLBACK_TRAVERSAL、Choreographer.CALLBACK_COMMIT
看 doCallbacks 里的逻辑
5.3 Choreographer#doCallbacks
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
final long now = System.nanoTime();
//1. 从队列取出任务,任务什么时候添加到队列的,上面有说过哈
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
now / TimeUtils.NANOS_PER_MS);
if (callbacks == null) {
return;
}
mCallbacksRunning = true;
…
//2.更新这一帧的时间,确保提交这一帧的时间总是在最后一帧之后
if (callbackType == Choreographer.CALLBACK_COMMIT) {
final long jitterNanos = now - frameTimeNanos;
Trace.traceCounter(Trace.TRACE_TAG_VIEW, “jitterNanos”, (int) jitterNanos);
if (jitterNanos >= 2 * mFrameIntervalNanos) {
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos
- mFrameIntervalNanos;
if (DEBUG_JANK) {
Log.d(TAG, "Commit callback delayed by " + (jitterNanos * 0.000001f)
-
" ms which is more than twice the frame interval of "
-
(mFrameIntervalNanos * 0.000001f) + " ms! "
-
"Setting frame time to " + (lastFrameOffset * 0.000001f)
-
" ms in the past.");
mDebugPrintNextFrameTimeDelta = true;
}
frameTimeNanos = now - lastFrameOffset;
mLastFrameTimeNanos = frameTimeNanos;
}
}
}
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
for (CallbackRecord c = callbacks; c != null; c = c.next) {
if (DEBUG_FRAMES) {
Log.d(TAG, “RunCallback: type=” + callbackType
-
“, action=” + c.action + “, token=” + c.token
-
“, latencyMillis=” + (SystemClock.uptimeMillis() - c.dueTime));
}
// 3. 执行任务,
c.run(frameTimeNanos);
}
} …
}
这里主要就是取出对应类型的任务,然后执行任务。
注释2:if (callbackType == Choreographer.CALLBACK_COMMIT)是流程的最后一步,数据已经绘制完准备提交的时候,会更正一下时间戳,确保提交时间总是在最后一次vsync时间之后。这里文字可能不太好理解,引用一张图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hqTdjG6e-1570801774295)(https://upload-images.jianshu.io/upload_images/15679108-d7e425b137500558?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
图中 doCallbacks 从 frameTimeNanos2 开始执行,执行到进入 CALLBACK_COMMIT 时,经过了2.2帧,判断 now - frameTimeNanos >= 2 * mFrameIntervalNanos,lastFrameOffset = jitterNanos % mFrameIntervalNanos取余就是0.2了,于是修正的时间戳 frameTimeNanos = now - lastFrameOffset 刚好就是3的位置。
注释3,还没到最后一步的时候,取出其它任务出来run,这个任务肯定就是跟View的绘制相关了,记得开始requestLayout传过来的类型吗,Choreographer.CALLBACK_TRAVERSAL,从队列get出来的任务类对应是mTraversalRunnable,类型是TraversalRunnable,定义在ViewRootImpl里面,饶了一圈,回到ViewRootImpl继续看~
6. ViewRootImpl
刚开始看的是ViewRootImpl#scheduleTraversals,继续往下分析
6.1 ViewRootImpl#scheduleTraversals
void scheduleTraversals() {
if (!mTraversalScheduled) {
…
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
这个mTraversalRunnable 任务绕了一圈,通过请求vsync信号,到收到信号,然后终于被调用了。
6.2 ViewRootImpl$TraversalRunnable
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
6.3 ViewRootImpl#doTraversal
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
performTraversals();
}
}
先移除同步屏障消息,然后调用performTraversals 方法,performTraversals 这个方法代码有点多,挑重点看
6.4 ViewRootImpl#performTraversals
private void performTraversals() {
// mAttachInfo 赋值给View
host.dispatchAttachedToWindow(mAttachInfo, 0);
// Execute enqueued actions on every traversal in case a detached view enqueued an action
getRunQueue().executeActions(mAttachInfo.mHandler);
…
//1 测量
if (!mStopped || mReportNextDraw) {
// Ask host how big it wants to be
//1.1测量一次
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
/ Implementation of weights from WindowManager.LayoutParams
// We just grow the dimensions as needed and re-measure if
// needs be
if (lp.horizontalWeight > 0.0f) {
width += (int) ((mWidth - width) * lp.horizontalWeight);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (lp.verticalWeight > 0.0f) {
height += (int) ((mHeight - height) * lp.verticalWeight);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
MeasureSpec.EXACTLY);
measureAgain = true;
}
//1.2、如果有设置权重,比如LinearLayout设置了weight,需要测量两次
if (measureAgain) {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
…
//2.布局
if (didLayout) {
// 会回调View的layout方法,然后会调用View的onLayout方法
performLayout(lp, mWidth, mHeight);
}
…
//3.画
if (!cancelDraw && !newSurface) {
performDraw();
}
}
可以看到,View的三个方法回调measure、layout、draw是在performTraversals 里面,需要注意的点是LinearLayout设置权重的情况下会measure两次。
到这里,屏幕刷新机制就分析完了,整个流程总结一下:
7. 小结
View 的 requestLayout 会调到ViewRootImpl 的 requestLayout方法,然后通过 scheduleTraversals 方法向Choreographer 提交一个绘制任务,然后再通过DisplayEventReceiver向底层请求vsync信号,当vsync信号来的时候,会通过JNI回调回来,通过Handler往主线程消息队列post一个异步任务,最终是ViewRootImpl去执行那个绘制任务,调用performTraversals方法,里面是View的三个方法的回调。
网上的流程图虽然很漂亮,但是不如自己画一张印象深刻
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A6r1jX7I-1570801774297)(https://upload-images.jianshu.io/upload_images/15679108-e001071f4c85b6ee?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
认真看完,想必大家对屏幕刷新机制应该清楚了:
应用需要主动请求vsync,vsync来的时候才会通过JNI通知到应用,然后才调用View的三个绘制方法。如果没有发起绘制请求,例如没有requestLayout,View的绘制方法是不会被调用的。ViewRootImpl里面的这个View其实是DecorView。
那么有两个地方会造成掉帧,一个是主线程有其它耗时操作,导致doFrame没有机会在vsync信号发出之后16毫秒内调用,对应下图的3;还有一个就是当前doFrame方法耗时,绘制太久,下一个vsync信号来的时候这一帧还没画完,造成掉帧,对应下图的2。1是正常的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SqEmex9v-1570801774297)(https://upload-images.jianshu.io/upload_images/15679108-7554826b74f2e76e?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
这一张图很形象,大家可以参考这张图自己研究研究。
=======================================================================
上面从源码角度分析了屏幕刷新机制,为什么主线程有耗时操作会导致卡顿?原理想必大家已经心中有数,那么平时开发中如何去发现那些会造成卡顿的代码呢?
接下来总结几种比较流行、有效的卡顿监控方式:
2.1 基于消息队列
2.1.1 替换 Looper 的 Printer
Looper 暴露了一个方法
public void setMessageLogging(@Nullable Printer printer) {
mLogging = printer;
}
在Looper 的loop方法有这样一段代码
public static void loop() {
…
for (;😉 {
…
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
Looper轮循的时候,每次从消息队列取出一条消息,如果logging不为空,就会调用 logging.println,我们可以通过设置Printer,计算Looper两次获取消息的时间差,如果时间太长就说明Handler处理时间过长,直接把堆栈信息打印出来,就可以定位到耗时代码。
不过println 方法参数涉及到字符串拼接,考虑性能问题,所以这种方式只推荐在Debug模式下使用。基于此原理的开源库代表是:BlockCanary,看下BlockCanary核心代码:
https://github.com/markzhai/AndroidPerformanceMonitor
类:LooperMonitor
public void println(String x) {
if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
return;
}
if (!mPrintingStarted) {
//1、记录第一次执行时间,mStartTimestamp
mStartTimestamp = System.currentTimeMillis();
mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
mPrintingStarted = true;
startDump(); //2、开始dump堆栈信息
} else {
//3、第二次就进来这里了,调用isBlock 判断是否卡顿
final long endTime = System.currentTimeMillis();
mPrintingStarted = false;
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
stopDump(); //4、结束dump堆栈信息
}
}
//判断是否卡顿的代码很简单,跟上次处理消息时间比较,比如大于3秒,就认为卡顿了
private boolean isBlock(long endTime) {
return endTime - mStartTimestamp > mBlockThresholdMillis;
}
原理是这样,比较Looper两次处理消息的时间差,比如大于3秒,就认为卡顿了。细节的话大家可以自己去研究源码,比如消息队列只有一条消息,隔了很久才有消息入队,这种情况应该是要处理的,BlockCanary是怎么处理的呢?
在Android开发高手课中张绍文说过微信内部的基于消息队列的监控方案有缺陷:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZIFEh2JC-1570801774298)(https://upload-images.jianshu.io/upload_images/15679108-a082826f729402f8?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
这个我在BlockCanary 中测试,并没有出现此问题,所以BlockCanary 是怎么处理的?简单分析一下源码:
上面这段代码,注释1和注释2,记录第一次处理的时间,同时调用startDump()方法,startDump()最终会通过Handler 去执行一个AbstractSampler 类的mRunnable,代码如下:
abstract class AbstractSampler {
private static final int DEFAULT_SAMPLE_INTERVAL = 300;
protected AtomicBoolean mShouldSample = new AtomicBoolean(false);
protected long mSampleInterval;
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
doSample();
//调用startDump 的时候设置true了,stop时设置false
if (mShouldSample.get()) {
HandlerThreadFactory.getTimerThreadHandler()
.postDelayed(mRunnable, mSampleInterval);
}
}
};
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
最后
今天关于面试的分享就到这里,还是那句话,有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如Handler机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。
最后在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司19年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。
还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
【算法合集】
【延伸Android必备知识点】
【Android部分高级架构视频学习资源】
**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算
-ZG3GmphK-1712401735474)]
【延伸Android必备知识点】
[外链图片转存中…(img-I2sk3iKV-1712401735474)]
【Android部分高级架构视频学习资源】
**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算