如何计算Bitmap的内存占用

news2024/11/15 5:01:03

日常我们提到图片大小的时候,一般都会把关注点放在图片的文件大小。因为一般来说,图片文件越小,内存占用也会越小。但是其实图片文件大小和内存占用大小没有什么直接的必然联系,我们可以通过查看 Android 的 Bitmap 的内存分配,来查看 Bitmap 的内存大小是被哪些因素影响的。

在 Android 的架构里, Bitmap 相关的内容分为下面几个模块:

  • Java:包括 Bitmap、BitmapFactory等类,上层直接使用创建 Bitmap
  • native:包括 android::Bitmap 对象等,负责决定内存分配方式,调用skia
  • sk:包括 SkBitmap, skia 引擎去绘制 Bitmap

这里绘制一个简单的调用时序图方便缕清逻辑: image.png

在Android里,android5-8 和 android8 以上的 Bitmap 内存分配策略是不同的,但是通过源码对比,虽然代码有了比较大的改动,但是调用流程和内存大小的计算方式是基本没有什么大的变化。

解码配置-每像素字节

Bitmap里面,我们可以通过 getByteCount 方法来得到图片内存大小的字节数,它的计算方法则是:

getRowBytes() * getHeight();

而 getRowBytes 是调取了底层逻辑,最终调用到 SkBitmap里:

size_t rowBytes() const { return fRowBytes; }

skkia里面则通过 minRowBytes 计算行字节数:

size_t minRowBytes() const {
        uint64_t minRowBytes = this->minRowBytes64();
        if (!SkTFitsIn<int32_t>(minRowBytes)) {
            return 0;
        }
        return (size_t)minRowBytes;
}

uint64_t minRowBytes64() const {
	return (uint64_t)sk_64_mul(this->width(), this->bytesPerPixel());
}

int bytesPerPixel() const { return fColorInfo.bytesPerPixel(); }

这里我们得到行字节数的计算:

行字节 = 行像素 * 每像素字节数

这里的 fColorInfo 就对应 Option里的 inPreferredConfig。这个代表了图片的解码配置,包括:

  • ALPHA_8 单通道,总共8位,1个字节
  • RGB_565 每像素16为
  • ARGB-4444 每像素16位,(2字节),已经废弃,传的话会被改为 ARGB_8888
  • ARGB_8888 每个像素32位(总共4字节),也就是 argb 四个通过各8位
  • RGBA_F16 每个像素16位,总共8个字节
  • HARDWARE 硬件加速,如果图片只在内存中,使用这个配置最合适

这里我们可以先简单理解为图片内存大小就是

*(尺寸) * 每像素字节数

图片尺寸

在上层,我们会通过 BitmapFactory 去创建一个 Bitmap,例如通过

public static Bitmap decodeResource(Resources res, int id)

通过resource里的图片资源创建 Bitmap。类似的函数比较多,但是都会转成stream执行到

public static Bitmap decodeStream( InputStream is,  Rect outPadding,
             Options opts)

这里传入的 Options 参数其实就会影响最终图片尺寸的计算。 接着我们继续看 decodeStream的逻辑。这个会执行 native 的nativeDecodeStream函数。进行图片的解码: 解码之前会读取java层传入的配置。其中当 inScale 为ture(默认也是true)的时候:

if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
		const int density = env->GetIntField(options, gOptions_densityFieldID);
		const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
		const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
		if (density != 0 && targetDensity != 0 && density != screenDensity) {
			scale = (float) targetDensity / density;
		}
}

这里读取 inDensity 、inTargetDensity和 inScreenDensity 参数,来确定缩放比例。 这几个参数看着挺抽象的,我们看下传入的具体是什么东西 inDensity

final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
	opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
	opts.inDensity = density;
}

传入源图的density,如果是默认值的话就传160, inTargetDensity

opts.inTargetDensity = res.getDisplayMetrics().densityDpi;

这个其实也是设备的 dpi。这个值具体可以通过

adb shell dumpsys window displays

进行查看。 image.png screenDensity

static int resolveDensity( Resources r, int parentDensity) {
	final int densityDpi = r == null ? parentDensity : r.getDisplayMetrics().densityDpi;
	return densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi;
}

一般情况下和 inTargetDensity 的一样的。 所以这里计算出来的scale是用来适配屏幕分辨率的。

然后会通过 sampleSize 来计算输出的宽高:

SkISize size = codec->getSampledDimensions(sampleSize);

