Android SharedPreferences转为MMKV

news2024/11/26 18:37:11

开篇

开局一张图,说明一切问题。

MMKV优势

可以看出MMKV相比SP的优势还是比较大的,除了需要引入库,有一些修改上的成本以外,就没有什么能够阻挡MMKV了。当然了,MMKV也有着不广为人知的缺点,放在最后。
MMKV还直接支持了将SharedPreferences的历史数据转换为MMKV进行存储,只不过需要注意一点,不可回退。

且听我慢慢道来

SP具体存在哪些问题

  • 容易anr,无论是commit、apply、getxxx都可能导致ANR。
    SharedPreferences 本身是一个接口,其具体的实现类是 SharedPreferencesImpl,而 Context 的各个和 SharedPreferences 相关的方法则是由 ContextImpl 来实现的。而每当我们获取到一个 SharedPreferences 对象时,这个对象将一直被保存在内存当中,如果SP文件过大,那么会对内存的占用是有很大的影响的。
    如果SP文件过大的话,在App启动的时候也会造成启动慢,甚至ANR的。
class ContextImpl extends Context {
    
    //根据应用包名缓存所有 SharedPreferences,根据 xmlFile 和具体的 SharedPreferencesImpl 对应上
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
 
    //根据 fileName 拿到对应的 xmlFile
    private ArrayMap<String, File> mSharedPrefsPaths;
 
}

