你的APP内存还在暴增吗?试着用Bitmap管理下内存~

作者:layz4android

相信伙伴们在日常的开发中,一定对图片加载有所涉猎,而且对于图片加载现有的第三方库也很多,例如Glide、coil等,使用这些三方库我们好像就没有啥担忧的,他们内部的内存管理和缓存策略做的很好,但是一旦在某些场景中无法使用图片加载库,或者项目中没有使用图片加载库而且重构难度大的情况下,对于Bitmap内存的管理就显得尤为重要了,一旦使用出现问题,那么OOM是常有的事。

在Android 8.0之后,Bitmap的内存分配从Java堆转移到了Native堆中,所以我们可以通过Android profiler性能检测工具查看内存使用情况。

未经过内存管理,列表滑动前内存状态:

列表滑动时,内存状态:

通过上面两张图我们可以发现,Java堆区的内存没有变化,但是Native的内存发生了剧烈的抖动,而且伴随着频繁的GC,如果有了解JVM的伙伴,这种情况下必定伴随着应用的卡顿,所以对于Bitmap加载,就要避免频繁地创建和回收,因此本章将会着重介绍Bitmap的内存管理。

1 Bitmap“整容”

首先我们需要明确一点,既然是内存管理,难道只是对图片压缩保证不会OOM吗?其实不是的,内存管理一定是多面多点的,压缩是一方面,为什么起标题为“整容”,是因为最终加载到内存的Bitmap一定不是单纯地通过decodeFile就能完成的。

1.1 Bitmap内存复用

上图内存状态对应的列表代码如下:

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    bindBitmap(holder)
}

///sdcard/img.png
private fun bindBitmap(holder: ViewHolder) {
    val bitmap = BitmapFactory.decodeFile("/sdcard/img.png")
    holder.binding.ivImg.setImageBitmap(bitmap)
}

如果熟悉RecyclerView的缓存机制应该了解,当RecyclerView的Item移出页面之后,会放在缓存池当中;当下面的item显示的时候,首先会从缓存池中取出缓存,直接调用onBindViewHolder方法,所以依然会重新创建一个Bitmap,因此针对列表的缓存特性可以选择Bitmap内存复用机制。

看上面这张图,因为顶部的Item在新建的时候,已经在native堆区中分配了一块内存,所以当这块区域被移出屏幕的时候,下面显示的Item不需要再次分配内存空间,而是复用移出屏幕的Item的内存区域,从而避免了频繁地创建Bitmap导致内存抖动。

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    bindBitmap(holder)
}

///sdcard/img.png
private fun bindBitmap(holder: ViewHolder) {

    if (option == null) {
        option = BitmapFactory.Options()
        //开启内存复用
        option?.inMutable = true
    }
    val bitmap = BitmapFactory.decodeFile("/sdcard/img.png", option)
    option?.inBitmap = bitmap
    holder.binding.ivImg.setImageBitmap(bitmap)
}
复制代码

那么如何实现内存复用,在BitmapFactory中提供了Options选项,当设置inMutable属性为true之后,就代表开启了内存复用,此时如果新建了一个Bitmap,并将其添加到inBitmap中,那么后续所有Bitmap的创建,只要比这块内存小,那么都会放在这块内存中,避免重复创建。

滑动前:

滑动时:

通过上图我们发现,即便是在滑动的时候,Native内存都没有明显的变化。

1.2 Bitmap压缩

像1.1中这种加载形式,其实都是会直接将Bitmap加载到native内存中,例如我们设置的ImageView只有100*100,那么图片的大小为1000 * 800,其实是不需要将这么大体量的图片直接加载到内存中,那么有没有一种方式,在图片加载到内存之前就能拿到这些基础信息呢?

当然有了,这里还是要搬出BitmapFactory.Option这个类,其中inJustDecodeBounds这个属性的含义,从字面意思上就可以看出,只解码边界,也就是意味着在加载内存之前,是会拿到Bitmap的宽高的,注意需要成对出现,开启后也需要关闭。

