Android Datastore 动态创建与源码解析

news2024/11/26 22:27:21
涉及到的知识点

1、协程原理---->很好的博客介绍,一个小故事讲明白进程、线程、Kotlin 协程到底啥关系?
2、Channel知识点---->Android—kotlin-Channel超详细讲解
3、Coroutines : CompletableDeferred and structured concurrency

封装的DataStoreUtils工具—>gitHub

本篇博客目的

公司使用SharedPreferences容易导致ANR,调研能否使用DataStore替换公司目前的SharedPreferences解决ANR问题,所以需要先研究一下源码

目录
  • 版本引入
  • 迁移SharedPreferences数据到dataStore
  • 动态创建DataStore
  • 存储参数
  • 总结
版本引入
implementation "androidx.datastore:datastore-preferences:1.0.0"
迁移SharedPreferences数据到dataStore

既然是迁移数据,那么需要将SharedPreferences已存储的数据迁移到dataStore,所以需要先构建dataStore。
目前网上构建迁移DataStore的案例Demo如下

//迁移使用
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = "userSharePreFile",
    produceMigrations = { context ->
        listOf(
            SharedPreferencesMigration(
                context,
                 "userSharePreFile"
            )
        )
    }
)

//或 
//这种构建DataStore写法是alpha版本有的,在1.0.0版本就找不到了
var dataStore: DataStore<Preferences> = context.createDataStore(
        name = "userSharePreFile"
 )
//或
//直接构建
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
        name = "userSharePreFile"
)

上面3种写法都是对Context进行扩展创建的DataStore,所以上面创建的方式,都有一个缺点,就是需要提前知道name才能创建,如果你之前创建SharedPreferences的方式,是通过外部传递进来name构建的话,上面直接创建DataStore方式就显然不适合你了。

翻阅旧版本(alpha版本)源码,一探究竟如何构建DataStore
//alpha版本构建方式
var dataStore: DataStore<Preferences> = context.createDataStore(
        name = "userSharePreFile"
 )

fun Context.createDataStore(
    name: String,
    corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
    //①
    migrations: List<DataMigration<Preferences>> = listOf(),
    //②
    scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): DataStore<Preferences> =
    PreferenceDataStoreFactory.create(
        //③
        produceFile = {
            File(this.filesDir, "datastore/$name.preferences_pb")
        },
        corruptionHandler = corruptionHandler,
        migrations = migrations,
        scope = scope
    )

可以明显看到是使用PreferenceDataStoreFactory.create返回DataStore
① 是构建需要迁移SharedPreferences文件名称
② 指明协程是在IO运行
③ 新文件存储的位置
再看看另外一种通过 by preferencesDataStore 创建DataStore方式

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
        name = "userSharePreFile"
)

public fun preferencesDataStore(
    name: String,
    corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
    //①
    produceMigrations: (Context) -> List<DataMigration<Preferences>> = { listOf() },
    //②
    scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): ReadOnlyProperty<Context, DataStore<Preferences>> {
    return PreferenceDataStoreSingletonDelegate(name, corruptionHandler, produceMigrations, scope)
}

internal class PreferenceDataStoreSingletonDelegate internal constructor(
    private val name: String,
    private val corruptionHandler: ReplaceFileCorruptionHandler<Preferences>?,
    private val produceMigrations: (Context) -> List<DataMigration<Preferences>>,
    private val scope: CoroutineScope
) : ReadOnlyProperty<Context, DataStore<Preferences>> {

    private val lock = Any()

    @GuardedBy("lock")
    @Volatile
    private var INSTANCE: DataStore<Preferences>? = null

    override fun getValue(thisRef: Context, property: KProperty<*>): DataStore<Preferences> {
        return INSTANCE ?: synchronized(lock) {
            if (INSTANCE == null) {
                val applicationContext = thisRef.applicationContext

                INSTANCE = PreferenceDataStoreFactory.create(
                    corruptionHandler = corruptionHandler,
                    migrations = produceMigrations(applicationContext),
                    scope = scope
                ) {
                    applicationContext.preferencesDataStoreFile(name)
                }
            }
            INSTANCE!!
        }
    }
}

//文件存储位置
public fun Context.preferencesDataStoreFile(name: String): File =
    this.dataStoreFile("$name.preferences_pb")

题外话:这里有利用kotlin委托属性by关键字语法
① 需要迁移的SharedPreferences文件
② 协程运行在IO