如果我们在初始化 SharedPreferencesImpl 后紧接着就去 getValue 的话,势必也需要确保子线程已经加载完成后才去进行取值操作。SharedPreferencesImpl 就通过在每个 getValue 方法中调用 awaitLoadedLocked()方法来判断是否需要阻塞外部线程,确保取值操作一定会在子线程执行完毕后才执行。loadFromDisk()方法会在任务执行完毕后调用 mLock.notifyAll()唤醒所有被阻塞的线程。所以说,如果 SharedPreferences 存储的数据量很大的话,那么就有可能导致外部的调用者线程被阻塞,严重时甚至可能导致 ANR。当然,这种可能性也只是发生在加载磁盘文件完成之前,当加载完成后 awaitLoadedLocked()方法自然不会阻塞线程。这也是为什么第一次写入或者读取sp相比mmkv慢十多倍最主要的原因。

    @Override
    @Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            //判断是否需要让外部线程等待
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }
    
    @GuardedBy("mLock")
    private void awaitLoadedLocked() {
        if (!mLoaded) {
            // Raise an explicit StrictMode onReadFromDisk for this
            // thread, since the real read will be in a different
            // thread and otherwise ignored by StrictMode.
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
                //还未加载线程,让外部线程暂停等待
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }
    
    private void loadFromDisk() {
        ···
        synchronized (mLock) {
            mLoaded = true;
            mThrowable = thrown;
            // It's important that we always signal waiters, even if we'll make
            // them fail with an exception. The try-finally is pretty wide, but
            // better safe than sorry.
            try {
                if (thrown == null) {
                    if (map != null) {
                        mMap = map;
                        mStatTimestamp = stat.st_mtim;
                        mStatSize = stat.st_size;
                    } else {
                        mMap = new HashMap<>();
                    }
                }
                // In case of a thrown exception, we retain the old map. That allows
                // any open editors to commit and store updates.
            } catch (Throwable t) {
                mThrowable = t;
            } finally {
                //唤醒所有被阻塞的线程
                mLock.notifyAll();
            }
        }
    }
  • SP数据保存的格式为xml。相比ProtoBuffer来说,性能较弱。
    之前也是做过ProtoBuffer的原理,首先我们知道ProtoBuffer体积非常小,所以在存储上就占据了很大的优势。MMKV底层序列化和反序列化是ProtoBuffer实现的,所以在存储速度上也有着很大的优势。
  • 每次写入数据的时候是全量写入。假如xml有100条数据,当插入一条新的数据或者更新一条数据,SP会将全部的数据全部重新写入文件,这是造成SP写入慢的原因。
  • 当保存的数据较多时,会在进程中占用过多的内存。
    commit() 和 apply() 两个方法都会通过调用 commitToMemory() 方法拿到修改后的全量数据commitToMemory(),SharedPreferences 包含的所有键值对数据都存储在 mapToWriteToDisk 中,Editor 改动到的所有键值对数据都存储在 mModified 中。如果 mClear 为 true,则会先清空 mapToWriteToDisk,然后再遍历 mModified,将 mModified 中的所有改动都同步给 mapToWriteToDisk。最终 mapToWriteToDisk 就保存了要重新写入到磁盘文件中的全量数据,SharedPreferences 会根据 mapToWriteToDisk 完全覆盖掉旧的 xml 文件。
    // Returns true if any changes were made
    private MemoryCommitResult commitToMemory() {
        long memoryStateGeneration;
        boolean keysCleared = false;
        List<String> keysModified = null;
        Set<OnSharedPreferenceChangeListener> listeners = null;
        Map<String, Object> mapToWriteToDisk;
        synchronized (SharedPreferencesImpl.this.mLock) {
            // We optimistically don't make a deep copy until
            // a memory commit comes in when we're already
            // writing to disk.
            if (mDiskWritesInFlight > 0) {
                // We can't modify our mMap as a currently
                // in-flight write owns it.  Clone it before
                // modifying it.
                // noinspection unchecked
                mMap = new HashMap<String, Object>(mMap);
            }
            //拿到内存中的全量数据
            mapToWriteToDisk = mMap;
            mDiskWritesInFlight++;
            boolean hasListeners = mListeners.size() > 0;
            if (hasListeners) {
                keysModified = new ArrayList<String>();
                listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
            }
            synchronized (mEditorLock) {
                //用于标记最终是否改动到了 mapToWriteToDisk
                boolean changesMade = false;
                if (mClear) {
                    if (!mapToWriteToDisk.isEmpty()) {
                        changesMade = true;
                        //清空所有在内存中的数据
                        mapToWriteToDisk.clear();
                    }
                    keysCleared = true;
                    //恢复状态,避免二次修改时状态错位
                    mClear = false;
                }
                for (Map.Entry<String, Object> e : mModified.entrySet()) {
                    String k = e.getKey();
                    Object v = e.getValue();
                    // "this" is the magic value for a removal mutation. In addition,
                    // setting a value to "null" for a given key is specified to be
                    // equivalent to calling remove on that key.
                    if (v == this || v == null) { //意味着要移除该键值对
                        if (!mapToWriteToDisk.containsKey(k)) {
                            continue;
                        }
                        mapToWriteToDisk.remove(k);
                    } else { //对应修改键值对值的情况
                        if (mapToWriteToDisk.containsKey(k)) {
                            Object existingValue = mapToWriteToDisk.get(k);
                            if (existingValue != null && existingValue.equals(v)) {
                                continue;
                            }
                        }
                        //只有在的确是修改了或新插入键值对的情况才需要保存值
                        mapToWriteToDisk.put(k, v);
                    }
                    changesMade = true;
                    if (hasListeners) {
                        keysModified.add(k);
                    }
                }
                //恢复状态,避免二次修改时状态错位
                mModified.clear();
                if (changesMade) {
                    mCurrentMemoryStateGeneration++;
                }
                memoryStateGeneration = mCurrentMemoryStateGeneration;
            }
        }
        return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
                listeners, mapToWriteToDisk);
    }
  • 不支持多进程模式,想实现需要配合跨进程通讯。
    如果想要实现多进程共享数据,就需要自己去实现跨进程通讯,比如ContentProvider、AIDL、或者自己直接实现Binder等方式。

MMKV的优点

  • MMKV实现了SharedPreferences接口,基本可以无缝切换。
    MMKV提供了API可以直接将SP存储的内容直接转向MMKV存储,不可回退。
SharedPreferences sources = context.getSharedPreferences(name, mode);
mmkv.importFromSharedPreferences(sources);
  • 通过mmap映射文件,通过一次拷贝。
    通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。通过内存映射实现了文件到用户空间只需要一次拷贝,而SP则需要两次拷贝。
    mmap 是 linux 提供的一种内存映射文件的方法,即将一个文件或者其他对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对应关系;实现这样的映射关系后,进程就可以采用指针的方式读写操作这一块内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必调用read,write等系统调用函数。
    Binder 的底层也是通过了 mmap 来实现一次内存拷贝的多进程通讯,所以MMKV也不用担心多进程下的数据持久化。
  • MMKV数据存储序列化方面选用 protobuf 协议。
    该协议类比xml有如下几个有点:
    • 语言无关、平台无关。即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台
    • 高效。即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单
    • 扩展性、兼容性好。你可以更新数据结构,而不影响和破坏原有的旧程序
  • MMKV是增量更新,有性能优势。
    增量 kv 对象序列化后,直接 append 到内存末尾;这样同一个 key 会有新旧若干份数据,最新的数据在最后;那么只需在程序启动第一次打开 mmkv 时,不断用后读入的 value 替换之前的值,就可以保证数据是最新有效的。用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。例如同一个 key 不断更新的话,是可能耗尽几百 M 甚至上 G 空间,而事实上整个 kv 文件就这一个 key,不到 1k 空间就存得下。这明显是不可取的。所以需要在性能和空间上做个折中:以内存 pagesize 为单位申请空间,在空间用尽之前都是 append 模式;当 append 到文件末尾时,进行文件重整、key 排重,尝试序列化保存排重结果;排重后空间还是不够用的话,将文件扩大一倍,直到空间足够。

MMKV的缺点

  • 由上可知,Linux 采用了分页来管理内存,存入数据先要创建一个文件,并要给这个文件分配一个固定的大小。如果存入了一个很小的数据,那么这个文件其余的内存就会被浪费。相反如果存入的数据比文件大,就需要动态扩容。
  • 还有一点就是 SP 转 MMKV 简单,如果想要再将 MMKV 转换为其它方式的话,现在是不支持的。如果哪一天 Jetpack DataStore 崛起了,迁移起来可能会比较麻烦。

如何替换并且兼容

如何替换才能更好的兼容之前的代码呢?直接上代码,代码很简单,一看就懂。

dependencies {
  implementation 'com.tencent:mmkv:1.2.7'
  implementation 'com.getkeepsafe.relinker:relinker:1.4.4'
}

import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;

import androidx.annotation.Nullable;

import com.getkeepsafe.relinker.ReLinker;
import com.tencent.mmkv.MMKV;
import com.tencent.mmkv.MMKVLogLevel;

import java.util.Set;

/**
 * 替换SharedPreferences为MMKV
 */
public class MySharedPreferences {

    public static MySharedPreferences getDefaultSharedPreferences() {
        Context context = MyApplication.getAppContext();
        String defaultName = context.getPackageName() + "_preferences";
        return new MySharedPreferences(context, defaultName, Context.MODE_PRIVATE);
    }

    public static MySharedPreferences getSharedPreferences(String name) {
        return new MySharedPreferences(MyApplication.getAppContext(), name, Context.MODE_PRIVATE);
    }

    public static MySharedPreferences getSharedPreferences(String name, int mode) {
        return new MySharedPreferences(null, name, mode);
    }

    public static MySharedPreferences getSharedPreferences(Context context, String name, int mode) {
        return new MySharedPreferences(context, name, mode);
    }

    /**
     * WRITE_TO_MMKV 为ture表示数据写入MMKV,为false,表示数据从MMKV写入SharedPreferences
     */
    private static boolean mMMKVEnabled = true;
    public static void setMMKVEnable(boolean enable) {
        mMMKVEnabled = enable;
    }
    public static boolean isMMKVEnable() {
        return mMMKVEnabled;
    }

    private MMKV mmkv, defaultMMKV;
    private SharedPreferences spData;
    private SharedPreferences.Editor spEditor;

