本文共 20757 字,大约阅读时间需要 69 分钟。
分享一下我老师大神的人工智能教程!零基础,通俗易懂!
也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!
1. 简介
Android系统--视图绘制主要由以下几部分组成:
1) Canvas(画布)
提供画图所需要的所有工具,即各种draw函数;当前可用的画布对象有:具有硬件加速的GLES20Canvas和GLES20RecordingCanvas,不使用硬件加速的CompatibleCanvas)。
2) View(视图)
在具体画布对象上完成各种绘制图形的操作,可根据需要选择以上三种画布中的一种。
3) Gl20Renderer(把图直接绘制到屏幕上)
它是硬件加速视图绘制的引擎,负责整个与硬件加速相关的视图绘制过程,具体功能如下:
(1) 创建以下实例对象:
• GLES20Canvas (Canvas)
• GLES20DisplayList (DisplayList) • GLES20TextureLayer (HardwareLayer) • GLES20RenderLayer (HardwareLayer) • SurfaceTexture • Gl20Renderer (HardwareRenderer)(2) 在GlRenderer.draw中调用View.getDisplayList()完成DisplayList命令的录制,并返回DisplayList
注:只有View被attach到硬件加速,才能创建DisplayList;
真正的命令录制工作在View.getDisplayList(DisplayList displayList, boolean isLayer)中完成。
(3) 在GlRenderer.draw中调用GLES20Canvas.drawDisplayList,把DisplayList中录制的命令回放在画布上。
(4) Gl20Renderer对象对应的画布为GLES20Canvas,Gl20Renderer对象在画布上的绘制实际上是绘制在OPENGL绘制上下文对应的主缓冲区。
4) GLES20DisplayList(把录制命令录制到Native的DiplayList中)
GLES20DisplayList对象创建具体的DisplayList对象及绘制用的画布(GLES20RecordingCanvas画布),完成视图绘制操作的DisplayList命令录制等工作。
5) GLES20RenderLayer(绘制到FOB Layer中,当作纹理)
负责创建硬件Layer层(GLES20Canvas.nCreateLayer)和绘制用到的画布(GLES20Canvas,使用Layer实例化GLES20Canvas(layer, trans))等工作。
为了有效支持视图的多层绘制,视图对象可以创建一个HardwareLayer层完成视图的图形在硬件纹理上的绘制操作或者其它特效操作,这就是GLES20RenderLayer对象的作用,创建独立的层并返回相应的画布供视图绘制使用。
1.1 View.draw(Canvas canvas)六步曲
此函数将把View自身及其所有的子子孙孙绘制到给定的画布上。其画图过程主要分为以下六步:
1) 画背景
2) 如果需要,保存画布的层为未来的淡入淡出做好准备 3) 画View本身的内容 4) 画它的孩子 5) 如果需要,画淡入淡出的边缘并恢复层 6) 画装饰部分(如:滚动条)/** * Manually render this view (and all of its children) to the given Canvas. * The view must have already done a full layout before this function is * called. When implementing a view, implement * { @link #onDraw(android.graphics.Canvas)} instead of overriding this method. * If you do need to override this method, call the superclass version. * * @param canvas The Canvas to which the View is rendered. */ public void draw(Canvas canvas) { final int privateFlags = mPrivateFlags; final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; /* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. Draw the background * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content * 4. Draw children * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance) */ // Step 1, draw the background, if needed int saveCount; if (!dirtyOpaque) { final Drawable background = mBackground; if (background != null) { final int scrollX = mScrollX; final int scrollY = mScrollY; if (mBackgroundSizeChanged) { background.setBounds(0, 0, mRight - mLeft, mBottom - mTop); mBackgroundSizeChanged = false; } if ((scrollX | scrollY) == 0) { background.draw(canvas); } else { canvas.translate(scrollX, scrollY); background.draw(canvas); canvas.translate(-scrollX, -scrollY); } } } // skip step 2 & 5 if possible (common case) final int viewFlags = mViewFlags; boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; if (!verticalEdges && !horizontalEdges) { // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Step 6, draw decorations (scrollbars) onDrawScrollBars(canvas); // we're done... return; } }
2. 家族图谱
2.1 View家族图谱
2.2 Canvas和HardwareRenderer家族图谱
2.3 DisplayList和HardwareLayer家族图谱
2.4 Native OpenGLRenderer家族图谱
DisplayList.replay调用流程如下:
//JavaGlRenderer.draw((View view, View.AttachInfo attachInfo,...)GLES20Canvas.drawDisplayList(DisplayList displayList, Rect dirty, int flags)->//JNInDrawDisplayList->android_view_GLES20Canvas_drawDisplayList(...,OpenGLRenderer* renderer, DisplayList* displayList,...)->//NativeOpenGLRenderer.drawDisplayList(DisplayList* displayList,...)->DisplayList::replay(OpenGLRenderer& renderer,...)
2.5 Activity与Window家族图谱
• WindowManagerImpl:
允许把View子类增加到高级Window,应用不应该使用它,而是使用更高级的android.app.Activity或android.app.Dialog。
• PhoneWindow.mDecor(DecorView):窗口最高级的装饰View,即窗口的View树的树根,它包含:标准的Window框架、装饰、和放于窗口中的内容,可把它当作一个Window增加到Window Manager。
2.6 Activity与PhoneWindow的关系
每一个Activity都有一个关联的PhoneWindow对象,用来描述一个应用程序的窗口。每一个应用程序窗口(PhoneWindow)内部又包含有一个装饰View对象(mDecor:DecorView,View树的树根)和一个内容View对象(mContentParent: ViewGroup),用来描述应用程序窗口的视图。
1) mContentParent(ViewGroup):是Layout 资源描述的视图树的树根,它是mDecor的孩子
2) mDecor(DecorView):是PhoneWindow窗口中的视图树的树根
2.7 Activity实例化PhoneWindow流程
Activity在attach函数中实例化了PhoneWindow和WindowManagerImpl,以供后面绘制视图使用。
Activity.attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config)-> 1) PolicyManager.makeNewWindow(Context context) // 返回PhoneWindow实例并保存在mWindow中 { Policy.makeNewWindow(Context context)-> //new PhoneWindow(context) PhoneWindow(Context context) //获取LayoutInflater,它可以把Layout XML实例化为对应的View Objects } 2) Window.setWindowManager( (WindowManager)context.getSystemService(Context.WINDOW_SERVICE), mToken, mComponent.flattenToString(), (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0)-> { WindowManagerImpl.createLocalWindowManager(Window parentWindow) // 返回WindowManagerImpl实例,并保存在Window.mWindowManager } 3)mWindow.setContainer(mParent.getWindow()) 4)mWindowManager = mWindow.getWindowManager() //返回Window.mWindowManager //即Activity与Window中的mWindowManager为同一个 //WindowManagerImpl实例
2.8 放置窗口内容setContentView
setContentView的功能是:把Layout XML资源文件实例化为一棵视图树,然后把它加入PhoneWindow.mContentParent中,即成为PhoneWindow.mContentParent的子女。
Activity.setContentView(int layoutResID)-> //设置Activity内容PhoneWindow.setContentView(int layoutResID)->LayoutInflater.inflate(layoutResID, mContentParent)-> //把XML资源文件实例化为一棵视图树,并返回视图树的树根(root) //树根为ViewGroup实例LayoutInflater.inflate(layoutResID, root, root != null)-> //根据资源生成XmlResourceParserLayoutInflater.inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)->ViewGroup.addView(View child, LayoutParams params)-> { //把资源文件中的视图树加入PhoneWindow中的mContentParent中 View.requestLayout() View.invalidate(true)-> addViewInner(child, index, params, false) //它将调用child.requestLayout(),依次递归下去 } ViewGroup.invalidateChild(View child, final Rect dirty)-> ViewGroup.invalidateChildInParent(final int[] location, final Rect dirty) //更新dirty区域
3. FBO (Frame Buffer Object)和RBO (Render Buffer Object)
在了解下面的内容之前,需要了解一下FBO和RBO,除了使用窗口系统固有的Frame Buffer外,可以创建Frame Buffer Object,用于off-screen rendering(离线渲染:就是通过OpenGL将绘制结果渲染到显存中的一张图片上,通过gl接口函数可以从显存读取到内存中)。FBO是一种特殊的Buffer Object。
3.1 FBO
1) glGenFramebuffers: 生成FBO对象的名字
2) glBindFramebuffer: 绑定FBO并进行实始化
3) glDeleteFramebuffers: 删除FBO对象
FBO创建后,还不能绘图,必须为FBO提供绘图的目标buffer,也即Render Buffer Object。
3.2 RBO
Render Buffer Object包含具体的图像数据:
1) glGenRenderbuffers:生成对象的名字
2) glBindRenderbuffer: 绑定名字到RenderBuffer
3) glRenderbufferStorage: 为render buffer分配存储空间
3.3 把RBO连接到FBO上
glFramebufferRenderbuffer(GLenum target, GLenum attachment,GLenum renderbuffertarget, GLuint renderbuffer);
1) target:GL_DRAW_FRAMEBUFFER: 指定写入用途FBO
GL_READ_FRAMEBUFFER:指定读取用途FBO
GL_FRAMEBUFFER:指定两者
2) attachment:GL_COLOR_ATTACHMENT
GL_DEPTH_ATTACHMENT
GL_STENCIL_ATTACHMENT
GL_DEPTH_STENCIL_ATTACHMENT
3)renderbuffertarget: GL_RENDERBUFFER
4) renderbuffer: RBO名字
使用FBO可以创建出动态的texture,glFramebufferTexture可以绑定texture到FBO。4. 各种各样的画布
从上面的家族图谱中可以看到,可用的画布有以下几种:
1) GLES20RecordingCanvas: 此GL画布用于录制画图操作
2) GLES20Canvas: 在OpenGL ES 2.0之上实现的画布
3) HardwareCanvas: 硬件加速的画布
3) Canvas: 实现画图操作的画布
4.1 Gl20Renderer使用的画布(直接绘制在屏幕上)
其相关代码如下:
Gl20Renderer.createCanvas()->
GLES20Canvas.GLES20Canvas(false, boolean translucent)->
nCreateRenderer()
/** * Hardware renderer using OpenGL ES 2.0. */static class Gl20Renderer extends GlRenderer { private GLES20Canvas mGlCanvas; final boolean mTranslucent; //... @Override HardwareCanvas createCanvas() { //创建直接绘制到屏幕上的画布,而不会录制到DisplayList中 return mGlCanvas = new GLES20Canvas(mTranslucent); } //...}/** * An implementation of Canvas on top of OpenGL ES 2.0. */class GLES20Canvas extends HardwareCanvas { private final boolean mOpaque; private int mRenderer; //... /** * Creates a canvas to render directly on screen. * 创建直接绘制到屏幕上的画布,而不会录制到DisplayList中 * 由Gl20Renderer.createCanvas调用 */ GLES20Canvas(boolean translucent) { // 直接调用OpenGL函数把图画到屏幕上 this(false, translucent); } /** * Creates a canvas to render into an FBO. * 创建离线绘制到FBO的画布 */ GLES20Canvas(int layer, boolean translucent) { mOpaque = !translucent; // mRenderer为Native中LayerRenderer类的实例 mRenderer = nCreateLayerRenderer(layer); setupFinalizer(); } protected GLES20Canvas(boolean record, boolean translucent) { mOpaque = !translucent; if (record) { // 把画图命令录制到DisplayListRenderer.mWriter中 // mRender为Native中DisplayListRenderer类的实例 mRenderer = nCreateDisplayListRenderer(); } else { // 直接调用OpenGL函数把图画到屏幕上 // mRenderer为Native中OpenGLRenderer类的实例 mRenderer = nCreateRenderer(); } setupFinalizer(); } //...}
4.2 GLES20RenderLayer使用的画布(离线绘制到FBO Layer)
GLES20RenderLayer.GLES20RenderLayer(w,h,trans)->
GLES20Canvas.nCreateLayer(w,h,trans,layerInfo) GLES20Canvas.GLES20Canvas(int layer, boolean translucent)-> nCreateLayerRenderer(layer)/** * An OpenGL ES 2.0 implementation of { @link HardwareLayer}. This * implementation can be used a rendering target. It generates a * { @link Canvas} that can be used to render into an FBO using OpenGL. */class GLES20RenderLayer extends GLES20Layer { private int mLayerWidth; private int mLayerHeight; private final GLES20Canvas mCanvas; GLES20RenderLayer(int width, int height, boolean isOpaque) { super(width, height, isOpaque); int[] layerInfo = new int[2]; // 请求Native LayerRenderer创建一个FBO Layer,以用于离线Render mLayer = GLES20Canvas.nCreateLayer(width, height, isOpaque, layerInfo); if (mLayer != 0) { mLayerWidth = layerInfo[0]; mLayerHeight = layerInfo[1]; // 使用FBO Layer创建一个Native LayerRenderer,然后实例化画布 mCanvas = new GLES20Canvas(mLayer, !isOpaque); mFinalizer = new Finalizer(mLayer); } else { mCanvas = null; mFinalizer = null; } } //...}/** * An implementation of Canvas on top of OpenGL ES 2.0. */class GLES20Canvas extends HardwareCanvas { private final boolean mOpaque; private int mRenderer; //... /** * Creates a canvas to render directly on screen. * 创建直接绘制到屏幕上的画布,而不会录制到DisplayList中 * 由Gl20Renderer.createCanvas调用 */ GLES20Canvas(boolean translucent) { // 直接调用OpenGL函数把图画到屏幕上 this(false, translucent); } /** * Creates a canvas to render into an FBO. * 创建离线绘制到FBO的画布 */ GLES20Canvas(int layer, boolean translucent) { mOpaque = !translucent; // mRenderer为Native中LayerRenderer类的实例 mRenderer = nCreateLayerRenderer(layer); setupFinalizer(); } protected GLES20Canvas(boolean record, boolean translucent) { mOpaque = !translucent; if (record) { // 把画图命令录制到DisplayListRenderer.mWriter中 // mRender为Native中DisplayListRenderer类的实例 mRenderer = nCreateDisplayListRenderer(); } else { // 直接调用OpenGL函数把图画到屏幕上 // mRenderer为Native中OpenGLRenderer类的实例 mRenderer = nCreateRenderer(); } setupFinalizer(); } //...}
4.3 GLES20RecordingCanvas使用的画布 (绘制到DisplayList)
GLES20DisplayList.start->
GLES20RecordingCanvas.obtain(GLES20DisplayList displayList)-> (static) GLES20RecordingCanvas.GLES20RecordingCanvas()-> GLES20Canvas(true /*record*/, true /*translucent*/)-> nCreateDisplayListRenderer()/** * An implementation of display list for OpenGL ES 2.0. */class GLES20DisplayList extends DisplayList { // These lists ensure that any Bitmaps and DisplayLists recorded by a DisplayList are kept // alive as long as the DisplayList is alive. The Bitmap and DisplayList lists // are populated by the GLES20RecordingCanvas during appropriate drawing calls and are // cleared at the start of a new drawing frame or when the view is detached from the window. final ArrayListmBitmaps = new ArrayList (5); final ArrayList mChildDisplayLists = new ArrayList (); private GLES20RecordingCanvas mCanvas; // Used for debugging private final String mName; // The native display list will be destroyed when this object dies. // DO NOT overwrite this reference once it is set. private DisplayListFinalizer mFinalizer; GLES20DisplayList(String name) { mName = name; } @Override public HardwareCanvas start() { if (mCanvas != null) { throw new IllegalStateException("Recording has already started"); } mValid = false; // 使用GLES20DisplayList创建GLES20RecordingCanvas mCanvas = GLES20RecordingCanvas.obtain(this); mCanvas.start(); return mCanvas; } //...}/** * An implementation of a GL canvas that records drawing operations. * This is intended for use with a DisplayList. This class keeps a list of all the Paint and * Bitmap objects that it draws, preventing the backing memory of Bitmaps from being freed while * the DisplayList is still holding a native reference to the memory. */class GLES20RecordingCanvas extends GLES20Canvas implements Poolable { // The recording canvas pool should be large enough to handle a deeply nested // view hierarchy because display lists are generated recursively. private static final int POOL_LIMIT = 25; private static final Pool sPool = Pools.synchronizedPool( Pools.finitePool(new PoolableManager () { public GLES20RecordingCanvas newInstance() { return new GLES20RecordingCanvas(); } @Override public void onAcquired(GLES20RecordingCanvas element) { } @Override public void onReleased(GLES20RecordingCanvas element) { } }, POOL_LIMIT)); private GLES20DisplayList mDisplayList; private GLES20RecordingCanvas() { //实例化DisplayListRender super(true /*record*/, true /*translucent*/); } static GLES20RecordingCanvas obtain(GLES20DisplayList displayList) { GLES20RecordingCanvas canvas = sPool.acquire(); canvas.mDisplayList = displayList; return canvas; } // ...}/** * An implementation of Canvas on top of OpenGL ES 2.0. */class GLES20Canvas extends HardwareCanvas { private final boolean mOpaque; private int mRenderer; //... /** * Creates a canvas to render directly on screen. * 创建直接绘制到屏幕上的画布,而不会录制到DisplayList中 * 由Gl20Renderer.createCanvas调用 */ GLES20Canvas(boolean translucent) { // 直接调用OpenGL函数把图画到屏幕上 this(false, translucent); } /** * Creates a canvas to render into an FBO. * 创建离线绘制到FBO的画布 */ GLES20Canvas(int layer, boolean translucent) { mOpaque = !translucent; // mRenderer为Native中LayerRenderer类的实例 mRenderer = nCreateLayerRenderer(layer); setupFinalizer(); } protected GLES20Canvas(boolean record, boolean translucent) { mOpaque = !translucent; if (record) { // 把画图命令录制到DisplayListRenderer.mWriter中 // mRender为Native中DisplayListRenderer类的实例 mRenderer = nCreateDisplayListRenderer(); } else { // 直接调用OpenGL函数把图画到屏幕上 // mRenderer为Native中OpenGLRenderer类的实例 mRenderer = nCreateRenderer(); } setupFinalizer(); } //...}
5. 绘制(Render)元素小结
为了把图形绘制到屏幕上,必须使用Native Render,其最本质的东东是OpenGL,为了方便管理,其Native Render分为三类,其相关信息如下表所示:
Native Render类名 | OpenGLRenderer | DisplayListRenderer | LayerRenderer |
绘制到哪儿? | 直接绘制到屏幕上 | 录制到SkWriter32 | 离线绘制到FBO Layer |
JNI创建函数 | nCreateRenderer | nCreateDisplayListRenderer | nCreateLayerRenderer |
由谁管理? | GLES20Canvas | GLES20Canvas | GLES20Canvas |
由谁使用? | Gl20Renderer | GLES20DisplayList | GLES20RenderLayer |
使用者类别 | HardwareRenderer | DisplayList | HardwareLayer |
画图五元素间的暧昧关系如下图所示:
6. 创建窗口(Window)流程
其基本流程如下所示:
ActivityThread.handleResumeActivity->{ ActivityClientRecord.window = ActivityClientRecord.activity.getWindow(); View decor = ActivityClientRecord.window.getDecorView(); //实际调用PhoneWindow.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = Activity.getWindowManager(); //wm是WindowManagerImpl实例 Activity.mDecor = decor; WindowManager.LayoutParams l = r.window.getAttributes(); l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (a.mVisibleFromClient) { a.mWindowAdded = true; wm.addView(decor, l);---> //即调用WindowManagerImpl.addView, //把PhoneWindow.mDecor加入WindowManager中 }}WindowManagerImpl.addView(View view, ViewGroup.LayoutParams params)-> WindowManagerGlobal.addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow)-> (root = new ViewRootImpl(view.getContext(), display)) ViewRootImpl.setView(View view, WindowManager.LayoutParams attrs, View panelParentView)-> requestLayout() mWindowSession.addToDisplay(mWindow,...) --> //to Window Manager Server Session.addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outContentInsets, InputChannel outInputChannel)-> WindowManagerService.addWindow(Session session, IWindow client, int seq, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outContentInsets, InputChannel outInputChannel)-> new WindowState(WindowManagerService service, Session s, IWindow c, WindowToken token, WindowState attachedWindow, int seq, WindowManager.LayoutParams a, int viewVisibility, final DisplayContent displayContent) // WindowState实例即为Window Manager中刚创建的窗口在以上过程中,ViewRootImpl为客户端,通过IWindowSession接口把创建窗口的请求发送给Server端(Session),然后调用WindowManagerService中对应的功能函数。同时把IWindow.Stub (W extends IWindow.Stub)的实现实例发送给服务器端,以便WindowMangerService通过IWindow接口向ViewRootImpl发送命令。其相互关系如下图所示:
Activity<-->PhoneWindow<-->ViewRootImpl<-->WindowManagerService
在以上关系中,WindowManagerService系统中仅一个实例,其它三个实例一一对应,即一个Activity实例有一个唯一的PhoneWindow和ViewRootImpl实例与之一一对应。
7. 硬件绘制(Hardware Render)窗口流程
WMS.performLayoutAndPlaceSurfacesLocked->WMS.performLayoutAndPlaceSurfacesLockedLoop->WMS.performLayoutAndPlaceSurfacesLockedInner->mRelayoutWhileAnimating.get(j).mClient.doneAnimating()-> //即IWindow.doneAnimating()ViewRootImpl.W.doneAnimating()->ViewRootImpl.dispatchDoneAnimating()->ViewRootImpl.handleDispatchDoneAnimating()->ViewRootImpl.scheduleTraversals()->ViewRootImpl.mTraversalRunnable.run()->ViewRootImpl.doTraversal()->ViewRootImpl.performTraversals() ->ViewRootImpl.performDraw()->ViewRootImpl.draw()-> // ViewRootImpl.drawSoftware(..),软件render,暂且不讲,以下为硬件RenderattachInfo.mHardwareRenderer.draw(mView,...)-> // attachInfo.mHardwareRenderer实际为Gl20Renderer实例, // 在enableHardwareAcceleration中实例化 // Gl20Renderer.createDisplayList: 实例化GLES20DisplayList // Gl20Renderer.createHardwareLayer(w,h):实例化GLES20RenderLayer // Gl20Renderer.create:实例化Gl20RendererGlRenderer.draw(View view, View.AttachInfo attachInfo,...)-> // 其mCanvas为GLES20Canvas,其对应的Native Render为OpenGLRenderer GLES20Canvas.onPreDraw(dirty) View.getDisplayList(mDisplayList, false) // 如果mDisplayList为null, 则调用 mAttachInfo.mHardwareRenderer.createDisplayList // 返回的DisplayList为GLES20DisplayList实例 GLES20DisplayList.start() // 返回GLES20RecordingCanvas实例 //如果mLayerType为LAYER_TYPE_HARDWARE,则调用 mAttachInfo.mHardwareRenderer.createHardwareLayer(w,h,..) //返回GLES20RenderLayer实例到mHardwareLayer中 GLES20RecordingCanvas.drawHardwareLayer(mHardwareLayer,...) GLES20Canvas.drawDisplayList(displayList,...) GLES20Canvas.onPostDraw() EGL10.eglSwapBuffers(sEglDisplay, mEglSurface)
给我老师的人工智能教程打call!
转载地址:https://blog.csdn.net/hddghhfd/article/details/84024286 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!