d. display & window 之渲染

计算机将图形显示到显示设备上的过程,可以划分为两个阶段:第一阶段是渲染(render)过程,第二阶段才是显示(display)过程,所谓渲染就是将使用数学模型描述的图形,转化为像素阵列,或者叫像素数组。像素数组中的每个元素是一个颜色值或者颜色索引,对应图像的一个像素。简单来说就是渲染是生成像素数组,显示是将像素在屏幕上显示出来。

本文基于安卓12之前的版本。

SurfaceFinger按英文翻译过来就是Surface投递者。Google经过如下一层层的屏蔽,让开发者不需要对Android底层的渲染体系有任何理解,也能绘制出不错的效果。那我们为什么还要一层层的剖析呢,因为Surfaceflinger才是真正渲染工作的根基和管理者,如果你想在系统未完全加载完成的时机,或者不依赖上层应用就在屏幕上绘制内容,就要在Surfaceflinger上直接执行操作,比如开机动动画,屏显调试信息等。

1. Surfaceflinger服务的位置

Surfaceflinger是一个系统服务,核心功能就是汇总管理整个系统的渲染数据(Buffer Data),作为其他模块生产图形数据的消费者,对图层计算处理后,再作为生产者发送给HWC去做显示,如官图示:

image-20240218-025831.png

SurfaceFlinger 是可以修改所显示部分内容的唯一服务。Surfaceflinger使用 OpenGL 和 Hardware Composer 来合成一组 Surface。

image-20240218-030156.png

2. BufferQueue介绍

BufferQueue是很重要的传递数据的组件,Android显示系统依赖于BufferQueue,只要显示内容到“屏幕”(此处指抽象的屏幕,有时候还可以包含编码器),就一定需要用到BufferQueue,可以说在显示/播放器相关的领悟中,BufferQueue无处不在。即使直接调用Opengl ES来绘制,底层依然需要BufferQueue才能显示到屏幕上。

BufferQueue创建实际是创建了BufferQueueProducer/BufferQueueConsumer生产者和消费者通过接口和回调传递buffer,数据结构是在如下代码中实现:

  1. BufferBufferCore:BufferQueue的实际实现

  2. BufferSlot:用来存储GraphicBuffer

  3. BufferState:表示GraphicBuffer的状态

  4. IGraphicBufferProducer:BufferQueue的生产者接口,实现类是BufferQueueProducer

  5. IGraphicBufferConsumer:BufferQueue的消费者接口,实现类是BufferQueueConsumer

  6. GraphicBuffer:表示一个Buffer,可以填充图像数据

  7. ANativeWindow_Buffer:GraphicBuffer的父类

  8. ConsumerBase:实现了ConsumerListener接口,在数据入队列时会被调用到,用来通知消费者

而图像数据的真正数据buffer在:/frameworks/native/libs/ui/include/ui/GraphicBuffer.h

static sp<GraphicBuffer> from(ANativeWindowBuffer *);

GraphicBuffer 是 Android 操作系统中用于处理图形和缓冲区的数据结构。它是 Android 图形框架的一部分,主要用于高效和底层处理图形缓冲区,通常用于多媒体和硬件加速渲染的上下文中。

native_handle_t数据结构如下

typedef struct native_handle { int version; int numFds; int numInts; } native_handle_t;

native_handle_t是上层抽象的可以在进程间传递的数据结构,对private_handle_t的抽象包装。保存了数据buffer的fd,再通过传递fd来传递buffer。

image-20240218-030345.png

从安卓8.0开始Android 操作系统框架在架构方面的一项重大改变,提出了treble 项目。Vendor的实现和Androd的实现分开,Android和HAL,采用HwBinder进行通信,减少Android对HAL的直接依赖。GraphicBufferAllocator和GraphicBufferMapper来分配映射GraphicBuffer,在Android的部分实现的是alloc和Mapper的接口,在vendor才有真正的实现,IonAlloc 是一个具体实现,依赖linux ion驱动,可以理解成池化的buffer管理机制,也可以提供给用户空间调用,这里的buffer管理机制是一个细分和版本变化的模块,不做展开。

3. Surfaceflinger的作用概述

