Android Surface 分析+源码

news2024/9/23 5:20:37

Surface的创建涉及三个部分:

  1. App 进程 App需要将自己的内容显示在屏幕上,所以App负责发起Surface创建请求,创建好Surface后, 就可以直接可以在canvas上画图等,最终都会保存到Surface里的buffer里,最后由SurfaceFlinger合成并显示。
  2. System_Server进程 主要是其中的WindowManagerService, 负责接收APP请求,向SurfaceFlinger发起具体的请求创建Surface, 且WMS需要创建Surface的辅助管理类,如SurfaceControl。
  3. SurfaceFlinger 为App进程创建具体的Surface, 在SurfaceFlinger里对应成Layer, 然后负责管理、合成显示。

Surface相关的基础知识介绍

显示层(Layer)和屏幕组成

图8-10 屏幕组成示意图

从图8-10中可以看出:

· 屏幕位于一个三维坐标系中,其中Z轴从屏幕内指向屏幕外。

· 编号为①②③的矩形块叫显示层(Layer)。每一层有自己的属性,例如颜色、透明度、所处屏幕的位置、宽、高等。除了属性之外,每一层还有自己对应的显示内容,也就是需要显示的图像。

在Android中,Surface系统工作时,会由SurfaceFlinger对这些按照Z轴排好序的显示层进行图像混合,混合后的图像就是在屏幕上看到的美妙画面了。这种按Z轴排序的方式符合我们在日常生活中的体验,例如前面的物体会遮挡住后面的物体。

注意,Surface系统中定义了一个名为Layer类型的类,为了区分广义概念上的Layer和代码中的Layer,这里称广义层的Layer为显示层,以免混淆。

FrameBuffer和PageFlipping

我们知道,在Audio系统中,音频数据传输的过程是: · 由客户端把数据写到共享内存中。

· 然后由AudioFlinger从共享内存中取出数据再往Audio HAL中发送。

根据以上介绍可知,在音频数据传输的过程中,共享内存起到了数据承载的重要作用。 无独有偶,Surface系统中的数据传输也存在同样的过程,但承载图像数据的是鼎鼎大名的FrameBuffer(简称FB)。下面先来介绍FrameBuffer,然后再介绍Surface的数据传输过程。

(1)FrameBuffer的介绍

FrameBuffer的中文名叫帧缓冲,它实际上包括两个不同的方面:

· Frame:帧,就是指一幅图像。在屏幕上看到的那幅图像就是一帧。

· Buffer:缓冲,就是一段存储区域,可这个区域存储的是帧。

FrameBuffer的概念很清晰,它就是一个存储图形/图像帧数据的缓冲。这个缓冲来自哪里?理解这个问题,需要简单介绍一下Linux平台的虚拟显示设备FrameBuffer Device(简称FBD)。FBD是Linux系统中的一个虚拟设备,设备文件对应为/dev/fb%d(比如/dev/fb0)。这个虚拟设备将不同硬件厂商实现的真实设备统一在一个框架下,这样应用层就可以通过标准的接口进行图形/图像的输入和输出了。图8-12展示了FBD示意图:

图8-12 Linux系统中的FBD示意图

从上图中可以看出,应用层通过标准的ioctl或mmap等系统调用,就可以操作显示设备,用起来非常方便。这里,把mmap的调用列出来,相信大部分读者都知道它的作用了。

FrameBuffer中的Buffer,就是通过mmap把设备中的显存映射到用户空间的,在这块缓冲上写数据,就相当于在屏幕上绘画。

(2)PageFlipping

图形/图像数据和音频数据不太一样,我们一般把音频数据叫音频流,它是没有边界的, 而图形/图像数据是一帧一帧的,是有边界的。这一点非常类似UDP和TCP之间的区别。所以在图形/图像数据的生产/消费过程中,人们使用了一种叫PageFlipping的技术。

PageFlipping的中文名叫画面交换,其操作过程如下所示:

· 分配一个能容纳两帧数据的缓冲,前面一个缓冲叫FrontBuffer,后面一个缓冲叫BackBuffer。

· 消费者使用FrontBuffer中的旧数据,而生产者用新数据填充BackBuffer,二者互不干扰。

· 当需要更新显示时,BackBuffer变成FrontBuffer,FrontBuffer变成BackBuffer。如此循环,这样就总能显示最新的内容了。这个过程很像我们平常的翻书动作,所以它被形象地称为PageFlipping。

