内网的一篇好文 征得大佬同意拿来分享一下。硬核介绍Android闪现体系,深化每一个细节,从硬件到软件,带你体会不一样的景色。
- Android烘托系列(1)之原理概述篇
- Android烘托系列(2)之怎样烘托UI
- Android烘托系列(3)之Choreographer
- Android烘托系列(4)之Surface与SurfaceFlinger
- Android烘托系列(5)之Window、Activity、View之间联系
- Android烘托系列(6)之Vsync
面试官的小抄 面试进阶一扫而光,或许是东半球最好的面试资料
硬件篇
一 硬件闪现模块
无论软件架构多么的高端大气上档次,都离不开硬件的支撑,软件架构是构建的硬件的运转原理之上的,闪现模块更是如此。
1 常见的闪现设备:
- LCD(liquid crystal display)液晶屏
液晶是一种资料,液晶这种资料具有一种特色:能够在电信号的驱动下液晶分子进行旋转,旋转时会影响透光性,因而咱们能够在整个液晶面板后面用白光照(称为背光),能够经过不同电信号让液晶分子进行选择性的透光,此刻在液晶面板前面看到的便是各式各样不同的颜色,这便是 LCD 闪现画面的原理。有些闪现器(譬如 LED 闪现器、CRT 闪现器)自己本身会发光称为自动发光,有些(LCD)本身不会发光只会透光,需求背光的协助才干看起来是发光的,称为被迫发光。
- CRT(cathode ray tube)阴极射线管闪现器
首要有五部分组成:电子枪(Electron Gun)、偏转线圈(Deflection coils)、荫罩(Shadow mask)、高压石墨电极和荧光粉涂层(Phosphor)及玻璃外壳。CRT闪现器靠电子束激发屏幕内表面的荧光粉来闪现图画的,因为荧光粉被点亮后很快会平息,所以电子枪有必要循环地不断激发这些点。
- OLED(organic light-emitting diode)有机发光二极管
有机半导体资料和发光资料在电场驱动下,经过载流子注入和复合导致发光的现象,很容易制造,并且只需求低的驱动电压,这些首要的特征使得OLED在满意平面闪现器的运用上显得非常杰出。OLED闪现屏比LCD更轻薄、亮度高、功耗低、呼应快、清晰度高、柔性好、发光效率高,能满意顾客对闪现技术的新需求。
- LED( light-emitting diode ) 发光二极管
由一个个小的LED模块面板组成,用来闪现文字、图画、视频等各种信息的设备。 LED电子闪现屏集微电子技术、核算机技术、信息处理于一体,具有颜色鲜艳、动态范围广、亮度高、寿命长、作业稳定牢靠等优点。
2 视频闪现接口
- 对外闪现器的视频接口
设备与设备之间的视频传输接口。依据出现的时刻以及功用,能够分为五代:
- 第一代:亮色混合,视频接口代表:CVBS、AV
- 第二代:亮色分离,视频接口代表:S-端子、色差信号
- 第三代:模数共存,视频接口代表:VGA
- 第四代:纯数字,视频接口代表:DVI(DVI-D、DVI-A、DVI-I)
- 第五代:更强功用,视频接口代表:HDMI、MHL、DP、thunderbolt

- CVBS(RF)

- C端子(RCA/AV)

- S端子

- YPbPr

- VGA、DVI、HDMI、DP

- SDI

- HDBASE-T

- 对内闪现器视频接口
对内的闪现器视频传输接口指的是设备内部,板与板之间或板上的器材与器材之间的视频传输接口,现在常见的对内闪现器视频接口首要有以下几种:
- DVP(ITU BT601/656/1120)
- LVDS(差异于Serdes)
- MIPI(DSI/CSI)
- eDP
- DVP digital video parallel

- LVDS low voltage differential signaling
4/5/10 lane差分信号
- MIPI mobile indurstry processor interface
2/3/4/5 lane差分信号
- eDP embeded DP
1/2/3/4 lane 差分信号,无clk,依据displayport架构和协议的一种全数字化接口

