前言

上一篇我们分析了,app与SurfaceFlinger建立连接的过程,现在我们就可以继续往下分析,看下创建Surface的过程。

我们可以将Surface理解为一个绘图表面,Android应用程序负责往这个绘图表面上填内容,而SurfaceFlinger服务负责将这个绘图表面的内容取出来,并且渲染在显示屏上。

Android 图形架构 之一 ——概述
Android 图形架构 之二—— SurfaceFlinger 启动和连接
Android 图形架构 之三—— 创建Layer、Surface、SurfaceControl
Android 图形架构 之四——图形缓冲区的申请和消费流程及核心类
Android 图形架构 之五——深入分析addView所发生的的一切
Android 图形架构 之六——深入分析draw()是如何工作的
Android 图形架构 之七——Choreographer 源码分析
Android图形架构 之八——硬件VSync、VSync-app、Vsync-sf

概述

在Android中,Window与Surface一一对应。 如果说Window关心的是层次和布局,是从设计者角度定义的类,Surface则从实现角度出发,是工程师关系和考虑的类。Window的内容是变化 的,Surface需要有空间来记录每个时刻Window的内容。

在Android的SurfaceFlinger实现里,使用了三缓冲,保证了界面更新的稳定性和低延时。可参考这篇文章 了解更多

SurfaceFlinger 是运行在独立进程的Service, 它接收所有Window的Surface作为输入,根据Z-Order, 透明度,大小,位置等参数,计算出每个Surface在最终合成图像中的位置,然后交由HWComposer或OpenGL生成最终的显示Buffer, 然后显示到特定的显示设备上。SurfaceFlinger的会定期检查所有Layer的参数更新(LayerStack等),计算新的DirtyRegion,然后将结果推送给底层显示驱动进行显示。

在SurfaceFlinger服务中,绘图使用Layer类来描述。Layer是SurfaceFlinger 进行合成的基本操作单元。当应用创建Surface的时候在SurfaceFlinger内部创建Layer,因此一个Surface对应一个 Layer, 但注意,Surface不一定对应于Window,Android中有些Surface并不跟某个Window相关,而是有程序直接创建,比如说 StrictMode, 一块红色的背景,用于提示示Java代码中的一些异常, 还有SurfaceView, 用于显示有硬件输出的视频内容等。

Each Layer has:

  • Z order
  • Alpha value from 0 to 255
  • visibleRegion
  • crop region
  • transformation: rotate 0, 90, 180, 270: flip H, V: scale

当多个Layer进行合成的时候,并不是整个Layer的空间都会被完全显示,根据这个Layer最终的显示效果,一个Layer可以被划分成很多的Region, Android SurfaceFlinger 定义了以下一些Region类型:

  • TransparantRegion: 完全透明的区域,在它之下的区域将被显示出来。
  • OpaqueRegion: 完全不透明的区域,是否显示取决于它上面是否有遮挡或是否透明。
  • VisibleRegion: 可见区域,包括完全不透明无遮挡区域或半透明区域。 visibleRegion = Region - above OpaqueRegion.
  • CoveredRegion: 被遮挡区域,在它之上,有不透明或半透明区域。
  • DirtyRegion: 可见部分改变区域,包括新的被遮挡区域,和新的露出区域。

Android 系统支持多种显示设备,比如说,输出到手机屏幕,或者通过WiFi 投射到电视屏幕。Android用DisplayDevice类来表示这样的设备。不是所有的Layer都会输出到所有的Display, 比如说,我们可以只将Video Layer投射到电视, 而非整个屏幕。LayerStack 就是为此设计,LayerStack 是一个Display 对象的一个数值, 而类Layer里成员State结构体也有成员变量mLayerStack, 只有两者的mLayerStack 值相同,Layer才会被输出到给该Display设备。所以LayerStack 决定了每个Display设备上可以显示的Layer数目。

上面描述的几个概念,均是针对于显示这个层面,更多是涉及到中下层模块,应用层并不参与也无需关心。对于应用而言,它关心的是如何将内容画出来。Canvas 是Java层定义的一个类,它对应与Surface上的某个区域并提供了很多的2D绘制函数(借助于底层的Skia或OpenGL)。应用只需通过 LockCanvas() 来获取一个Canvas对象,并调用它的绘画方法,然后 unLockCanvasAndPost()来通知底层将更新内容进行显示。当然,并不是所有应用程序都需要直接操作Canva, 事实上只有少量应用需要直接操作Canvas, Android提供了很多封装好的控件 Widget,应用只需提供素材,如文字,图片,属性等等,这些控件会调用Canvas提供的接口帮用户完成绘制工作。

一、App进程中 相关 过程分析

下图是主要函数的时序图,接下来我们分析,如何调用和获取 SurfaceFlinger、SurfaceControl、Surface。也就是从下图中的第8步,开始分析, 在后续的文章会分析第8步之前的流程

和Native 层创建Surface 一样,通过 SurfaceFlinger,先创建一个SurfaceControl,然后利用SurfaceControl创建Surface。下面就来具体看看在Java层是如何实现的,其实都是通过jni 调用对应的Native函数

1.1、创建SurfaceSession

代码位于frameworks/base/core/java/android/view/SurfaceSession.java:

代码一:
public final class SurfaceSession {// Note: This field is accessed by native code.private long mNativeClient; // SurfaceComposerClient*private static native long nativeCreate();/** Create a new connection with the surface flinger. */public SurfaceSession() {mNativeClient = nativeCreate();}
}

nativeCreate 是一个JNI调用,来看看对应代码frameworks/base/core/jni/android_view_SurfaceSession.cpp:

代码二:
static jlong nativeCreate(JNIEnv* env, jclass clazz) {//SurfaceComposerClient专门用来和surfaceflinger建立connection(ISurfaceComposerClient)SurfaceComposerClient* client = new SurfaceComposerClient();client->incStrong((void*)nativeCreate);return reinterpret_cast<jlong>(client);
}

在文章Android 图形架构 之二—— SurfaceFlinger 启动和连接介绍过,SurfaceFlinger 进行通信的就是通过SurfaceComposerClient,他们是进程间通信,在SurfaceComposerClient中有对应的Binder代理对象mClient,对应 SurfaceFlinger 的Binder本地 Client 对象。

在SurfaceSession中获取到了SurfaceComposerClient对象,接下来就可以与SurfaceFlinger 进行通信了。

1.2、创建SurfaceControl

在App进程中,每一个界面都用一个Surface对象来描述,每一个Surface对象都是由一个SurfaceControl对象来创建的。Surface类和SurfaceControl类的关系以及实现如图所示:

位于frameworks/native/libs/gui/SurfaceControl.java:

代码三:
public SurfaceControl(SurfaceSession session,String name, int w, int h, int format, int flags)throws OutOfResourcesException {......mName = name;//调用nativeCreate本地方法mNativeObject = nativeCreate(session, name, w, h, format, flags);......
}
private final String mName;
long mNativeObject; // package visibility only for Surface.java access
private static native long nativeCreate(SurfaceSession session, String name,int w, int h, int format, int flags)throws OutOfResourcesException;

nativeCreate 是一个JNI调用,来看看对应代码frameworks/base/core/jni/android_view_SurfaceControl.cpp:

代码四:
static jlong nativeCreate(JNIEnv* env, jclass clazz, jobject sessionObj,jstring nameStr, jint w, jint h, jint format, jint flags) {ScopedUtfChars name(env, nameStr);sp<SurfaceComposerClient> client(android_view_SurfaceSession_getClient(env, sessionObj));//调用SurfaceComposerClient的createSurface,就会调用到代码五的createSurfaceChecked,在Native层,//createSurface 返回是的 SurfaceControl 对象sp<SurfaceControl> surface = client->createSurface(String8(name.c_str()), w, h, format, flags);。。。surface->incStrong((void *)nativeCreate);return reinterpret_cast<jlong>(surface.get());
}

调用到SurfaceComposerClient类的createSurfaceChecked,创建Layer(到SurfaceFling进程中)和Surface(在App进程中)

代码五:
status_t SurfaceComposerClient::createSurfaceChecked(const String8& name, uint32_t w, uint32_t h,PixelFormat format,sp<SurfaceControl>* outSurface, uint32_t flags,SurfaceControl* parent,LayerMetadata metadata) {sp<SurfaceControl> sur;status_t err = mStatus;if (mStatus == NO_ERROR) {sp<IBinder> handle;sp<IBinder> parentHandle;sp<IGraphicBufferProducer> gbp;if (parent != nullptr) {parentHandle = parent->getHandle();}// 这里是创建Layer,使用mClient 进程间通信,最终在Client类中进行具体操作err = mClient->createSurface(name, w, h, format, flags, parentHandle, std::move(metadata),&handle, &gbp);if (err == NO_ERROR) {//创建一个SurfaceControl,使用它创建surface*outSurface = new SurfaceControl(this, handle, gbp, true /* owned */);}}return err;
}

该函数的流程图如下

在文章Android 图形架构 之二—— SurfaceFlinger 启动和连接分析过,SurfaceComposerClient中的mClient 是通过SurfaceFling进程返回的,用于进程间通信。所以mClient->createSurface 会调用到SurfaceFling进程中。

下面先分析 new SurfaceControl流程(上图第11步之后)。后面章节再分析SurfaceFling进程的mClient->createSurface流程(上图第2步之后)

SurfaceControl的构造函数也比较简单,位于frameworks/native/libs/gui/SurfaceControl.cpp中:

代码六:
SurfaceControl::SurfaceControl(const sp<SurfaceComposerClient>& client,const sp<IBinder>& handle,const sp<IGraphicBufferProducer>& gbp,bool owned): mClient(client), mHandle(handle), mGraphicBufferProducer(gbp), mOwned(owned)
{}
  • SurfaceControl类的成员变量mClient是一个类型为SurfaceComposerClient对象,Android应用程序主要就是通过SurfaceComposerClient来和SurfaceFlinger服务建立连接的

  • SurfaceControl类的成员变量mHandle是指向创建Layer时,将Layer和SurfaceFlinger作为构造函数的参数创建一个Handle对象,这个Handle是一个Binder的实现,就是标识Surface的全局唯一性。

  • SurfaceControl类的成员变量mGraphicBufferProducer是指向Layer中的gbp参数,

  • SurfaceControl类的成员变量mSurfaceData是一个类型为Surface的强指针,它指向了一个Surface对象。

1.3、 创建Surface

从SurfaceControl获取Surface信息。位于frameworks/base/core/java/android/view/Surface.java:

代码七:
public void copyFrom(SurfaceControl other) {。。。 long surfaceControlPtr = other.mNativeObject;。。。//在Native层创建surface,并返回给newNativeObject//代码八分析long newNativeObject = nativeCreateFromSurfaceControl(surfaceControlPtr);synchronized (mLock) {if (mNativeObject != 0) {nativeRelease(mNativeObject);}//把Native的surface 句柄 ,赋值给java层的surface 的成员变量//代码十一分析setNativeObjectLocked(newNativeObject);}
}

nativeCreateFromSurfaceControl 是jni调用
对应的代码 位于 frameworks/base/core/jni/android_view_Surface.cpp:

代码八:
static jlong nativeCreateFromSurfaceControl(JNIEnv* env, jclass clazz,jlong surfaceControlNativeObj) {sp<SurfaceControl> ctrl(reinterpret_cast<SurfaceControl *>(surfaceControlNativeObj));//调用SurfaceControl的getSurface函数,最终会调用代码九 generateSurfaceLocked,创建了Native层的surface,sp<Surface> surface(ctrl->getSurface());if (surface != NULL) {surface->incStrong(&sRefBaseOwner);}return reinterpret_cast<jlong>(surface.get());
}

最终调用到SurfaceControl的generateSurfaceLocked() ,来创建一个Surface

代码九:
sp<Surface> SurfaceControl::generateSurfaceLocked() const
{// This surface is always consumed by SurfaceFlinger, so the// producerControlledByApp value doesn't matter; using false.mSurfaceData = new Surface(mGraphicBufferProducer, false);return mSurfaceData;
}

参数 mGraphicBufferProducer 是Layer中的gbp 参数,它是一个sp< IGraphicBufferProducer > 类型的对象,MonitoredProducer只是一个代理类,真正的实现是producer参数。

Android系统是通过OpenGL库来绘制UI的。OpenGL库在绘制UI的时候,需要底层的系统提供一个本地窗口给它,以便它可以将UI绘制在这个本地窗口上。Android系统为OpenGL库定提供的本地窗口是ANativeWindow,Surface类通过ANativeObjectBase类间接地继承了ANativeWindow类,因此,Surface类就是一个本地窗口。因此可以将Surface类看作OpenGL库与Android的UI系统之间的一个桥梁。

看看Surface的构造函数,位于frameworks/native/libs/gui/Surface.cpp:

代码十:
Surface::Surface(const sp<IGraphicBufferProducer>& bufferProducer, bool controlledByApp): mGraphicBufferProducer(bufferProducer),mCrop(Rect::EMPTY_RECT),mBufferAge(0),mGenerationNumber(0),mSharedBufferMode(false),mAutoRefresh(false),mSharedBufferSlot(BufferItem::INVALID_BUFFER_SLOT),mSharedBufferHasBeenQueued(false),mQueriedSupportedTimestamps(false),mFrameTimestampsSupportsPresent(false),mEnableFrameTimestamps(false),mFrameEventHistory(std::make_unique<ProducerFrameEventHistory>()) {// Initialize the ANativeWindow function pointers.ANativeWindow::setSwapInterval  = hook_setSwapInterval;ANativeWindow::dequeueBuffer    = hook_dequeueBuffer;ANativeWindow::cancelBuffer     = hook_cancelBuffer;ANativeWindow::queueBuffer      = hook_queueBuffer;ANativeWindow::query            = hook_query;ANativeWindow::perform          = hook_perform;ANativeWindow::dequeueBuffer_DEPRECATED = hook_dequeueBuffer_DEPRECATED;ANativeWindow::cancelBuffer_DEPRECATED  = hook_cancelBuffer_DEPRECATED;ANativeWindow::lockBuffer_DEPRECATED    = hook_lockBuffer_DEPRECATED;ANativeWindow::queueBuffer_DEPRECATED   = hook_queueBuffer_DEPRECATED;const_cast<int&>(ANativeWindow::minSwapInterval) = 0;const_cast<int&>(ANativeWindow::maxSwapInterval) = 1;mReqWidth = 0;mReqHeight = 0;mReqFormat = 0;mReqUsage = 0;mTimestamp = NATIVE_WINDOW_TIMESTAMP_AUTO;mDataSpace = Dataspace::UNKNOWN;mScalingMode = NATIVE_WINDOW_SCALING_MODE_FREEZE;mTransform = 0;mStickyTransform = 0;mDefaultWidth = 0;mDefaultHeight = 0;mUserWidth = 0;mUserHeight = 0;mTransformHint = 0;mConsumerRunningBehind = false;mConnectedToCpu = false;mProducerControlledByApp = controlledByApp;mSwapIntervalZero = false;
}

主要是设置了一些钩子方法,用于创建GraphicBuffer等等。还有一些变量的初始化。所以重点应该在这些钩子方法当中。

Surface 创建完成了,回到代码七,继续执行 setNativeObjectLocked()

代码十一:private void setNativeObjectLocked(long ptr) {//句柄值,是否与上一次的一样if (mNativeObject != ptr) {if (mNativeObject == 0 && ptr != 0) {mCloseGuard.open("release");} else if (mNativeObject != 0 && ptr == 0) {mCloseGuard.close();}//赋值句柄值,到surface 的成员变量mNativeObjectmNativeObject = ptr;mGenerationId += 1;if (mHwuiContext != null) {mHwuiContext.updateSurface();}}}

到此,Layer、Surface、SurfaceControl 的创建过程,分析完了,但是前后的流程还不完整。
例如: -> java 层surface -> native 层 surface ,谁来调用java层的,也就是这个? 是什么样的逻辑,实现什么功能。在文章Android 图形架构 之五—— 深入分析addView所发生的的一切进行了详细分析

下面我们接着上面代码五的流程分析 mClient->createSurface ,进入到SurfaceFlinger进程中

二、 SurfaceFlinger进程中创建Layer

Android应用程序主要就是通过SurfaceComposerClient来和SurfaceFlinger服务建立连接,建立完成后,会得到一个Client的Binder代理对象,保存变量mClient中。(这个过程查看 Android 图形架构 之二—— SurfaceFlinger 启动和连接)

2.1、 Layer创建

因为mClient 是Client 的代理对象,所以会执行到 SurfaceFling进程中 Client的createSurface 函数
在frameworks/native/services/surfaceflinger/Client.cpp中

代码十二:
status_t Client::createSurface(const String8& name, uint32_t w, uint32_t h, PixelFormat format,uint32_t flags, const sp<IBinder>& parentHandle,LayerMetadata metadata, sp<IBinder>* handle,sp<IGraphicBufferProducer>* gbp) {// We rely on createLayer to check permissions.//这个mFlinger 就是SurfaceFlinger对象return mFlinger->createLayer(name, this, w, h, format, flags, std::move(metadata), handle, gbp,parentHandle);
}

下面来看看SurfaceFlinger的createLayer函数

代码十三:
status_t SurfaceFlinger::createLayer(const String8& name, const sp<Client>& client, uint32_t w,uint32_t h, PixelFormat format, uint32_t flags,LayerMetadata metadata, sp<IBinder>* handle,sp<IGraphicBufferProducer>* gbp,const sp<IBinder>& parentHandle,const sp<Layer>& parentLayer) {。。。省略代码。。。switch (flags & ISurfaceComposerClient::eFXSurfaceMask) {case ISurfaceComposerClient::eFXSurfaceBufferQueue:result = createBufferQueueLayer(client, uniqueName, w, h, flags, std::move(metadata),format, handle, gbp, &layer);break;。。。省略代码,这里是有多种Layer的,由于时间和能力有限、暂不分析其他的Layer,和本系列文章关系最紧要的就是BufferQueueLayer。。。default:result = BAD_VALUE;break;}if (result != NO_ERROR) {return result;}if (primaryDisplayOnly) {layer->setPrimaryDisplayOnly();}bool addToCurrentState = callingThreadHasUnscopedSurfaceFlingerAccess();result = addClientLayer(client, *handle, *gbp, layer, parentHandle, parentLayer,addToCurrentState);if (result != NO_ERROR) {return result;}mInterceptor->saveSurfaceCreation(layer);setTransactionFlags(eTransactionNeeded);return result;
}

下面来看看createBufferQueueLayer

代码十四:
status_t SurfaceFlinger::createBufferQueueLayer(const sp<Client>& client, const String8& name,uint32_t w, uint32_t h, uint32_t flags,LayerMetadata metadata, PixelFormat& format,sp<IBinder>* handle,sp<IGraphicBufferProducer>* gbp,sp<Layer>* outLayer) {// initialize the surfacesswitch (format) {case PIXEL_FORMAT_TRANSPARENT:case PIXEL_FORMAT_TRANSLUCENT:format = PIXEL_FORMAT_RGBA_8888;break;case PIXEL_FORMAT_OPAQUE:format = PIXEL_FORMAT_RGBX_8888;break;}//使用工厂模式,创建BufferQueueLayer,sp<BufferQueueLayer> layer = getFactory().createBufferQueueLayer(LayerCreationArgs(this, client, name, w, h, flags, std::move(metadata)));//设置layer的缓冲区默认属性        status_t err = layer->setDefaultBufferProperties(w, h, format);if (err == NO_ERROR) {//参数handle 和 gbp,在创建应用端 的surface 使用到了,下面会分析*handle = layer->getHandle();*gbp = layer->getProducer();*outLayer = layer;}ALOGE_IF(err, "createBufferQueueLayer() failed (%s)", strerror(-err));return err;
}

BufferQueueLayer 继承了BufferLayer,后者继承了Layer,主要是在Layer构造函数中进行了一些初始化的工作,宽高、z轴、透明度等(其实很多值 的含义,我还不知道)

代码十五:
BufferQueueLayer::BufferQueueLayer(const LayerCreationArgs& args) : BufferLayer(args) {}

对象第一次被赋值给强指针会调用onFirstRef函数:

代码十六:
void BufferQueueLayer::onFirstRef() {BufferLayer::onFirstRef();// Creates a custom BufferQueue for SurfaceFlingerConsumer to use//创建了生产者和消费者缓冲区,这两个缓冲区将来会存放UI的内容数据//这里是重点,会面会有文章详细分析sp<IGraphicBufferProducer> producer;sp<IGraphicBufferConsumer> consumer;//BufferQueue创建图形缓冲区管理成员,我们以后分析图形缓冲区管理会讲到BufferQueue::createBufferQueue(&producer, &consumer, true);//new 一个 生成者,它继承了BnGraphicBufferProducer,MonitoredProducer只是一个代理类,真正的实现是producer参数。// *gbp 就是mProducermProducer = new MonitoredProducer(producer, mFlinger, this);{// Grab the SF state lock during this since it's the only safe way to access RenderEngineMutex::Autolock lock(mFlinger->mStateLock);//new 一个 消费者,mConsumer = new BufferLayerConsumer(consumer, mFlinger->getRenderEngine(), mTextureName, this);}//消费者相关设置mConsumer->setConsumerUsageBits(getEffectiveUsage(0));mConsumer->setContentsChangedListener(this);mConsumer->setName(mName);// BufferQueueCore::mMaxDequeuedBufferCount is default to 1if (!mFlinger->isLayerTripleBufferingDisabled()) {//如果不是三级缓冲,就使用两级缓冲mProducer->setMaxDequeuedBufferCount(2);}//获取默认显示器if (const auto display = mFlinger->getDefaultDisplayDevice()) {//updateTransformHint(display);}
}

*gbp = layer->getProducer(); 这里就是返回mProducer 给*gbp ,类型是sp< IGraphicBufferProducer >

下面来看看 *handle = layer->getHandle();

代码十七:
sp<IBinder> Layer::getHandle() {Mutex::Autolock _l(mLock);if (mGetHandleCalled) {ALOGE("Get handle called twice" );return nullptr;}mGetHandleCalled = true;return new Handle(mFlinger, this);
}class Handle : public BBinder, public LayerCleaner {public:Handle(const sp<SurfaceFlinger>& flinger, const sp<Layer>& layer): LayerCleaner(flinger, layer), owner(layer) {}wp<Layer> owner;};

新建一个Handle,而这个Handle只是一个Binder的实现,每一个Layer 对应一个Handle

至此,Layer创建的粗略流程,分析完了

请点赞、收藏,感谢大家的支持,欢迎评论区提问、吐槽

Android 图形架构 之三—— 创建Layer、Surface、SurfaceControl相关推荐

  1. Android 图形架构之一 ——概述

    前言 本系列的文章,可以让你明白,一个View最终是如何显示到屏幕上的,从应用层到硬件抽象层.对分析app的卡顿,掉帧等 有很大帮助. 由于图形架构的涉及到的代码量很大,所以本篇先来个总体的概述,有个 ...

  2. Android 图形架构

    每一个开发者都应该了解的关于 Surface,SurfaceHolder,EGLSurface,SurfaceView,GLSurfaceView,SurfaceTexture,TextureView ...

  3. Android P图形架构之三屏异显实现

    相关源码: \frameworks\base\core\java\android\view\SurfaceControl.java \frameworks\base\services\core\jav ...

  4. 基于android2.3.5系统:源码下载及android体系架构

    **************************************************************************************************** ...

  5. iOS vs Android 系统架构

    iOS是基于UNIX内核,Android是基于Linux内核,iOS和android作为两款优秀的手机操作系统,他们有共性有区别. iOS的系统架构 分为四个层次: 核心操作系统层(Core OS l ...

  6. Android图形合成和显示系统---基于高通MSM8k MDP4平台

    介绍了Android SurfaceFlinger层次以下的图形合成和显示系统,主要基于高通MSM8k MDP4x平台. 做为Android Display专题.SurfaceFlinger的详细介绍 ...

  7. Android display架构分析-SW架构分析(1-8)

    参考: Android display架构分析二-SW架构分析 Android display架构分析三-Kernel Space Display架构介绍 Android display架构分析四-m ...

  8. 高通Android display架构分析

    目录(?)[-] Kernel Space Display架构介绍 函数和数据结构介绍 函数和数据结构介绍 函数和数据结构介绍 数据流分析 初始化过程分析 User Space display接口 K ...

  9. android核心架构Framework组件介绍

    作为一个android开发者,核心架构是必须要了解的.只有了解每个核心层的作用,才能更深入的理解和学习.本篇主要讲解Java Framework层核心代码流程. 文章目录 一,Android系统架构 ...

最新文章

  1. Linux编程题:信号量同步三进程依次打印若干次ABC
  2. 在redhat9上安装firefox
  3. 【比赛】论如何七天内在研究生电子设计竞赛中拿国奖
  4. 原理c++_浅谈C/S和B/S架构的工作原理及优缺点
  5. java 先入先出,java_阻塞队列(FIFO先进先出)
  6. [转载] Java8-Stream API 详解
  7. FW: How to use Hibernate Lazy Fetch and Eager Fetch Type – Spring Boot + MySQL
  8. idea常用快捷键和设置
  9. Docker学习总结(2)——Docker实战之入门以及Dockerfile(二)
  10. python 将字符串转换为字典
  11. 图片命名html,网页切图div+css命名规则
  12. 如何制作万有特性曲线图
  13. 硬盘安装Linux(ubuntu,centos)
  14. 和i77700性能一样的服务器cpu,Intel八代i7 8700K和七代i7 7700K性能对比测验
  15. linux calloc
  16. 如何科学评估疫情对业务的影响?
  17. VC2010 中 MFC 的改进导致 XTP 停靠窗体无法显示的解决方法
  18. Antd给表格一个斜线分隔
  19. 汇编语言笔记(王爽)
  20. 199美元的iPhone和磨刀霍霍的程序员

热门文章

  1. 在到达胜利之前无法回头
  2. VB问题——ByRef参数类型不符
  3. c语言勾股数编程,C++勾股数公式讲解
  4. 装机春天时刻,盘点4月高性价比国产SSD
  5. 一个或多个listeners启动失败,更多详细信息查看对应的容器日志文件
  6. mysql 唯一性榆树_榆树有什么特点?
  7. unity倍镜瞄准镜
  8. “人生苦短,我用Python“——身份认证攻击
  9. 项目管理挣值分析应用
  10. spring boot 集成 websocket 实现消息主动推送