private fun bindBitmap(holder: ViewHolder) {

    if (option == null) {
        option = BitmapFactory.Options()
        //开启内存复用
        option?.inMutable = true
    }

    //在加载到内存之前,获取图片的基础信息
    option?.inJustDecodeBounds = true
    BitmapFactory.decodeFile("/sdcard/img.png",option)
    //获取宽高
    val outWidth = option?.outWidth ?: 100
    val outHeight = option?.outHeight ?: 100
    //计算缩放系数
    option?.inSampleSize = calculateSampleSize(outWidth, outHeight, 100, 100)
    option?.inJustDecodeBounds = false
    val bitmap = BitmapFactory.decodeFile("/sdcard/img.png", option)
    option?.inBitmap = bitmap
    holder.binding.ivImg.setImageBitmap(bitmap)
}

private fun calculateSampleSize(
    outWidth: Int,
    outHeight: Int,
    maxWidth: Int,
    maxHeight: Int
): Int? {
    var sampleSize = 1
    Log.e("TAG","outWidth $outWidth outHeight $outHeight")
    if (outWidth > maxWidth && outHeight > maxHeight) {
        sampleSize = 2
        while (outWidth / sampleSize > maxWidth && outHeight / sampleSize > maxHeight) {
            sampleSize *= 2
        }
    }
    return sampleSize
}

然后会需要计算一个压缩的系数,给BitmapFactory.Option类的inSampleSize赋值,这样Bitmap就完成了缩放,我们再次看运行时的内存状态。

Native内存几乎下降了一半。

2 手写图片缓存框架

在第一节中,我们对于Bitmap自身做了一些处理,例如压缩、内存复用。虽然做了这些处理,但是不足以作为一个优秀的框架对外输出。

为什么呢?像1.2节中,我们虽然做了内存复用以及压缩,但是每次加载图片都需要重新调用decodeFile拿到一个bitmap对象,其实这都是同一张图片,即便是在项目中,肯定也存在相同的图片,那么我们肯定不能重复加载,因此对于加载过的图片我们想缓存起来,等到下次加载的时候,直接拿缓存中的Bitmap,其实也是加速了响应时间。

2.1 内存缓存

首先一个成熟的图片加载框架,三级缓存是必须的,像Glide、coil的缓存策略,如果能把这篇文章搞懂了,那么就全通了。

在Android中,提供了LruCache这个类,也是内存缓存的首选,如果熟悉LruCache的伙伴,应该明白其中的原理。它其实是一个双向链表,以最近少用原则,当缓存中的数据长时间不用,而且有新的成员加入进来之后,就会移除尾部的成员,那么我们首先搞定内存缓存。

class BitmapImageCache {

    private var context: Context? = null

    //默认关闭
    private var isEnableMemoryCache: Boolean = false
    private var isEnableDiskCache: Boolean = false

    constructor(builder: Builder) {
        this.context = context
        this.isEnableMemoryCache = builder.isEnableMemoryCache
        this.isEnableDiskCache = builder.isEnableDiskCache
    }

    class Builder {

        var context: Context? = null

        //是否开启内存缓存
        var isEnableMemoryCache: Boolean = false

        //是否开启磁盘缓存
        var isEnableDiskCache: Boolean = false

        fun with(context: Context): Builder {
            this.context = context
            return this
        }

        fun enableMemoryCache(isEnable: Boolean): Builder {
            this.isEnableMemoryCache = isEnable
            return this
        }

        fun enableDiskCache(isEnable: Boolean): Builder {
            this.isEnableDiskCache = isEnable
            return this
        }

        fun build(): BitmapImageCache {
            return BitmapImageCache(this)
        }
    }
}

基础框架采用建造者设计模式,基本都是一些开关,控制是否开启内存缓存,或者磁盘缓存,接下来进行一些初始化操作。

首先对于内存缓存,我们使用LruCache,其中有两个核心的方法:sizeOf和entryRemoved,方法的作用已经在注释里了。

class BitmapLruCache(
    val size: Int
) : LruCache<String, Bitmap>(size) {

    /**
     * 告诉系统Bitmap内存的大小
     */
    override fun sizeOf(key: String, value: Bitmap): Int {
        return value.allocationByteCount
    }

    /**
     * 当Lru中的成员被移除之后,会走到这个回调
     * @param oldValue 被移除的Bitmap
     */
    override fun entryRemoved(evicted: Boolean, key: String, oldValue: Bitmap, newValue: Bitmap?) {
        super.entryRemoved(evicted, key, oldValue, newValue)

    }
}