总之,结合以上两项说明,Surfaceflinger通过以上的方式,实现了作用:Surfaceflinger是 Android 操作系统中的核心图形合成引擎,它协调多个应用程序的图形元素,确保它们在设备的屏幕上正确显示,并提供流畅的用户界面和多媒体体验。它的工作对于 Android 设备的图形性能和用户体验至关重要。

image-20240218-030415.png

总结下来Surfaceflinger管理图形显示数据,他所处的系统位置和buffer数据的处理机制基本如上图,surface提供了跨进程的buffer接口,Surfaceflinger再经bufferqueue用Surfaceflinger来维护layer,把layer进行合成,交给HWC绘制,显示在屏幕上。

4. Surfaceflinger的成员函数

4. 1. 生命周期函数

简单来看Surfaceflinger的源码,部分主要函数作用如下:

  1. SurfaceFlinger::onFirstRef():

    • 初始化SurfaceFlinger,创建核心线程,设置默认参数。

  2. SurfaceFlinger::createTransaction():

    • 创建一个事务,用于更改Surface属性和渲染信息。事务是SurfaceFlinger管理界面元素的基本单位。

  3. SurfaceFlinger::composeSurfaces():

    • 执行界面合成,将各个应用程序的Surface组合成最终的显示帧,包括合并各个层级、应用透明度、处理动画等。

  4. SurfaceFlinger::onMessageReceived():

    • 处理来自其他组件或进程的消息,用于更新界面信息。

  5. SurfaceFlinger::screenReleased():

    • 处理屏幕释放事件,通常用于关闭屏幕。

  6. SurfaceFlinger::doTransaction():

    • 处理当前事务,将界面更改应用到显示帧上。

  7. SurfaceFlinger::init():

    • 初始化SurfaceFlinger,设置默认参数和配置。

  8. SurfaceFlinger::clearLayerClient()

    • 从层级中移除客户端。

  9. SurfaceFlinger::createLayer():

    • 创建新的层级,用于显示应用程序界面元素。

  10. SurfaceFlinger::updateLayer():

    • 更新层级的属性,例如位置、大小、透明度等。

  11. SurfaceFlinger::setTransactionState():

    • 设置事务的状态,标记事务已经准备好应用。

  12. SurfaceFlinger::doDisplayComposition():

    • 执行显示合成,将合成好的图形渲染到屏幕上。

  13. SurfaceFlinger::getDisplayComposition():

    • 获取当前显示合成的信息,用于绘制屏幕。

  14. SurfaceFlinger::setDisplayPowerMode():

    • 设置显示器的电源模式,例如打开、关闭或进入低功耗模式。

  15. SurfaceFlinger::transactionDone():

    • 标记当前事务已完成,准备进行下一次事务。

4.2 dump函数

另外还有一堆dump函数,是在命令行调用记录系统信息的,遍历相关数据生成string:

4.3 HWC接口函数

Surfaceflinger与HWC的通信是注册回调实现的,注册HWC接口如下:

  mCompositionEngine->getHwComposer().setCallback(this);           //包含如下接口函数,显示插拔,垂直同步信号,合成刷新等   class ComposerCallbackBridge : public hal::IComposerCallback { public:   ComposerCallbackBridge(ComposerCallback* callback, bool vsyncSwitchingSupported)         : mCallback(callback), mVsyncSwitchingSupported(vsyncSwitchingSupported) {}   Return<void> onHotplug(hal::HWDisplayId display, hal::Connection connection) override {       mCallback->onComposerHalHotplug(display, connection);       return Void();   }   Return<void> onRefresh(hal::HWDisplayId display) override {       mCallback->onComposerHalRefresh(display);       return Void();   }   Return<void> onVsync(hal::HWDisplayId display, int64_t timestamp) override {       if (!mVsyncSwitchingSupported) {           mCallback->onComposerHalVsync(display, timestamp, std::nullopt);       } else {           ALOGW("Unexpected onVsync callback on composer >= 2.4, ignoring.");       }       return Void();   }   Return<void> onVsync_2_4(hal::HWDisplayId display, int64_t timestamp,                             hal::VsyncPeriodNanos vsyncPeriodNanos) override {       if (mVsyncSwitchingSupported) {           mCallback->onComposerHalVsync(display, timestamp, vsyncPeriodNanos);       } else {           ALOGW("Unexpected onVsync_2_4 callback on composer <= 2.3, ignoring.");       }       return Void();   }   Return<void> onVsyncPeriodTimingChanged(           hal::HWDisplayId display, const hal::VsyncPeriodChangeTimeline& timeline) override {       mCallback->onComposerHalVsyncPeriodTimingChanged(display, timeline);       return Void();   }   Return<void> onSeamlessPossible(hal::HWDisplayId display) override {       mCallback->onComposerHalSeamlessPossible(display);       return Void();   } private:   ComposerCallback* const mCallback;   const bool mVsyncSwitchingSupported; };      

