十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
1.圆角对性能的影响
成都创新互联公司专注于惠民企业网站建设,响应式网站,商城网站开发。惠民网站建设公司,为惠民等地区提供建站服务。全流程按需制作网站,专业设计,全程项目跟踪,成都创新互联公司专业和态度为您提供的服务
尽量避免用Clipxxx组件,建议用BoxDecoration的image属性实现,如果用Clipxxx组件,圆角取整后性能会提升。
2.减少重绘
根据场景合理使用RePaintBoundary,使绘制独立于父布局,避免重绘,提升性能,但过度使用增加的图层会带来Raster合成的耗时。例如scrollview是滑动过程会导致所有的节点都重绘,可以在scrollview下一层使用RePaintBoundary。
3.滚动步长插值器优化(了解)
官方的滚动差值器在出现小卡顿时,滚动步长会出现大的跳跃,导致体感上出现很明显的抖动,优化步长偏移量算法与原生效果对齐。
4.开启SurfaceView
官方推荐Flutter用SurfaceView ,因为SurfaceView与应用窗口内容分隔开,在专有硬件中合成,产生的中间副本少于TextureView,所以性能高,占用内存少,但是在混合栈遇到的问题需要突破
5.使用RepaintBoundary 提升频繁重绘控件的性能。使用RelayoutBoundary提升频繁修改大小,增删的布局中也可以提升性能。
6.build中不要去写大量的耗时逻辑,因为数据更新会触发build的多次调用,在里面做耗时逻辑会降低性能。
7.尽量使用statelessWidget代替statefulWidget,因为statefulWidget的销毁重建会引起子widget的销毁与重建。
8.解析json可以放到子线程线程中,开Isolate去解析,这样,当返回数据特别大的时候也不会阻塞界面。
9.使用不变的组件的时候可以添加const,const组件不会进行build更新
10.由于flutter通过widget.runtimeType和key来判断是否需要跟新组建,所以我们写组件的时候尽量保持key不变,或者不写key。对于一些需要频繁改变,例如新增、删除、排序的最好加上key。如果type一直,如果不写key容易导致,element无法区分新旧widget,导致无法更新。
第一阶段 网页重构:HTML+CSS核心技术、PC端网站布局、HTML5+CSS3核心技术、移动端网站布局、响应式布局实战
第二阶段 JS高级程序设计:JavaScript核心技术-网站动效交互、JavaScript核心技术-网站前后端交互、jQuery交互效果开发
第三阶段 NodeJS前端架构及后台开发:版本控制工具、NodeJS核心技术
第四阶段 前端框架:Vuejs全家桶-Vue、React框架-React、微信小程序、Flutter
第五阶段 小程序开发:能够独立开发小程序,提高就业能力,是就业的加分项。
对于Android开发者来说,我们或多或少有了解过Android图像显示的知识点,刚刚学习Android开发的人会知道,在Actvity的onCreate方法中设置我们的View后,再经过onMeasure,onLayout,onDraw的流程,界面就显示出来了;对Android比较熟悉的开发者会知道,onDraw流程分为软件绘制和硬件绘制两种模式,软绘是通过调用Skia来操作,硬绘是通过调用Opengl ES来操作;对Android非常熟悉的开发者会知道绘制出来的图形数据最终都通过GraphiBuffer内共享内存传递给SurfaceFlinger去做图层混合,图层混合完成后将图形数据送到帧缓冲区,于是,图形就在我们的屏幕显示出来了。
但我们所知道的Activity或者是应用App界面的显示,只属于Android图形显示的一部分。同样可以在Android系统上展示图像的WebView,Flutter,或者是通过Unity开发的3D游戏,他们的界面又是如何被绘制和显现出来的呢?他们和我们所熟悉的Acitvity的界面显示又有什么异同点呢?我们可以不借助Activity的setView或者InflateView机制来实现在屏幕上显示出我们想要的界面吗?Android系统显示界面的方式又和IOS,或者Windows等系统有什么区别呢?……
去探究这些问题,比仅仅知道Acitvity的界面是如何显示出来更加的有价值,因为想要回答这些问题,就需要我们真正的掌握Android图像显示的底层原理,当我们掌握了底层的显示原理后,我们会发现WebView,Flutter或者未来会出现的各种新的图形显示技术,原来都是大同小异。
我会花三篇文章的篇幅,去深入的讲解Android图形显示的原理,OpenGL ES和Skia的绘制图像的方式,他们如何使用,以及他们在Android中的使用场景,如开机动画,Activity界面的软件绘制和硬件绘制,以及Flutter的界面绘制。那么,我们开始对Android图像显示原理的探索吧。
在讲解Android图像的显示之前,我会先讲一下屏幕图像的显示原理,毕竟我们图像,最终都是在手机屏幕上显示出来的,了解这一块的知识会让我们更容易的理解Android在图像显示上的机制。
图像显示的完整过程,分为下面几个阶段:
图像数据→CPU→显卡驱动→显卡(GPU)→显存(帧缓冲)→显示器
我详细介绍一下这几个阶段:
实际上显卡驱动,显卡和显存,包括数模转换模块都是属于显卡的模块。但为了能能详细的讲解经历的步骤,这里做了拆分。
当显存中有数据后,显示器又是怎么根据显存里面的数据来进行界面的显示的呢?这里以LCD液晶屏为例,显卡会将显存里的数据,按照从左至右,从上到下的顺序同步到屏幕上的每一个像素晶体管,一个像素晶体管就代表了一个像素。
如果我们的屏幕分辨率是1080x1920像素,就表示有1080x1920个像素像素晶体管,每个橡素点的颜色越丰富,描述这个像素的数据就越大,比如单色,每个像素只需要1bit,16色时,只需要4bit,256色时,就需要一个字节。那么1080x1920的分辨率的屏幕下,如果要以256色显示,显卡至少需要1080x1920个字节,也就是2M的大小。
刚刚说了,屏幕上的像素数据是从左到右,从上到下进行同步的,当这个过程完成了,就表示一帧绘制完成了,于是会开始下一帧的绘制,大部分的显示屏都是以60HZ的频率在屏幕上绘制完一帧,也就是16ms,并且每次绘制新的一帧时,都会发出一个垂直同步信号(VSync)。我们已经知道,图像数据都是放在帧缓冲中的,如果帧缓冲的缓冲区只有一个,那么屏幕在绘制这一帧的时候,图像数据便没法放入帧缓冲中了,只能等待这一帧绘制完成,在这种情况下,会有很大了效率问题。所以为了解决这一问题,帧缓冲引入两个缓冲区,即 双缓冲机制 。双缓冲虽然能解决效率问题,但会引入一个新的问题。当屏幕这一帧还没绘制完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,显卡的像素同步模块就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象。
为了解决撕裂问题,就需要在收到垂直同步的时候才将帧缓冲中的两个缓冲区进行交换。Android4.1黄油计划中有一个优化点,就是CPU和GPU都只有收到垂直同步的信号时,才会开始进行图像的绘制操作,以及缓冲区的交换工作。
我们已经了解了屏幕图像显示的原理了,那么接着开始对Android图像显示的学习。
从上一章已经知道,计算机渲染界面必须要有GPU和帧缓冲。对于Linux系统来说,用户进程是没法直接操作帧缓冲的,但我们想要显示图像就必须要操作帧缓冲,所以Linux系统设计了一个虚拟设备文件,来作为对帧缓冲的映射,通过对该文件的I/O读写,我们就可以实现读写屏操作。帧缓冲对应的设备文件于/dev/fb* ,*表示对多个显示设备的支持, 设备号从0到31,如/dev/fb0就表示第一块显示屏,/dev/fb1就表示第二块显示屏。对于Android系统来说,默认使用/dev/fb0这一个设帧缓冲作为主屏幕,也就是我们的手机屏幕。我们Android手机屏幕上显示的图像数据,都是存储在/dev/fb0里,早期AndroidStuio中的DDMS工具实现截屏的原理就是直接读取/dev/fb0设备文件。
我们知道了手机屏幕上的图形数据都存储在帧缓冲中,所以Android手机图像界面的原理就是将我们的图像数据写入到帧缓冲内。那么,写入到帧缓冲的图像数据是怎么生成的,又是怎样加工的呢?图形数据是怎样送到帧缓冲去的,中间经历了哪些步骤和过程呢?了解了这几个问题,我们就了解了Android图形渲染的原理,那么带着这几个疑问,接着往下看。
想要知道图像数据是怎么产生的,我们需要知道 图像生产者 有哪些,他们分别是如何生成图像的,想要知道图像数据是怎么被消费的,我们需要知道 图像消费者 有哪些,他们又分别是如何消费图像的,想要知道中间经历的步骤和过程,我们需要知道 图像缓冲区 有哪些,他们是如何被创建,如何分配存储空间,又是如何将数据从生产者传递到消费者的,图像显示是一个很经典的消费者生产者的模型,只有对这个模型各个模块的击破,了解他们之间的流动关系,我们才能找到一条更容易的路径去掌握Android图形显示原理。我们看看谷歌提供的官方的架构图是怎样描述这一模型的模块及关系的。
如图, 图像的生产者 主要有MediaPlayer,CameraPrevier,NDK,OpenGl ES。MediaPlayer和Camera Previer是通过直接读取图像源来生成图像数据,NDK(Skia),OpenGL ES是通过自身的绘制能力生产的图像数据; 图像的消费者 有SurfaceFlinger,OpenGL ES Apps,以及HAL中的Hardware Composer。OpenGl ES既可以是图像的生产者,也可以是图像的消费者,所以它也放在了图像消费模块中; 图像缓冲区 主要有Surface以及前面提到帧缓冲。
Android图像显示的原理,会仅仅围绕 图像的生产者 , 图像的消费者 , 图像缓冲区 来展开,在这一篇文章中,我们先看看Android系统中的图像消费者。
SurfaceFlinger是Android系统中最重要的一个图像消费者,Activity绘制的界面图像,都会传递到SurfaceFlinger来,SurfaceFlinger的作用主要是接收图像缓冲区数据,然后交给HWComposer或者OpenGL做合成,合成完成后,SurfaceFlinger会把最终的数据提交给帧缓冲。
那么SurfaceFlinger是如何接收图像缓冲区的数据的呢?我们需要先了解一下Layer(层)的概念,一个Layer包含了一个Surface,一个Surface对应了一块图形缓冲区,而一个界面是由多个Surface组成的,所以他们会一一对应到SurfaceFlinger的Layer中。SurfaceFlinger通过读取Layer中的缓冲数据,就相当于读取界面上Surface的图像数据。Layer本质上是 Surface和SurfaceControl的组合 ,Surface是图形生产者和图像消费之间传递数据的缓冲区,SurfaceControl是Surface的控制类。
前面在屏幕图像显示原理中讲到,为了防止图像的撕裂,Android系统会在收到VSync垂直同步时才会开始处理图像的绘制和合成工作,而Surfaceflinger作为一个图像的消费者,同样也是遵守这一规则,所以我们通过源码来看看SurfaceFlinger是如何在这一规则下,消费图像数据的。
SurfaceFlinger专门创建了一个EventThread线程用来接收VSync。EventThread通过Socket将VSync信号同步到EventQueue中,而EventQueue又通过回调的方式,将VSync信号同步到SurfaceFlinger内。我们看一下源码实现。
上面主要是SurfaceFlinger初始化接收VSYNC垂直同步信号的操作,主要有这几个过程:
经过上面几个步骤,我们接收VSync的初始化工作都准备好了,EventThread也开始运转了,接着看一下EventThread的运转函数threadLoop做的事情。
threadLoop主要是两件事情
mConditon又是怎么接收VSync的呢?我们来看一下
可以看到,mCondition的VSync信号实际是DispSyncSource通过onVSyncEvent回调传入的,但是DispSyncSource的VSync又是怎么接收的呢?在上面讲到的SurfaceFlinger的init函数,在创建EventThread的实现中,我们可以发现答案—— mPrimaryDispSync 。
DispSyncSource的构造方法传入了mPrimaryDispSync,mPrimaryDispSync实际是一个DispSyncThread线程,我们看看这个线程的threadLoop方法
DispSyncThread的threadLoop会通过mPeriod来判断是否进行阻塞或者进行VSync回调,那么mPeriod又是哪儿被设置的呢?这里又回到SurfaceFlinger了,我们可以发现在SurfaceFlinger的 resyncToHardwareVsync 函数中有对mPeriod的赋值。
可以看到,这里最终通过HWComposer,也就是硬件层拿到了period。终于追踪到了VSync的最终来源了, 它从HWCompser产生,回调至DispSync线程,然后DispSync线程回调到DispSyncSource,DispSyncSource又回调到EventThread,EventThread再通过Socket分发到MessageQueue中 。
我们已经知道了VSync信号来自于HWCompser,但SurfaceFlinger并不会一直监听VSync信号,监听VSync的线程大部分时间都是休眠状态,只有需要做合成工作时,才会监听VSync,这样即保证图像合成的操作能和VSync保持一致,也节省了性能。SurfaceFlinger提供了一些主动注册监听VSync的操作函数。
可以看到,只有当SurfaceFlinger调用 signalTransaction 或者 signalLayerUpdate 函数时,才会注册监听VSync信号。那么signalTransaction或者signalLayerUpdate什么时候被调用呢?它可以由图像的生产者通知调用,也可以由SurfaceFlinger根据自己的逻辑来判断是否调用。
现在假设App层已经生成了我们界面的图像数据,并调用了 signalTransaction 通知SurfaceFlinger注册监听VSync,于是VSync信号便会传递到了MessageQueue中了,我们接着看看MessageQueue又是怎么处理VSync的吧。
MessageQueue收到VSync信号后,最终回调到了SurfaceFlinger的 onMessageReceived 中,当SurfaceFlinger接收到VSync后,便开始以一个图像消费者的角色来处理图像数据了。我们接着看SurfaceFlinger是以什么样的方式消费图像数据的。
VSync信号最终被SurfaceFlinger的onMessageReceived函数中的INVALIDATE模块处理。
INVALIDATE的流程如下:
handleMessageTransaction的处理比较长,处理的事情也比较多,它主要做的事情有这些
handleMessageRefresh函数,便是SurfaceFlinger真正处理图层合成的地方,它主要下面五个步骤。
我会详细介绍每一个步骤的具体操作
合成前预处理会判断Layer是否发生变化,当Layer中有新的待处理的Buffer帧(mQueuedFrames0),或者mSidebandStreamChanged发生了变化, 都表示Layer发生了变化,如果变化了,就调用signalLayerUpdate,注册下一次的VSync信号。如果Layer没有发生变化,便只会做这一次的合成工作,不会注册下一次VSync了。
重建Layer栈会遍历Layer,计算和存储每个Layer的脏区, 然后和当前的显示设备进行比较,看Layer的脏区域是否在显示设备的显示区域内,如果在显示区域内的话说明该layer是需要绘制的,则更新到显示设备的VisibleLayersSortedByZ列表中,等待被合成
rebuildLayerStacks中最重要的一步是 computeVisibleRegions ,也就是对Layer的变化区域和非透明区域的计算,为什么要对变化区域做计算呢?我们先看看SurfaceFlinger对界面显示区域的分类:
还是以这张图做例子,可以看到我们的状态栏是半透明的,所以它是一个opaqueRegion区域,微信界面和虚拟按键是完全不透明的,他是一个visibleRegion,除了这三个Layer外,还有一个我们看不到的Layer——壁纸,它被上方visibleRegion遮挡了,所以是coveredRegion
对这几个区域的概念清楚了,我们就可以去了解computeVisibleRegions中做的事情了,它主要是这几步操作:
页面中的各界面元素(Widget)以树的形式组织,即控件树。Flutter通过控件树中的每个控件创建不同类型的渲染对象,组成渲染对象树。而渲染对象树在Flutter的展示过程分为三个阶段:布局、绘制、合成和渲染。
(一)布局
Flutter采用深度优先机制遍历渲染对象树,决定渲染对象树中各渲染对象在屏幕上的位置和尺寸。在布局过程中,渲染对象树中的每个渲染对象都会接收父对象的布局约束参数,决定自己的大小,然后父对象按照控件逻辑决定各个子对象的位置,完成布局过程。
为了防止因子节点发生变化而导致整个控件树重新布局,Flutter加入了一个机制——布局边界(Relayout Boundary),可以在某些节点自动或手动地设置布局边界,当边界内的任何对象发生重新布局时,不会影响边界外的对象,反之亦然。
二)绘制
布局完成后,渲染对象树中的每个节点都有了明确的尺寸和位置。Flutter会把所有的渲染对象绘制到不同的图层上。与布局过程一样,绘制过程也是深度优先遍历,而且总是先绘制自身,再绘制子节点。
以下图为例:节点1在绘制完自身后,会再绘制节点2,然后绘制它的子节点3、4和5,最后绘制节点6。
可以看到,由于一些其他原因(比如,视图手动合并)导致2的子节点5与它的兄弟节点6处于了同一层,这样会导致当节点2需要重绘的时候,与其无关的节点6也会被重绘,带来性能损耗。
为了解决这一问题,Flutter提出了与布局边界对应的机制——重绘边界(Repaint Boundary)。在重绘边界内,Flutter会强制切换新的图层,这样就可以避免边界内外的互相影响,避免无关内容置于同一图层引起不必要的重绘。
重绘边界的一个典型场景是Scrollview。ScrollView滚动的时候需要刷新视图内容,从而触发内容重绘。而当滚动内容重绘时,一般情况下其他内容是不需要重绘的,这时候重绘边界就派上用场了。
(三)合成和渲染
终端设备的页面越来越复杂,因此Flutter的渲染树层级通常很多,直接交付给渲染引擎进行多图层渲染,可能会出现大量渲染内容的重复绘制,所以还需要先进行一次图层合成,即将所有的图层根据大小、层级、透明度等规则计算出最终的显示效果,将相同的图层归类合并,简化渲染树,提高渲染效率。
合并完成后,Flutter会将几何图层数据交由Skia引擎加工成二维图像数据,最终交由GPU进行渲染,完成界面的展示。
四、总结
咱们从各种业界主流跨端方案与Flutter的对比开始,到Flutter的简要介绍以及Flutter的运行机制,并以界面渲染过程为例,从布局、绘制、合成和渲染三个阶段讲述了Flutter的实现原理。相信大家对Flutter已经有一个整体认知,赶快一起上手操作起来吧!
本文将从三个方面介绍Android 图形系统。
图形系统提供绘图和图形处理支持。
Android 框架提供了各种用于 2D 和 3D 图形渲染的 API、图片解码库,以及各种Driver支持。
• 绘图API:2D引擎 Skia,3D引擎 OpenGL ES,RenderScript,OpenCV和Vulkan。
• 图片解码库:jpg,png,gif等。
应用开发者可通过三种方式将图像绘制到屏幕:
• Canvas : 2D图形API,Android View树实际的绘制者。
• OpenGL ES : 嵌入式设备的OpenGL 三维图形API子集。
• Vulkan :跨平台的2D和3D绘图引擎,Android 7.0后支持,NDK。
整个图形系统架构是一个生产者和消费者模式,五层依次介绍:
2D绘制:Canvas api / view 的子类 (button ,list)/自定义view
3D绘制:应用直接使用OpenGL 接口绘制图形(PixelFlinger对应的是openGl 1.0 ,GUP driver 对应的是2.0和3.0)
所有情况下的绘图都渲染到一个包含 GraphicBuffer的Surface上,当一块 Surface 显示在屏幕上时,就是用户所看到的窗口。
• Canvas:画布,2D图形API,Android View树实际的渲染者。
• Skia绘制:Android4.0之前默认使用,主线程通过CPU完成绘图指令操作,在复杂场景下单帧容易超过16ms导致卡顿。
WindowManagerService(WMS)窗口管理服务,管理系统中所有的窗口。
• 管理window (view的容器)
• Window与surface对应,一块显示区域。添加一个window,就是 WMS 为其分配一块 Surface 的过程。
Google 在Android source官网提示:
这里就对这些控件进行简单介绍:
Surface : Handle onto a raw buffer that is being managed by the screen compositor.
Surface 对应一块屏幕缓冲区。生产者是: SurfaceTexture、MediaRecorder 等,消费者是: OpenGL、MediaPlayer 或 CameraDevice等。每个window对应一个Surface。Canvas或OpenGL ES等最终都渲染到Surface上。
• Flutter在Android平台上也是直接渲染到Surface。例如:一个Activity/Dialog都是一个Surface,它承载了上层的图形数据,与SurfaceFlinger侧的Layer相对应。
Canvas(画布)实现由 Skia 图形库提供。为了确保两个客户端不会同时更新某个缓冲区,使用以下命令处理画布锁:
使用双缓冲机制,有自己的 surface,View只是一个透明的占位符,Surface可以在后台线程中绘制。双缓冲机制提高渲染效率,独立线程
绘制,提升流畅性。适合一些场景:需要界面迅速更新、UI绘制时间长、对帧率要求较高的情况。
提供访问和控制Surface 相关的方法 。通过SurfaceView的getHolder()函数可以获取SurfaceHolder对象,Surface 就在SurfaceHolder对象内。
addCallback(SurfaceHolder.Callbackcallback) /Canvas lockCanvas() /unlockCanvasAndPost(Canvascanvas)
SurfaceTexture: Surface 和 OpenGL ES (GLES) 纹理(Texture)的组合。将图像流转为 OpenGL 外部纹理。
TextureView:持有 SurfaceTexture,将图像处理为 OpenGL 纹理更新到 HardwareLayer。
GLSurfaceView:加入 EGL 管理,自带 GL 上下文和 GL 渲染线程
这些View通常涉及到Android音视频相关,需要高效的渲染能力。如下面的SurfaceTexture在camera中的应用。
简称Buffer, 一个Buffer包含一帧图像,Buffer由gralloc分配和回收。Buffer 属性包含:width, height, format, usage等
BufferQueue 的引入是为了解决显示和性能问题。
• Surface属于APP进程,Layer属于系统进程,如果它们之间只用一个Buffer,会存在显示和性能问题。
• 一些Buffer用于绘制,一些Buffer用于显示,双方处理完之后,交换一下Buffer,提高效率。
• BufferQueue中包含多个Buffer对象。
Android图形系统包含了两对生产者和消费者模型,它们都通过BufferQueue进行连接:
1.Canvas和OpenGL ES生产图形数据,SurfaceFlinger消费图形数据。
2.SurfaceFlinger合成所有图层的图形数据,Display显示合成结果。
code:frameworks/native/services/surfaceflinger
• Surface表示APP进程的一个窗口,承载了窗口的图形数据。
• SurfaceFlinger是系统进程合成所有窗口的系统服务,负责合成所有Surface提供的图形数据,然后送显到屏幕。
• SurfaceFlinger既是上层应用的消费者,又是Display的生产者,起到了承上启下的作用。
数据流:
合成示意图:
在介绍Vsync机制之前先介绍两个重要概念:
屏幕刷新率:屏幕每秒钟可以刷新多少次。60HZ刷新率,16.7ms刷新一次。(120HZ/8.3ms),硬件指标。
GPU 绘制帧率:GPU 每秒能够合成绘制多少帧。
软件层触发 View 绘制的时机是随机的,当下一次屏幕刷新时,屏幕从 Frame Buffer 中拿到的数据还是“帧1”的数据,导致“丢帧”。
每隔 16ms 硬件层发出 vsync 信号,应用层接收到此信号后会触发UI 的渲染流程,同时 vsync 信号也会触发 SurfaceFlinger 读取Buffer 中的数据,进行合成显示到屏幕上。
总结:Vsync机制将 CPU 和 GPU 的开始时间与屏幕刷新强行拖拽到同一起跑线
Android提供的Graphics流程相对比较复杂对其进行具象后的流程如下两张图所示: