Android SharedPreference详解

news2024/9/28 1:23:35

Android SharedPreference详解

SharedPreferences作为一种数据持久化的方式,是处理简单的key-value类型数据时的首选。

一般用法:

//demo是该sharedpreference对应文件名,对应的是一个xml文件,里面存放key-value格式的数据.
SharedPreferences sharedPreferences = context.getSharedPreferences("demo", MODE_WORLD_WRITEABLE);
//提供了getXXX的读取数据方法
boolean xxx = sharedPreferences.getBoolean("xxx", false);
//通过Editor提供了putXXX系列的存储方法,调用完需要使用apply()或commit()使之生效,不同点后面介绍
SharedPreferences.Editor edit = sharedPreferences.edit();
edit.putBoolean("xxx", true);
edit.apply();//使存储生效
//edit.commit();//使存储生效

每个SharedPreferences都对应了当前package的data/data/package_name/share_prefs/目录下的一个文件

源码解析

Context.java中getSharedPreferences接口说明:

/**
     * Retrieve and hold the contents of the preferences file 'name', returning
     * a SharedPreferences through which you can retrieve and modify its
     * values.  Only one instance of the SharedPreferences object is returned
     * to any callers for the same name, meaning they will see each other's
     * edits as soon as they are made.
     *
     * @param name Desired preferences file. If a preferences file by this name
     * does not exist, it will be created when you retrieve an
     * editor (SharedPreferences.edit()) and then commit changes (Editor.commit()).
     * @param mode Operating mode.  Use 0 or {@link #MODE_PRIVATE} for the
     * default operation, {@link #MODE_WORLD_READABLE}
     * and {@link #MODE_WORLD_WRITEABLE} to control permissions.
     *
     * @return The single {@link SharedPreferences} instance that can be used
     *         to retrieve and modify the preference values.
     *
     * @see #MODE_PRIVATE
     * @see #MODE_WORLD_READABLE
     * @see #MODE_WORLD_WRITEABLE
     */
    public abstract SharedPreferences getSharedPreferences(String name,
            int mode);

ContextImpl中getSharedPreferences实现:

@Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            if (sSharedPrefs == null) {
                sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
            }

            final String packageName = getPackageName();
            ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
            if (packagePrefs == null) {
                packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
                sSharedPrefs.put(packageName, packagePrefs);
            }

            // At least one application in the world actually passes in a null
            // name.  This happened to work because when we generated the file name
            // we would stringify it to "null.xml".  Nice.
            if (mPackageInfo.getApplicationInfo().targetSdkVersion <
                    Build.VERSION_CODES.KITKAT) {
                if (name == null) {
                    name = "null";
                }
            }

            sp = packagePrefs.get(name);
            if (sp == null) {
                File prefsFile = getSharedPrefsFile(name);
                sp = new SharedPreferencesImpl(prefsFile, mode);
                packagePrefs.put(name, sp);
                return sp;
            }
        }
        if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
            getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
            // If somebody else (some other process) changed the prefs
            // file behind our back, we reload it.  This has been the
            // historical (if undocumented) behavior.
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }

这段代码里,我们可以看出,

  1. SharedPreferencesImpl是保存在全局个map cache里的,只会创建一次。
  2. MODE_MULTI_PROCESS模式下,每次获取都会尝试去读取文件reload。当然会有一些逻辑尽量减少读取次数,比如当前是否有正在进行的读取操作,文件的修改时间和大小与上次有没有变化等。

Context.java中提供了以下四种mode:

//这是默认模式,仅caller uid的进程可访问
/**
     * File creation mode: the default mode, where the created file can only
     * be accessed by the calling application (or all applications sharing the
     * same user ID).
     * @see #MODE_WORLD_READABLE
     * @see #MODE_WORLD_WRITEABLE
     */
int MODE_PRIVATE = 0x0000;

//所有人可写,也就是任何应用都可修改它,这是极其危险的,因此改选项已被Deprected
/**
     * @deprecated Creating world-readable files is very dangerous, and likely
     * to cause security holes in applications.  It is strongly discouraged;
     * instead, applications should use more formal mechanism for interactions
     * such as {@link ContentProvider}, {@link BroadcastReceiver}, and
     * {@link android.app.Service}.  There are no guarantees that this
     * access mode will remain on a file, such as when it goes through a
     * backup and restore.
     * File creation mode: allow all other applications to have read access
     * to the created file.
     * @see #MODE_PRIVATE
     * @see #MODE_WORLD_WRITEABLE
     */
