校园生活真快乐

时间:2018-03-19 02:40:26来源:杰瑞文章网点击:作文字数:400字
如需转载请评论或简信,并注明出处,未经允许不得转载 目录 前言 现在Android的应用界面越来越复杂,很多时候页面中还有各种动画,所以页面卡顿、掉帧等问题就随之而来,所以就想研究一下屏幕刷新的原理,以便于更快的定位和解决问题 基本概念 Android的屏幕刷新中涉及到最重要的三个概念(为便于理解,这里先做简单介绍) CPU:执行应用层的measure、layout、draw等操作,绘制完成后将数据提交给GPU GPU:进一步处理数据,并将数据缓存起来 屏幕:由一个个像素点组成,以固定的频率(16.6ms,即1秒60帧)从缓冲区中取出数据来填充像素点 总结一句话就是:CPU 绘制后提交数据、GPU 进一步处理和缓存数据、最后屏幕从缓冲区中读取数据并显示 我们开发过程中主要关心CPU绘制部分,对GPU和屏幕基本不用关心。所以,看到这里,有的人可能就会想说,我对view的绘制流程(measure、layout、draw)已经非常熟悉,至于GPU和屏幕,和我也没有太大关系吧。其实这里面还有更多的细节值得我们去探索,了解和掌握了这些细节,有助于我们解决一些实际开发过程中的问题,我们不妨一步步往下看 双缓冲机制 看完上面的流程图,我们很容易想到一个问题,屏幕是以16.6ms的固定频率进行刷新的,但是我们应用层触发绘制的时机是完全随机的(比如我们随时都可以触摸屏幕触发绘制),如果在GPU向缓冲区写入数据的同时,屏幕也在向缓冲区读取数据,会发生什么情况呢?有可能屏幕上就会出现一部分是前一帧的画面,一部分是另一帧的画面,这显然是无法接受的,那怎么解决这个问题呢? 这个其实和我们平时使用代码管理工具Git的一些思路有相似之处,首先我们有一个master分支,对应线上版本的代码,当有新的需求来的时候,我们往往不会在master分支上直接进行开发,都会拉出一个新的分支,比如develop分支,在develop分支上开发新需求,等开发完成测试通过后才会合并到master分支 所以,在屏幕刷新中,Android系统引入了双缓冲机制。GPU只向Back Buffer中写入绘制数据,且GPU会定期交换Back Buffer和Frame Buffer,也就是让Back Buffer 变成Frame Buffer交给屏幕进行绘制,让原先的Frame Buffer变成Back Buffer进行数据写入。交换的频率也是60次/秒,这就与屏幕的刷新频率保持了同步 虽然我们引入了双缓冲机制,但是我们知道,当布局比较复杂,或设备性能较差的时候,CPU并不能保证在16.6ms内就完成绘制数据的计算,所以这里系统又做了一个处理 当你的应用正在往Back Buffer中填充数据时,系统会将Back Buffer锁定。如果到了GPU交换两个Buffer的时间点,你的应用还在往Back Buffer中填充数据,GPU会发现Back Buffer被锁定了,它会放弃这次交换 这样做的后果就是手机屏幕仍然显示原先的图像,这就是我们常常说的丢帧,所以为了避免丢帧的发生,我们就要尽量减少布局层级,减少不必要的View的invalidate调用,减少大量对象的创建(GC也会占用CPU时间)等等。对这方面有兴趣的可以看我的性能优化专题下的文章 Choreographer 我们看下面这张图,这里已经是基于双缓冲机制,且应用层的优化已经做得非常好,绘制时间均少于16.6ms,但依然出现了丢帧,为什么呢? 原因是第2帧虽然绘制时间少于16.6ms,但是绘制开始的时间距离vsync信号(就是一个发起屏幕刷新的信号,Vertical Synchronization的缩写)发出的时间比较短暂,导致当vsync信号来的时候,第2帧还没有绘制完成,所以Back Buffer依然是锁定的状态,也就出现了丢帧 如果我们可以保证每次绘制开始的时间和vsync信号发起的时间一致(如下图所示),是不是就可以解决这个问题呢? Android在每一帧中实际上只是在完成三个操作,分别是输入(Input)、动画(Animation)、绘制(Draw)。在Android4.1(API 16)之后,Android系统开始加入Choreographer这个类,这个类名翻译过来是“舞蹈指导”,字面上的意思就是指挥以上三个UI操作一起完成一支舞蹈。这个类就可以解决vsync和绘制不同步的问题,其实它的原理用一句话总结就是往Choreographer里发一个消息,最快也要等到下一个vsync信号来的时候才会开始处理消息 下面我们通过源码分析来看看Choreographer的实现原理 Activity中的布局首次绘制,以及每次调用View 的 invalidate() 时,都会调用到ViewRootImp#requestLayout(),对于这块不是很清楚的具体可以看最全的View绘制流程(上)— Window、DecorView、ViewRootImp的关系,所以我们接下来分析一下ViewRootImp#requestLayout()里面做了什么 ViewRootImp#requestLayout() public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { //检查是否是主线程,不然会抛出异常 checkThread(); mLayoutRequested = true; scheduleTraversals(); } } ViewRootImp#scheduleTraversals() 如果在同一帧中出现多次requestLayout()调用,其实最终也只会绘制一次,为什么呢?我们可以看到下面有个mTraversalScheduled标志位,稍后我们可以看看这个标志位是哪里被置为false的 void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; //添加同步消息屏障,这个方法也比较关键,这里先不关心,我们说完Choreographer再分析 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); //向Choreographer中发送消息 mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); //... } } Choreographer#postCallbackDelayedInternal() mChoreographer.postCallback()接着会调用这个方法 private void postCallbackDelayedInternal(int callbackType, Object action, Object token, long delayMillis) { synchronized (mLock) { final long now = SystemClock.uptimeMillis(); final long dueTime = now + delayMillis; //将消息以当前的时间戳放进mCallbackQueue 队列里 mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token); if (dueTime <= now) { //如果没有设置消息延时,直接执行 scheduleFrameLocked(now); } else { //消息延时,但是最终依然会调用scheduleFrameLocked Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action); msg.arg1 = callbackType; msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, dueTime); } } } Choreographer#scheduleFrameLocked() private void scheduleFrameLocked(long now) { if (!mFrameScheduled) { mFrameScheduled = true; if (USE_VSYNC) { if (isRunningOnLooperThreadLocked()) { //如果当前线程是Choreographer的工作线程,我理解就是主线程 scheduleVsyncLocked(); } else { //否则发一条消息到主线程 Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC); //设置消息为异步消息,其实就是一个标志位,具体作用我们后面会讲 msg.setAsynchronous(true); //插到消息队列头部,可以理解为设置最高优先级 mHandler.sendMessageAtFrontOfQueue(msg); } } else { final long nextFrameTime = Math.max( mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now); if (DEBUG_FRAMES) { Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms."); } Message msg = mHandler.obtainMessage(MSG_DO_FRAME); msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, nextFrameTime); } } } 接下来最终会调用到一个native方法 private void scheduleVsyncLocked() { mDisplayEventReceiver.scheduleVsync(); } 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); } } @FastNative private static native void nativeScheduleVsync(long receiverPtr); native方法我们在Android Studio中不能直接查看,这里我们换一种思路。前面Choreographer#postCallbackDelayedInternal()方法中,我们看到了将消息以当前的时间戳放进队列里,那消息什么时候被取出来执行呢? CallbackQueue private final class CallbackQueue { private CallbackRecord mHead; public boolean hasDueCallbacksLocked(long now) { return mHead != null && mHead.dueTime <= now; } //这就是取出消息的方法 public CallbackRecord extractDueCallbacksLocked(long now) { CallbackRecord callbacks = mHead; if (callbacks == null || callbacks.dueTime > now) { return null; } CallbackRecord last = callbacks; CallbackRecord next = last.next; while (next != null) { if (next.dueTime > now) { last.next = null; break; } last = next; next = next.next; } mHead = next; return callbacks; } //添加消息 public void addCallbackLocked(long dueTime, Object action, Object token) {...} //删除消息 public void removeCallbacksLocked(Object action, Object token) {...} } 跟踪代码发现,这个CallbackQueue#extractDueCallbacksLocked()会被Choreographer#doCallbacks()调用,Choreographer#doCallbacks()又会被Choreographer#doFrame()调用,最终我们跟到了FrameDisplayEventReceiver类 FrameDisplayEventReceiver 因为上面的native方法我们没有跟进去分析,担心给大家绕晕了,我们会用一个新的章节来分析native层做的事情,这里先直接给出结论 nativeScheduleVsync()会向SurfaceFlinger注册Vsync信号的监听,VSync信号由SurfaceFlinger实现并定时发送,当Vsync信号来的时候就会回调FrameDisplayEventReceiver#onVsync(),这个方法给发送一个带时间戳Runnable消息,这个Runnable消息的run()实现就是FrameDisplayEventReceiver# run(), 接着就会执行doFrame() private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable { private boolean mHavePendingVsync; private long mTimestampNanos; private int mFrame; public FrameDisplayEventReceiver(Looper looper, int vsyncSource) { super(looper, vsyncSource); } @Override public void onVsync(long timestampNanos, int builtInDisplayId, int frame) { if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) { scheduleVsync(); return; } long now = System.nanoTime(); if (timestampNanos > now) { timestampNanos = now; } if (mHavePendingVsync) { } else { mHavePendingVsync = true; } mTimestampNanos = timestampNanos; mFrame = frame; Message msg = Message.obtain(mHandler, this); //设置异步消息 msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS); } @Override public void run() { mHavePendingVsync = false; doFrame(mTimestampNanos, mFrame); } } doFrame()会计算当前时间与时间戳的间隔,间隔越大表示这一帧处理的时间越久,如果间隔超过一个周期,就会去计算跳过了多少帧,并打印出一个日志,这个日志我想很多人可能都见过 Log.i(TAG, "Skipped " + skippedFrames + " frames! " + "The application may be doing too much work on its main thread."); 最终doFrame()会从mCallbackQueue 中取出消息并按照时间戳顺序调用mTraversalRunnable的run()函数,mTraversalRunnable就是最初被加入到Choreographer中的Runnable() //ViewRootImp mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); TraversalRunnable doTraversal()中就会开始我们View的绘制流程,View的绘制流程不是本文的重点,感兴趣的可以看最全的View绘制流程(下)— Measure、Layout、Draw final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } } ViewRootImp#doTraversal() void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; //移除同步消息屏障 mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); if (mProfile) { Debug.startMethodTracing("ViewAncestor"); } performTraversals(); if (mProfile) { Debug.stopMethodTracing(); mProfile = false; } } } 到此为止,从触发绘制到屏幕真正开始绘制的过程就基本讲完了,但是这里还有最后一个细节没有进行分析 同步消息屏障 还记不记得前面说有mHandler.getLooper().getQueue().postSyncBarrier()这个方法还没有进行分析,这个方法的作用是什么呢? void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; //☆ mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); //向Choreographer中发送消息 mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); //... } } 我们知道,Android是基于消息机制的,每一个操作都是一个Message,如果在触发绘制的时候,消息队列中还有很多消息没有被执行,那是不是意味着要等到消息队列中的消息执行完成后,绘制消息才能被执行到,那么依然无法保证Vsync信号和绘制的同步,所以依然可能出现丢帧的现象 还记不记得我们之前在Choreographer#scheduleFrameLocked()和FrameDisplayEventReceiver#onVsync()中提到,我们会给与Message有关的绘制请求设置成异步消息(msg.setAsynchronous(true)),为什么要这么做呢?这时候MessageQueue#postSyncBarrier()就发挥它的作用了,简单来说,它的作用就是一个同步消息屏障,能够把我们的异步消息(也就是绘制消息)的优先级提到最高 MessageQueue#postSyncBarrier() 主线程的 Looper 会一直循环调用 MessageQueue 的 next() 来取出队头的 Message 执行,当 Message 执行完后再去取下一个。当 next() 方法在取 Message 时发现队头是一个同步屏障的消息时,就会去遍历整个队列,只寻找设置了异步标志的消息,如果有找到异步消息,那么就取出这个异步消息来执行,否则就让 next() 方法陷入阻塞状态。如果 next() 方法陷入阻塞状态,那么主线程此时就是处于空闲状态的,也就是没在干任何事。所以,如果队头是一个同步屏障的消息的话,那么在它后面的所有同步消息就都被拦截住了,直到这个同步屏障消息被移除,否则主线程就一直不会去处理同步屏障后面的同步消息 那这么同步屏障是什么时候被移除的呢? 其实我们就是在我们上面提到的ViewRootImp#doTraversal()方法中 总结 本文讲了屏幕刷新的基本原理,以及双缓冲机制、Choreographer的作用、同步消息屏障,不同的地方出了问题都可能引起丢帧,所以了解这些细节有助于我们更好的排查项目开发过程中的问题,最后,来梳理一下屏幕刷新的流程图
作文投稿