当LruCache中元素被移除之后,我们想是不是就需要回收了,那这样的话其实就错了。记不记得我们前面做的内存复用策略,如果当前Bitmap内存是可以被复用的,直接回收掉,那内存复用就没有意义了,所以针对可复用的Bitmap,可以放到一个复用池中,保证其在内存中

/**
 * 当Lru中的成员被移除之后,会走到这个回调
 * @param oldValue 被移除的Bitmap
 */
override fun entryRemoved(evicted: Boolean, key: String, oldValue: Bitmap, newValue: Bitmap?) {

    if (oldValue.isMutable) {
        //放入复用池
        reusePool?.add(WeakReference(oldValue))
    } else {
        //回收即可
        oldValue.recycle()
    }
}

所以这里加了一个判断,当这个Bitmap是支持内存复用的话,就加到复用池中,保证其他Item在复用内存的时候不至于找不到内存地址,前提是还没有被回收;那么这里就有一个问题,当复用池中的对象(弱引用)被释放之后,Bitmap如何回收呢?与弱引用配套的有一个引用队列,当弱引用被GC回收之后,会被加到引用队列中。

class BitmapLruCache(
    val size: Int,
    val reusePool: MutableSet<WeakReference<Bitmap>>?,
    val referenceQueue: ReferenceQueue<Bitmap>?
) : LruCache<String, Bitmap>(size) {

    /**
     * 告诉系统Bitmap内存的大小
     */
    override fun sizeOf(key: String, value: Bitmap): Int {
        return value.allocationByteCount
    }

    /**
     * 当Lru中的成员被移除之后,会走到这个回调
     * @param oldValue 被移除的Bitmap
     */
    override fun entryRemoved(evicted: Boolean, key: String, oldValue: Bitmap, newValue: Bitmap?) {

        if (oldValue.isMutable) {
            //放入复用池
            reusePool?.add(WeakReference(oldValue, referenceQueue))
        } else {
            //回收即可
            oldValue.recycle()
        }
    }
}

这里需要公开一个方法,开启一个线程一直检测引用队列中是否有复用池回收的对象,如果拿到了那么就主动销毁即可。

/**
 * 开启弱引用回收检测,目的为了回收Bitmap
 */
fun startWeakReferenceCheck() {
    //开启一个线程
    Thread {
        try {
            while (!shotDown) {
                val reference = referenceQueue?.remove()
                val bitmap = reference?.get()
                if (bitmap != null && !bitmap.isRecycled) {
                    bitmap.recycle()
                }
            }
        } catch (e: Exception) {

        }

    }.start()
}

另外再加几个方法,主要就是往缓存中加数据。

fun putCache(key: String, bitmap: Bitmap) {
    lruCache?.put(key, bitmap)
}

fun getCache(key: String): Bitmap? {
    return lruCache?.get(key)
}

fun clearCache() {
    lruCache?.evictAll()
}

初始化的操作,我们把它放在Application中进行初始化操作

class MyApp : Application() {

    override fun onCreate() {
        super.onCreate()
        bitmapImageCache = BitmapImageCache.Builder()
            .enableMemoryCache(true)
            .with(this)
            .build()
        //开启内存检测
        bitmapImageCache?.startWeakReferenceCheck()
    }

    companion object {
        @SuppressLint("StaticFieldLeak")
        @JvmStatic
        var bitmapImageCache: BitmapImageCache? = null
    }
}

从实际的效果中,我们可以看到:

2023-02-18 17:54:10.154 32517-32517/com.lay.nowinandroid E/TAG: outWidth 800 outHeight 560
2023-02-18 17:54:10.154 32517-32517/com.lay.nowinandroid E/TAG: 没有从缓存中获取
2023-02-18 17:54:10.169 32517-32517/com.lay.nowinandroid E/TAG: 从缓存中获取 Bitmap
2023-02-18 17:54:10.187 32517-32517/com.lay.nowinandroid E/TAG: 从缓存中获取 Bitmap
2023-02-18 17:54:16.740 32517-32517/com.lay.nowinandroid E/TAG: 从缓存中获取 Bitmap
2023-02-18 17:54:16.756 32517-32517/com.lay.nowinandroid E/TAG: 从缓存中获取 Bitmap
2023-02-18 17:54:16.926 32517-32517/com.lay.nowinandroid E/TAG: 从缓存中获取 Bitmap
2023-02-18 17:54:17.102 32517-32517/com.lay.nowinandroid E/TAG: 从缓存中获取 Bitmap

其实加了内存缓存之后,跟inBitmap的价值基本就是等价的了,也是为了避免频繁地申请内存,可以认为是一个双保险,加上对图片压缩以及LruCache的缓存策略,真正内存打满的场景还是比较少的。

2.2 复用池的处理

在前面我们提到了,为了保证可复用的Bitmap不被回收,从而加到了一个复用池中,那么当从缓存中没有取到数据的时候,就会从复用池中取,相当于是在内存缓存中加了一个二级缓存。

针对上述图中的流程,可以对复用池进行处理。

/**
 * 从复用池中取数据
 */
fun getBitmapFromReusePool(width: Int, height: Int, sampleSize: Int): Bitmap? {

    var bitmap: Bitmap? = null
    //遍历缓存池
    val iterator = reusePool?.iterator() ?: return null
    while (iterator.hasNext()) {
        val checkedBitmap = iterator.next().get()
        if (checkBitmapIsAvailable(width, height, sampleSize, bitmap)) {
            bitmap = checkedBitmap
            iterator.remove()
            //放在
            break
        }
    }

    return bitmap

}

/**
 * 检查当前Bitmap内存是否可复用
 */
private fun checkBitmapIsAvailable(
    width: Int,
    height: Int,
    sampleSize: Int,
    bitmap: Bitmap?
): Boolean {
    if (bitmap == null) {
        return false
    }
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
        return width < bitmap.width && height < bitmap.height && sampleSize == 1
    }
    var realWidth = 0
    var realHeight = 0
    //支持缩放
    if (sampleSize > 1) {
        realWidth = width / sampleSize
        realHeight = height / sampleSize
    }
    val allocationSize = realHeight * realWidth * getBitmapPixel(bitmap.config)
    return allocationSize <= bitmap.allocationByteCount
}

/**
 * 获取Bitmap的像素点位数
 */
private fun getBitmapPixel(config: Bitmap.Config): Int {
    return if (config == Bitmap.Config.ARGB_8888) {
        4
    } else {
        2
    }
}

这里需要注意一点就是,如果想要复用内存,那么申请的内存一定要比复用的这块内存小,否则就不能匹配上。

所以最终的一个流程就是(这里没考虑磁盘缓存,如果用过Glide就会知道,磁盘缓存会有问题),首先从内存中取,如果取到了,那么就直接渲染展示;如果没有取到,那么就从复用池中取出一块内存,然后让新创建的Bitmap复用这块内存。

//从内存中取
var bitmap = BitmapImageCache.getCache(position.toString())
if (bitmap == null) {
    //从复用池池中取
    val reuse = BitmapImageCache.getBitmapFromReusePool(100, 100, 1)
    Log.e("TAG", "从网络加载了数据")
    bitmap = ImageUtils.load(imagePath, reuse)
    //放入内存缓存
    BitmapImageCache.putCache(position.toString(), bitmap)
} else {
    Log.e("TAG", "从内存加载了数据")
}

最终的一个呈现就是:

2023-02-18 21:31:57.805 29198-29198/com.lay.nowinandroid E/TAG: 从网络加载了数据
2023-02-18 21:31:57.819 29198-29198/com.lay.nowinandroid E/TAG: outWidth 800 outHeight 560
2023-02-18 21:31:57.830 29198-29198/com.lay.nowinandroid E/TAG: 加入复用池 android.graphics.Bitmap@6c19c7b
2023-02-18 21:31:57.830 29198-29198/com.lay.nowinandroid E/TAG: setImageBitmap android.graphics.Bitmap@473ed07
2023-02-18 21:31:57.849 29198-29198/com.lay.nowinandroid E/TAG: 从网络加载了数据
2023-02-18 21:31:57.857 29198-29198/com.lay.nowinandroid E/TAG: outWidth 788 outHeight 514
2023-02-18 21:31:57.871 29198-29198/com.lay.nowinandroid E/TAG: 加入复用池 android.graphics.Bitmap@2a7844
2023-02-18 21:31:57.872 29198-29198/com.lay.nowinandroid E/TAG: setImageBitmap android.graphics.Bitmap@4d852a3
2023-02-18 21:31:57.917 29198-29198/com.lay.nowinandroid E/TAG: 从网络加载了数据
2023-02-18 21:31:57.943 29198-29198/com.lay.nowinandroid E/TAG: outWidth 34 outHeight 8
2023-02-18 21:31:57.958 29198-29198/com.lay.nowinandroid E/TAG: setImageBitmap android.graphics.Bitmap@a3d491e
2023-02-18 21:31:58.651 29198-29198/com.lay.nowinandroid E/TAG: 从内存加载了数据
2023-02-18 21:31:58.651 29198-29198/com.lay.nowinandroid E/TAG: setImageBitmap android.graphics.Bitmap@62fcf27
2023-02-18 21:31:58.706 29198-29198/com.lay.nowinandroid E/TAG: 从内存加载了数据
2023-02-18 21:31:58.707 29198-29198/com.lay.nowinandroid E/TAG: setImageBitmap android.graphics.Bitmap@e2f8a1a
2023-02-18 21:31:58.766 29198-29198/com.lay.nowinandroid E/TAG: 从内存加载了数据

其实真正要保证我们的内存稳定,就是尽量避免重复创建对象,尤其是大图片,在加载的时候尤其需要注意,在项目中出现内存始终不降的主要原因也是对Bitmap的内存管理不当,所以掌握了上面的内容,就可以针对这些问题进行优化。总之万变不离其宗,内存是App的生命线,如果在面试的时候问你如何设计一个图片加载框架,内存管理是核心,当出现文章一开头那样的内存曲线的时候,就需要重点关注你的Bitmap是不是又“乱飙”了。

附录 - ImageUtils

object ImageUtils {

    private val MAX_WIDTH = 100
    private val MAX_HEIGHT = 100

    /**
     * 加载本地图片
     * @param reuse 可以复用的Bitmap内存
     */
    fun load(imagePath: String, reuse: Bitmap?): Bitmap {

        val option = BitmapFactory.Options()
        option.inMutable = true
        option.inJustDecodeBounds = true

        BitmapFactory.decodeFile(imagePath, option)
        val outHeight = option.outHeight
        val outWidth = option.outWidth
        option.inSampleSize = calculateSampleSize(outWidth, outHeight, MAX_WIDTH, MAX_HEIGHT)

        option.inJustDecodeBounds = false
        option.inBitmap = reuse
        //新创建的Bitmap复用这块内存
        return BitmapFactory.decodeFile(imagePath, option)
    }

    private fun calculateSampleSize(
        outWidth: Int,
        outHeight: Int,
        maxWidth: Int,
        maxHeight: Int
    ): Int {
        var sampleSize = 1
        Log.e("TAG", "outWidth $outWidth outHeight $outHeight")
        if (outWidth > maxWidth && outHeight > maxHeight) {
            sampleSize = 2
            while (outWidth / sampleSize > maxWidth && outHeight / sampleSize > maxHeight) {
                sampleSize *= 2
            }
        }
        return sampleSize
    }
}

根据性能问题出现的情况不同,我们需要采用不同的性能优化手段,而目前还是有些人群对于性能优化中间的一些优化手段掌握的不是很熟练,因此针对性能优化中间的所有不同类型的优化手段进行了归类整理,有启动优化、内存优化、网络优化、卡顿优化、存储优化……等,整合成名为《Android 性能优化核心知识点手册》,大家可以参考下:

《APP 性能调优进阶手册》:https://qr18.cn/FVlo89

启动优化

内存优化

UI优化

网络优化