//skia
SkISize SkSampledCodec::onGetSampledDimensions(int sampleSize) const {
	const SkISize size = this->accountForNativeScaling(&sampleSize);
	return SkISize::Make(get_scaled_dimension(size.width(), sampleSize),
		get_scaled_dimension(size.height(), sampleSize));
}

static inline int get_scaled_dimension(int srcDimension, int sampleSize) {
	if (sampleSize > srcDimension) {
		return 1;
	}
	return srcDimension / sampleSize;
}

这里宽高会变成

初始宽高 / simpleSize

接着会使用上面提到是 scale 进行缩放:

if (scale != 1.0f) {
	willScale = true;
	scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
	scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
}

这里可以看到我们最后传给Java层去创建 Bitmap 的尺寸就是一系列计算得到的 scaleWidth * scaleHeight,即:

= 原始宽度 * (targetDensity / density) / sampleSize + 0.5f

Bitmap内存分配

在对应用的内存情况做进一步分析后,了解到了 Bitmap 的内存分配与回收在不同的 Android 版本中又不一样的机制。最近对这块也做了一些了解。 根据 Android 系统版本,可以把分配方式分成几组:

  • Android 3以前:图片数据分配在 native。这个已经是历史了,不关系
  • Android8 以前: 图片数据分配在java堆。 这个虽然也挺旧了,但是应用基本还会支持很大一部分,
  • Android8 及以后:图片数据分配在 native

所以我copy了 2 份源码来分析这部分,一份 Android6 的, 一份 Android 10 的。

创建过程

8.0以上

顺着 8.0 的 BitmapFactory#nativeDecodeStream 往下看,在 native 层代码里面,最终会调用 Bitmap 的构造方法去创建 Bitmap 的 java 层对象:

// now create the java bitmap
return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
    bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);


// createBitmap
BitmapWrapper* bitmapWrapper = new BitmapWrapper(bitmap);
jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
            reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), bitmap->height(), density,
            isPremultiplied, ninePatchChunk, ninePatchInsets, fromMalloc);

这里 BitmapWrapper 是对 native Bitmap 的一层包装。这里传递的是它的指针。 这个对应了Java层的构造方法:

Bitmap(long nativeBitmap, int width, int height, int density,
            boolean requestPremultiplied, byte[] ninePatchChunk,
            NinePatch.InsetStruct ninePatchInsets, boolean fromMalloc)

到这里 Bitmap就创建完毕了 这里得到一个简单的指向关系: image.png

接下来看详细的分配逻辑,在 native 层创建 Bitmap 的时候会有预分配的逻辑:

decodingBitmap.tryAllocPixels(decodeAllocator)

这里的 decodingBitmapSkBitmap,可以直接 google SkBitmap 对象的源码

bool SkBitmap::tryAllocPixels(Allocator* allocator) {
    HeapAllocator stdalloc;

    if (nullptr == allocator) {
        allocator = &stdalloc;
    }
    return allocator->allocPixelRef(this);
}

//上面调用的 HeapAllocator#allocPixelRef
// Graphics.cpp
bool HeapAllocator::allocPixelRef(SkBitmap* bitmap) {
    mStorage = android::Bitmap::allocateHeapBitmap(bitmap);
    return !!mStorage;
}

allocateHeapBitmap里面是真正的分配逻辑:

sk_sp<Bitmap> Bitmap::allocateHeapBitmap(const SkImageInfo& info) {
    size_t size;
    if (!computeAllocationSize(info.minRowBytes(), info.height(), &size)) {
        LOG_ALWAYS_FATAL("trying to allocate too large bitmap");
        return nullptr;
    }
    return allocateHeapBitmap(size, info, info.minRowBytes());
}

sk_sp<Bitmap> Bitmap::allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes) {
    void* addr = calloc(size, 1);
    if (!addr) {
        return nullptr;
    }
    return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes));
}

使用 calloc函数分配需要的size。并且创建 Bitmap,把分配后的指针指向 addr.

8.0以下

8.0以下的 decode 里面最后会使用 JavaAllocator 分配图片像素:

// now create the java bitmap
return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(),
	bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);

分配的逻辑放在了 SkImageDecoder 里面:

SkImageDecoder* decoder = SkImageDecoder::Factory(stream);
// ...
decoder->decode(
    stream,
    &decodingBitmap,
    prefColorType, decodeMode) != SkImageDecoder::kSuccess
)