校园生活真快乐一文由杰瑞文章网免费提供,本站为公益性作文网站,此作文为网上收集或网友提供,版权归原作者所有,如果侵犯了您的权益,请及时与我们联系,我们会立即删除!

杰瑞文章网友情提示:请不要直接抄作文用来交作业。你可以学习、借鉴、期待你写出更好的作文。

说说你对这篇作文的看法吧

最新发表的关于快乐的作文

    SQL Error: select id,classid,ttid,onclick,plnum,totaldown,newspath,filename,userid,username,firsttitle,isgood,ispic,istop,isqf,ismember,isurl,truetime,lastdotime,havehtml,groupid,userfen,titlefont,titleurl,stb,fstb,restb,keyboard,eckuid,title,ftitle,newstime,titlepic,smalltext,writer,diggtop from ***_ecms_news where (classid='269') order by newstime desc limit 10

最受欢迎的关于快乐的作文

    SQL Error: select id,classid,ttid,onclick,plnum,totaldown,newspath,filename,userid,username,firsttitle,isgood,ispic,istop,isqf,ismember,isurl,truetime,lastdotime,havehtml,groupid,userfen,titlefont,titleurl,stb,fstb,restb,keyboard,eckuid,title,ftitle,newstime,titlepic,smalltext,writer,diggtop from ***_ecms_news where (classid='269') order by onclick desc limit 10