Bitmap优化与图片压缩优化

多线程并发优化与数据传输效率优化

体积包优化

《Android 性能调优核心笔记汇总》:https://qr18.cn/FVlo89

《Android 性能监控框架》:https://qr18.cn/FVlo89

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何实现Chatgpt写文章(附chatgpt3.5免费接口)

申明&#xff1a;本次只是说一下实现思路&#xff0c;官方的接口以及如何实现方式&#xff0c;本文没有提及&#xff0c;这次只是一个思路&#xff0c;若想代替人工完成质量还差的很远&#xff0c;请审核大大放行 今天再次优化了代码&#xff0c;修复了一些bug&#xff0c;考虑…

VUE 学习笔记(一)开发环境搭建

1、Visual Studio Code安装及使用 下载地址官网&#xff1a;https://code.visualstudio.com/ 直接点击下载按钮即可&#xff0c;会根据系统自动下载合适的版本&#xff0c;无需自行选择。 2、VSCode 上安装&#xff1a;JavaScript Debugger 目前 Debugger for Chrome 已经处…

使用向量机(SVM)算法的推荐系统部署实现

包括3个模块&#xff1a;数据预处理、模型训练及保存、模型测试&#xff0c;下面分别给出各模块的功能介绍及相关代码。 数据集下载链接为https://www.aitechclub.com/data-detail? data_id29&#xff0c;停用词典下载链接为http://www.datasoldier.net/archives/636。 1.数…

232:vue+openlayers选择左右两部分的地图,不重复,横向卷帘

第232个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+openlayers项目中自定义js实现横向卷帘。这个示例中从左右两个选择框中来选择不同的地图,做了不重复的处理,即同一个数组,两部分根据选择后的状态做disabled处理,避免重复选择。 直接复制下面的 vue+openlayers…

c语言—指针进阶

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; 给大家跳段街舞感谢支持&#xff01;ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ…

第13届蓝桥杯省赛真题剖析-2022年4月17日Scratch编程初中级组

[导读]&#xff1a;超平老师的《Scratch蓝桥杯真题解析100讲》已经全部完成&#xff0c;后续会不定期解读蓝桥杯真题&#xff0c;这是Scratch蓝桥杯真题解析第122讲。 第13届蓝桥杯省赛举办了两次&#xff0c;这是2022年4月17日举行的第一次省赛&#xff0c;比赛仍然采取线上形…

ChatGPT技术原理、研究框架,应用实践及发展趋势(附166份报告)

​ 一、AI框架重要性日益突显&#xff0c;框架技术发展进入繁荣期&#xff0c;国内AI框架技术加速发展&#xff1a; 1、AI框架作为衔接数据和模型的重要桥梁&#xff0c;发展进入繁荣期&#xff0c;国内外框架功能及性能加速迭代&#xff1b; 2、Pytorch、Tensorflow占据AI框…

因果推断14--DRNet论文和代码学习

目录 论文介绍 代码实现 DRNet ReadMe 因果森林 论文介绍 因果推断3--DRNet&#xff08;个人笔记&#xff09;_万三豹的博客-CSDN博客 摘要&#xff1a;估计个体在不同程度的治疗暴露下的潜在反应&#xff0c;对于医疗保健、经济学和公共政策等几个重要领域具有很高的实…

GFD563A101 3BHE046836R0101

GFD563A101 3BHE046836R0101 ABB 7寸触摸屏 PP874K 3BSE069273R1 控制面板 原装进口 ABB 7寸触摸屏 PP874M 3BSE069279R1 黑色坚固 船用认证面板 ABB AC 800M PM865K01 处理器单元 3BSE031151R6 PLC库存 ABB AC 800M控制器模块 PM861AK01 3BSE018157R1 PM861A ABB AC 800PEC PC…

Kafka系统整理 一

一、Kafka 概述 1.1 定义 Kafka传统定义&#xff1a;Kafka是一个分布式的基于发布/订阅模式的消息队列 (Message Queue), 主要应用于大数据实时处理领域。 kafka最新定义&#xff1a;kafka是一个开源的分布式事件流平台&#xff08;Event Streaming Platform&#xff09;, 被…