跟着接口onComposerHalRefresh基本找出来重绘全流程,这里不做展开:

void SurfaceFlinger::onComposerHalRefresh(hal::HWDisplayId) {   Mutex::Autolock lock(mStateLock);   repaintEverythingForHWC(); }

4.4 渲染函数drawLayes

layer数据经过RendeRengine的渲染计算生成每个layer的图像数据,如果需要对Surfaceflinger的显示问题进行排查,可以dump layers的信息,dump的信息大致如下,各个layer的单独信息也可以查看,但是却没办法确认layer经过RendeRengine生成的图像数据是否正确。

Display 0 HWC layers: --------------------------------------------------------------------------------------------------------------------------------------------------------------- Layer name           Z | Window Type | Comp Type | Transform |   Disp Frame (LTRB) |         Source Crop (LTRB) |     Frame Rate (Explicit) (Seamlessness) [Focused] --------------------------------------------------------------------------------------------------------------------------------------------------------------- Wallpaper BBQ wrapper#0 rel     0 |         2013 |     DEVICE |         0 |   0   0 1080 2400 |   0.0   0.0 1080.0 2400.0 |                                 [ ] --------------------------------------------------------------------------------------------------------------------------------------------------------------- com.miui.home/com.miui.home.launcher.Launcher#0 rel     0 |           1 |     DEVICE |         0 |   0   0 1080 2400 |   0.0   0.0 1080.0 2400.0 |                                 [*] --------------------------------------------------------------------------------------------------------------------------------------------------------------- StatusBar#0 rel     0 |         2000 |     DEVICE |         0 |   0   0 1080   96 |   0.0   0.0 1080.0   96.0 |                                 [ ] --------------------------------------------------------------------------------------------------------------------------------------------------------------- NavigationBar0#0 rel     0 |         2019 |     DEVICE |         0 |   0 2356 1080 2400 |   0.0   0.0 1080.0   44.0 |                                 [ ] ---------------------------------------------------------------------------------------------------------------------------------------------------------------

这时候可以在Surfaceflinger的layer渲染后的图像数据写到文件,再通过RGB或者YUV软件工具来查看,确定哪个阶段的图像数据是否正常。Surfaceflinger提供一种保存全屏图像的函数:

status_t SurfaceFlinger::captureScreenCommon(RenderAreaFuture renderAreaFuture,                                             TraverseLayersFunction traverseLayers,                                             ui::Size bufferSize, ui::PixelFormat reqPixelFormat,                                             bool allowProtected, bool grayscale,                                             const sp<IScreenCaptureListener>& captureListener)
//全屏渲染后,保存到文件 getRenderEngine().drawLayers(clientCompositionDisplay, clientCompositionLayerPointers, buffer,                                 kUseFramebufferCache, std::move(bufferFence), &drawFence);                                 captureListener->onScreenCaptureCompleted(captureResults);

无论layer实际显示时合成方式是CLIENT还是DEVICE,这个方法通过GPU合成的方式把所有图层合成到一个 GraphicBuffer中。

另外还有四种场景的图层数据,需要针对性的在不同环节加入逻辑来实现保存:

  1. GPU渲染的图层数据:GLESRenderEngine::drawLayers

  2. GPU合成的图层数据:RenderSurface::queueBuffer

  3. 硬件渲染的图层数据:BufferStateLayer::setBuffer

  4. HWC端的图层数据:SetClientTarget(client合成)/SetLayerBuffer(device合成)

在这些函数内部拿到GraphicBuffer或者GraphicBuffer的句柄,再保存到文件,再用工具按格式打开,就可以确认图层数据是否正常。

4.5 渲染合成分类

渲染通常又被分为两种:一种渲染过程是由CPU完成的,通常称为软件渲染,另外一种是由GPU完成的,通常称为硬件渲染,也就是我们常常提到的硬件加速。