// skia
SkImageDecoder::Result SkImageDecoder::decode(SkStream* stream, SkBitmap* bm, SkColorType pref,
                                              Mode mode) {
    // we reset this to false before calling onDecode
    fShouldCancelDecode = false;
    // assign this, for use by getPrefColorType(), in case fUsePrefTable is false
    fDefaultPref = pref;

    // pass a temporary bitmap, so that if we return false, we are assured of
    // leaving the caller's bitmap untouched.
    SkBitmap tmp;
    const Result result = this->onDecode(stream, &tmp, mode);
    if (kFailure != result) {
        bm->swap(tmp);
    }
    return result;
}

这里调用 onDecode 函数,onDecode是一个模板方法,实际上调用子类 SkPNGImageDecoder 的 onDecode:

// SkPNGImageDecoder
SkImageDecoder::Result SkPNGImageDecoder::onDecode(SkStream* sk_stream, SkBitmap* decodedBitmap, Mode mode) {
    //...

    if (!this->allocPixelRef(decodedBitmap,
		kIndex_8_SkColorType == colorType ? colorTable : NULL)) {
        return kFailure;
    }

    //...
}

这里使用的就是 JavaAllocator。 和 10.0 的代码一样,我们先看 createBitmap 之后的逻辑。也会调用 Java Bitmap 的构造方法:

Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
            boolean isMutable, boolean requestPremultiplied,
            byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets)

和 Android 10 相比,这里多传入了一个 byte 数组叫buffer:

/**
* Backing buffer for the Bitmap.
*/
private byte[] mBuffer;

mBuffer = buffer;
mNativePtr = nativeBitmap;

这里的 mBuffer 就存储了 Bitmap 的像素内容,所以在 Android6 上对象间关系是这样: image.png 接下来在 allocateJavaPixelRef里面看一下具体的内存分配流程:

android::Bitmap* GraphicsJNI::allocateJavaPixelRef(JNIEnv* env, SkBitmap* bitmap,
                                             SkColorTable* ctable) {
	// 省略...

    jbyteArray arrayObj = (jbyteArray) env->CallObjectMethod(gVMRuntime,
                                                             gVMRuntime_newNonMovableArray,
                                                             gByte_class, size);
    jbyte* addr = (jbyte*) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj);
    
	android::Bitmap* wrapper = new android::Bitmap(env, arrayObj, (void*) addr,
            info, rowBytes, ctable);
    wrapper->getSkBitmap(bitmap);
    bitmap->lockPixels();
    return wrapper;
}

这里 byte 数组是通过 VMRuntimenewNonMovableArray分配的,然后通过 addressOf把地址传递给 android::Bitmap。

Bitmap内存释放

现在我们继续看一下 Bitmap 的内存释放机制。 Bitmap 在 Java 层提供了 recycle方法来释放内存。我们同样也通过 Android 10 和 Android 6的源码进行分析。

8.0以上

Android 8以上的 recycle 方法逻辑如下:

public void recycle() {
    if (!mRecycled) {
        nativeRecycle(mNativePtr);
        mNinePatchChunk = null;
        mRecycled = true;
    }
}

这里直接调了 native 层的 nativeRecycle 方法,传入的是 mNativePtr,即 native 层 BitmapWrapper指针。 nativeRecycle的代码如下:

static void Bitmap_recycle(JNIEnv* env, jobject, jlong bitmapHandle) {
    LocalScopedBitmap bitmap(bitmapHandle);
    bitmap->freePixels();
}

这里调了 LocalScopedBitmapfreePixelsLocalScopeBitmap则是代理了 BitmapWrapper这个类。

void freePixels() {
	mInfo = mBitmap->info();
	mHasHardwareMipMap = mBitmap->hasHardwareMipMap();
	mAllocationSize = mBitmap->getAllocationByteCount();
	mRowBytes = mBitmap->rowBytes();
	mGenerationId = mBitmap->getGenerationID();
	mIsHardware = mBitmap->isHardware();
	mBitmap.reset();
}

最后会调用 bitmap 指针的 reset, 那么最后会执行 Bitmap 的析构函数:

// hwui/Bitmap.cpp
Bitmap::~Bitmap() {
    switch (mPixelStorageType) {
        case PixelStorageType::Heap:
            free(mPixelStorage.heap.address);
        	break;
        // 省略...
    }
}

这里释放了图片的内存数据。 但是如果没有手动调用 recycle , Bitmap 会释放内存吗,其实也是会的。这里要从 Java 层的 Bitmap 说起。 在 Bitmap 的构造方法里,有如下代码:

NativeAllocationRegistry registry;
registry = NativeAllocationRegistry.createMalloced(
	Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), allocationByteCount);
registry.registerNativeAllocation(this, nativeBitmap);

这样,当Bitmap被Android虚拟机回收的时候,会自动调用 nativeGetNativeFinalizer。关于 NativeAllocationRegistry的细节,我们不做深入讨论。

// nativeGetNativeFinalizer
static void Bitmap_destruct(BitmapWrapper* bitmap) {
    delete bitmap;
}

static jlong Bitmap_getNativeFinalizer(JNIEnv*, jobject) {
    return static_cast<jlong>(reinterpret_cast<uintptr_t>(&Bitmap_destruct));
}

这里会调用 bitmap 的 delete,自然也会调 Bitmap 的析构函数,清理图片的像素内存。 我们把 8 以上的 Bitmap 内存回收整理一个结构图: image.png

6.0

分析完 Android 10 的代码,我们继续了解下 8 以下是怎么回收 Bitmap 的。 同样先看 recycle:

public void recycle() {
    if (!mRecycled && mFinalizer.mNativeBitmap != 0) {
        if (nativeRecycle(mFinalizer.mNativeBitmap)) {
            mBuffer = null;
            mNinePatchChunk = null;
        }
        mRecycled = true;
    }
}

nativeRecycle 里面调用 android/graphics/Bitmap.cpp 的 Bitmap_recycle方法,这里的逻辑和 8 以上是一样的。只是这里传入的 bitmapHandle

mFinalizer.mNativeBitmap

这里也是在 Bitmap 创建的时候把 native 的 Bitmap 传给了 BitmapFinalizer对象。 继续看 Bitmap#freePixels:

void Bitmap::freePixels() {
    AutoMutex _lock(mLock);
    if (mPinnedRefCount == 0) {
        doFreePixels();
        mPixelStorageType = PixelStorageType::Invalid;
    }
}

这里的 doFreePixels 也和 8 以上类似,不过走的是 PixelStorageType::Java 的分支:

// 省略其他代码...
case PixelStorageType::Java:
	JNIEnv* env = jniEnv();
	env->DeleteWeakGlobalRef(mPixelStorage.java.jweakRef);
	break;

这里会把 jweakRef 给回收。这个引用指向的的就是存储了图片像素数据的 Java byte 数组。 在 8 以下没有 NativeAllocationRegistry的时候,会依赖 Java 对象的 finalize进行内存回收。

@Override
public void finalize() {
    try {
        super.finalize();
	} catch (Throwable t) {
		// Ignore
	} finally {
		setNativeAllocationByteCount(0);
		nativeDestructor(mNativeBitmap);
		mNativeBitmap = 0;
	}
}

这里会调用 nativeDestructor,即 Bitmap_destructor:

static void Bitmap_destructor(JNIEnv* env, jobject, jlong bitmapHandle) {
    LocalScopedBitmap bitmap(bitmapHandle);
    bitmap->detachFromJava();
}

void Bitmap::detachFromJava() {
    bool disposeSelf;
    {
        android::AutoMutex _lock(mLock);
        mAttachedToJava = false;
        disposeSelf = shouldDisposeSelfLocked();
    }
    if (disposeSelf) {
        delete this;
    }
}

这里最后会调用 delete this,即调用 Bitmap 的析构函数:

Bitmap::~Bitmap() {
    doFreePixels();
}

这里和 recycle一样,最后也会通过 doFreePixels 一样回收图片像素内存。 整理流程如下:image.png

总结

阅读到这里,我们总结几个有用的结论:

  • Android Bitmap 内存占用和图片的尺寸,质量强相关,日常治理大图的时候要对这些参数适当做降级方案。
  • Android8以下图片分配在 Java 堆内,容易 OOM,可以通过一些 hook 方案把内存移到堆外。并且虽然 Bitmap 有自己兜底的内存释放机制,但是主动及时调用 recycle也不是坏事。
  • Android8 以上虽然 Bitmap 内存分配在 native 部分,可以避免 Java 层的 OOM,但是虚拟内存不足的 OOM 还是可能会引发的,所以大图还是需要治理的。

Android 技术提升知识点归整