实验二 图像空间域频率域滤波

一&#xff0e;实验目的&#xff1a; 1. 模板运算是空间域图象增强的方法&#xff0c;也叫模板卷积。 &#xff08;1&#xff09;平滑&#xff1a;平滑的目的是模糊和消除噪声。平滑是用低通滤波器来完成&#xff0c;在空域中全是正值。 &#xff08;2&#xff09;锐化&…

Centos7安装部署Jenkins

Jenkins简介&#xff1a; Jenkins只是一个平台&#xff0c;真正运作的都是插件。这就是jenkins流行的原因&#xff0c;因为jenkins什么插件都有 Hudson是Jenkins的前身&#xff0c;是基于Java开发的一种持续集成工具&#xff0c;用于监控程序重复的工作&#xff0c;Hudson后来被…

【如何使用Arduino控制WS2812B可单独寻址的LED】

【如何使用Arduino控制WS2812B可单独寻址的LED】 1. 概述2. WS2812B 发光二极管的工作原理3. Arduino 和 WS2812B LED 示例3.1 例 13.2 例 24. 使用 WS2812B LED 的交互式 LED 咖啡桌4.1 原理图4.2 源代码在本教程中,我们将学习如何使用 Arduino 控制可单独寻址的 RGB LED 或 …

教育大数据总体解决方案(3)

为区县教育局提供标准制定、流程把控、实施监控、决策支持等服务&#xff0c;支持在全市统一的评价指标体系基础上&#xff0c;为各个区县提供个性化定制功能&#xff0c;各县能够在市统一评价指标体系内任意调整、增加二三级评价指标项&#xff0c;并可以调整对应指标项的分数…

SpringBoot 介绍

1.简介 SpringBoot最开始基于Spring4.0设计&#xff0c;是由Pivotal公司提供的框架。 SpringBoot发展史&#xff1a; 2003年Rod Johnson成立Interface公司&#xff0c;产品是SpringFramework2004年&#xff0c;Spring框架开源&#xff0c;公司改名为Spring Source2008年&…

我的面试八股(Java集合篇)

Java集合 两个抽象接口派生&#xff1a;一个是Collection接口,存放单一元素&#xff1b;一个是Map接口存放键值对。 Vector为什么是线程安全 简单&#xff0c;因为官方在可能涉及到线程不安全的操作都进行了synchronized操作&#xff0c;就自身源码就给你加了把锁。 Vector…

走进Vue【三】vue-router详解

目录&#x1f31f;前言&#x1f31f;路由&#x1f31f;什么是前端路由&#xff1f;&#x1f31f;前端路由优点缺点&#x1f31f;vue-router&#x1f31f;安装&#x1f31f;路由初体验1.路由组件router-linkrouter-view2.步骤1. 定义路由组件2. 定义路由3. 创建 router 实例4. 挂…

【Spark】RDD缓存机制

1. RDD缓存机制是什么&#xff1f; 把RDD的数据缓存起来&#xff0c;其他job可以从缓存中获取RDD数据而无需重复加工。 2. 如何对RDD进行缓存&#xff1f; 有两种方式&#xff0c;分别调用RDD的两个方法&#xff1a;persist 或 cache。 注意&#xff1a;调用这两个方法后并不…

处理用户输入

shell脚本编程系列 传递参数 向shell脚本传递数据的最简单方法是使用命令行参数 比如 ./add 10 30读取参数 bash shell会将所有的命令行参数都指派给位置参数的特殊变量。其中$0对应脚本名、$1是第一个参数、$2是第二个参数&#xff0c;依次类推&#xff0c;直到$9 #!/bin/b…

【星界探索——通信卫星】铱星:从“星光坠落”到“涅槃重生”,万字长文分析铱星卫星系统市场

【星界探索——通信卫星】铱星&#xff1a;从“星光坠落”到“涅槃重生”一、铱星简介二、铱星系统设计思路2.1 工作原理2.2 铱星布局三、铱星优势四、发展历程五、第一代铱星公司的破产原因分析5.1 终端和资费价格高昂&#xff0c;市场用户群体小5.2 财务危机5.3 市场分析不足…
最新文章