Android SharedPreferences转为MMKV

开篇

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

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是不可逆操作,一定要慎重。

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

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

相关文章

C# | 线性回归算法的实现,只需采集少量数据点,即可拟合整个数据集

C#线性回归算法的实现 文章目录 C#线性回归算法的实现前言示例代码实现思路测试结果结束语 前言 什么是线性回归呢&#xff1f; 简单来说&#xff0c;线性回归是一种用于建立两个变量之间线性关系的统计方法。在我们的软件开发中&#xff0c;线性回归可以应用于数据分析、预测和…

计算机组成原理-指令系统-机器级语言表示(汇编)

目录 一、X86汇编语言指令基础 寄存器 常见的算数运算指令​编辑 常见逻辑运算指令 AT&T格式和 Intel格式指令 选择语句的机器级表示 循环语句的机器级表示 二、CISC和RISC 一、X86汇编语言指令基础 寄存器 常见的算数运算指令 常见逻辑运算指令 AT&T格式和 Intel格…

使用Python将《青花瓷》歌词生成词云图

哈喽大家好&#xff0c;因为上次有小伙伴问我&#xff0c;歌曲的歌词和评论怎么生成词云图&#xff0c;想买代码… 当时我就拒绝了&#xff0c;直接免费送给了他。 所以今天来分享给大家 我们以周董的《青花瓷》为例&#xff0c;要对《青花瓷》歌词生成词云图&#xff0c;需…

代码创作世界——pytorch深度学习框架数据类型

代码创作世界——pytorch深度学习框架数据类型 torch中的数据类型张量&#xff08;tensor&#xff09; pytorch中的 在数学中&#xff0c;一个单独的数可以成为标量&#xff0c;一行或者一列数组可以称为向量&#xff0c;一个二维数组称为一个矩阵&#xff0c;矩阵中的每一个元…

市值蒸发21亿港元,王一博还能拉着乐华走多久?

5月22日&#xff0c;#乐华被冻结2390万财产#话题冲上热搜。 近日&#xff0c;头顶“王一博”光环的乐华娱乐集团&#xff08;下称“乐华娱乐”&#xff0c;02306.HK&#xff09;交出了上市后的首份财报。 4月25日&#xff0c;财报公布的首个交易日&#xff0c;其股价下跌2.06…

商代数与积代数

商代数 设 R R R使 A < S , ∗ 1 , ∗ 2 , ⋯ , ∗ n > A \left<S, *_1, *_2,\cdots, *_n\right> A⟨S,∗1​,∗2​,⋯,∗n​⟩上的同余关系&#xff0c;则 R R R使 S S S上的等价关系&#xff0c;因此 R R R可诱导出 S S S的一个划分 S / R { [ a ] R ∣ a ∈…

【Linux】Linux环境基础工具的基本使用及配置(yum、vim)

​ ​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;Linux &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 文章目录 Linux软件包管理器 - y…

【深入浅出Spring Security(二)】Spring Security的实现原理

Spring Security的实现原理 一、实现原理二、内置Filter以及默认加载的Filter三、自动配置分析&#xff08;SpringBootWebSecurityConfiguration&#xff09;ConditionalOnMissingBean 四、自己配置SecurityFilterChain 一、实现原理 在 Spring Security 中&#xff0c;认证、…

高压功率放大器ATA4014VS高压功率放大器HSA42014

高压功率放大器ATA4014VS高压功率放大器HSA42014 一、企业背景&#xff1a; Aigtek是一家来自中国的专业从事测量仪器研发、生产和销售的高科技企业。公司主要研发和生产功率放大器、功率放大器模块、功率信号源、计量校准源等产品。核心团队主要是来自西安交通大学及西北工业大…

LiangGaRy-学习笔记-Day17

1、磁盘的介绍 自动分区、手工分区、命令工具分区 1.1、磁盘分类 根据介质来区分&#xff1a; 机械硬盘和固态硬盘 通过盘大小&#xff1a; 3.5英寸和2.5英寸 通过接口分类&#xff1a; SAS、SATA、FC scisi 根据功能&#xff1a; 桌面和企业级别 1.2、磁盘类型 HD…

数据在内存中的存储(1)——整形

目录 1、数据类型介绍 1.1、类型的基本归类 整形家族 浮点数家族 指针类型 空类型 构造类型 2、整形在内存中的存储 2.1、原码、反码、补码 2.2、大小端介绍 2.3、有符号与无符号 2.4、练习 例一 例二 例三 例四 例五 1、数据类型介绍 我们先来简单了解一下我们前面所学的基…

STM32F4_软件模拟SPI

目录 1. 硬件连接 2. SPI通讯协议 3. W25Q64 简介 4. 程序详解 4.1 main.c 4.2 SPI.c 4.3 SPI.h 4.4 W25Q128.c 4.5 W25Q128.h 4.6 OLED.c 4.7 OLED.h 4.8 OLED_Font.h 5. 实验结果 我们都知道&#xff0c;SPI 和 IIC 一样&#xff0c;都可以通过硬件方式和软件方…

Phaser笔记-精灵(sprite)的使用

如下&#xff1a; 使用逻辑如下&#xff1a; ①设置好physics&#xff1b; ②将资源添加到spritesheet&#xff1b; ③在physics中添加sprite&#xff1b; ④创建anims&#xff1b; ⑤播放anims&#xff1b; 完整代码如下&#xff1a; import phaser;class PlayGame exte…

Linux 系统上的库文件生成与使用

1.库文件 库是一组预先编译好的方法的集合。Linux系统存储的库的位置一般在&#xff1a;/lib 和 /usr/lib。 在 64 位的系统上有些库也可能被存储在/usr/lib64 下。库的头文件一般会被存储在 /usr/include 下或其子目录下。 库有两种&#xff0c;一种是静态库&#xff0c;其…

springboot + vue3实现视频播放Demo(video.js Vue3-video-play视频播放器)

文章目录 学习链接前言ffmpeg安装ffmpeg配置环境变量分割视频文件 后台配置WebConfig 前端代码video.js示例安装依赖视频播放组件效果 Vue3-video-play示例安装依赖main.js中使用视频播放组件效果 学习链接 ffmpeg官网 长时长视频java存储及vue播放解决方法 【 攻城略地 】vue…

Packet Tracer - 配置区域策略防火墙

Packet Tracer - 配置区域策略防火墙 拓扑 地址表 设备 接口 IP地址 子网掩码 默认网关 交换机端口 R1 F0/1 192.168.1.1 255.255.255.0 N/A S1 F0/2 S0/3/0 (DCE) 10.1.1.1 255.255.255.252 N/A N/A R2 S0/3/0 10.1.1.2 255.255.255.252 N/A N/A S0/3…

C++ priority_queue

C priority_queue &#x1f4df;作者主页&#xff1a;慢热的陕西人 &#x1f334;专栏链接&#xff1a;C &#x1f4e3;欢迎各位大佬&#x1f44d;点赞&#x1f525;关注&#x1f693;收藏&#xff0c;&#x1f349;留言 本博客主要内容讲解了优先队列的对应接口的使用 文章目录…

[GUET-CTF2019]number_game[数独]

目录 题目 学到的知识点&#xff1a; 题目 在buu上看到了一道数独题&#xff0c;没见过&#xff0c;记录一下 下载附件&#xff0c;查壳&#xff0c;无壳&#xff0c;在IDA中打开&#xff0c;直接找到主函数 unsigned __int64 __fastcall main(int a1, char **a2, char **a3…

工程swift与OC混编改造

最近公司项目准备引入swift&#xff0c;由于目前工程已经完成了组件化不再是简单的单仓工程&#xff0c;所以需要进行混编改造。下面记录一下自己对工程进行混编改造的思考以及过程。 混编原理 看了很多文档&#xff0c;比较少有讲混编原理的&#xff0c;这里简单介绍一下语言…

springboot+springsecurity+jwt+elementui图书管理系统

​​图书管理系统​​ 关注公号&#xff1a;java大师&#xff0c;回复“图书”&#xff0c;获取源码 一、springboot后台 1、mybatis-plus整合 1.1添加pom.xml <!--mp逆向工程 --><dependency><groupId>org.projectlombok</groupId><artifactId&…