- LCD时序
一个典型的 Android 闪现体系中,一般包括 SOC、DDIC、Panel 三个部分,
- SOC 担任绘画与多图层的组成,把组成好的数据经过硬件接口按某种协议传输给 DDIC,
- 闪现驱动芯片DDIC(Display Driver IC)担任把 buffer 里的数据呈现到 Panel 上。
DDIC 是面板的首要操控元件之一。 DDIC 经过电信号的方式向闪现面板发送驱动信号和数据,继而完结对屏幕亮度和颜色的操控,使得比如字母、图片等图画信息得以在屏幕上闪现。

- 并口时序信号


- 多帧画面输出时序

- LCD上的画面更新流程


二 闪现模块驱动及体系接口
DRM,英文全称 Direct Rendering Manager, 即直接烘托办理器。DRM是linux内核的一个子体系,它供给一组 API,用户空间程序能够经过它发送画面数据到 GPU 或许专用图形处理硬件(如高通的MDP/海思的VDP/瑞芯微的VOP),也能够运用它履行比如装备分辨率,刷新率之类的设置操作。原本是规划供给给 PC 运用来支撑杂乱的图形设备,后来也用于嵌入式体系上。现在在各大渠道Android体系上的闪现体系也是运用的这组API来完结画面的烘托更新。 在 DRM 之前 Linux 内核现已有一个叫 FBDEV 的 API,用于办理图形适配器的帧缓存区,但不能用于满意依据 3D 加快的现代依据 GPU 的视频硬件的需求,FBDEV 社区维护者也较少; 且无法供给 overlay hw cursor 这样的 features; 开发者们本身就鼓舞以后迁移到DRM 上。

1 KMS kernel mode setting
CRTC 对闪现buffer进行扫描,并发生时序信号的硬件模块,一般指Display Controller ENCODER 将CRTC输出的timing时序转换成外部设备所需求的信号,如HDMI转换器或DSI Controller CONNECTOR 连接物理闪现设备的连接器,如HDMI、DisplayPort、DSI总线,一般和Encoder驱动绑定 PLANE 硬件图层,有的Display硬件支撑多层组成闪现,但一切的Display Controller至少要有1个plane Bridge 桥接设备,一般用于注册 encoder 后面另外再接的转换芯片,如 DSI2HDMI 转换芯片; Panel:泛指屏,各种 LCD、HDMI 等闪现设备的抽象; FB Framebuffer,单个图层的闪现内容,仅有一个和硬件无关的根本元素 VBLANK 软件和硬件的同步机制,RGB时序中的笔直消影区,软件一般运用硬件VSYNC来完结 PROPERTY 任何想设置的参数,都可做成property,是DRM驱动中最灵敏、最便利的Mode setting机制
2 GEM graphic execution manager
DUMB 只支撑接连物理内存,依据kernel中通用CMA API完结,多用于小分辨率简略场景 PRIME 接连、非接连物理内存都支撑,依据DMA-BUF机制,能够完结buffer同享,用于大内存杂乱场景 FENCE buffer同步机制,依据内核dma_fence机制完结,用于防止闪现内容出现异步问题
3 libdrm
DRM是Linux下的图形烘托架构,用来办理闪现输出和分配buffer。运用程序能够直接操纵DRM的ioctl或许是用framebuffer供给的接口进行闪现相关操作。libdrm库封装了这些接口,让用户能够更加便利的进行闪现操控。 各模块的souce code方位如下(rockchip android12 渠道):
libdrm | external/libdrm |
---|---|
hwcomposer | hardware/rockchip/hwcomposer/drmhwc2 |
drm driver | kernel-5.18/drivers/gpu/drm |
三 用户空间的帧数据流
在 Android 体系上运用要制作一个画面,首要要向 SurfaceFlinger 请求一个画布,这个画布所运用的 buffer 是 SurfaceFlinger 经过 allocator service(android.hardware.graphics.allocator@4.0-service)来分配出来的,allocator service 是经过 ION 从 kernel 开辟出来的一块同享内存,这里请求的都是每个图层所具有独立 buffer, 这个 buffer 会同享到 HWC Service 里,由 SurfaceFlinger 来作为中枢操控这块 buffer 的一切权,其一切权会随状况不同在 App, SurfaceFlinger, HWC Service 间来回流转。

1 App到Allocator Service
Android 12版别,APP首要经过 Surface 的接口向 Allocator请求3块 buffer,并经过importBuffer 把内存映射到运用的进程空间里,这种提前分配buffer的模式能够避免在烘托时分配buffer带来的delay,该进程能够在perfetto上观察到:


2 App到SurfaceFlinger
App经过dequeueBuffer拿到画布,经过queueBuffer来提交制作好的数据,这个进程能够在perfetto上看到:


3 SurfaceFlinger到HWC service
HWC Service 担任将 SurfaceFlinger 送来的图层做组成,形成终究的画面,然后经过 DRM 的接口更新到屏幕上去。






- handleMessageTransaction->setBuffer App完结了制作之后,会调用setBuffer设置对应的transactionflag,并终究告诉到Surfaceflinger。
- handleMessageInvalidate->handlePageFlip->latchBuffer SurfaceFlinger获知App提交了制作完结的buffer,会调用handlePageFlip,并进一步骤用latchBuffer锁定acquire这个buffer,以便接下来进行画面的组成刷新。
- updateInputFlinger 该函数用于处理画面组成进程的input作业。
- preComposition 预组成函数会检测是否还有其他图元需求消费,并调用signalLayerUpdate进行下一轮的invalidate消费。
- prepare 该函数调用rebuildLayerStacks构建Layer栈,从头核算一切需求制作的Layer的脏区域。
- updateCompositionState 从头核算ouputlayer的几何状况,核算DisplayFrame、SourceCrop等参数
- writeCompositionState 将outputLayer特点设置给hwcomposer
- prepareFrame 确定layer的组成办法,gpu组成或许hwc组成,这一步需求和hwc service进行binder通信,hwc service调用了ValidateDisplay进行相关处理。
- finishFrame 对于需求gpu进行制作的layer,进行组成制作操作。
- postFramebuffer 提交frame进入闪现流程,并经过binder音讯告诉hwc service调用了PresentDisplay,该函数会调用drm相关接口操作内核drm驱动。完结frame的闪现处理
- postComposition 组成送显完毕之后的buffer fence释放,vsync的同步等善后作业。
软件篇
四 BufferQueue机制
1 BufferQueue要处理什么问题
APP 绘画的画布是由Allocator Service供给的,而画布是一块同享内存,APP 向 Allocator Service请求到画布,是将同享内存的地址映射到本身进程空间。 App担任在画布上作画,画完的作品提交给 SurfaceFlinger,这个提交操作并不是把内存仿制一份给 SurfaceFlinger,而是把同享内存的操控权交还给 SurfaceFlinger。
SurfaceFlinger 把拿来的多个运用的同享内存再送给HWC Service去组成,HWC Service 把组成的数据交给 DRM 去输出完结 app 画面闪现到屏幕上的进程。为了更有效地利用时刻这样的同享内存不止一份,或许有两份或三份,即常说的 double buffering, triple buffering。那么咱们就需求规划一个机制能够办理 buffer 的操控权,这个便是BufferQueue。BufferQueue 要处理的是生产者和顾客的同步问题,
运用程序生产画面,SurfaceFlinger 消费画面;SurfaceFlinger 生产画面而 HWC Service 消费画面。用来存储这些画面的存储区咱们称其为帧缓冲区 buffer。
2 Buffer State的切换
在 BufferQueue 的规划中,一个 buffer 的状况有以下几种:
- FREE :表明该 buffer 能够给到运用程序,由运用程序来绘画
- DEQUEUED: 表明该 buffer 的操控权现已给到运用程序侧,这个状况下运用程序能够在上面绘画了
- QUEUED: 表明该 buffer 现已由运用程序绘画完结,buffer 的操控权现已回到 SurfaceFlinger 手上了
- ACQUIRED: 表明该 buffer 现已交由 HWC Service 去组成了,这时操控权已给到 HWC Service 了


