美文网首页
Android GUI扫盲,渲染架构浅析

Android GUI扫盲,渲染架构浅析

作者: 像程序那样去思考 | 来源:发表于2022-11-29 22:14 被阅读0次

    工作时最开始接触的就是AMS和WMS,真正工作和学习还是有很大区别的,在工作中我们始终作为一颗螺丝钉来support某一个局域的功能,但学习又是整体的,我们没办法脱离上下文去学习或应用某一个局部的东西,这个道理和Android 中的Context也是很像的,脱离了Context我们的学习就像无根之水,不知道为何学习,也不知道如何应用。

    在刚开始学习View的时候,还停留在一步一步加log看源码的阶段,在这个阶段整个人其实非常疑惑,总觉得View,直白点说,就是measure,layout和draw呗,测量下尺寸,找个相对位置,画出来就完事了。但处理问题往往没有这么简单,比如为什么黑屏、白屏,为什那些软件层的显示问题需要从View的角度去协助分析解决?为什么不是WMS?View到底是怎么onDraw?它和GPU,CPU有啥联系?surface又是什么东西,应用层写进去的xml,到底是怎么显示到屏幕上的?

    在理解这篇之后,后面我会再整理一份显示异常分析思路,就十分容易理解了。 本篇是我从事这方面的工作和学习以来,根据自己的体会整理出来的一些基础框架,先对Android 渲染架构有个整理的认识,然后再去学习其中某一个子模块,就能做到知其然,知其所以然了。

    一、基础概念扫盲

    1. 屏幕硬件的显示原理

    如果有一定硬件基础或者嵌入式基础,应该很好理解这里的显示原理,屏幕由一个个的像素点组成,简化来看实际就是二极管嘛,控制它通断就好了。理解一下什么叫硬件,什么叫驱动。 打个简单点的比方,我们做单片机开发应该也会接触到二极管,数位管,比如通过数位管去显示一个1

    我们只需要写一个Api,在需要显示1时,我们就通过这个Api去输出true or false给IO口,比如这里我们只需要给b和c 置为高电平,给其他的二极管拉低,那么就会显示一个1出来。这个数位管就叫硬件,而我们写的Api就叫驱动。而现在市面上的显示设备也是这样子的,分为硬件和驱动,一般硬件的厂家会自己适配驱动,这些驱动会根据接收的数据,转换成一系列的0和1控制屏幕显示成想要的样子。那么它接收的数据是什么?就是Bitmap--位图

    所以我们就知道了,只要能把想要显示的画面转换成Bitmap格式,就可以直接把这个玩意塞给屏幕驱动了,它会自己根据Bitmap去控制屏幕中的无数个晶体管,最终把这个画面显示到屏幕上。

    2. Android的数据转化过程

    实际上把图像转换成Bitmap,app自己就可以做完了,因为View本身就是应用层的东西,这也是为什么有时候我们在debug过程中遇到一些黑屏问题,别人会告诉你,你应用层的图送下来就是黑的,请你从应用层的角度分析。因为Bitmap就是Android 做出来的呀,由此引出了渲染架构中的关键工具:SkiaOpenGL

    这里先不去管View的measure、layout流程了,先了解下View的onDraw /frameworks/base/core/java/android/view/View.java

    23167      public void draw(Canvas canvas) {
    23168          final int privateFlags = mPrivateFlags;
    23169          mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    23170  
    23171          /*
    23172           * Draw traversal performs several drawing steps which must be executed
    23173           * in the appropriate order:
    23174           *
    23175           *      1\. Draw the background
    23176           *      2\. If necessary, save the canvas' layers to prepare for fading
    23177           *      3\. Draw view's content
    23178           *      4\. Draw children
    23179           *      5\. If necessary, draw the fading edges and restore layers
    23180           *      6\. Draw decorations (scrollbars for instance)
    23181           *      7\. If necessary, draw the default focus highlight
    23182           */
    23183  
    23184          // Step 1, draw the background, if needed
    23185          int saveCount;
    23186  
    23187          drawBackground(canvas);
    23188  
    23189          // skip step 2 & 5 if possible (common case)
    23190          final int viewFlags = mViewFlags;
    23191          boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    23192          boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    23193          if (!verticalEdges && !horizontalEdges) {
    23194              // Step 3, draw the content
    23195              onDraw(canvas);
    

    后面的就不去看了,这里注释也写的很清楚,draw里面的7个步骤:

    1. 绘制背景(drawBackground)
    2. 如果需要,保存当前layer用于动画过渡
    3. 绘制View的内容(onDraw)
    4. 绘制子View(dispatchDraw)
    5. 如果需要,则绘制View的褪色边缘,类似于阴影效果
    6. 绘制装饰,比如滚动条(onDrawForeground)
    7. 如果需要,绘制默认焦点高亮效果(drawDefaultFocusHighlight)

    而我们只用关注其中onDraw是怎么做的

    20717      protected void onDraw(Canvas canvas) {
    20718      }
    

    会发现,这里的onDraw()奇奇怪怪的,怎么是个空的?当然要是空的,因为每一个View并没有固定的显示模式,所以View想要绘制成什么样子,当然是自己决定,所以onDraw()由各种子View自己实现,而不会在父类中实现。在ViewRoot中,创建了画布Canvas并调用了View的draw()方法,实际上draw的过程和Canvas这个类是密不可分的,虽然各个子View会自己决定要怎么draw,但最终要绘制出来,目的就是要把自己的样子转换成Bitmap,这个流程依赖于Canvas。随便找几个view的例子

    110     @Override
    111     protected void onDraw(Canvas canvas) {
    112         super.onDraw(canvas);
    113         canvas.drawRect(0.0f, 0.0f, getWidth(), getHeight(), mPaint);
    114     }
    
    81      @Override
    82      protected void onDraw(Canvas canvas) {
    83          if (mBitmap != null) {
    84              mRect.set(0, 0, getWidth(), getHeight());
    85              canvas.drawBitmap(mBitmap, null, mRect, null);
    86          }
    
    58      @Override
    59      protected void onDraw(Canvas canvas) {
    60          Drawable drawable = getDrawable();
    61          BitmapDrawable bitmapDrawable = null;
    62          // support state list drawable by getting the current state
    63          if (drawable instanceof StateListDrawable) {
    64              if (((StateListDrawable) drawable).getCurrent() != null) {
    65                  bitmapDrawable = (BitmapDrawable) drawable.getCurrent();
    66              }
    67          } else {
    68              bitmapDrawable = (BitmapDrawable) drawable;
    69          }
    70  
    71          if (bitmapDrawable == null) {
    72              return;
    73          }
    74          Bitmap bitmap = bitmapDrawable.getBitmap();
    75          if (bitmap == null) {
    76              return;
    77          }
    78  
    79          source.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
    80          destination.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
    81                  getHeight() - getPaddingBottom());
    82  
    83          drawBitmapWithCircleOnCanvas(bitmap, canvas, source, destination);
    84      }
    

    这里可以看到,子View重写的onDraw方法,有的简单,有的复杂,但是都根据ViewRoot给定的画布区域,调用canvas这个对象本身来绘制,那么可以理解渲染架构中canvas的含义了吧,作用就是一块画布,我们measure的目的就是为了申请一块画布,layout的目的是确定这个画布摆放在屏幕上的相对位置,draw就是给这块画布上面填充内容。当然依赖的也是这个画布自己的一些api。

    canvas怎么把Java层的draw,转变成Bitmap的格式?就是前面提到的,SkiaOpenGL,这两个工具从渲染架构宏观上来理解,可以把它们俩看成黑盒,它们俩都属于图形引擎,作用也是一样的,draw画面时调用canvas通过JNI到Native方法,然后通过Skia或OpenGL图形引擎,就可以得到Bitmap数据。

    3. 刷新率和帧速率

    OK有了前面的基础,我们知道页面绘制可以通过App层重写onDraw()来绘制,onDraw()通过canvas工具,再使用Skia或OpenGL图形引擎就能转换成Bitmap数据,我们也知道Bitmap数据可以直接丢给屏幕驱动,然后屏幕上就会显示出这一帧Bitmap对应的图像。那么问题就来了,是不是可以App绘制一张,就给屏幕驱动丢一张Bitmap,再绘制一张,再给驱动丢一张Bitmap?并没有这么简单。

    我们首先要知道两个概念,刷新率和帧速率。显示屏幕并不是接收一次Bitmap就绘制一次,实际上屏幕提供给外面的有三个东西,一个是存放当前帧Bitmap的区域,还有一个是缓冲区存放下一帧Bitmap区域,然后还提供了一个接口,触发这个接口时,屏幕就会用缓冲区的Bitmap替换当前的Bitmap。

    image.png

    这个屏幕在一分钟之内,最多可以切换多少次,就是这个屏幕的刷新率。比如我们看到很多屏幕是75HZ,144HZ,200HZ等等。

    那么我们App在绘制的时候,每一秒钟可以绘制多少次?也就是可以执行多少次将画面转换成Bitmap的操作?这个当然和系统计算能力有关,显然GPU计算比CPU计算快,更好的GPU计算也更快。那么一分钟可以绘制多少张Bitmap,这个就叫系统的帧速率(fps),比如当前系统帧速率是60fps,120fps,就是说当前系统一分钟分别可以绘制60或120张Bitmap。

    如果说我们让App直接和屏幕驱动对话,会是什么效果:

    应用层在绘制完Bitmap之后,不通过系统层,直接放到屏幕的缓冲区里面。这样带来的第一个问题就是叠图顺序紊乱。因为当前并不是只有一个应用啊,也不是只有一个进程。很显然,我们除了当前FocusWindow的进程,还有system_server进程嘛,还有systemUI需要绘制,还有Launcher的TaskBar需要绘制,大家都是各绘各的,各自搞完了就放到Bitmap里面去,毫无顺序的排列,也就没有办法有序叠图,显示成最终想要的效果。

    此外还有一个问题,就是刷新率和帧速率无法匹配。比如当前屏幕1秒钟切换75次,但App只送过来30张Bitmap,那么在没有Bitmap送到的周期里,缓冲区就没有更新数据,最终显示的效果就是黑屏或者白屏;如果当前的刷新率是30HZ,但帧速率达到了60或更高,也就是说App送过来的Bitmap,有很多根本没有显示出来就被丢掉了,这就是掉帧,结果就是显示的画面有卡顿。显然,要系统的考虑整个渲染架构,必须要解决刷新率和帧速率相匹配的问题。

    从Android 系统的角度来讲,我们不可能为每一个不同的屏幕专门适配一套渲染体系,那么就需要在软件层做一个约定,在不知道屏幕硬件性能的情况下,通过一个体系来均衡硬件指标和软件指标,比如:

    1. 每分钟固定60次调用屏幕刷新
    2. 每分钟固定绘制60张Bitmap

    所以就需要控制硬件驱动的刷新调用频率,比如每秒刷新60次的话,那么每次时间间隔就是16.66毫秒,那么就依托屏幕上一次显示的时间,加上16.66毫秒,作为触发下一次显示切换的时机,这个触发的脉冲信号就叫垂直同步信号(Vsync信号),Android 把控制硬件调用频率策略相关的内容都写到一个进程——surfaceflinger

    二、SurfaceFlinger是什么

    简单解释下SurfaceFlinger是什么,它就是控制屏幕刷新的一个进程。一个应用可以有很多个window,每一个window绘制的Bitmap,实际上在内存中表现为一块buffer,它是通过OpenGL或Skia图形库绘制出来的,这个Bitmap在Java层的数据存储在一块Surface当中,在底层通过JNI对应到一个NativeSurface。那么SurfaceFlinger的作用就是在每一个间隔16.66毫秒的Vsync信号到来时,将所有的Surface重新绘制到一块Framebuffer的内存,也就是最终的Bitmap,最后SurfaceFlinger会把最终的Framebuffer交给驱动,并触发屏幕的刷新,让这一帧图片显示出来。

    好了,现在我们知道,在整个渲染架构中,有surfaceflinger进程,通过按照固定周期叠图、送图和刷新屏幕的操作,实现了屏幕显示速率的控制,那么系统中又是如何控制App绘制图片的速率,以及如何让App绘制图片的速率和屏幕显示速率同步呢?答案就是——Choreographer

    三、Choreographer是什么

    前面有简单讲过View的绘制流程,那么View的绘制时机由谁来控制,又是如何控制的呢?其实完整的View绘制流程,应该从我们熟悉的setContentView()开始,在onCreate中会调用setContentView,在这里会完成对Xml文件的加载,在AMS callback回ActivityThread,进行Resume的时候,会通过WindowManager的AIDL方法addView()将所有的子View添加到ViewRootImpl里面去,然后在ViewRootImpl中,会走到requestLayout()并执行scheduleTraversals() /frameworks/base/core/java/android/view/ViewRootImpl.java

    2257      void scheduleTraversals() {
    2258          if (!mTraversalScheduled) {
    2259              mTraversalScheduled = true;
    2260              mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
    2261              mChoreographer.postCallback(
    2262                      Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    2263              notifyRendererOfFramePending();
    2264              pokeDrawLockIfNeeded();
    2265          }
    2266      }
    

    mChoreographer在这里就出现了,它调用的是postCallback方法,传进去的参数是Runnable,在这个Runnable中做的工作就是doTraversal(),最终到performTraversal()真正开始绘制,再往下就是熟悉的performMeasure、performLayout和performDraw了。所以Choreographer是通过postCallback的形式,给出了一个Runnable来做measure、layout和draw。

    也就是说,只有调到performTraversal()才会真正进行图形的绘制。所以整个图像的制作过程就是先去加载Xml文件,然后把要显示的View都通过addView的形式给ViewRootImpl,并交给Choreographer来主导真正的绘制流程。看看Choreographer的postCallback,层层调用最终做事情的在postCallbackDelayedInternal

    /frameworks/base/core/java/android/view/Choreographer.java

    470      private void postCallbackDelayedInternal(int callbackType,
    471              Object action, Object token, long delayMillis) {
    472          if (DEBUG_FRAMES) {
    473              Log.d(TAG, "PostCallback: type=" + callbackType
    474                      + ", action=" + action + ", token=" + token
    475                      + ", delayMillis=" + delayMillis);
    476          }
    477  
    478          synchronized (mLock) {
    479              final long now = SystemClock.uptimeMillis();
    480              final long dueTime = now + delayMillis;
                     //把scheduleTraversals()时要做的action放进了一个Callback队列
    481              mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
    482  
                     //这里是一个等待机制,第一次进来的时候是会走else去发送Msg的
    483              if (dueTime <= now) {
    484                  scheduleFrameLocked(now);
    485              } else {
                         //MSG_DO_SCHEDULE_CALLBACK 这个msg可以在当前线程handle中查看
    486                  Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
    487                  msg.arg1 = callbackType;
    488                  msg.setAsynchronous(true);
    489                  mHandler.sendMessageAtTime(msg, dueTime);
    490              }
    491          }
    492      }
    
             //这个msg在当前线程会去调用scheduleVsyncLocked()
    934      private void scheduleVsyncLocked() {
    935          try {
    936              Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#scheduleVsyncLocked");
    937              mDisplayEventReceiver.scheduleVsync();
    938          } finally {
    939              Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    940          }
    941      }
    

    这里scheduleVsync()最终调用到Native层,等待垂直同步信号。DisplayEventReceiver在JNI层也有对应的实现,它的作用就是管理垂直同步信号,当Vsync到来的时候,会发送dispatchVsync,callback回JAVA层执行onVsync()通知应用,然后才会到应用的绘制逻辑。

    1172          public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
    1173                  VsyncEventData vsyncEventData) {
                          //这里发送的msg没有写内容,那么就是默认值,会调用到0
    1202                  mLastVsyncEventData = vsyncEventData;
    1203                  Message msg = Message.obtain(mHandler, this);
    1204                  msg.setAsynchronous(true);
    1205                  mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    
                  //调用到0也就是这里的doFrame()
    1141          @Override
    1142          public void handleMessage(Message msg) {
    1143              switch (msg.what) {
    1144                  case MSG_DO_FRAME:
    1145                      doFrame(System.nanoTime(), 0, new DisplayEventReceiver.VsyncEventData());
    1146                      break;
    1147                  case MSG_DO_SCHEDULE_VSYNC:
    1148                      doScheduleVsync();
    1149                      break;
    1150                  case MSG_DO_SCHEDULE_CALLBACK:
    1151                      doScheduleCallback(msg.arg1);
    1152                      break;
    1153              }
    1154          }
    

    当Vsync到来时通过handle形式去调用doFrame(),这里面的代码看看,主要就是和帧相关的一些计算,比如这个Vsync到来的时间和上一个相比,是不是跳过了一些帧,计算当前离上一帧有多久,修正掉帧等等操作。如果当前的时间不满足,会重复请求下一次Vsync信号。doFrame()里面正常走下去到绘制流程,调用run方法,就回到了上面提到的doTraversal()

    所以Choreographer的主要作用就是协调Vsync信号和计算跳帧状况,然后判定时间是否符合标准,如果不符合,不进行callback中的doTraversal(),继续请求Vsync,如果符合,就会开始跑渲染,到doTraversal()继续走View的流程。

    其中协调Vsync信号主要是通过DisplayEventReceiver这个重要工具来申请,或等待它通知Vsync,而对跳帧状况的计算和回调渲染流程,是在Java层做的。

    四、Android 渲染流程

    梳理一下基本流程和几个重要进程的作用,首先是在onResume时addView到ViewRootImpl,然后通过Choreographer工具,向底层申请或等待底层回调的Vsync信号,当Vsync合适的时候才会执行自己的callback正式开始绘制,绘制的流程在各子View重写的onDraw()中,重要工具是Canvas,通过Canvas与Skia或OpenGL图形库对话,生成Bitmap,整个绘制流程以unlockCanvasAndPost()作为终点,通知surfaceflinger当前页面已经绘制好了。

    Surface是另外一条路,Surface在JAVA层由WMS管理,可以将Surface理解成一块区域,这块区域也就是一块内存,对应的画面就是Bitmap的内容,由于Bitmap依赖于Skia or OpenGL图形库,而这两个库是在C环境下实现的,所以framework的Surface需要通过JNI层的Surface来与Bitmap建立联系。

    五、画面卡顿产生的原因

    基于这个渲染架构,我们知道在画面显示过程中,应用层每秒钟会生产60帧图,屏幕也会每秒钟刷新60张图,那么画面卡顿就很好理解了,肉眼可见的卡顿基本就是掉帧,掉帧越多卡顿也就越明显,总的来说就是当前系统的绘制速率,跟不上屏幕的刷新速率。那么当然和CPU、GPU的能力有关,比如CPU loading高,得不到足够的时间片来完成绘制相关的流程。

    此外应用自身的问题,也可能会导致自己画面卡顿。如果是系统状态良好,但唯独这个应用自己卡顿的情况,我们还是从原理来理解,那么原因一定是这个app自己绘制流程调用的慢,会慢在哪里呢?当然会有很多种可能性,比如加载的View或动画太复杂,增加了绘制的时间;比如这个进程自己的主线程或UI线程卡住。

    比如我们在Android Handler中,Google建议不要在Handler中处理复杂函数,保证Handle线程的效率,如果Handler线程阻塞导致慢了,那么Handle处理msg当然也会慢,在Choreographer和绘制流程,很多都是依赖于Handler处理。

    作者:光谷黑马
    链接:https://juejin.cn/post/7170286979760783397
    来源:稀土掘金

    相关文章

      网友评论

          本文标题:Android GUI扫盲,渲染架构浅析

          本文链接:https://www.haomeiwen.com/subject/pdkjfdtx.html