说白了,PageFlipping其实就是使用了一个只有两个成员的帧缓冲队列,以后在分析数据传输的时候还会见到诸如dequeue和queue的操作。

图像混合

我们知道,在AudioFlinger中有混音线程,它能将来自多个数据源的数据混合后输出,那么,SurfaceFlinger是不是也具有同样的功能呢? 答案是肯定的,否则它就不会叫Flinger了。Surface系统支持软硬两个层面的图像混合:

· 软件层面的混合:例如使用copyBlt进行源数据和目标数据的混合。

· 硬件层面的混合:使用Overlay系统提供的接口。

无论是硬件还是软件层面,都需将源数据和目标数据进行混合,混合需考虑很多内容,例如源的颜色和目标的颜色叠加后所产生的颜色。关于这方面的知识,读者可以学习计算机图形/图像学。这里只简单介绍一下copyBlt和Overlay。

· copyBlt,从名字上看,是数据拷贝,它也可以由硬件实现,例如现在很多的2D图形加速就是将copyBlt改由硬件来实现,以提高速度的。但不必关心这些,我们只需关心如何调用copyBlt相关的函数进行数据混合即可。

· Overlay方法必须有硬件支持才可以,它主要用于视频的输出,例如视频播放、摄像机摄像等,因为视频的内容往往变化很快,所以如改用硬件进行混合效率会更高。

Surface源码分析

public class Surface implements Parcelable {
    private static final String TAG = "Surface";
    // Guarded state.
    final Object mLock = new Object(); // protects the native state
    private String mName;
    long mNativeObject; // package scope only for SurfaceControl access
    private long mLockedObject;
    private int mGenerationId; // incremented each time mNativeObject changes
    private final Canvas mCanvas = new CompatibleCanvas();
    }
复制代码

我们可以看到,surface是parcelable的,这意味着它是可以跨进程传递的。 既然是parcelable,那么就离不开两个函数:

  • writeToParcel
  • readFromParcel

下面具体看看这两个函数的实现。

1.1 writeToParcel()

 @Override
    public void writeToParcel(Parcel dest, int flags) {
        if (dest == null) {
            throw new IllegalArgumentException("dest must not be null");
        }
        synchronized (mLock) {
            // NOTE: This must be kept synchronized with the native parceling code
            // in frameworks/native/libs/Surface.cpp
            dest.writeString(mName);
            dest.writeInt(mIsSingleBuffered ? 1 : 0);
            nativeWriteToParcel(mNativeObject, dest);
        }
        if ((flags & Parcelable.PARCELABLE_WRITE_RETURN_VALUE) != 0) {
            release();
        }
    }
复制代码

这里的代码很简单,就几行:

  • 写入name
  • 写入是否是singleBuffer
  • 写入一个native层的指针

关键在于nativeWriteToParcel这个函数:

---》/frameworks/base/core/jni/android_view_Surface.cpp
​
static void nativeWriteToParcel(JNIEnv* env, jclass clazz,
   jlong nativeObject, jobject parcelObj) {
   Parcel* parcel = parcelForJavaObject(env, parcelObj);
    if (parcel == NULL) {
       doThrowNPE(env);
      return;
   }
   //通过Java层的指针,还原出Native层的surface对象
   sp<Surface> self(reinterpret_cast<Surface *>(nativeObject));
   parcel->writeStrongBinder( self != 0 ? IInterface::asBinder(self->getIGraphicBufferProducer()) : NULL);
}
复制代码

看源码网址:xref

在这里我们也可以看到,其实surface对象是一对的,应用层有一个surface,native层也有一个。如果看过Handler机制源码应该知道,MesageQueue和Looper也是一样的,java层有一个,native层还有一个。不过这里就不过多展开了,我们继续往下看。

1.2 self->getIGraphicBufferProducer()

----> /frameworks/av/media/libstagefright/filters/GraphicBufferListener.h
​
​
sp<IGraphicBufferProducer> getIGraphicBufferProducer() const {
        return mProducer;
}
复制代码

我们这里看到得到了个IGraphicBufferProducer对象,这个对象是什么呢? 我们先在这里打住,先去看看readFromParcel这个方法,结合这个方法我们来看IGraphicBufferProducer这个类的作用、