可以看出旧版本(alpha) 与 by preferencesDataStore 2种方案,都最终通过PreferenceDataStoreFactory.create,返回DataStore,我们就继续再看看PreferenceDataStoreFactory.kt的具体实现逻辑

//PreferenceDataStoreFactory.kt
 public fun create(
        corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
        //迁移的share文件集合
        migrations: List<DataMigration<Preferences>> = listOf(),
         //IO
        scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
        //dataStore文件存储的目录位置
        produceFile: () -> File 
    ): DataStore<Preferences> {
        val delegate = DataStoreFactory.create(//创建SingleProcessDataStore
            serializer = PreferencesSerializer,
            corruptionHandler = corruptionHandler,
            migrations = migrations,
            scope = scope
        ) {
            //省略代码
        } 
        //传入SingleProcessDataStore
        return PreferenceDataStore(delegate)
    }

//这里有主动的去调用updateData 方法,如果不去主动调用,就不会触发迁移的逻辑
//下文的扩展函数DataStore<Preferences>.edit会说到这里
internal class PreferenceDataStore(private val delegate: DataStore<Preferences>) :
    DataStore<Preferences> by delegate {
    override suspend fun updateData(transform: suspend (t: Preferences) -> Preferences):
        Preferences {
            return delegate.updateData {
                val transformed = transform(it)
                (transformed as MutablePreferences).freeze()
                transformed
            }
        }
}

继续看DataStoreFactory.create

//DataStoreFactory.kt
fun <T> create(
        produceFile: () -> File,
        serializer: Serializer<T>,
        corruptionHandler: ReplaceFileCorruptionHandler<T>? = null,
        migrations: List<DataMigration<T>> = listOf(),
        scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
    ): DataStore<T> =
        //找到最终创建的类
        SingleProcessDataStore(
            produceFile = produceFile,
            serializer = serializer,
            corruptionHandler = corruptionHandler ?: NoOpCorruptionHandler(),
            initTasksList = listOf(DataMigrationInitializer.getInitializer(migrations)),
            scope = scope
        )

到目前为止已经知道真相了,最终是通过SingleProcessDataStore返回DataStore。

下面我们通过一张图片来小结一下,旧版本alpha版本的创建与新版本 by preferencesDataStore的调用逻辑链

DataStore.jpg

好,已经知道这么多了,那么我们就开始动态构建DataStore

动态创建DataStore
 fun preferencesMigrationDataStore(sharedPreferName: String) {
    val dataStore = PreferenceDataStoreFactory.create(
      corruptionHandler =  ReplaceFileCorruptionHandler<Preferences>(
        produceNewData = { emptyPreferences() }
      ),
    //需要迁移的sharePrefer文件的名称
     migrations = listOf(SharedPreferencesMigration(mContext, sharedPreferName)),
    //IO
     scope = CoroutineScope(Dispatchers.IO + SupervisorJob())) {
    //dataStore文件名称
     mContext.preferencesDataStoreFile(sharedPreferName)
     }
  
    runBlocking {
        //必须要执行这行代码,否是不会走迁移的逻辑
         dataStore.updateData {
              it.toPreferences()
           }
      }
    }

migrations:表示你要迁移的sharedPreference文件
scope :表示写数据是在IO
执行完上述代码后,.xml就会消失,然后会在files目录下多出一个/datastore/xxx.preferences_pb文件
切勿重复对某个SharedPreferences执行文件迁移方案,否则会报错。比如你前一秒在执行迁移,后一秒又继续执行迁移
SharedPrefs.png
dataStore_migrate.jpg

####存储参数

/**
 * @key 参数
 * @value 具体的值
 */
 private fun putInt(key:String, value: Int) {
    runBlocking {
         dataStore.edit {//①
                it[intPreferencesKey(key)] = value
          }
       }
   }
//类似的还有如下,这些都是google提供的参数
intPreferencesKey
doublePreferencesKey
stringPreferencesKey
....

看①详情,点击edit,发现他是一个扩展函数

public suspend fun DataStore<Preferences>.edit(
    transform: suspend (MutablePreferences) -> Unit
): Preferences {
    return this.updateData {//调用的是PreferenceDataStore.updateData()
        //it.toMutablePreferences() 返回类似map
        it.toMutablePreferences().apply { transform(this) }
    }
}

transform 就是调用者{}里面的内容,接下来我们看看 PreferenceDataStore 类的代码