3 BufferSlot办理
**每一个运用程序的图层在 SurfaceFlinger 里称为一个 Layer, 而每个 Layer 都具有一个独立的 BufferQueue, **每个 BufferQueue 都有多个 Buffer,Android 体系上现在支撑每个Layer最多64个buffer,每个 buffer 用一个结构体 BufferSlot 来代表。
struct BufferSlot{
BufferState mBufferState;// Buffer的状况 FREE/DEQUEUED/QUEUED/ACQUIRED
sp<GraphicBuffer> mGraphicBuffer;//真实的buffer的存储空间
uint64_t mFrameNumber;//表⽰这个slot被queued的编号
sp<Fence> mFence;// gpu和cpu之间的信号同步
}
BufferSlot 能够分红两个部分,Used Slots 和 Unused Slots, 而 Used Slots 又能够分为 Active Slots 和 UnActive Slots, 处在 DEQUEUED, QUEUED, ACQUIRED 状况的被称为 Active Slots, 剩下 FREE 状况的称为 UnActive Slots, 所以一切 Active Slots 都是正在有人运用中的 slot, 运用者或许是生产者也或许是顾客。而 FREE 状况的 Slot 依据是否现已为其分配过内存来分红两个部分,一是现已分配过内存的, 在 Android 源码中称为 mFreeBuffers, 没有分配过内存的称为 mFreeSlots, 所以假如咱们在代码中看到是从 mFreeSlots 里拿出一个 BufferSlot 那阐明这个 BufferSlot 是还没有装备 GraphicBuffer 的,这个 slot 或许是第一次被运用到。其分类如下图所示:








从App侧看,前三帧都会有requestBuffer, 都会有importBuffer,在第4帧时就没有requestBuffer/importBuffer了,因为咱们当时体系总共运用了三个buffer,在Android12上表现为App侧allocate并import了3个buffer。



4 Buffer办理
上边说到每个图层Layer都有最多64个BufferSlot,每个BufferSlot都会记载本身的状况BufferState,以及自己的GraphicBuffer指针mGraphicBuffer。



- Time1: 在上图中,初始状况下,有 0, 1, 2 这三个 BufferSlot, 因为它们都没有分配过 GraphicBuffer, 所以它们都坐落 mFreeSlots 行列里,当运用来 dequeueBuffer 时,SurfaceFlinger 会先检查在 mFreeBuffers 行列中是否有 Slot, 假如有则直接分配该 Slot 给运用。明显此刻 mFreeBuffers 里是空的,这时 Surfaceflinger 会去 mFreeSlots 里去找出第一个 Slot, 这时就找到了 0 号 Slot, dequeueBuffer 完毕时运用就拿到了 0 号 Slot 的运用权,于此一起 SurfaceFlinger 也会为 0 号 Slot 分配 GraphicBuffer, 之后运用将经过 requestBuffer 和 importBuffer 来获取到该 Slot 的实际内存空间。运用 dequeueBuffer 之后 0 号 Slot 切换到 DEQUEUED 状况,并被放入 mActiveBuffers 列表。
- Time2: 运用完结制作后经过 queueBuffer 来提交制作好的画面,完结后 0 号 Slot 状况变为 QUEUED 状况,放入 mQueue 行列,此刻 1,2 号 Slot 还停留在 mFreeSlots 行列中。
- Time3: 上面这个状况会继续到下一个 Vsync-sf 信号到来,当 Vsync-sf 信号到来时,SurfaceFlinger 主线程会检查 mQueue 行列中是否有 Slot, 有就意味着有运用上帧,这时它会把该 Slot 从 mQueue 中取出放入 mActiveBuffers 行列,并将 Slot 的状况切换到 ACQUIRED, 代表这个 Slot 已被拿去做画面组成。那么这之后 0 号 Slot 被从 mQueue 行列拿出放入 mActivieBuffers 里
- Time4: 接下来运用继续调用 dequeueBuffer 请求 buffer, 此刻 0 号 Slot 在 mActiveBuffers 里,1,2 号在 mFreeSlots 里,SurfaceFlinger 仍然是先检查 mFreeBuffers 里有没有 Slot, 发现还是没有,再检查 mFreeSlots 里是否有,所以取出了 1 号 Slot 给到运用侧,一起 1 号 Slot 状况切换到 DEQUEUED 状况, 放入 mActiveBuffers
- Time5:1 号 Slot 运用绘画完毕,经过 queueBuffer 提交上来,这时 1 号 Slot 状况由 DEQUEUED 状况切换到了 QUEUED 状况,进入 mQueue 行列,之后将保持该状况直到下一个 Vsync-sf 信号到来。
- Time6: 此刻 Vsync-sf 信号到来,发现 mQueue 中有个 Slot 1, 这时 SurfaceFlinger 主线程会把它取出,把状况切换到 ACQUIRED, 并放入 mActiveBuffers 里。
- Time7: 这时 0 号 Slot HWC Service 运用完毕,经过 releaseBuffer 还了回来,0 号 Slot 的状况将从 ACQUIRED 切换回 FREE, Surfaceflinger 会把它从 mActivieBuffers 里拿出来放入 mFreeBuffers 里。留意这时放入的是 mFreeBuffers 里而不是 mFreeSlots 里,因为此刻 0 号 Slot 是有 GraphicBuffer 的。