1.3 readFromParcel()

   public void readFromParcel(Parcel source) {
        if (source == null) {
            throw new IllegalArgumentException("source must not be null");
        }
​
        synchronized (mLock) {
           mName = source.readString();
            mIsSingleBuffered = source.readInt() != 0;
            setNativeObjectLocked(nativeReadFromParcel(mNativeObject, source));
        }
复制代码

它做的事情也很简单:

  • 读取到Name
  • 读取是否是singleBuffer,并设置给mIsSingleBuffered
  • 读取native层的一个指针,并赋值给mNativeObject这个变量

1.4 nativeReadFromParcel()

static jlong nativeReadFromParcel(JNIEnv* env, jclass clazz,
    jlong nativeObject, jobject parcelObj) {
     ....
     Parcel* parcel = parcelForJavaObject(env, parcelObj);
      sp<Surface> self(reinterpret_cast<Surface *>(nativeObject));
    sp<Surface> sur;
    if (surfaceShim.graphicBufferProducer != nullptr) {
        // we have a new IGraphicBufferProducer, create a new Surface for it
        sur = new Surface(surfaceShim.graphicBufferProducer, true);
    ...
    }
复制代码

这一段的逻辑就是:

  • 根据java层的parcel对象,拿到native层的parcel对象
  • 根据指针拿到native层的surface对象
  • 从parcel中读出一个Binder对象,这个Binder对象就是一个IGraphicBufferProducer对象
  • 之后,根据这个IGraphicBufferProducer又重新构造了一个Surface对象
  • 最后,返回这个新创建的surface对象

到这里,不知道有没有看晕,我们来简单总结一下:

  • 对于java层的surface来说,它的核心在于Native层的surface对象
  • 对于Native层的surface对象来说,它的核心又在于IGraphicBufferProducer

由于这一次不打算把Surface的绘制也讲了,所以关于IGraphicBufferProducer这个Binder对象的分析就留到下次(如果我记得填坑的话~~~)

Activity的Surface怎么传输?

我们知道,应用想进行绘制,是需要向SurfaceFlinger去申请一块内存的,大致流程如下:

这个申请的流程会涉及到surface的传递,那么surface是怎么传递到应用端的呢?

我们现在就撸起袖子研究一下。

2.1 performTraversals()

 -----> 类:ViewRootImpl
//1
    private void performTraversals() {
        // cache mView since it is used so much below...
        final View host = mView;
    ....
   relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
   ...
        
    }  
    
    //2
        private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
            boolean insetsPending) throws RemoteException {
            //注意看最后一个参数mSurface
            int relayoutResult = mWindowSession.relayout(mWindow, mSeq, params,
                (int) (mView.getMeasuredWidth() * appScale + 0.5f),
                (int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility,
                insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
                mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
                mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout,
                mPendingMergedConfiguration, mSurface);
                
            }
复制代码

这里,一个新的对象出现了————WindowSession,它是干嘛的呢?

2.2 IWindowSession

/frameworks/base/core/java/android/view/IWindowSession.aidl
​
interface IWindowSession {
    int add(IWindow window, int seq, in WindowManager.LayoutParams attrs,
            in int viewVisibility, out Rect outContentInsets, out Rect outStableInsets,
            out InputChannel outInputChannel);
    int addToDisplay(IWindow window, int seq, in WindowManager.LayoutParams attrs,
            in int viewVisibility, in int layerStackId, out Rect outContentInsets,
            out Rect outStableInsets, out Rect outOutsets, out InputChannel outInputChannel);
    int addWithoutInputChannel(IWindow window, int seq, in WindowManager.LayoutParams attrs,
            in int viewVisibility, out Rect outContentInsets, out Rect outStableInsets);
    int addToDisplayWithoutInputChannel(IWindow window, int seq, in WindowManager.LayoutParams attrs,
            in int viewVisibility, in int layerStackId, out Rect outContentInsets,
            out Rect outStableInsets);
    void remove(IWindow window);
}
复制代码

WindowSessioin原来是个AIDL文件,它是跟WMS进行通信的对象,相当于打开了一条通信通道。

我们接下来看一下IWindowSession是如何初始化的:

可以看到它的实现类是Session,那接下来就可以继续跟踪了。

2.3 relayout(…)

class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
     @Override
    public int relayout(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int requestedWidth, int requestedHeight, int viewFlags, int flags, long frameNumber,
            Rect outFrame, Rect outOverscanInsets, Rect outContentInsets, Rect outVisibleInsets,
            Rect outStableInsets, Rect outsets, Rect outBackdropFrame,
            DisplayCutout.ParcelableWrapper cutout, MergedConfiguration mergedConfiguration,
            Surface outSurface) {
            ....
                int res = mService.relayoutWindow(this, window, seq, attrs,
                requestedWidth, requestedHeight, viewFlags, flags, frameNumber,
                outFrame, outOverscanInsets, outContentInsets, outVisibleInsets,
                outStableInsets, outsets, outBackdropFrame, cutout,
                mergedConfiguration, outSurface);
                ...
            }
}
复制代码

可以看到,它最终是调用到了WMS的relayoutWindow方法,大家重点关注一下最后传递的参数:outSurface。 这个outSurface此时其实是空的,这里只是传递了一个壳过去给WMS。

接下来,我们就要去看看传递给空壳给WMS,那WMS是怎么处理这个空壳的?怎么给它填充数据的?

2.4 service.relayoutWindow(…)

----> 类:WindowManagerService.java
​
 public int relayoutWindow(Session session, IWindow client, int seq, LayoutParams attrs,
            int requestedWidth, int requestedHeight, int viewVisibility, int flags,
            long frameNumber, Rect outFrame, Rect outOverscanInsets, Rect outContentInsets,
            Rect outVisibleInsets, Rect outStableInsets, Rect outOutsets, Rect outBackdropFrame,
            DisplayCutout.ParcelableWrapper outCutout, MergedConfiguration mergedConfiguration,
            Surface outSurface) {
            ...
                result = createSurfaceControl(outSurface, result, win, winAnimator); 
                if (surfaceController != null) {
            surfaceController.getSurface(outSurface);
            if (SHOW_TRANSACTIONS) Slog.i(TAG_WM, "  OUT SURFACE " + outSurface + ": copied");
        }
                ...
            }
            
            
-----》 WindowSurfaceController.java
    void getSurface(Surface outSurface) {
        outSurface.copyFrom(mSurfaceControl);
    }
            
复制代码

上面这一段代码的逻辑是:

  • 创建一个SurfaceControl
  • 从SurfaceControl中拷贝数据到之前的空壳的Surface中

那它是如何实现这个复制的过程的呢?

我们继续看copyFrom()函数:

2.5 surface.copyFrom()

---->Surface.java
​
 public void copyFrom(SurfaceControl other) {
        ...
        long surfaceControlPtr = other.mNativeObject;
        ...
        long newNativeObject = nativeGetFromSurfaceControl(surfaceControlPtr);
       ...
         setNativeObjectLocked(newNativeObject);
        }
    }
