• home > OS > Android > Develop >

    iOS/Android离屏渲染分析:离屏渲染大发真香么?

    Author:zhoulujun Date:

    首先,建议先读《安卓渲染原理:Android布局渲染优化底层原理分析》GPU 屏幕渲染有以下两种方式:On-Screen Rendering,意为当前屏幕渲染

    首先,建议先读《安卓渲染原理:Android布局渲染优化底层原理分析》GPU 屏幕渲染有以下两种方式:

    • On-Screen Rendering,意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。

    • Off-Screen Rendering,意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

    特殊的离屏渲染:如果将不在GPU的当前屏幕缓冲区中进行的渲染都称为离屏渲染,那么就还有另一种特殊的“离屏渲染”方式:CPU渲染。如果我们重写了drawRect方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲染。整个渲染过程由CPU在App内同步地完成,渲染得到的bitmap最后再交由GPU用于显示。

    备注:Core Graphics通常是线程安全的,所以可以进行异步绘制,显示的时候再放回主线程,一个简单的异步绘制过程大致如下:

    (void)display {
      dispatch_async(backgroundQueue, {
        CGContextRef ctx = CGBitmapContextCreate(...);
        // draw in context...
        CGImageRef img = CGBitmapContextCreateImage(ctx);
        CFRelease(ctx);
        dispatch_async(mainQueue, ^{
        layer.contents = img;
        });
      });
    }


    离屏渲染的触发方式:

    • shouldRasterize(光栅化)

    • masks(遮罩)

    • shadows(阴影)

    • edge antialiasing(抗锯齿)

    • group opacity(不透明)

    • 复杂形状设置圆角等

    • 渐变

    详细举例说明

    iOS

    在iOS中,以下情况会触发离屏渲染:

    1. 图层有透明度(alpha):设置 UIView 的 alpha 属性为小于1的值。

    2. 遮罩(masking):使用 mask 属性或 layer.mask 属性。

    3. 阴影(shadows):设置 layer.shadow 属性。

    4. 圆角(corner radius):设置 layer.cornerRadius 属性。

    5. 滤镜(filters):使用 CIFilter。

    6. 混合模式(blending modes):一些特殊的混合模式。

    7. 光栅化(shouldRasterize):视图的layer.shouldRasterize属性为YES

      1. 复杂的图层效果:当视图层次结构非常复杂,并且这些视图不经常变化时,可以通过光栅化来减少重复的绘制开销。

      2. 频繁重绘:如果视图或其子视图频繁发生变化(例如有动画效果),可以通过光栅化来缓存这些视图,以减少每帧的绘制时间。

      3. 动态内容:视图中包含复杂的动态内容(如文本、图片等),并且这些内容需要频繁更新时,光栅化可以帮助提升性能。

      4. 静态背景图:对于复杂的静态背景图,启用光栅化可以减少重绘开销。

    Android

    在Android中,以下情况会触发离屏渲染:

    1. 视图有透明度(alpha):设置 View 的 view.setAlpha(0.5f)。

    2. 阴影(shadows):设置 View.setElevation 和 ViewOutlineProvider。

    3. 复杂的视图层次结构:某些复杂的视图组合也可能导致离屏渲染。

    4. 裁剪路径(clipping with path):使用 Canvas.clipPath 方法。

    5. 圆角裁剪(clipping with rounded corners):使用 Outline 或 ViewOutlineProvider。使用clipPath或设置背景圆角(如android:background="@drawable/rounded_corner")

    6. 硬件加速:某些情况下,使用硬件加速(如android:hardwareAccelerated="true")会触发离屏渲染。

    7. 光栅化(shouldRasterize):Android中,没有直接等同于shouldRasterize的属性,但可以通过以下方式实现类似效果:

      1. 缓存视图:使用setLayerType(View.LAYER_TYPE_HARDWARE, null)或setLayerType(View.LAYER_TYPE_SOFTWARE, null)来启用硬件或软件层,从而实现视图的缓存。

      2. View Caching:在复杂视图中,可以使用View.setDrawingCacheEnabled(true)来启用视图缓存,以减少重绘开销。


    光栅化(shouldRasterize)注意事项

    • 内存开销:光栅化会增加内存使用,因为需要将视图缓存为位图。对于大型视图或复杂的视图层次结构,可能会导致较大的内存占用。

    • 动态变化:如果视图的内容频繁变化,光栅化可能得不偿失,因为每次变化都需要重新生成位图缓存。

    为什么会使用离屏渲染:

    当使用圆角,阴影,遮罩的时候,图层属性的混合体被指定为在未预合成之前(下一个 VSync 信号开始前)不能直接在屏幕中绘制,所以就需要屏幕外渲染被唤起。屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论 CPU 还是 GPU)。所以当使用离屏渲染的时候会很容易造成性能消耗,因为离屏渲染会单独在内存中创建一个屏幕外缓冲区并进行渲染,而屏幕外缓冲区跟当前屏幕缓冲区上下文切换是很耗性能的。由于垂直同步的机制,如果在一个 VSync 时间内,CPU或者GPU没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因

    GPU离屏渲染的性能影响

    相比于当前屏幕渲染,离屏渲染的代价是很高的,主要体现在两个方面:

    • 创建新缓冲区:要想进行离屏渲染,首先要创建一个新的缓冲区。

    • 上下文切换:离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。

    善用离屏渲染

    尽管离屏渲染开销很大,但是当我们无法避免它的时候,可以想办法把性能影响降到最低。优化思路也很简单:既然已经花了不少精力把图片裁出了圆角,如果我能把结果缓存下来,那么下一帧渲染就可以复用这个成果,不需要再重新画一遍了。

    CALayer为这个方案提供了对应的解法:shouldRasterize。一旦被设置为true,Render Server就会强制把layer的渲染结果(包括其子layer,以及圆角、阴影、group opacity等等)保存在一块内存中,这样一来在下一帧仍然可以被复用,而不会再次触发离屏渲染。有几个需要注意的点:

    1. shouldRasterize的主旨在于降低性能损失,但总是至少会触发一次离屏渲染。如果你的layer本来并不复杂,也没有圆角阴影等等,打开这个开关反而会增加一次不必要的离屏渲染

    2. 离屏渲染缓存有空间上限,最多不超过屏幕总像素的2.5倍大小

    3. 一旦缓存超过100ms没有被使用,会自动被丢弃

    4.  layer的内容(包括子layer)必须是静态的,因为一旦发生变化(如resize,动画),之前辛苦处理得到的缓存就失效了。如果这件事频繁发生,我们就又回到了“每一帧都需要离屏渲染”的情景,而这正是开发者需要极力避免的。针对这种情况,Xcode提供了“Color Hits Green and Misses Red”的选项,帮助我们查看缓存的使用是否符合预期

    其实除了解决多次离屏渲染的开销,shouldRasterize在另一个场景中也可以使用:如果layer的子结构非常复杂,渲染一次所需时间较长,同样可以打开这个开关,把layer绘制到一块缓存,然后在接下来复用这个结果,这样就不需要每次都重新绘制整个layer树了


    手工触发离屏渲染

    Android

    1. 自定义View:当开发者创建自定义View或SurfaceView并需要在非主线程中绘制时,可能会使用离屏渲染。

    2. Canvas Layers:通过调用Canvas#saveLayer()方法可以创建一个离屏层。

    3. Bitmap Rendering:将UI元素渲染到Bitmap对象中也是离屏渲染的一种形式。

    4. OpenGL ES:在使用OpenGL ES进行3D渲染时,经常会在帧缓冲对象(Frame Buffer Object, FBO)中进行离屏渲染。

    iOS

    1. Core Graphics Offscreen Context:开发者可以通过创建一个CGContextRef并指定一个CGColorSpaceRef和void* buffer来创建离屏渲染上下文。

    2. UIKit Rendering:通过UIGraphicsBeginImageContextWithOptions可以开始在一个图像上下文中渲染,该上下文可以是离屏的。

    3. Metal Offscreen Rendering:在使用Metal进行图形处理时,可以在纹理(Texture)上进行渲染,这些纹理并不直接与屏幕关联。

    底层原理

    • Android: Android的视图系统使用了Skia作为其2D渲染引擎,具体的绘制和渲染工作由Skia负责。Android中的离屏渲染通常会利用到Skia的功能来实现。

    • iOS: iOS使用Core Animation作为其核心动画引擎,视图的渲染和动画处理由Core Animation负责。离屏渲染在iOS中也是通过Core Animation来管理和实现的。


    Android下离屏渲染的三种方式

    在Android系统下可以使用三种方法实现同时将OpenGL的内容输出给多个目标(屏幕和编码器)。

    1. 二次渲染法:使用 SurfaceView+自己创建渲染线程 这个组合,在自己创建的渲染线程中使用EGL API,通过多次渲染并将结果输送给多个目标Surface来实现二次渲染

    2. 使用FBO上面的方法虽然能够将同一个源输送给不同的目标,但每次都要调用OpenGL进行重绘,效率上确实不高。

      OpenGL为我们提供了一种高效的办法,即FBO(FrameBufferObject),不过通过OpenGL渲染的结果不再直接送给不同的Surface,而是将结果输出到FBO中,这里你可以先将FBO想像为一块特殊的空间。

      然后通过另外一种Shader程序(这种Shader程序是专门用于处理FBO纹理的)再次进行渲染。当然,这种Shader程序由于处理的是纹理,所以会比第一次使用的Shader程序效率高很多。渲染成功后将结果输出给屏幕,然后切换Surface再次调用Shader(第二个Shader),这次渲染的结果将会输送给MediaCodec的Surface进行编码。

      因此,通过FBO方法我们只需要对原模型渲染一次,将结果保存到FBO。之后再对FBO中的内容进行多次渲染,通过这种方式来提高效率。

    3. 使用BlitFramebuffer:有没有更好的方法,只渲染一次就可以将结果输送给多个目标呢?到了OpenGL3.0出现了一种更高效的方法,即BlitFramebuffer。该方法不再使用FBO做缓存,而是像二次渲染法一样,先将渲染的内容输出到当前Surface中,但并不展示到屏幕上。

      相当于把当前的Surface当作一个缓冲区,然后切换Surface,此时MediaCodec的Surface变成了当前Surface,接下来利用OpenGL3.0提供的API BlitFramebuffer从原来的Surface拷贝数据到当前Surface中,再调用EGL的eglSwapBuffers将Surface中的内容送编码器编码。之后再将当前Surface切回原来的Surface,也就是SurfaceView的Surface,同样调用EGL的eglSwapBuffers方法,将其内容显示到屏幕上。

      至此,实现了OpenGL仅渲染一次,却可以输出给多个目标,这种方法是最高效的。



    参考文章:

    在工作中离屏真的不重要吗,代码优化就真的不考虑0.1%的离屏问题吗,懂得离屏渲染原理,让程序员的路走的更长 https://www.cnblogs.com/mysweetAngleBaby/p/16341632.html

    Android下三种离屏渲染技术 https://www.imooc.com/article/340844



    转载本站文章《iOS/Android离屏渲染分析:离屏渲染大发真香么?》,
    请注明出处:https://www.zhoulujun.cn/html/OS/Android/AndroidDevelop/2024_0803_9203.html