Android 性能调优系列https://qr18.cn/FVlo89
Android 车载学习指南https://qr18.cn/F05ZCM
Android Framework核心知识点笔记https://qr18.cn/AQpN4J
Android 音视频学习笔记https://qr18.cn/Ei3VPD
Jetpack全家桶(含Compose)https://qr18.cn/A0gajp
Kotlin 入门到精进https://qr18.cn/CdjtAF
Flutter 基础到进阶实战https://qr18.cn/DIvKma
Android 八大知识体系https://qr18.cn/CyxarU
Android 中高级面试题锦https://qr18.cn/CKV8OZ

后续如有新知识点,将会持续更新,尽请期待……

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

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

相关文章

GitLab 专家分享|关于 DevSecOps ,你需要知道这几点

本文来源&#xff1a;about.gitlab.com 译者&#xff1a;极狐(GitLab) 市场部内容团队 ❔ 灵魂拷问&#xff1a; 你的安全测试&#xff0c;能否跟上现代软件开发模式的步伐&#xff1f; GitLab 预测到&#xff0c;2023 年企业会将更多的时间和资源投入到持续的安全左移上&…

Day08-网页布局实战表单和表格

文章目录网页布局实战一 表单案例1-表单前后端交互案例2-常用表单元素知识点input的属性总结&#xff1a;type属性的其他值(了解)&#xff1a;按钮label标签二 结构伪类选择器三 表单布局案例案例1贯穿案例-登录页面制作案例2贯穿案例-注册页面制作四 表格案例1-表格设计案例2-…

taobao.item.propimg.upload( 添加或修改属性图片 )

&#xffe5;开放平台基础API必须用户授权 添加一张商品属性图片到num_iid指定的商品中 传入的num_iid所对应的商品必须属于当前会话的用户 图片的属性必须要是颜色的属性&#xff0c;这个在前台显示的时候需要和sku进行关联的 商品属性图片只有享有服务的卖家&#xff08;如&a…

JVM对象实例化详解

1、对象创建方式你知道几种呢&#xff1f;new&#xff1a;最常见的方式、Xxx的静态方法&#xff0c;XxxBuilder/XxxFactory的静态方法Class的newInstance方法&#xff1a;反射的方式&#xff0c;只能调用空参的构造器&#xff0c;权限必须是publicConstructor的newInstance(XXX…

maven 私服nexus安装与使用

一、下载nexus Sonatype公司的一款maven私服产品 1、官网下载地址&#xff1a;https://help.sonatype.com/repomanager3/product-information/download 2、csdn下载地址&#xff1a;https://download.csdn.net/download/u010197591/87522994 二、安装与配置 1、下载后解压如…

【Rust日报】2023-02-28 Rust 和 WASM 如何驱动 1.1.1.1

为什么用 Rust 构建原生 UI 程序这么困难Rust 正被用于构建 Linux 内核和 Android 操作系统&#xff0c;也被 AWS 用于关键基础设施&#xff0c;以及用于 ChromeOS 和Firefox。尽管 Rust 很有前景&#xff0c;但它在 GUI 原生开发领域还有所欠缺。在 2019 年&#xff0c;GUI 是…

【Springboot】Springboot集成 Druid

Springboot集成 Druid 对于数据访问层&#xff0c;无论是Sql还是NoSql&#xff0c;SpringBoot默认采用整合SpringData的方式进行统一管理&#xff0c;添加大量的自动配置&#xff0c;屏蔽了很多设置。引入了各种XxxTemplate和XxxRepository来简化我们队数据访问层的操作。 Sp…

FCT: The Fully Convolutional Transformer for Medical Image Segmentation 论文解读

The Fully Convolutional Transformer for Medical Image Segmentation 论文&#xff1a;The Fully Convolutional Transformer for Medical Image Segmentation (thecvf.com) 代码&#xff1a;Thanos-DB/FullyConvolutionalTransformer (github.com) 期刊/会议&#xff1a;W…

性能调优,看过的都说会了...

在展开今天的内容之前&#xff0c;我们先来看一下&#xff0c;是不是任何一个测试都可以学习性能测试。 如果说需求、开发、DB、运维、测试是单一一门学科&#xff0c;那么性能就是综合学科&#xff0c;它包含了需求分析、DB、开发、测试、运维的所有学科。 所以说&#xff0…

深度学习在耐火砖项目的一些思考

1.项目概述 年前&#xff0c;产品经理找到我&#xff0c;让我去测试3D视觉耐火砖拆垛项目。大概就是这种转 2. 实际情况 去了现场&#xff0c;采集图像&#xff0c;标定相机和机器人。发现客户不是要顶点的中心点坐标&#xff0c;而是要侧面中心点坐标。因为他们是从侧面抓…