复制代码

分析这几行代码实现的功能:

  • 从SurfaceControl对象中获取到一个native对象的指针
  • 通过这个native层对象创建一个native层的surface对象
  • 然后把这个native层surface对象跟java层的surface对象绑定在一起

这里放上native层代码,大家就会更清楚了:

-----> /frameworks/base/core/jni/android_view_Surface.cpp
​
static jlong nativeGetFromSurfaceControl(JNIEnv* env, jclass clazz,
        jlong surfaceControlNativeObj) {
    /*
     * This is used by the WindowManagerService just after constructing
     * a Surface and is necessary for returning the Surface reference to
     * the caller. At this point, we should only have a SurfaceControl.
     大家看这个注释,很清楚地讲解了这个方法是被WMS调用,用于返回native层Surface的指针给到Java层
     */
​
    sp<SurfaceControl> ctrl(reinterpret_cast<SurfaceControl *>(surfaceControlNativeObj));
    sp<Surface> surface(ctrl->getSurface());
    if (surface != NULL) {
        surface->incStrong(&sRefBaseOwner);
    }
    return reinterpret_cast<jlong>(surface.get());
}
复制代码
-----> /frameworks/native/libs/gui/SurfaceControl.cpp
sp<Surface> SurfaceControl::getSurface() const
{
    Mutex::Autolock _l(mLock);
    if (mSurfaceData == 0) {
        return generateSurfaceLocked();
    }
    return mSurfaceData;
}
​
​
sp<Surface> SurfaceControl::generateSurfaceLocked() const
{
    // This surface is always consumed by SurfaceFlinger, so the
    // producerControlledByApp value doesn't matter; using false.
    //使用GraphicBufferProducer创建了一个Surface
    mSurfaceData = new Surface(mGraphicBufferProducer, false);
​
    return mSurfaceData;
}
​
复制代码