    private static boolean mmkvInited = false;
    public static void initMMKV(Application app) {
        if (mmkvInited) {
            return;
        }
        mmkvInited = true;

        if (MySharedPreferences.isMMKVEnable()) {
            String root = app.getFilesDir().getAbsolutePath() + "/mmkv";
            MMKVLogLevel logLevel = MyApplication.isDebuging() ? MMKVLogLevel.LevelDebug : MMKVLogLevel.LevelError;
            try {
                MMKV.initialize(root, new MMKV.LibLoader() {
                    @Override
                    public void loadLibrary(String libName) {
                        try {
                            ReLinker.loadLibrary(app, libName);
                        } catch (Throwable ex) {
                            MySharedPreferences.setMMKVEnable(false);
                        }
                    }
                }, logLevel);
            } catch (Throwable ex) {
                MySharedPreferences.setMMKVEnable(false);
            }
        }
    }

    private MySharedPreferences(Context context, String name, int mode) {
        if (mMMKVEnabled) {
            try {
                MMKV.initialize(MyApplication.getAppContext());
                this.mmkv = MMKV.mmkvWithID(name);
                this.defaultMMKV = MMKV.defaultMMKV();
            } catch (IllegalArgumentException iae) {
                String message = iae.getMessage();
                if (!TextUtils.isEmpty(message) && message.contains("Opening a multi-process MMKV")) {
                    try {
                        this.mmkv = MMKV.mmkvWithID(name, MMKV.MULTI_PROCESS_MODE);
                        this.defaultMMKV = MMKV.defaultMMKV(MMKV.MULTI_PROCESS_MODE, null);
                    } catch (Throwable ex) {
                        //如果出现异常抛埋点给服务端
                        MyStatistics.getEvent().eventNormal("MMKV", 0, 102, name);
                        return;
                    }
                }
            } catch (Throwable ex) {
                //如果出现异常抛埋点给服务端
                MyStatistics.getEvent().eventNormal("MMKV", 0, 101, name);
                return;
            }
        }

        if (null == context) {
            context = MyApplication.getAppContext();
        }

        if (null != context) {
            if (mMMKVEnabled) {
                if (null != defaultMMKV && !defaultMMKV.contains(name)) {
                    SharedPreferences sources = context.getSharedPreferences(name, mode);
                    mmkv.importFromSharedPreferences(sources);
                    defaultMMKV.encode(name, true);
                    Logger.i("MySharedPreferences", "transform SP-" + name + " to MMKV");
                }
            } else {
                spData = context.getSharedPreferences(name, mode);
            }
        }
    }

    public final class Editor {
        public Editor putString(String key, @Nullable String value) {
            if (mMMKVEnabled) {
                if (null != mmkv) {
                    mmkv.encode(key, value);
                }
            } else {
                if (null != spEditor) {
                    spEditor.putString(key, value);
                }
            }
            return this;
        }

        public Editor putStringSet(String key, @Nullable Set<String> values) {
            if (mMMKVEnabled) {
                if (null != mmkv) {
                    mmkv.encode(key, values);
                }
            } else {
                if (null != spEditor) {
                    spEditor.putStringSet(key, values);
                }
            }
            return this;
        }

        public Editor putInt(String key, int value) {
            if (mMMKVEnabled) {
                if (null != mmkv) {
                    mmkv.encode(key, value);
                }
            } else {
                if (null != spEditor) {
                    spEditor.putInt(key, value);
                }
            }
            return this;
        }

        public Editor putLong(String key, long value) {
            if (mMMKVEnabled) {
                if (null != mmkv) {
                    mmkv.encode(key, value);
                }
            } else {
                if (null != spEditor) {
                    spEditor.putLong(key, value);
                }
            }
            return this;
        }

        public Editor putFloat(String key, float value) {
            if (mMMKVEnabled) {
                if (null != mmkv) {
                    mmkv.encode(key, value);
                }
            } else {
                if (null != spEditor) {
                    spEditor.putFloat(key, value);
                }
            }
            return this;
        }