int MODE_WORLD_READABLE = 0x0001; 

//所有人可读,这个参数同样非常危险,可能导致隐私数据泄漏
/**
     * @deprecated Creating world-writable files is very dangerous, and likely
     * to cause security holes in applications.  It is strongly discouraged;
     * instead, applications should use more formal mechanism for interactions
     * such as {@link ContentProvider}, {@link BroadcastReceiver}, and
     * {@link android.app.Service}.  There are no guarantees that this
     * access mode will remain on a file, such as when it goes through a
     * backup and restore.
     * File creation mode: allow all other applications to have write access
     * to the created file.
     * @see #MODE_PRIVATE
     * @see #MODE_WORLD_READABLE
     */
int MODE_WORLD_READABLE = 0x0002

//设置该参数后,每次获取对应的SharedPreferences时都会尝试从磁盘中读取修改过的文件 
/**
     * SharedPreference loading flag: when set, the file on disk will
     * be checked for modification even if the shared preferences
     * instance is already loaded in this process.  This behavior is
     * sometimes desired in cases where the application has multiple
     * processes, all writing to the same SharedPreferences file.
     * Generally there are better forms of communication between
     * processes, though.
     *
     * <p>This was the legacy (but undocumented) behavior in and
     * before Gingerbread (Android 2.3) and this flag is implied when
     * targetting such releases.  For applications targetting SDK
     * versions <em>greater than</em> Android 2.3, this flag must be
     * explicitly set if desired.
     *
     * @see #getSharedPreferences
     *
     * @deprecated MODE_MULTI_PROCESS does not work reliably in
     * some versions of Android, and furthermore does not provide any
     * mechanism for reconciling concurrent modifications across
     * processes.  Applications should not attempt to use it.  Instead,
     * they should use an explicit cross-process data management
     * approach such as {@link android.content.ContentProvider ContentProvider}.
     */
int MODE_MULTI_PROCESS = 0x0004;
MODE_MULTI_PROCESS

当设置MODE_MULTI_PROCESS这个参数的时候,即使当前进程内已经创建了该SharedPreferences,仍然在每次获取的时候都会尝试从本地文件中刷新。在同一个进程中,同一个文件只有一个实例。MODE_MULTI_PROCESS的作用如上getSharedPreferences实现.这个方法先判断是否已创建SharedPreferences实例,若未创建,则先创建。之后判断mode如果为MODE_MULTI_PROCESS, 则调用startReloadIfChangeUnexpectedly(),看下其实现:
SharedPreferencesImpl.java

void startReloadIfChangedUnexpectedly() {
        synchronized (this) {
            // TODO: wait for any pending writes to disk?
            if (!hasFileChangedUnexpectedly()) {
                return;
            }
            startLoadFromDisk();
        }
    }

private void startLoadFromDisk() {
        synchronized (this) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                synchronized (SharedPreferencesImpl.this) {
                    loadFromDiskLocked();
                }
            }
        }.start();
    }

可以看出MODE_MULTI_PROCESS的作用就是在每次获取SharedPreferences实例的时候尝试从磁盘中加载修改过的数据,并且读取是在异步线程中,因此一个线程的修改最终会反映到另一个线程,但不能立即反映到另一个进程,所以通过SharedPreferences无法实现多进程同步。
综合: 如果仅仅让多进程可访问同一个SharedPref文件,不需要设置MODE_MULTI_PROCESS, 如果需要实现多进程同步,必须设置这个参数,但也只能实现最终一致,无法即时同步。

由于SharedPreference内容都会在内存里存一份,所以不要使用SharedPreference保存较大的内容,避免不必要的内存浪费。

注意有一个锁mLoaded ,在对SharedPreference做其他操作时,都必须等待该锁释放:

@Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (this) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

写操作有两个commit apply 。 commit 是同步的,写入内存的同时会等待写入文件完成,apply是异步的,先写入内存,在异步线程里再写入文件。apply肯定要快一些,优先推荐使用apply:

