面试官又来了:你的app卡顿过吗?,掌握这套精编Android高级面试题解析

调用 DisplayEventReceiver 的 scheduleVsync 方法

4. DisplayEventReceiver

4.1 DisplayEventReceiver#scheduleVsync

/**

  • Schedules a single vertical sync pulse to be delivered when the next

  • display frame begins.

*/

public void scheduleVsync() {

if (mReceiverPtr == 0) {

Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "

  • “receiver has already been disposed.”);

} else {

nativeScheduleVsync(mReceiverPtr); //1、请求vsync

}

}

// Called from native code. //2、vsync来的时候底层会通过JNI回调这个方法

@SuppressWarnings(“unused”)

private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {

onVsync(timestampNanos, builtInDisplayId, frame);

}

这里的逻辑就是:通过JNI,跟底层说,下一个vsync脉冲信号来的时候请通知我。然后在下一个vsync信号来的时候,就会收到底层的JNI回调,也就是dispatchVsync这个方法会被调用,然后会调用onVsync这个空方法,由实现类去自己做一些处理。

public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {}

这里是屏幕刷新机制的重点,应用必须向底层请求vsync信号,然后下一次vsync信号来的时候会通过JNI通知到应用,然后接下来才到应用绘制逻辑。

往回看,DisplayEventReceiver的实现类是 Choreographer 的内部类 FrameDisplayEventReceiver,代码不多,直接贴上来

5. Choreographer

5.1 Choreographer$FrameDisplayEventReceiver

private final class FrameDisplayEventReceiver extends DisplayEventReceiver

implements Runnable {

private boolean mHavePendingVsync;

private long mTimestampNanos;

private int mFrame;

public FrameDisplayEventReceiver(Looper looper) {

super(looper);

}

@Override

public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {

long now = System.nanoTime();

// 更正时间戳,当前纳秒

if (timestampNanos > now) {

Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)

  • " ms in the future! Check that graphics HAL is generating vsync "

  • “timestamps using the correct timebase.”);

timestampNanos = now;

}

if (mHavePendingVsync) {

Log.w(TAG, "Already have a pending vsync event. There should only be "

  • “one at a time.”);

} else {

mHavePendingVsync = true;

}

mTimestampNanos = timestampNanos;

mFrame = frame;

Message msg = Message.obtain(mHandler, this); //1 callback是this,会回调run方法

msg.setAsynchronous(true);

mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);

}

@Override

public void run() {

mHavePendingVsync = false;

doFrame(mTimestampNanos, mFrame); //2

}

}

根据上面4.1分析,收到vsync信号后,onVsync方法就会被调用,里面主要做了什么呢?

通过Handler,往消息队列插入一个异步消息,指定执行的时间,然后看注释1,callback传this,所以最终会回调run方法,run里面调用doFrame(mTimestampNanos, mFrame);

重点来了,如果Handler此时存在耗时操作,那么需要等耗时操作执行完,Looper才会轮循到下一条消息,run方法才会调用,然后才会调用到doFrame(mTimestampNanos, mFrame);,doFrame干了什么?调用慢了会怎么样?

继续看

5.2 Choreographer#doFrame

void doFrame(long frameTimeNanos, int frame) {

final long startNanos;

synchronized (mLock) {

long intendedFrameTimeNanos = frameTimeNanos;

startNanos = System.nanoTime();

// 1 当前时间戳减去vsync来的时间,也就是主线程的耗时时间

final long jitterNanos = startNanos - frameTimeNanos;

if (jitterNanos >= mFrameIntervalNanos) {

//1帧是16毫秒,计算当前跳过了多少帧,比如超时162毫秒,那么就是跳过了10帧

final long skippedFrames = jitterNanos / mFrameIntervalNanos;

// SKIPPED_FRAME_WARNING_LIMIT 默认是30,超时了30帧以上,那么就log提示

if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {

Log.i(TAG, "Skipped " + skippedFrames + " frames! "

  • “The application may be doing too much work on its main thread.”);

}

// 取余,计算离上一帧多久了,一帧是16毫秒,所以lastFrameOffset 在0-15毫秒之间,这里单位是纳秒

final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;

if (DEBUG_JANK) {

Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "

  • "which is more than the 8frame interval of "

  • (mFrameIntervalNanos * 0.000001f) + " ms! "

  • "Skipping " + skippedFrames + " frames and setting frame "

  • “time to " + (lastFrameOffset * 0.000001f) + " ms in the past.”);

}

// 出现掉帧,把时间修正一下,对比的是上一帧时间

frameTimeNanos = startNanos - lastFrameOffset;

}

//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)]

这一张图很形象,大家可以参考这张图自己研究研究。

_3_如何监控应用卡顿?

=======================================================================

上面从源码角度分析了屏幕刷新机制,为什么主线程有耗时操作会导致卡顿?原理想必大家已经心中有数,那么平时开发中如何去发现那些会造成卡顿的代码呢?

接下来总结几种比较流行、有效的卡顿监控方式:

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;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

资源分享

一线互联网面试专题

379页的Android进阶知识大全

379页的Android进阶知识大全

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

2020年虽然路途坎坷,都在说Android要没落,但是,不要慌,做自己的计划,学自己的习,竞争无处不在,每个行业都是如此。相信自己,没有做不到的,只有想不到的。祝大家2021年万事大吉。

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img
片转存中…(img-NQ7oySb2-1712776506853)]
[外链图片转存中…(img-zj8KDKWj-1712776506853)]
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-iOELAzWk-1712776506853)]

资源分享

[外链图片转存中…(img-hqegVoen-1712776506853)]

[外链图片转存中…(img-1PVO8kbU-1712776506854)]

[外链图片转存中…(img-M6XZaf0h-1712776506854)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

2020年虽然路途坎坷,都在说Android要没落,但是,不要慌,做自己的计划,学自己的习,竞争无处不在,每个行业都是如此。相信自己,没有做不到的,只有想不到的。祝大家2021年万事大吉。

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-ZqDxTpRy-1712776506855)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值