        public Editor putBoolean(String key, boolean value) {
            if (mMMKVEnabled) {
                if (null != mmkv) {
                    mmkv.encode(key, value);
                }
            } else {
                if (null != spEditor) {
                    spEditor.putBoolean(key, value);
                }
            }
            return this;
        }

        public Editor remove(String key) {
            if (mMMKVEnabled) {
                if (null != mmkv) {
                    mmkv.removeValueForKey(key);
                }
            } else {
                if (null != spEditor) {
                    spEditor.remove(key);
                }
            }
            return this;
        }

        public Editor clear() {
            if (mMMKVEnabled) {
                if (null != mmkv) {
                    mmkv.clearAll();
                }
            } else {
                if (null != spEditor) {
                    spEditor.clear();
                }
            }
            return this;
        }

        /**
         * 无实际意义,只是为了适配以前已经调用了commit的旧的方式
         */
        public boolean commit() {
            if (!mMMKVEnabled) {
                if (null != spEditor) {
                    return spEditor.commit();
                }
            }
            return true;
        }

        /**
         * 无实际意义,只是为了适配以前已经调用了apply的旧的方式
         */
        public void apply() {
            if (!mMMKVEnabled) {
                if (null != spEditor) {
                    spEditor.apply();
                }
            }
        }
    }

    public MySharedPreferences.Editor edit() {
        if (!mMMKVEnabled) {
            spEditor = spData.edit();
        }
        return new Editor();
    }

    @Nullable
    public String getString(String key, @Nullable String defValue) {
        if (mMMKVEnabled) {
            if (null != mmkv) {
                return mmkv.getString(key, defValue);
            }
        } else {
            if (null != spData) {
                return spData.getString(key, defValue);
            }
        }
        return defValue;
    }

    @Nullable
    Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
        if (mMMKVEnabled) {
            if (null != mmkv) {
                return mmkv.getStringSet(key, defValues);
            }
        } else {
            if (null != spData) {
                return spData.getStringSet(key, defValues);
            }
        }
        return defValues;
    }

    public int getInt(String key, int defValue) {
        if (mMMKVEnabled) {
            if (null != mmkv) {
                return mmkv.getInt(key, defValue);
            }
        } else {
            if (null != spData) {
                return spData.getInt(key, defValue);
            }
        }
        return defValue;
    }

    public long getLong(String key, long defValue) {
        if (mMMKVEnabled) {
            if (null != mmkv) {
                return mmkv.getLong(key, defValue);
            }
        } else {
            if (null != spData) {
                return spData.getLong(key, defValue);
            }
        }
        return defValue;
    }

    public float getFloat(String key, float defValue) {
        if (mMMKVEnabled) {
            if (null != mmkv) {
                return mmkv.getFloat(key, defValue);
            }
        } else {
            if (null != spData) {
                return spData.getFloat(key, defValue);
            }
        }
        return defValue;
    }

    public boolean getBoolean(String key, boolean defValue) {
        if (mMMKVEnabled) {
            if (null != mmkv) {
                return mmkv.getBoolean(key, defValue);
            }
        } else {
            if (null != spData) {
                return spData.getBoolean(key, defValue);
            }
        }
        return defValue;
    }

    public boolean contains(String key) {
        if (mMMKVEnabled) {
            if (null != mmkv) {
                return mmkv.containsKey(key);
            }
        } else {
            if (null != spData) {
                return spData.contains(key);
            }
        }
        return false;
    }

}

写到最后

最后,最重要的就是MMKV的缺点,迁移到MMKV是不可逆操作,一定要慎重。

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

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

相关文章

【STM32F103ZE实验】【实验1】点亮LED

STM32CubeMx生成keil工程 步骤1&#xff1a;打开STM32CubeMx&#xff0c; 选择MCU类型 步骤2&#xff1a; 设置Debug类型 步骤3&#xff1a; 选择时钟源 步骤4&#xff1a; 配置时钟 步骤5&#xff1a; 配置GPIO控制LED 首先配置PE5 点击GPIO_Output进行相关配置&#…