好了,到这一步,Surface跨进程通信的整个流程就结束了。我们最后总结回顾一下。以上就是Android开发中的Surface分析

总结

  • 应用通过ViewRootImpl创建一个空的Surface
  • 通过IWindowSession将这个空的Surface传递给WMS
  • WMS通过SurfaceControl创建一个native层Surface,并通过指针将Surface跟Java层的Surface进行绑定,从而完成Surface的跨进程传输

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/392630.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用docker搭建WordPress博客

文章目录一、前置条件二、拉取镜像三、安装工具及依赖1.安装2.升级pip3.获取docker-compose方法一另一种方法&#xff08;推荐&#xff09;四、创建容器&#xff0c;启用镜像&#xff0c;映射端口五、访问并设置一、前置条件 主机&#xff1a;20.0.0.142 安装docker docker的安…

SQL-刷题技巧-删除重复记录

一. 原题呈现 牛客 SQL236. 删除emp_no重复的记录&#xff0c;只保留最小的id对应的记录。 描述&#xff1a; 删除emp_no重复的记录&#xff0c;只保留最小的id对应的记录。 drop table if exists titles_test; CREATE TABLE titles_test (id int(11) not null primary key…

Matlab论文插图绘制模板第80期—羽状图(Feather)

在之前的文章中&#xff0c;分享了很多Matlab线图的绘制模板&#xff1a; 进一步&#xff0c;再来分享一种特殊的线图&#xff1a;羽状图。 先来看一下成品效果&#xff1a; 特别提示&#xff1a;Matlab论文插图绘制模板系列&#xff0c;旨在降低大家使用Matlab进行科研绘图的…

2.2 分治法的基本思想

分治法的基本思想是将一个规模为n的问题分解为化个规模较小的子问题&#xff0c;这些子问题互相独立且与原问题相同。递归地解这些子问题&#xff0c;然后将各子问题的解合并得到原问题的解。它的一般的算法设计模式如下&#xff1a;divide-and-conquer(P) { if ( P < no) a…

SRS4.0 源码分析- RTC模块相关类

前言 本文介绍SRS4.0涉及RTC模块的C类&#xff0c;主要包括RTC Server和Session相关的。 SrsGoApiRtcPlay 处理webrtc client的播放请求&#xff0c;解析client的offer&#xff0c;并且生成server的answer&#xff0c;并且为这次请求创建一个session。SrsRtcServer 监听udp端…

如何实现沉浸式旅游与非物质文化遗产的共同发展

中国非物质文化遗产资源丰富&#xff0c;是世界上非物质文化遗产数量最多的国家。丰富多样的资源为非物质文化遗产旅游业的建设提供了良好的基础。非物质文化遗产旅游是基于非物质文化遗产资源开发的文化旅游消费形式。文化资源包括各民族代代相传的传统文化表现形式。非物质文…

【面试题】Vue面试题整理

1. v-show 和 v-if 的区别&#xff1f; v-if 指令用于条件渲染&#xff0c;它会根据表达式的值的真假来决定是否渲染元素。如果表达式的值为 false&#xff0c;则该元素不会被渲染并且也不会保留在 DOM 中。v-show 指令用于条件展示&#xff0c;它不会从 DOM 中删除元素&#…

Windows openssl-1.1.1d vs2017编译

工具&#xff1a; 1. perl&#xff08;https://strawberryperl.com/&#xff09; 2. nasm&#xff08;https://nasm.us/&#xff09; 3. openssl源码&#xff08;https://www.openssl.org/&#xff09; 可以自己去下载 或者我的网盘提供下载&#xff1a; 链接&#xff1a;…

Redis List 底层三种数据结构原理剖析

1. Redis List 是什么作为 Java 开发者的你&#xff0c;看到这个词并不陌生。在 Java 开发中几乎每天都会使用这个数据结构。Redis 的 List 与 Java 中的 LinkedList 类似&#xff0c;是一种线性的有序结构&#xff0c;可以按照元素被推入列表中的顺序来存储元素&#xff0c;能…

2023年第九周总周结 | 开学第一周

为什么要做周总结&#xff1f; 1.避免跳相似的坑 2.客观了解上周学习进度并反思&#xff0c;制定可完成的下周规划 一、上周问题解决情况 不满却又喜欢“受害者”身份项目导向学习进展困难&#xff0c;进而产生挫败焦虑作息调整→学习时长变少and变碎 二、这周存在问题 and 反…