- Time11: 当下的状况是 0,1 两个 Slot 都在 mFreeBuffers 里,2 号 Slot 在 mActiveBuffers 里,这时运用来 dequeueBuffer
- Time12: SurfaceFlinger 仍然会先检查 mFreeBuffers 列表看是否有可用的 Slot, 发现 0 号可用,所以 0 号 Slot 状况由 FREE 切换到 DEQUEUED 状况,并被放入 mActiveBuffers 里
- Time13: 运用对 0 号 Slot 的绘图完结后提交上来,这时状况从 DEQUEUED 切换到 QUEUED 状况,0 号 Slot 被放入 mQueue 行列,之后会保持该状况直到下一下 Vsync-sf 信号到来
- Time14: 这时 Vsync-sf 信号到来,SurfaceFlinger 主线程中检查 mQueue 行列中是否有 Slot, 发现 0 号 Slot, 所以经过 aquireBuffer 操作把 0 号 Slot 状况切换到 ACQUIRED

- Time 23: 当时状况 mQueue 里有两个 buffer
- Time 24:Vsync-sf 信号抵达,从 mQueue 行列里取走了 0 号 Slot,
- Time 25: 再一次 Vsync-sf 到来,这时 SurfaceFlinger 会先检查 mQueue 行列是否有 buffer,发现有 2 号 Slot, 会先取走 2 号 Slot
- Time 26: 此刻 0 号 Slot 现已被 HWC Service 运用完毕,需求把 Slot 还回来,0 号 Slot 在此刻进入 mFreeBuffers 行列。
所以假如运用上帧速度较慢,比如其上帧周期时长大于两倍屏幕刷新周期时,每次运用来 dequeueBuffer 时前一次 queueBuffer 的 BufferSlot 都现已被 release 回来了,这时总会在 mFreeBuffers 里找到可用的,那么就不需求三个 Slot 都分配出 GraphicBuffer。 其中有两个时序需求留意:
- 每次 Vsync-sf 信号到来时总是先检查 mQueue 行列看是否有 Layer 上帧,然后才会走到 releaseBuffer 把 HWC Service 运用的 Slot 回收回来
- 本次 Vsync-sf 被 aquireBuffer 取走的 Slot 总是会在下一个 Vsync-sf 时才会被 release 回来


5上帧流程
- 经过surfaceview上帧观测完好流程:

- Tripple Buffer接连上帧进程

五 Fence机制
Fenc处理了什么问题
一般凡是同享的资源都要树立一个同步机制来办理,比如在多线程编程中对临界资源的经过加锁完结互斥拜访,再比如 BufferQueue 中 Surfaceflinger 和运用对同享内存(帧缓冲)的拜访中有 bufferstate 来标识同享内存操控权的办法来做同步。没有同步机制的无序拜访极或许造成数据紊乱。

上面图中的 BufferState 的办法仅仅处理了在 CPU 办理之下,当下同享内存的操控权归属问题,但当同享资源是在两个硬件之中时,状况就不同了,比如当一个帧缓冲区同享内存给到 GPU 时,GPU 并不清楚 CPU 还有没有在运用它,同样地,当 GPU 在运用同享内存时,CPU 也不清楚 GPU 是否已运用完毕。



2 Fence与BufferQueue的协作办法




六 画面闪现流程
上述章节剖析了Userspace、Kernel、App、Surfaceflinger、Hwcomposer的联系,帧活动的要害节点、同步问题处理等各种技术细节,那么App所制作的画面是怎样一步步进入Android规划的那些流程之中的呢?
1 App启动流程图

- handleLaunchActivity: 创立Activity实例,并调用其attach办法创立了PhoneWindow目标(Window的完结类),并为该Window目标设置了WindowManager(Windows的办理办法)。
- performLaunchActivity: 调用onCreate办法,经过setContentView加载界说的布局文件。初始化DecorView,将设置的布局文件解析为View树,增加到mContentParent中,并调用onStart函数进行其他初始化操作。
- handleResumeActivity: 调用onResume办法,获取Activity的WindowManager、Window、DecorView目标,调用WindowManager.addView办法初始化ViewRootImpl,然后调用ViewRootImpl.setView为Window增加DecorView,一起setView内部调用了requestLayout,该函数内部又调用了scheduleTraversals,该办法完结View三大流程<measure、layout、draw>