目前SurfaceFlinger中支持两种合成方式,一种是Device合成,一种是Client合成。SurfaceFlinger 在收集可见层的所有缓冲区之后,便会询问 Hardware Composer 应如何进行合成。

  • Client合成 Client合成方式是相对与硬件合成来说的,其合成方式是,将各个Layer的内容用GPU渲染到暂存缓冲区中,最后将暂存缓冲区传送到显示硬件。这个暂存缓冲区,我们称为FBTarget,每个Display设备有各自的FBTarget。Client合成,之前称为GLES合成,我们也可以称之为GPU合成。Client合成,采用RenderEngine进行合成。

  • Device合成 就是用专门的硬件合成器进行合成HWComposer,所以硬件合成的能力就取决于硬件的实现。其合成方式是将各个Layer的数据全部传给显示硬件,并告知它从不同的缓冲区读取屏幕不同部分的数据。HWComposer是Devicehec的抽象。

hardware/libhardware/include/hardware/hwcomposer2.h

typedef enum {   HWC2_COMPOSITION_INVALID = 0,   /* The client will composite this layer into the client target buffer     * (provided to the device through setClientTarget).     *     * The device must not request any composition type changes for layers of     * this type. */   HWC2_COMPOSITION_CLIENT = 1,   /* The device will handle the composition of this layer through a hardware     * overlay or other similar means.     *     * Upon validateDisplay, the device may request a change from this type to     * HWC2_COMPOSITION_CLIENT. */   HWC2_COMPOSITION_DEVICE = 2,

这是在硬件层定义的CompositionType类型,另外还有一些别的类型,主要也是基于一些硬件的区别。在实际的设置时,使用的类型名是

hal::Composition::DEVICE、hal::Composition::CLIENT等等,应该使用哪种方式合成,这是由Surfaceflinger初步设定和HWC最终确定。

Surfaceflinger中的代码位于layer的实现类frameworks/native/services/surfaceflinger/CompositionEngine/src/OutputLayer.cpp

​void OutputLayer::writeStateToHWC(bool includeGeometry, bool skipLayer, uint32_t z,                                 bool zIsOverridden, bool isPeekingThrough) {   const auto& state = getState();   // Skip doing this if there is no HWC interface   if (!state.hwc) {       return;   }   auto& hwcLayer = (*state.hwc).hwcLayer;   if (!hwcLayer) {       ALOGE("[%s] failed to write composition state to HWC -- no hwcLayer for output %s",             getLayerFE().getDebugName(), getOutput().getName().c_str());       return;   }   const auto* outputIndependentState = getLayerFE().getCompositionState();   if (!outputIndependentState) {       return;   }   auto requestedCompositionType = outputIndependentState->compositionType;   // TODO(b/181172795): We now update geometry for all flattened layers. We should update it   // only when the geometry actually changes   const bool isOverridden =           state.overrideInfo.buffer != nullptr || isPeekingThrough || zIsOverridden;   const bool prevOverridden = state.hwc->stateOverridden;   if (isOverridden || prevOverridden || skipLayer || includeGeometry) {       writeOutputDependentGeometryStateToHWC(hwcLayer.get(), requestedCompositionType, z);       writeOutputIndependentGeometryStateToHWC(hwcLayer.get(), *outputIndependentState,                                                 skipLayer);   }   writeOutputDependentPerFrameStateToHWC(hwcLayer.get());   writeOutputIndependentPerFrameStateToHWC(hwcLayer.get(), *outputIndependentState, skipLayer);   writeCompositionTypeToHWC(hwcLayer.get(), requestedCompositionType, isPeekingThrough,                             skipLayer);   // Always set the layer color after setting the composition type.   writeSolidColorStateToHWC(hwcLayer.get(), *outputIndependentState);   editState().hwc->stateOverridden = isOverridden;   editState().hwc->layerSkipped = skipLayer; }

其实确定合成类型分3步,第一步,SurfaceFlinger制定合成类型,callIntoHwc这个时候callIntoHwc为true,将类型制定给HWC。第二步,HWC根据实际情况,看看SurfaceFlinger制定的合成类型能不能执行,如果条件不满足,做出修改;第三步,SurfaceFlinger根据HWC的修改情况,再做调整,最终确认合成类型,这个时候callIntoHwc参数设置为false。

总的来说,这些合成类型的使用和切换通常由系统的图形管理组件(如Surfaceflinger)和硬件合成设备之间的协议来管理,以提供最佳的图形合成性能和质量。开发者通常不需要手动指定图层的合成方式,系统会自动根据图层的性质和硬件能力来选择。

哪些图层应该是client合成,哪些应该是device合成,是由系统决定,上层和framework层尽量不要去做修改,只需要知道他们对应的渲染和合成逻辑不一样,查询图像数据时路径不一样就可以了。

4.6 Surfaceflinger功能的应用场景(开机动画)

说了这么多原理和流程,Surfaceflinger其实基本可以认为他管理的是显示内容,而且他的显示仅仅依赖于HWC,做为一个init时期就拉起来的系统native进程,他可以独立实现很多功能:

  1. 开机动画

  2. 比如在无应用绘制时绘制其他内容(视频,录像,监控)

  3. 比如多屏系统在多屏未完成初始化时绘制加载内容和整体显示的改造

开机动画流程:SurfaceFlinger::init()

if (mStartPropertySetThread->Start() != NO_ERROR) { ALOGE("Run StartPropertySetThread failed!"); }

frameworks/native/services/surfaceflinger/StartPropertySetThread.cpp 中设置系统属性。

status_t StartPropertySetThread::Start() { return run("SurfaceFlinger::StartPropertySetThread", PRIORITY_NORMAL); } bool StartPropertySetThread::threadLoop() { // Set property service.sf.present_timestamp, consumer need check its readiness property_set(kTimestampProperty, mTimestampPropertyValue ? "1" : "0"); // Clear BootAnimation exit flag property_set("service.bootanim.exit", "0"); // Start BootAnimation if not started property_set("ctl.start", "bootanim"); // Exit immediately return false; }

frameworks/base/cmds/bootanimation/bootanimation_main.cpp 经过属性变化监听会执行到如下函数

int main() { setpriority(PRIO_PROCESS, 0, ANDROID_PRIORITY_DISPLAY); bool noBootAnimation = bootAnimationDisabled(); ALOGI_IF(noBootAnimation, "boot animation disabled"); if (!noBootAnimation) {   sp<ProcessState> proc(ProcessState::self());   ProcessState::self()->startThreadPool();   // create the boot animation object (may take up to 200ms for 2MB zip)   sp<BootAnimation> boot = new BootAnimation(audioplay::createAnimationCallbacks());   waitForSurfaceFlinger();   boot->run("BootAnimation", PRIORITY_DISPLAY);   ALOGV("Boot animation set up. Joining pool.");   IPCThreadState::self()->joinThreadPool(); } return 0; }

frameworks/base/cmds/bootanimation/BootAnimation.cpp 经过与Surfaceflinger的通信,执行到线程run函数

bool BootAnimation::threadLoop() { bool result; // We have no bootanimation file, so we use the stock android logo // animation. if (mZipFileName.isEmpty()) {   result = android(); } else {   result = movie(); } eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); eglDestroyContext(mDisplay, mContext); eglDestroySurface(mDisplay, mSurface); mFlingerSurface.clear(); mFlingerSurfaceControl.clear(); eglTerminate(mDisplay); eglReleaseThread(); IPCThreadState::self()->stopProcess(); return result; }

mZipFileName 来确定是执行哪类动画。

void BootAnimation::findBootAnimationFile() {   // If the device has encryption turned on or is in process   // of being encrypted we show the encrypted boot animation.   char decrypt[PROPERTY_VALUE_MAX];   property_get("vold.decrypt", decrypt, "");   bool encryptedAnimation = atoi(decrypt) != 0 ||       !strcmp("trigger_restart_min_framework", decrypt);   if (!mShuttingDown && encryptedAnimation) {       static const std::vector<std::string> encryptedBootFiles = {           PRODUCT_ENCRYPTED_BOOTANIMATION_FILE, SYSTEM_ENCRYPTED_BOOTANIMATION_FILE,       };       if (findBootAnimationFileInternal(encryptedBootFiles)) {           return;       }   }   const bool playDarkAnim = android::base::GetIntProperty("ro.boot.theme", 0) == 1;   static const std::vector<std::string> bootFiles = {       APEX_BOOTANIMATION_FILE, playDarkAnim ? PRODUCT_BOOTANIMATION_DARK_FILE : PRODUCT_BOOTANIMATION_FILE,       OEM_BOOTANIMATION_FILE, SYSTEM_BOOTANIMATION_FILE   };   static const std::vector<std::string> shutdownFiles = {       PRODUCT_SHUTDOWNANIMATION_FILE, OEM_SHUTDOWNANIMATION_FILE, SYSTEM_SHUTDOWNANIMATION_FILE, ""   };   static const std::vector<std::string> userspaceRebootFiles = {       PRODUCT_USERSPACE_REBOOT_ANIMATION_FILE, OEM_USERSPACE_REBOOT_ANIMATION_FILE,       SYSTEM_USERSPACE_REBOOT_ANIMATION_FILE,   };   if (android::base::GetBoolProperty("sys.init.userspace_reboot.in_progress", false)) {       findBootAnimationFileInternal(userspaceRebootFiles);   } else if (mShuttingDown) {       findBootAnimationFileInternal(shutdownFiles);   } else {       findBootAnimationFileInternal(bootFiles);   } }

 

static const char OEM_BOOTANIMATION_FILE[] = "/oem/media/bootanimation.zip"; static const char PRODUCT_BOOTANIMATION_DARK_FILE[] = "/product/media/bootanimation-dark.zip"; static const char PRODUCT_BOOTANIMATION_FILE[] = "/product/media/bootanimation.zip"; static const char SYSTEM_BOOTANIMATION_FILE[] = "/system/media/bootanimation.zip"; static const char APEX_BOOTANIMATION_FILE[] = "/apex/com.android.bootanimation/etc/bootanimation.zip"; static const char PRODUCT_ENCRYPTED_BOOTANIMATION_FILE[] = "/product/media/bootanimation-encrypted.zip"; static const char SYSTEM_ENCRYPTED_BOOTANIMATION_FILE[] = "/system/media/bootanimation-encrypted.zip"; static const char OEM_SHUTDOWNANIMATION_FILE[] = "/oem/media/shutdownanimation.zip"; static const char PRODUCT_SHUTDOWNANIMATION_FILE[] = "/product/media/shutdownanimation.zip"; static const char SYSTEM_SHUTDOWNANIMATION_FILE[] = "/system/media/shutdownanimation.zip"; static constexpr const char* PRODUCT_USERSPACE_REBOOT_ANIMATION_FILE = "/product/media/userspace-reboot.zip"; static constexpr const char* OEM_USERSPACE_REBOOT_ANIMATION_FILE = "/oem/media/userspace-reboot.zip"; static constexpr const char* SYSTEM_USERSPACE_REBOOT_ANIMATION_FILE = "/system/media/userspace-reboot.zip";

在文件夹里保存动画文件,就是一堆图片,还有一个描述文件

/system/media/bootanimation.zip

1080 360 60 c 1 0 part0 c 0 0 part1 c 1 0 part2 c 1 1 part3 c 1 0 part4

第一行的三个数字分别表示开机动画在屏幕中的显示宽度、高度以及帧速(fps)。剩余的每一行都用来描述一个动画片断,后面紧跟着两个数字以及一个文件目录路径名称。 第一个数字表示一个片断的循环显示次数,如果它的值等于0,那么就表示无限循环地显示该动画片断。 第二个数字表示每一个片断在两次循环显示之间的时间间隔。这个时间间隔是以一个帧的时间为单位的。 文件目录下面保存的是一系列png文件,这些png文件会被依次显示在屏幕中。

在这个类里面通过调用opengles进行动画绘制,退出函数是,是在Surfaceflinger当中调用的。

static const char EXIT_PROP_NAME[] = "service.bootanim.exit"; void BootAnimation::checkExit() {   // Allow surface flinger to gracefully request shutdown   char value[PROPERTY_VALUE_MAX];   property_get(EXIT_PROP_NAME, value, "0");   int exitnow = atoi(value);   if (exitnow) {       requestExit();       mCallbacks->shutdown();   } }
  property_set("service.bootanim.exit", "1");

此开机动画虽名为动画,实际是一个opengles程序,通过Surfaceflinger来控制,当你需要做一些更加独立和清晰的绘制显示时,这是一个可以参考的方案。