Spring中Emable和Import相关操作

05-SpringBoot自动配置-Enable注解原理 SpringBoot不能直接获取在其他工程中定义的Bean 演示代码&#xff1a; springboot-enable工程 /*** ComponentScan 扫描范围&#xff1a;当前引导类所在包及其子包** com.itheima.springbootenable* com.itheima.config* //1.使用Compo…

mysql数据库之sql优化

一、插入数据时的优化。 1、批量插入。 insert into tb_test values(1,Tom),(2,Cat),(3,Jerry); 2、手动提交事务。 start transaction; insert into tb_test values(1,Tom),(2,Cat),(3,Jerry); insert into tb_test values(4,Tom),(5,Cat),(6,Jerry); insert into tb_test…

【Web服务部署】使用gunicorn部署django程序并一键更新

Web服务部署 【Linux防火墙】网络ip和端口管理 Windows云服务器使用IIS搭建PythonDjangoMysql网站&#xff0c;以及如何部署多个网站 WindowsIIS部署多个Django网站 文章目录Web服务部署前言一、Gunicorn是什么&#xff1f;二、Gunicorn基本使用1.引入库2.常用命令3.gunicorn快…

【Servlet篇2】Servlet的工作过程,Servlet的api——HttpServletRequest

一、Servlet的工作过程 二、Tomcat的初始化 步骤1&#xff1a;寻找到当前目录下面所有需要加载的Servlet(也就是类) 步骤2&#xff1a;根据类加载的结果创建实例(通过反射)&#xff0c;并且放入集合当中 步骤3&#xff1a;实例创建好之后&#xff0c;调用Servlet的init()方…

SpringBoot接口+Redis解决用户重复提交问题

前言 1. 为什么会出现用户重复提交 网络延迟的情况下用户多次点击submit按钮导致表单重复提交&#xff1b;用户提交表单后&#xff0c;点击【刷新】按钮导致表单重复提交&#xff08;点击浏览器的刷新按钮&#xff0c;就是把浏览器上次做的事情再做一次&#xff0c;因为这样也…

前后端分页查询好大的一个坑(已解决)

前言&#xff1a;如果你在做前后端的分页查询&#xff0c;找不到错误&#xff0c;请你来看看是否是和我一样的情况&#xff1f;情况&#xff1a;做了一个前后盾UI的项目&#xff0c;有一个页面是查询系统日志&#xff0c;要进行分页查询&#xff1b;第一页的&#xff1a;第5页的…

MYSQL 基础篇 | 02-MYSQL基础应用

文章目录1 MySQL概述2 SQL2.1 SQL通用语法2.2 SQL分类2.3 DDL2.3.1 数据库操作2.3.2 表操作2.4 DML2.4.1 添加数据2.4.2 修改数据2.4.3 删除数据2.5 DQL2.5.1 基础查询2.5.2 条件查询2.5.3 聚合查询2.5.4 分组查询2.5.5 排序查询2.5.6 分页查询2.5.7 综合练习2.6 DCL2.6.1 管理…

SQLMAP使用

SQLMAPSQLMAP是什么SQLMAP可以对URL做什么&#xff1f;SQLMAP支持的注入技术SQLMAP检测注入漏洞流程&#xff1a;SQLMAP的误报检测机制SQLMAP基本使用常见用法常见参数tamper参数操作系统权限参数文件读写参数SQLMAP是什么 SQLMAP是一个开源的自动化SQL注入工具&#xff0c;主…

SAP 生产订单/流程订单中日期的解释

SAP 生产订单/流程订单中日期的解释 基本开始日期&#xff1a;表示订单的开始日期 基本完成日期&#xff1a;表示订单的完成日期 我们在输入基本开始日期和基本完成日期时需要关注 调度 下面的“类型”&#xff0c;其中有向前、向后、当天日期等&#xff1a; 调度类型 为向前…

上海交大陈海波教授、夏虞斌教授领衔巨作上市:《操作系统:原理与实现》

❤️作者主页&#xff1a;小虚竹 ❤️作者简介&#xff1a;大家好,我是小虚竹。2022年度博客之星评选TOP 10&#x1f3c6;&#xff0c;Java领域优质创作者&#x1f3c6;&#xff0c;CSDN博客专家&#x1f3c6;&#xff0c;华为云享专家&#x1f3c6;&#xff0c;掘金年度人气作…