/**
         * Commit your preferences changes back from this Editor to the
         * {@link SharedPreferences} object it is editing.  This atomically
         * performs the requested modifications, replacing whatever is currently
         * in the SharedPreferences.
         *
         * <p>Note that when two editors are modifying preferences at the same
         * time, the last one to call commit wins.
         *
         * <p>If you don't care about the return value and you're
         * using this from your application's main thread, consider
         * using {@link #apply} instead.
         *
         * @return Returns true if the new values were successfully written
         * to persistent storage.
         */
        boolean commit();
        /**
         * Commit your preferences changes back from this Editor to the
         * {@link SharedPreferences} object it is editing.  This atomically
         * performs the requested modifications, replacing whatever is currently
         * in the SharedPreferences.
         *
         * <p>Note that when two editors are modifying preferences at the same
         * time, the last one to call apply wins.
         *
         * <p>Unlike {@link #commit}, which writes its preferences out
         * to persistent storage synchronously, {@link #apply}
         * commits its changes to the in-memory
         * {@link SharedPreferences} immediately but starts an
         * asynchronous commit to disk and you won't be notified of
         * any failures.  If another editor on this
         * {@link SharedPreferences} does a regular {@link #commit}
         * while a {@link #apply} is still outstanding, the
         * {@link #commit} will block until all async commits are
         * completed as well as the commit itself.
         *
         * <p>As {@link SharedPreferences} instances are singletons within
         * a process, it's safe to replace any instance of {@link #commit} with
         * {@link #apply} if you were already ignoring the return value.
         *
         * <p>You don't need to worry about Android component
         * lifecycles and their interaction with <code>apply()</code>
         * writing to disk.  The framework makes sure in-flight disk
         * writes from <code>apply()</code> complete before switching
         * states.
         *
         * <p class='note'>The SharedPreferences.Editor interface
         * isn't expected to be implemented directly.  However, if you
         * previously did implement it and are now getting errors
         * about missing <code>apply()</code>, you can simply call
         * {@link #commit} from <code>apply()</code>.
         */
        void apply();

注册/解注册sharedpreference变动监听:

/**
     * Registers a callback to be invoked when a change happens to a preference.
     *
     * <p class="caution"><strong>Caution:</strong> The preference manager does
     * not currently store a strong reference to the listener. You must store a
     * strong reference to the listener, or it will be susceptible to garbage
     * collection. We recommend you keep a reference to the listener in the
     * instance data of an object that will exist as long as you need the
     * listener.</p>
     *
     * @param listener The callback that will run.
     * @see #unregisterOnSharedPreferenceChangeListener
     */
    void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
    
    /**
     * Unregisters a previous callback.
     * 
     * @param listener The callback that should be unregistered.
     * @see #registerOnSharedPreferenceChangeListener
     */
    void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
为什么不推荐使用MODE_MULTI_PROCESS?

android文档已经Deprected了这个flag,并且说明不应该通过SharedPreference做进程间数据共享?这是为啥呢?从前面但分析可看到当设置这个flag后,每次获取(获取而不是初次创建)SharedPreferences实例的时候,会判断shared_pref文件是否修改过:

private boolean hasFileChangedUnexpectedly() {
        synchronized (this) {
            if (mDiskWritesInFlight > 0) {
                // If we know we caused it, it's not unexpected.
                if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
                return false;
            }
        }

        final StructStat stat;
        try {
            /*
             * Metadata operations don't usually count as a block guard
             * violation, but we explicitly want this one.
             */
            BlockGuard.getThreadPolicy().onReadFromDisk();
            stat = Os.stat(mFile.getPath());
        } catch (ErrnoException e) {
            return true;
        }

        synchronized (this) {
            return mStatTimestamp != stat.st_mtime || mStatSize != stat.st_size;
        }
    }

这里先判断mDiskWritesInFlight>0,如果成立,说明是当前进程修改了文件,不需要重新读取。然后通过文件最后修改时间,判断文件是否修改过。如果修改了,则重新读取:

private void startLoadFromDisk() {
        synchronized (this) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                synchronized (SharedPreferencesImpl.this) {
                    loadFromDiskLocked();
                }
            }
        }.start();
}

private void loadFromDiskLocked() {
        if (mLoaded) {
            return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
        Map map = null;
        StructStat stat = null;
        try {
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16*1024);
                    map = XmlUtils.readMapXml(str);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
        }
        mLoaded = true;
        if (map != null) {
            mMap = map;
            mStatTimestamp = stat.st_mtime;
            mStatSize = stat.st_size;
        } else {
            mMap = new HashMap<String, Object>();
        }
        notifyAll();
}