Tomcat 配置文件数据库密码加密

几年前研究过Tomcat context.xml 中数据库密码改为密文的内容&#xff0c;因为当时在客户云桌面代码没有留备份也没有文章记录&#xff0c;最近项目又提出了这个需求就又重新拾起来学习一下。在网上找了一些资料&#xff0c;自己也大概试了一下&#xff0c;目前功能是实现了。参…

SpringCloud系列(十五)[分布式搜索引擎篇] - 结合实际应用场景学习并使用 RestClient 客户端 API

前面的文章具体介绍了是索引库及文档的一些基本操作指令, 指令还是挺简单的; 那么实际应用场景下, 我们是如何操作 ElasticSearch 的呢?  其实 ElasticSearch 官方已经为我们提供了各种不同语言的客户端, 目的就是为了来操作 ElasticSearch, 这些客户端的本质就是组装 DSL 语…

vmware虚拟机与树莓派4B安装ubuntu1804 + ros遇到的问题

如题所示&#xff0c;本人在虚拟机上安装ubuntu1804&#xff0c;可以很容易安装&#xff0c;并且更换系统apt源和ros源&#xff0c;然后安装ros&#xff0c;非常顺利&#xff0c;但是在树莓派4B上安装raspiberry系统就遇到了好多问题。 树莓派我烧录的是这个镜像&#xff1a;ub…

ASO优化之选择最佳关键词

ASO的关键字排名是指针对特定的关键词在应用商店搜索结果中所形成的位置。虽然这看起来很简单&#xff0c;但应用商店排名不仅仅是位置&#xff0c;应用的排名统计数据都要考虑进去。 应用商店搜索结果因国家/地区而异&#xff0c;这就意味着如果我们从不同的国家或地区进行搜…

经典文献阅读之--Lifelong SLAM(变化环境中Lifelong定位建图)

0. 简介 商场、超市等大多数现实场景的环境随时都在变化。不考虑这些变化的预建地图很容易变得过时。因此&#xff0c;有必要拥有一个最新的环境模型&#xff0c;以促进机器人的长期运行。为此《A General Framework for Lifelong Localization and Mapping in Changing Envir…

Oracle技术分享 exp导数据时报错ORA-01578 ORA-01110

问题描述&#xff1a;exp导数据时报错ORA-01578 ORA-01110&#xff0c;如下所示&#xff1a; 数据库&#xff1a;oracle 19.12 多租户 1、异常重现 [oracledbserver ~]$ exp ora1/ora1orclpdbfileemp.dmp tablesemp logexp.log Export: Release 19.0.0.0.0 - Production onS…

OpenEuler20.03源码安装配置PostgreSQL13.4详细图文版

OpenEuler安装配置PostgreSQL 序号更新内容更新日期更新人1完成第一至三章内容编辑&#xff1b;2021年9月18日liupp2增加PostgreSQL服务开机自动启动&#xff1b;2021年10月25日liupp 一、准备条件 OpenEuler(Hyper-V虚拟机)&#xff1a; 版本&#xff1a;20.03 LTS SP2下载地…

推荐几个超实用的开源自动化测试框架

有什么好的开源自动化测试框架可以推荐&#xff1f;为了让大家看文章不蒙圈&#xff0c;文章我将围绕3个方面来阐述&#xff1a; 1、通用自动化测试框架介绍 2、Java语言下的自动化测试框架 3、Python语言下的自动化测试框架 随着计算机技术人员的大量增加&#xff0c;通过编写…

什么是MyBatis?无论是基础教学还是技术精进,你都应该看这篇MyBatis

文章目录学习之前&#xff0c;跟你们说点事情&#xff0c;有助于你能快速看完文章一、先应用再学习&#xff0c;代码示例1. 第一个MyBatis程序2. MyBatis整合Spring3. SpringBoot整合MyBatis二、MyBatis整体流程&#xff0c;各组件的作用域和生命周期三、说说MyBatis-config.xm…

流域土壤保持及GIS实现

流域土壤保持及GIS实现 流域水土过程模拟与生态调控 01 土壤保持模拟 土壤侵蚀不仅会引起耕地生产力下降、河床抬升、泥沙淤积阻塞河道等生态环境问题&#xff0c;也会对人们正常的生产生活产生威胁。生态系统的土壤保持量&#xff08;吨/公顷/年&#xff09;&#xff0c;是…