Android GUI扫盲,渲染架构浅析

工作时最开始接触的就是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
来源:稀土掘金

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342