这里起码有3个坑!

  1. 使用MODE_MULTI_PROCESS时,不要保存SharedPreference变量,必须每次都从context.getSharedPreferences 获取。如果你图方便使用变量存了下来,那么无法触发reload,有可能两个进程数据不同步。
  2. 前面提到过,load数据是耗时的,并且其他操作会等待该锁。这意味着很多时候获取SharedPreference数据都不得不从文件再读一遍,大大降低了内存缓存的作用。文件读写耗时也影响了性能。
  3. 修改数据时得用commit,保证修改时写入了文件,这样其他进程才能通过文件大小或修改时间感知到。

重点是这段:

if (mBackupFile.exists()) {
      mFile.delete();
      mBackupFile.renameTo(mFile);
}

重新读取时,如果发现存在mBackupFile,则将原文件mFile删除,并将mBackupFile重命名为mFile。mBackupFile又是如何创建的呢?答案是在修改SharedPreferences时将内存中的数据写会磁盘时创建的:

private void writeToFile(MemoryCommitResult mcr) {
        // Rename the current file so it may be used as a backup during the next read
        if (mFile.exists()) {
            if (!mBackupFile.exists()) {
                if (!mFile.renameTo(mBackupFile)) {
                    mcr.setDiskWriteResult(false);
                    return;
                }
            } else {
                mFile.delete();
            }
        }
        FileOutputStream str = createFileOutputStream(mFile);
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        FileUtils.sync(str);
        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
        final StructStat stat = Os.stat(mFile.getPath());
        synchronized (this) {
            mStatTimestamp = stat.st_mtime;
            mStatSize = stat.st_size;
        }
        // Writing was successful, delete the backup file if there is one.
        mBackupFile.delete();
        mcr.setDiskWriteResult(true);
        return;
    }

这段代码只保留了核心流程,忽略了错误处理流程。可以看到,写文件的步骤大致是:

  1. 将原文件重命名为mBackupFile
  2. 重新创建原文件mFile, 并将内容写入其中
  3. 删除mBackupFile

所以,只有当一个进程正处于写文件的过程中的时候,如果另一个进程读文件,才会看到mBackupFile, 这时候读进程会将mBackupFile重命名为mFile, 这样读结果是,读进程只能读到修改前的文件,同时,由于mBackupFile重命名为了mFile, 所以写进程写那个文件就没有文件名引用了,因此其写入的内容无法再被任何进程访问到。所以其内容丢失了,可认为写入失败了,而SharedPreferences对这种失败情况没有任何重试机制,所以就可能出现数据丢失的情况。
回到这段的重点:为什么不推荐用MODE_MULTI_PROCESS?从前面分析可知,这种模式下,每次获取SharedPreferences都会检测文件是否改变,只要读的时候另一进程在写,就会导致写丢失。这样失败概率就会大幅度提高。反之,若不设置这个模式,则只在第一次创建SharedPreferences的时候读取,导致写失败的概率就会大幅度降低,当然,仍然存在失败的可能。

为什么不做写失败重试?

为什么android不做写失败重试呢?原因是写进程并不能发现写失败的情况。难道写的过程中,目标文件被删不会抛异常吗?答案是不会。删除文件只是从文件系统中删除了一个节点信息而已,重命名也是新建了一个具有相同名称的节点信息,并把文件地址指向另一个磁盘地址而已,原来,之前的写过程仍然会成功写到原来的磁盘地址。所以目前的实现方案并不能检测到失败。

有没有办法解决写失败呢?

个人觉得是可以做到的,读里面读那段关键操作:

if (mBackupFile.exists()) {
      mFile.delete();
      mBackupFile.renameTo(mFile);
}

mBackupFile存在,意味着当前正处于写读过程中,这时候是不是可以考虑直接读mBackupFile文件,而不删除mFile呢?这样读话,读取效果一样,都是读的mBackupFile,同时写进程写的mFile也不会被mBacupFile覆盖,写也就能成功了。即使通过这段代码重命名,写进程写完后发现mBackupFile不存在了,其实也能认为发生了读重命名,大可以重试一次。

多进程使用SharedPreference方案

说简单也简单,就是依据google的建议使用ContentProvider了。我看过网上很多的例子,但总是觉得少了点什么