如何使用Node.js REPL

目录 1、Nodejs REPL 2、_特殊变量 3、向上箭头键 4、点命令 5、从JavaScript文件运行REPL 1、Nodejs REPL REPL代表Read-Evaluate-Print-Loop&#xff0c;是交互式解释器。 node命令是我们用来运行Node.js脚本的命令&#xff1a; node script.js 如果我们运行node命令…

chatgpt赋能python:Python数据处理中如何选取指定范围的数据

Python数据处理中如何选取指定范围的数据 Python已经成为了数据科学家和工程师的标配&#xff0c;尤其在数据处理和数据分析中&#xff0c;Python具有广泛的应用。在数据处理中&#xff0c;选取指定范围的数据是一个很重要的功能。本文将介绍Python中如何实现指定范围的数据选…

SpringBoot——原理(起步依赖+自动配置(概述和案例))

在Spring家族中提供了很多优秀的框架&#xff0c;所有的框架都是基于同一个基础框架——Spring Framework. 使用spring框架开发麻烦的一批&#xff0c;光是搞依赖和配置就够人喝一壶了。因此在spring4.0版本之后又推出了springboot框架。springboot框架用起来比spring框架简单…

chatgpt赋能python:Python行长度的重要性及最佳实践

Python 行长度的重要性及最佳实践 Python 行长度的重要性 对于一门编程语言而言&#xff0c;行长度是指每一行代码的字符数&#xff0c;Python 也不例外。同时&#xff0c;Python 的行长度限制也是相当明确的&#xff0c;官方建议不要超过 79 个字符&#xff0c;而 PEP 8 规范…

【编译、链接、装载一】预处理、编译、汇编、链接

【编译和链接一】预处理、编译、汇编、链接 一、被隐藏了的过程二、预处理器&#xff08;Prepressing&#xff09;——cpp1、预处理指令2、预处理过程3、预处理生成的hello.i文件 三、编译器&#xff08;Compilation&#xff09;——cc1、编译指令2、编译的过程3、编译生成的文…

chatgpt赋能python:Python读取Mat文件的完整教程

Python 读取Mat文件的完整教程 在数据科学领域&#xff0c;Matlab&#xff08;或简称Mat&#xff09;是最受欢迎的编程语言之一。Matlab可用于数学计算、数据预处理、建模和数据分析。然而&#xff0c;Matlab的开销和许可证成本会限制公司和个人的使用。因此&#xff0c;Pytho…

渗透必学神器:BurpSuite教程(一)