- Activity 像个操控器,不担任视图部分,它仅仅操控生命周期和处理作业。
- Window 像个承载器,装着内部视图,并真实操控视图。
- ViewRootImpl 像个连接器,担任沟通,经过硬件的感知(分发作业,制作View内容)来告诉视图,进行用户之间的交互。
- DecorView 顶层视图,是一切View的最外层布局。
2 画布的请求
在Android体系中每个 Activity 都有一个独立的画布(在运用侧称为 Surface, 在 SurfaceFlinger 侧称为 Layer),无论这个 Activity 安排了多么杂乱的 view 结构,它们终究都是被画在了所属 Activity 的这块画布上,当然也有一个例外,SurfaceView 是有自已独立的画布的. 如上文剖析,每个运用都会创立有自已的 Activity, 一起 Android 会为每个运用的Activity 创立一个 ViewRootImpl,并在ViewRootImpl里向Choreographer注册一个回调,每当有Vsync信号降临就会履行mTraversalRunnable敞开绘图流程,这里边会调用到ViewRootImpl的 performTraversals 这个函数:


















3 帧数据的制作
App拿到画布之后,就能够进入制作流程了,剖析如下:












在 Android 的规划里 View 会对应一个RenderNode, RenderNode里的一个重要数据结构是DisplayList, 每个DisplayList都会包括一系列 DisplayListData。这些 DisplayList 也会同样以树形结构安排在一起。上图中的RecordingCanvas相当于一个绘图指令记载员,将这个View及其子View经过draw函数制作的指令以DisplayList的方式记载下来。

- draw最初的函数,根本的绘图指令
- push模版函数
- fBytes 存储区






- mRenderPipeline->getFrame



- mRenderPipeline->draw


- mRenderPipeline->swapBuffers





4 帧数据的提交
Android12中queueBuffer流程和之前有较大不同,提交流程如下: App的dequeueBuffer,会在native层调用BufferQueueProducer.cpp的queueBuffer函数,然后经过调用frameAvailableListener->onFrameAvailable告诉音讯监听目标ConsumerBase。














七 总结
整个流程下来,APP 的画面要闪现到屏幕上大致上要经过如下图所示体系组件的处理:

- 首要 App 向 SurfaceFlinger 请求画布 (Android12在内部请求),SurfaceFlinger 内部有一个 BufferQueue 的办理实体,它会分配一个 GraphicBuffer 给到 APP, App 拿到 buffer 后调用图形库向这块 buffer 内绘画。
- APP 绘画完结后运用向 SurfaceFlinger 提交制作完结的 buffer(经过 queueBuffer 接口), 当然这时候的制作完结仅仅说在 CPU 侧制作完结,此刻 GPU 或许还在该 buffer 上作画,所以这时向 SurfaceFlinger 提交数据的一起还会带上一个 acquireFence,运用接下来运用该 buffer 的人能知道什么时候 buffer 运用完毕了。
- SurfaceFlinger 收到运用提交的帧缓冲区 buffer 后是在下一个 vsync-sf 信号来时做处理,首要遍历一切的 Layer, 找到哪些 Layer 有上帧, 经过 latchBuffer 把 Buffer 拿出来,告诉给 HWC Service 去参与组成, 终究调用 HWC Service 的 presentDisplay 接口来奉告 HWC Service SurfaceFlinger 的作业已完结。
- HWC Service 收到组成使命后开端组成数据,在 SurfaceFlinger 调用 presetDisplay 时会去调用 DRM 接口 DRMAtomicReq::Commit 告诉 kernel 能够向 DDIC 发送数据了.
- 假如有 TE 信号来提示已进入消隐区,这时 DRM 驱动会马上开端经过 DSI 总线向 DDIC 传输数据,与此一起 Panel 的 Disp Scan 也在进行中,传输完结后这帧画面就完好地闪现到了屏幕上。
至此,一帧画面的更新进程就完结了。
本期就到这里了,更多精彩内容点击检查