//由前部分的代码,可以得知,delegate = SingleProcessDataStore 
internal class PreferenceDataStore(private val delegate: DataStore<Preferences>) :
    DataStore<Preferences> by delegate {
    override suspend fun updateData(transform: suspend (t: Preferences) -> Preferences):
        Preferences {
            return delegate.updateData {//调用SingleProcessDataStore.updateData 
                //返回给上一个{}也就是  it.toMutablePreferences().apply { transform(this) }
                val transformed = transform(it)
                (transformed as MutablePreferences).freeze()
                transformed //拿到用户的需要更改的内容数据
            }
        }
}

代码里调用了delegate.updateData(), 所以继续看SingleProcessDataStore的updateData

SingleProcessDataStore.kt
 override suspend fun updateData(transform: suspend (t: T) -> T): T {
        val ack = CompletableDeferred<T>()
        val currentDownStreamFlowState = downstreamFlow.value
        //协程体封装进Message.Update,coroutineContext 是协程的上下文,就是我们的 runBlocking 启动的线程,我这里是main
        val updateMsg = Message.Update(transform, ack, currentDownStreamFlowState, coroutineContext)
        //对消息进行分发,他的类是 SimpleActor
        actor.offer(updateMsg)
        //这里会拿到Preferences,如何拿?后面会有一个update.ack.completeWith方法,会返回回来
        return ack.await()
    }
internal class SimpleActor<T>(
    private val scope: CoroutineScope,//Dispatchers.IO + SupervisorJob()
    onComplete: (Throwable?) -> Unit,
    onUndeliveredElement: (T, Throwable?) -> Unit,
    private val consumeMessage: suspend (T) -> Unit
) {
    private val messageQueue = Channel<T>(capacity = UNLIMITED)
    private val remainingMessages = AtomicInteger(0)
    //......  省去
    //这里就是将刚刚封装的消息体,添加进这里
    fun offer(msg: T) {
        check(
            //发送封装的消息体
            messageQueue.trySend(msg)
                .onClosed { throw it ?: ClosedSendChannelException("Channel was closed normally") }
                .isSuccess
        )
        if (remainingMessages.getAndIncrement() == 0) {
            scope.launch {
                check(remainingMessages.get() > 0)
                do {
                   // scope = Dispatchers.IO + SupervisorJob()
                    scope.ensureActive()
                    //取出封装的消息体,然后进行任务处理
                    consumeMessage(messageQueue.receive())
                } while (remainingMessages.decrementAndGet() != 0)
            }
        }
    }
}

tip:这里有利用Channel进行协程通信,Channel是可以处理并发的情况
到这里,我们可以知道,我们由runBlocking(main主线程) 协程 到 Dispatchers.IO的任务分发