有的方案里将所有读取操作都写作静态方法,没有继承SharedPreference 。 这样做需要强制改变调用者的使用习惯,不怎么好。
大部分方案做成ContentProvider后,所有的调用都走的ContentProvider。但如果调用进程与SharedPreference 本身就是同一个进程,只用走原生的流程就行了,不用拐个弯去访问ContentProvider,减少不必要的性能损耗。

我这里也写了一个跨进程方案,简单介绍如下
SharedPreferenceProxy 继承SharedPreferences。其所有操作都是通过ContentProvider完成。简要代码:

public class SharedPreferenceProxy implements SharedPreferences {
@Nullable
    @Override
    public String getString(String key, @Nullable String defValue) {
        OpEntry result = getResult(OpEntry.obtainGetOperation(key).setStringValue(defValue));
        return result == null ? defValue : result.getStringValue(defValue);
    }

    @Override
    public Editor edit() {
        return new EditorImpl();
    }
    private OpEntry getResult(@NonNull OpEntry input) {
        try {
            Bundle res = ctx.getContentResolver().call(PreferenceUtil.URI
                    , PreferenceUtil.METHOD_QUERY_VALUE
                    , preferName
                    , input.getBundle());
            return new OpEntry(res);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
...

    public class EditorImpl implements Editor {
        private ArrayList<OpEntry> mModified = new ArrayList<>();
        @Override
        public Editor putString(String key, @Nullable String value) {
            OpEntry entry = OpEntry.obtainPutOperation(key).setStringValue(value);
            return addOps(entry);
        }
       @Override
        public void apply() {
            Bundle intput = new Bundle();
            intput.putParcelableArrayList(PreferenceUtil.KEY_VALUES, convertBundleList());
            intput.putInt(OpEntry.KEY_OP_TYPE, OpEntry.OP_TYPE_APPLY);
            try {
                ctx.getContentResolver().call(PreferenceUtil.URI, PreferenceUtil.METHOD_EIDIT_VALUE, preferName, intput);
            } catch (Exception e) {
                e.printStackTrace();
            }
...
        }
...
    }

OpEntry只是一个对Bundle操作封装的类。
所有跨进程的操作都是通过SharedPreferenceProvider的call方法完成。SharedPreferenceProvider里会访问真正的SharedPreference

public class SharedPreferenceProvider extends ContentProvider{

    private Map<String, MethodProcess> processerMap = new ArrayMap<>();
    @Override
    public boolean onCreate() {
        processerMap.put(PreferenceUtil.METHOD_QUERY_VALUE, methodQueryValues);
        processerMap.put(PreferenceUtil.METHOD_CONTAIN_KEY, methodContainKey);
        processerMap.put(PreferenceUtil.METHOD_EIDIT_VALUE, methodEditor);
        processerMap.put(PreferenceUtil.METHOD_QUERY_PID, methodQueryPid);
        return true;
    }
    @Nullable
    @Override
    public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
        MethodProcess processer = processerMap.get(method);
        return processer == null?null:processer.process(arg, extras);
    }
...
}

重要差别的地方在这里:在调用getSharedPreferences时,会先判断caller的进程pid是否与SharedPreferenceProvider相同。如果不同,则返回SharedPreferenceProxy。如果相同,则返回ctx.getSharedPreferences。只会在第一次调用时进行判断,结果会保存起来。

public static SharedPreferences getSharedPreferences(@NonNull Context ctx, String preferName) {
        //First check if the same process
        if (processFlag.get() == 0) {
            Bundle bundle = ctx.getContentResolver().call(PreferenceUtil.URI, PreferenceUtil.METHOD_QUERY_PID, "", null);
            int pid = 0;
            if (bundle != null) {
                pid = bundle.getInt(PreferenceUtil.KEY_VALUES);
            }
            //Can not get the pid, something wrong!
            if (pid == 0) {
                return getFromLocalProcess(ctx, preferName);
            }
            processFlag.set(Process.myPid() == pid ? 1 : -1);
            return getSharedPreferences(ctx, preferName);
        } else if (processFlag.get() > 0) {
            return getFromLocalProcess(ctx, preferName);
        } else {
            return getFromRemoteProcess(ctx, preferName);
        }
    }


    private static SharedPreferences getFromRemoteProcess(@NonNull Context ctx, String preferName) {
        synchronized (SharedPreferenceProxy.class) {
            if (sharedPreferenceProxyMap == null) {
                sharedPreferenceProxyMap = new ArrayMap<>();
            }
            SharedPreferenceProxy preferenceProxy = sharedPreferenceProxyMap.get(preferName);
            if (preferenceProxy == null) {
                preferenceProxy = new SharedPreferenceProxy(ctx.getApplicationContext(), preferName);
                sharedPreferenceProxyMap.put(preferName, preferenceProxy);
            }
            return preferenceProxy;
        }
    }

    private static SharedPreferences getFromLocalProcess(@NonNull Context ctx, String preferName) {
        return ctx.getSharedPreferences(preferName, Context.MODE_PRIVATE);
    }

这样,只有当调用者是正真跨进程时才走的contentProvider。对于同进程的情况,就没有必要走contentProvider了。对调用者来说,这都是透明的,只需要获取SharedPreferences就行了,不用关心获得的是SharedPreferenceProxy,还是SharedPreferenceImpl。即使你当前没有涉及到多进程使用,将所有获取SharedPreference的地方封装并替换后,对当前逻辑也没有任何影响。

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

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

相关文章

D20【python接口自动化学习】-python基础之内置数据类型

day20 内置数据类型的常见错误 学习日期&#xff1a;20240927 学习目标&#xff1a;内置数据类型--29 内置数据类型的常见错误 学习笔记&#xff1a; 访问错误 不同数据类型之间的操作报错 解决错误的方法 对只读类型进行写入报错 解决错误的方法 引用错误 解决错误的方法 …

产销皆下行,造势口碑遭“反噬”,魏建军能否重振长城汽车?

今年以来&#xff0c;长城汽车可谓多次被打在舆论聚光灯下&#xff0c;既有“一把手”魏建军一反此前低调务实作风而在今年多次于公共场合慨慷激昂频出“金句”宏观层面的&#xff1b;也有旗下多款车型销量数据出现下滑的微观层面。 近日&#xff0c;长城汽车披露了2024年1-8月…

python的 __name__和__doc__属性

__name__属性 __name__属性 用于判断当前模块是不是程序入口&#xff0c;如果当前程序正在使用&#xff0c;__name__的值为__main__。 在编写程序时&#xff0c;通常需要给每个模块添加条件语句&#xff0c;用于单独测试该模块的功能。 每个模块都有一个名称&#xff0c;当一…

ArduSub程序学习(11)--EKF实现逻辑①

1.read_AHRS() 进入EKF&#xff0c;路径ArduSub.cpp里面的fast_loop()里面的read_AHRS(); //从 AHRS&#xff08;姿态与航向参考系统&#xff09;中读取并更新与飞行器姿态有关的信息 void Sub::read_AHRS() {// Perform IMU calculations and get attitude info//----------…

WinForm程序嵌入Web网页

文章目录 前言一、三方库或控件的选择测试二、Microsoft Edge WebView2安装、使用步骤1.安装2.使用 前言 由于此项目需要winform客户端嵌入web网页并于JAVA端交互数据&#xff0c;所以研究了一下嵌入web网页这部分&#xff0c;趟了一遍雷&#xff0c;这里做下记录。 一、三方库…

软件设计之Maven(2)

软件设计之Maven(2) 路线图推荐&#xff1a; 【Java学习路线-极速版】【Java架构师技术图谱】 尚硅谷新版Maven教程&#xff08;高效入门maven&#xff0c;上手又快又稳&#xff09; 资料可以去尚硅谷官网免费领取 学习内容&#xff1a; 依赖管理版本统一及维护依赖范围Buil…

Comfyui 学习笔记1

如果图像输出被裁剪&#xff0c;则需要使用PrepImageForClipVision&#xff0c;来设置图像距离上边沿的位置. 决定绘画的作用区域&#xff0c;后面的KSample只作用到 mask标记的范围。 图像位置偏移了&#xff0c;可以考虑通过Image crop 裁剪 IPAdapter face 提取时&…

基于Python大数据可视化的白酒数据推荐及数据分析系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码 精品专栏&#xff1a;Java精选实战项目…

Java并发:互斥锁,读写锁,公平锁,Condition,StampedLock

阅读本文之前可以看一看 Java 多线程基础&#xff1a; Java&#xff1a;多线程&#xff08;进程线程&#xff0c;线程状态&#xff0c;创建线程&#xff0c;线程操作&#xff09; Java&#xff1a;多线程&#xff08;同步死锁&#xff0c;锁&原子变量&#xff0c;线程通信&…

《Linux从小白到高手》理论篇(二):Linux的目录结构和磁盘管理

List item 本篇主要介绍Linux的目录结构和磁盘管理相关的命令。 Linux目录结构 在Linux的世界里&#xff0c;一切皆文件&#xff0c;连目录也不例外。linux的文件系统是采用级层式的树状目录结构&#xff0c;在此结构中的最上层是根目录“/”&#xff08;相当于windows下的&…

【VUE】案例:商场会员管理系统

编写vuedfr实现对会员进行基本增删改查 1. drf项目初始化 请求&#xff1a; POST http://127/0.0.0.1:8000/api/auth/ {"username":"cqn", "password":"123"}返回&#xff1a; {"username":"cqn", "token&q…

开源 AI 智能名片 O2O 商城小程序与抖音:品牌传播的新机遇与挑战

摘要&#xff1a;本文探讨了开源 AI 智能名片 O2O 商城小程序在品牌传播中的作用&#xff0c;以及与抖音平台相结合所带来的机遇与挑战。分析了抖音如何利用算法适配品牌调性为门店找到目标消费者&#xff0c;放大品牌势能&#xff0c;同时阐述了新品牌在抖音上进行品牌传播的优…

后端返回内容有换行标识,前端如何识别换行

<br/>的话 用 v-html \n 可以用css样式 white-space: pre-wrap 后端返回结果 前端

集成电路发展的两条主线

集成电路发展的两条主线&#xff0c; 1、增大晶圆尺寸&#xff0c;6、8、12英寸 晶圆尺寸不断增大&#xff0c;其设备要求不断增大。目前主流的8英寸&#xff0c;12英寸正在发展 2、芯片工艺制程不断减小

基于Hive和Hadoop的用电量分析系统

本项目是一个基于大数据技术的用电量分析系统&#xff0c;旨在为用户提供全面的电力消耗信息和深入的用电量分析。系统采用 Hadoop 平台进行大规模数据存储和处理&#xff0c;利用 MapReduce 进行数据分析和处理&#xff0c;通过 Sqoop 实现数据的导入导出&#xff0c;以 Spark…

quill富文本插入表格quill-better-table

使用quill-better-table插件&#xff0c;官网GitCode - 全球开发者的开源社区,开源代码托管平台 安装 首先quill-better-table插件&#xff0c;官网有写需要 quills v2.0.0-dev3 我这里使用的是 quills v2.0.0-dev4&#xff0c;自行安装 然后就是安装我们的插件 quill-bett…

【项目资料】项目售后服务方案(Word)

1.1 售后服务方案 概述 售后服务体系 售后服务流程 售后服务承诺 售后服务计划 技术支持响应承诺 售后服务响应时间 1.2 项目培训方案 项目培训体系 项目培训管理 培训目的与措施 项目培训安排 培训告知下达 培训人员贯彻 培训签到表 软件全套资料部分文档清单&#xff1a; 工作…

10款超好用的文档加密软件|2024企业文档加密软件分享

在信息安全日益受到重视的今天&#xff0c;选择合适的文档加密软件至关重要。以下是2024年值得关注的10款超好用的文档加密软件&#xff0c;帮助企业提升数据安全性。 1. Ping32文档加密软件 Ping32专注于文档加密&#xff0c;采用强大的AES加密技术&#xff0c;确保敏感信息在…

领夹式无线麦克风哪个品牌好,口碑最好的领夹麦克风品牌推荐

对于追求专业音频录制的创作者而言&#xff0c;无线领夹麦克风不仅是录制设备&#xff0c;更是表达自我的艺术工具。从市场反馈中&#xff0c;我们发现西圣、大疆、罗德等品牌深受消费者喜爱。不过我看事情不能只看一面&#xff0c;在市场繁荣的表象下&#xff0c;一些劣质产品…

单片机的两种看门狗原理解析——IWDG和WWDG

一、IWDG独立开门狗的主要性能 计时机制&#xff1a; 递减计数器 独立开门狗的初始频率&#xff1a; LSI低速内部时钟&#xff1a;RC震荡器&#xff0c;40kHz 独立开门狗是以LSI为初始频率的&#xff0c;所以独立开门狗的初始时钟频率取决与单片机本身&#xff0c;因此在使…