0x00 前言 Burp Suite (简称BP&#xff0c;下同)是用于攻击web 应用程序的集成平台。它包含了许多工具&#xff0c;并为这些工具设计了许多接口&#xff0c;以促进加快攻击应用程序的过程。 从本节开始将为大家陆续带来BP各个模块的使用说明 0x01 中间人攻击 中间人攻击&am…

ChatGPT | Bing | Google Bard | 讯飞星火 | 到底哪家强?实测

最近AIGC战场依然热闹&#xff0c;微软的new bing、Google的Bard、国内的讯飞星火认知大模型&#xff0c;都接连上阵&#xff0c;我们对比ChatGPT一起来看看&#xff0c;我把实际使用测试结果发出&#xff0c;供大家参考。有些测试结果可能会出乎大家的预料哦… 今天我们暂时主…

第十四章 (Set)

一、Set 接口&#xff08;P518&#xff09; 1. Set 接口基本介绍 &#xff08;1&#xff09;无序&#xff08;添加和取出的顺序不一致&#xff09;&#xff0c;没有索引。 &#xff08;2&#xff09;不允许重复元素&#xff0c;所以最多包含一个 null。 2. Set 接口的常用方法…

阿里云服务器ECS云盘扩容

前言 对于云服务器&#xff0c;相信大多数开发的铁子们都玩过&#xff0c;但是云盘爆满的情况&#xff0c;对于新手或者没有自己运营业务的铁子们&#xff0c;平台给的初始容量也不算小&#xff0c;所以这种情况碰到的概率还是比较小。由于我的服务器应用的复杂度随着业务的发…

ubuntu安装搜狗输入法,图文详解+踩坑解决

搜狗输入法已支持Ubuntu16.04、18.04、19.10、20.04、20.10&#xff0c;本教程系统是基于ubuntu18.04 一、添加中文语言支持 系统设置—>区域和语言—>管理已安装的语言—>在“语言”tab下—>点击“添加或删除语言”。 弹出“已安装语言”窗口&#xff0c;勾选中文…

chatgpt赋能python:Python在SEO领域的优势

Python在SEO领域的优势 Python作为一种高效、灵活的编程语言&#xff0c;已经被广泛应用于多个领域&#xff0c;包括Web应用、数据科学、自然语言处理等。在SEO领域&#xff0c;Python也有其独特的优势。 爬虫 Python强大的爬虫库和框架&#xff0c;如BeautifulSoup、Scrapy…

chatgpt赋能python:Python行列转换教程:如何轻松实现行列转换

Python行列转换教程&#xff1a;如何轻松实现行列转换 在数据处理和分析中&#xff0c;经常需要将行和列进行转换。Python是一种优秀的编程语言&#xff0c;提供了多种方法来实现行列转换。在本教程中&#xff0c;我们将介绍如何使用Python实现行列转换&#xff0c;并提供简单…

《Spring Guides系列学习》guide66 - guide68及小结

要想全面快速学习Spring的内容&#xff0c;最好的方法肯定是先去Spring官网去查阅文档&#xff0c;在Spring官网中找到了适合新手了解的官网Guides&#xff0c;一共68篇&#xff0c;打算全部过一遍&#xff0c;能尽量全面的了解Spring框架的每个特性和功能。 接着上篇看过的gui…

深入了解Golang中的反射机制

目录 反射 反射的分类 值反射 类型反射 运行时反射 编译时反射 接口反射 结构体反射 常用函数 值反射 类型反射 值反射和类型反射的区别 结构体反射 示例代码 反射 反射是指在程序运行时动态地检查和修改对象的能力。在Go语言中&#xff0c;通过反射可以在运行时…

chatgpt赋能python:Python逆序对:什么是逆序对,如何使用Python进行逆序对计算?

Python逆序对&#xff1a;什么是逆序对&#xff0c;如何使用Python进行逆序对计算&#xff1f; 在计算机科学中&#xff0c;逆序对是指在一个数组中&#xff0c;如果存在下标i < j&#xff0c;但是a[i] > a[j]&#xff0c;则a[i]和a[j]构成一个逆序对。逆序对对于理解和…

pthread多线程:传入参数并检查 data race

文章目录 1. 目的2. 给子线程传入参数&#xff1a;万能类型 void*3. data race3.1 什么是 data race3.2 怎样检测 data race 4. data race 的例子4.1 子线程传入同一个 data4.2 使用栈内存 5. 解决 data race 问题5.1 忽视问题&#xff1f;5.2 避开同一个变量的使用5.3 使用互斥…

Office project 2010安装教程

哈喽&#xff0c;大家好。今天一起学习的是project 2010的安装&#xff0c;Microsoft Office project项目管理工具软件&#xff0c;凝集了许多成熟的项目管理现代理论和方法&#xff0c;可以帮助项目管理者实现时间、资源、成本计划、控制。有兴趣的小伙伴也可以来一起试试手。…

每天一道面试题之String str=“i“与 String str=new String(“i”)一样吗?

String str"i"与 String strnew String(“i”)一样吗&#xff1f; 要想知道二者是否一样&#xff0c;我们只需要通过进行比较&#xff0c;为什么不用equals的原因&#xff0c;大家可以认真阅读这篇文章 测试代码如下&#xff1a; public class Test1 {public stati…