private val actor = SimpleActor<Message<T>>(
        scope = scope,// CoroutineScope(Dispatchers.IO + SupervisorJob())
        onComplete = {//.....省略},
        onUndeliveredElement = { msg, ex ->
          //.....省略
      ) { msg ->
        //处理分发的任务,msg 为刚刚封装的updateMsg 
        when (msg) { 
            is Message.Read -> {//读取
                handleRead(msg)
            }
            is Message.Update -> {//更新
                handleUpdate(msg)
            }
        }
    }
 private suspend fun handleUpdate(update: Message.Update<T>) {
        update.ack.completeWith(
            runCatching {
                when (val currentState = downstreamFlow.value) {
                    is Data -> {
                        //写数据到file
                        transformAndWrite(update.transform, update.callerContext)
                    }
                    is ReadException, is UnInitialized -> {
                        if (currentState === update.lastState) {           
                            //读取file文件      ①          
                            readAndInitOrPropagateAndThrowFailure()
                            //写数据到file       ②
                            transformAndWrite(update.transform, update.callerContext)
                        } else {
                            throw (currentState as ReadException).readException
                        }
                    }

                    is Final -> throw currentState.finalException // won't happen
                }
            }
        )
    }

第一次使用 downstreamFlow.value = UnInitialized 。
这里要注意一下update.ack.completeWith这个函数,他是拿到结果成功返回

这里再次展示出来,是告诉大家,在哪里会等待结果返回
 override suspend fun updateData(transform: suspend (t: T) -> T): T {
        val ack = CompletableDeferred<T>()
        val currentDownStreamFlowState = downstreamFlow.value
        val updateMsg =
            Message.Update(transform, ack, currentDownStreamFlowState, coroutineContext)
        actor.offer(updateMsg)
        return ack.await() //这里就是等待 update.ack.completeWith的结果返回,所以如果不加这行,是不会卡主线程的
    }

所以使用runBlocking是会卡主线程的,如果你还有UI刷新情况,严重的情况会导致ANR问题

不扯之前的了,我们继续继续,看① 的读取

 private suspend fun readAndInitOrPropagateAndThrowFailure() {
        try {
            readAndInit()
        } catch (throwable: Throwable) {
            downstreamFlow.value = ReadException(throwable)
            throw throwable
        }
    }

 private suspend fun readAndInit() {
        check(downstreamFlow.value == UnInitialized || downstreamFlow.value is ReadException)
        //这个是锁,协程里面专有的,详情可以看 https://www.kotlincn.net/docs/reference/coroutines/shared-mutable-state-and-concurrency.html
        val updateLock = Mutex()
        //读取dataStore文件
        var initData = readDataOrHandleCorruption()
        var initializationComplete: Boolean = false
        
        //这里就是shareprefence转dataStore
        val api = object : InitializerApi<T> {
            override suspend fun updateData(transform: suspend (t: T) -> T): T {
                return updateLock.withLock() {
                    if (initializationComplete) {
                        throw IllegalStateException(
                            "InitializerApi.updateData should not be " +
                                "called after initialization is complete."
                        )
                    }
                    //transform里面就是去迁移数据的方法
                    val newData = transform(initData)
                    //这里有做,新 旧值比较,如果不同,就去写入
                    if (newData != initData) {
                        //写文件
                        writeData(newData)
                        initData = newData
                    }
                    initData
                }
            }
        }
        //initTasks 里面装的就是需要转换的 SharedPreferences集合
        initTasks?.forEach { it(api) }
        initTasks = null
        updateLock.withLock {
            initializationComplete = true
        }
        //这里有将迁移完成后的数据,存储在flow.value里面
        downstreamFlow.value = Data(initData, initData.hashCode())
    }

//读取dataStore文件
private suspend fun readDataOrHandleCorruption(): T {
        try {
            return readData()
        } catch (ex: CorruptionException) {
            val newData: T = corruptionHandler.handleCorruption(ex)
            try {
                writeData(newData)
            } catch (writeEx: IOException) {
                ex.addSuppressed(writeEx)
                throw ex
            }
            return newData
        }
    }

 private suspend fun readData(): T {
        try {
            FileInputStream(file).use { stream ->
                return serializer.readFrom(stream)
            }
        } catch (ex: FileNotFoundException) {
            if (file.exists()) {
                throw ex
            }
            return serializer.defaultValue
        }
    }

file就是我们存储的dataStore,目录是在 “datastore/$name.preferences_pb”

看完了①,再来看看② 写入数据到file,写数据的方法是 transformAndWrite()

//....
transformAndWrite(update.transform, update.callerContext)
//...

 private suspend fun transformAndWrite(
         //来源于 Message.Update.transform封装
        transform: suspend (t: T) -> T,
        //来源于 Message.Update.callerContext封装
        callerContext: CoroutineContext
    ): T {
        val curDataAndHash = downstreamFlow.value as Data<T>
        curDataAndHash.checkHashCode()

        val curData = curDataAndHash.value
        //这里callerContext  就是我们的 runBlocking,main(主线程)
        //这里是将旧的值给回调用者,然后从调用者获取到新参数
        val newData = withContext(callerContext) { transform(curData) }

        curDataAndHash.checkHashCode()
        //这里有做数据比较
        return if (curData == newData) {
            curData
        } else {
            //写入数据
            writeData(newData)
            //保存到flow.value里面
            downstreamFlow.value = Data(newData, newData.hashCode())
            newData
        }
    }

private val SCRATCH_SUFFIX = ".tmp"
//写入数据
internal suspend fun writeData(newData: T) {
        file.createParentDirectories()
        //这里创建出来的文件是"datastore/$name.preferences_pb.tmp"
        val scratchFile = File(file.absolutePath + SCRATCH_SUFFIX)
        try {
            FileOutputStream(scratchFile).use { stream ->
                serializer.writeTo(newData, UncloseableOutputStream(stream))
                stream.fd.sync()
            }
            //重新命名回去file,这里的file是我们目标的文件dataStore名称
            if (!scratchFile.renameTo(file)) {
                //重新命名失败,抛出异常
                throw IOException(
                    "Unable to rename $scratchFile." +
                        "This likely means that there are multiple instances of DataStore " +
                        "for this file. Ensure that you are only creating a single instance of " +
                        "datastore for this file."
                )
            }
        } catch (ex: IOException) {
            if (scratchFile.exists()) {
                scratchFile.delete() 
            }
            throw ex
        }
    }

到此,更新值的操作,我们已经全部走完了流程

总结

1、文件的写入是发生在IO层面
2、使用runBlocking是会卡主线程,如果此时存在需要刷新UI的情况,严重会ANR


/**
 * @key 参数
 * @value 具体的值
 */
 private fun putInt(key:String, value: Int) {
    runBlocking {
         dataStore.edit {
                it[intPreferencesKey(key)] = value
          }
       }
   }

public suspend fun DataStore<Preferences>.edit(
    transform: suspend (MutablePreferences) -> Unit
): Preferences {
    return this.updateData {
        it.toMutablePreferences().apply { transform(this) }
    }
}

//更新逻辑
 private suspend fun handleUpdate(update: Message.Update<T>) {
        update.ack.completeWith(//通知结果回调
            //.....省去
        )
    }

//transform 就是上面的{}里面的内容
 override suspend fun updateData(transform: suspend (t: T) -> T): T {
        val ack = CompletableDeferred<T>()
        val currentDownStreamFlowState = downstreamFlow.value
        val updateMsg =
            Message.Update(transform, ack, currentDownStreamFlowState, coroutineContext)
        actor.offer(updateMsg)
        return ack.await() //这里就是等待 update.ack.completeWith的结果返回,所以如果不加这行,是不会卡主线程的
        //题外话不加ack.await() 代码也会执行
    }

所以,可以考虑使用withContext(IO){读取/更新等待操作}

3、更新参数的时候,是会跟旧的值比较,如果值相同就不写入了,否则就写入到文件里面,并且更新flow.value的值

 return if (curData == newData) {
            curData
        } else {
            writeData(newData)
            downstreamFlow.value = Data(newData, newData.hashCode())
            newData
        }

4、解决并发问题,使用channel解决协程之间沟通与并发,单线程的IO更新文件与并发

5、如果已将SharedPreference迁移到DataStore,你就不要继续使用SharedPreferences了,如果继续使用SharedPreferences,会与DataStore的值不同了

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

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

相关文章

动态规划-丑数

** 描述 把只包含质因子2、3和5的数称作丑数&#xff08;Ugly Number&#xff09;。例如6、8都是丑数&#xff0c;但14不是&#xff0c;因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第 n个丑数。 数据范围&#xff1a; 0≤n≤2000 要求&#x…

Mysql数据库 9.SQL语言 查询语句 连接查询、子查询

连接查询 通过查询多张表&#xff0c;用连接查询进行多表联合查询 关键字&#xff1a;inner join 内连接 left join 左连接 right join 右连接 数据准备 创建新的数据库&#xff1a;create database 数据库名; create database db_test2; 使用数据库&#xff1a;use 数据…

企业如何搭建智能客服系统?

在数字化时代&#xff0c;企业面临着客户需求多样化、市场竞争日益激烈等多重挑战。为了更好地满足客户的需求、提供高效的服务&#xff0c;越来越多的企业开始搭建智能客服系统。智能客服系统结合了人工智能和自然语言处理技术&#xff0c;可以实现自动回复、智能推荐以及数据…

Linux内核移植之主频设置

一. Linux内核移植 正点原子 ALPHA开发板已经添加到 Linux内核里面去了&#xff0c;前面文章关于如何添加已经掌握。但是&#xff0c;还有一些驱动的问题需要修改。 正点原子 I.MX6U-ALPHA 开发板所使用的 I.MX6ULL 芯片主频都是 792MHz 的&#xff0c;也就是NXP 官方宣…

MySQL的event的使用方法

MySQL的event的使用方法 一、事件定时策略 1、查看event事件开启状态 SHOW VARIABLES LIKE event_scheduler;如图&#xff0c;Value值 ON&#xff1a;打开&#xff0c;OFF&#xff1a;关闭。 2、设置event事件打开 SET GLOBAL event_scheduler ON;如果MySQL重启了&#x…

Python模块导入出现ModuleNotFoundError: No module named ‘***’解决方法

概述 几年没弄python了&#xff0c;全部还会给老师&#xff0c;今天弄了个demo&#xff0c;老是报错&#xff0c;在此记录下&#xff0c;方便后续查阅。 环境&#xff1a;Windows10 开发IDEA&#xff1a;PyCharm 2023.1.3 1、报错如下所示 2、解决方法&#xff1a;安装execjs…

AJAX-解决回调函数地狱问题

一、同步代码和异步代码 1.同步代码 浏览器是按照我们书写代码的顺序一行一行地执行程序的。浏览器会等待代码的解析和工作&#xff0c;在上一行完成之后才会执行下一行。这也使得它成为一个同步程序。 总结来说&#xff1a;逐行执行&#xff0c;需原地等待结果后&#xff0…

[python 刷题] 437 Path Sum III

[python 刷题] 437 Path Sum III 之前有写过 Path Sum I & II, leetcode 112 & 113&#xff0c;虽然使用 JS 写的&#xff0c;不过 python 的实现也更新了一下 题目如下&#xff1a; Given the root of a binary tree and an integer targetSum, return the number o…

解释一下Node.js中的事件循环(event loop)

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

目前安卓、鸿蒙、澎湃的关系

1、了解AOSP是什么 AOSP全名为Android Open-Source Project&#xff0c;中文为安卓开源项目&#xff0c;开源即开放源代码。Android是一个基于Linux&#xff0c;由Google主导的开源系统。 2、AOSP谁的贡献最大&#xff1f; 3、华为的鸿蒙、小米的澎湃是套壳安卓吗&#xff1…

docker基础用法

docker基础用法 什么是docker docker中的容器&#xff1a; lxc --> libcontainer --> runC OCI Open Container-initiative 由Linux基金会主导于2015年6月创立旨在围绕容器格式和运行时制定一个开放的工业化标准contains two specifications the Runtime Specificat…

类的基本概念

类的概念是为了让程序设计语言能更清楚地描述日常生活中的事物。类是对某一类事物的描述&#xff0c;是抽象的、概念上的定义&#xff1b;而对象则是实际存在的属该类事物的具体个体&#xff0c;因而也称为实例&#xff08;instance&#xff09;。下面用一个现实生活中的例子来…

远程管理SSH服务

一、搭建SSH服务 1、关闭防火墙与SELinux # 关闭firewalld防火墙 # 临时关闭 systemctl stop firewalld # 关闭开机自启动 systemctl disable firewalld ​ # 关闭selinux # 临时关闭 setenforce 0 # 修改配置文件 永久关闭 vim /etc/selinux/config SELINUXdisabled 2、配置…

XShelll-修改快捷键-xftp-修改编辑器

文章目录 1.XShelll-修改快捷键2.Xftp-修改文本编辑器3.总结 1.XShelll-修改快捷键 工具>选项 鼠标键盘&#xff0c;右键编辑&#xff0c;新建快捷键。 复制粘贴改成shiftc,shiftv。更习惯一些。 2.Xftp-修改文本编辑器 xftp修改服务器文件默认的编辑器&#xff0c;是记…

2.3 - 网络协议 - ICMP协议工作原理,报文格式,抓包实战

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;对网络安全感兴趣的小伙伴可以关注专栏《网络安全入门到精通》 ICMP协议 1、ICMP协议工作原理2、ICMP协议报文格式…

mysql 全文检索 demo

mysql5.6.7之后开始支持中文全文检索一直没用过&#xff0c;这次试试。 创建表 CREATE TABLE articles (id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,title VARCHAR (200),body TEXT,FULLTEXT (title, body) WITH PARSER ngram ) ENGINE INNODB DEFAULT CHARSETut…

维度使用AOP添加Name

1.添加文件 2.DimName注解,实体使用 package annotation;import MateTypeEnum;import java.lang.annotation.*;/*** 字典翻译注解** author pw*/ Documented Target(ElementType.FIELD)// 可用在方法名上 Retention(RetentionPolicy.RUNTIME)// 运行时有效 public interface…

Angew. Chem. Int. Ed.:Pt/Cu(111)上持续的氢溢出:气体诱导化学过程的动态观察

氢溢出是指游离氢原子从活性金属位点向相对惰性催化剂载体的表面迁移&#xff0c;在涉及氢的催化过程中起着至关重要的作用。然而&#xff0c;对氢原子如何从活性位点溢出到催化剂载体上的全面理解仍然缺乏。 基于此&#xff0c;福州大学林森教授等人报道了利用基于DFT的机器学…

Git 行结束符:LF will be replaced by CRLF the next time Git touches it问题解决指南

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

访问者模式-操作复杂对象结构

商场里有许多的店铺&#xff0c;大致分为服装区、饮食区及休闲区&#xff0c;每天都会有络绎不绝的不同角色&#xff08;打工人、学生、有钱人&#xff09;的人来逛商场。商场想开发个系统用来模拟这些人的在这些店铺的行为。 public class SuperMarket {public static void m…