【Android】SharedPreferences阻塞问题深度分析

前言

Android中SharedPreferences已经广为诟病,它虽然是Android SDK中自带的数据存储API,但是因为存在设计上的缺陷,在处理大量数据时很容易导致UI线程阻塞或者ANR,Android官方最终在Jetpack库中提供了DataStore解决方案,用来替代SharedPreferences。

总结起来,SharedPreferences有以下几个缺点:

  • 在初始化SharedPreferences对象时,会将文件中所有数据都读取到内存,非常浪费内存,并且是同步操作,如果在主线程中操作就会导致页面启动慢或者卡顿问题。
  • 在调用SharedPreferences对象的edit()方法时会一直阻塞直到数据从磁盘上读取完毕。
  • 每次调用apply和commit都会将内存的数据一并同步到磁盘,影响性能。
  • 在调用Activity和Service的生命周期时,会阻塞等待SP数据写出完毕,因此导致页面出现卡顿甚至ANR。

下面来从源码的设计角度来深入分析一下这些问题存在的根源。

笔者原创,转载请注明出处:https://blog.csdn.net/devnn/article/details/138086118

本文基于Android 30源码。

SharedPreferences的初始化

一般我们会使用context的getSharedPreferences(String name, int mode)方法获取一个SharedPreferences对象,而context一般直接使用当前Activity对象。Activity虽然继承了Context但其实它是一个代理Context,真实的Context是通过attachBaseContext方法传入的,实际上就是ContextImpl。对这个不熟悉的同学可以去看看ActivityThread中Activity创建过程。

我们看看ContextImpl中getSharedPreferences方法是如何实现的。

//android.app.ContextImpl
  @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {
                checkMode(mode);
                if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
                    if (isCredentialProtectedStorage()
                            && !getSystemService(UserManager.class)
                                    .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
                        throw new IllegalStateException("SharedPreferences in credential encrypted "
                                + "storage are not available until after user is unlocked");
                    }
                }
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, 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;
    }

这里从缓存中取出SharedPreferencesImpl对象,缓存没有会新建一个SharedPreferencesImpl:

//android.app.SharedPreferencesImpl
   SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        startLoadFromDisk();
    }
    
   private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }

构建SharedPreferencesImpl会在子线程中读取xml文件,同时将标记位mLoaded置为了false。

//android.app.SharedPreferencesImpl
 private void loadFromDisk() {
        synchronized (mLock) {
            if (mLoaded) {
                return;
            }
            if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }

        // Debugging
        if (mFile.exists() && !mFile.canRead()) {
            Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
        }

        Map<String, Object> map = null;
        StructStat stat = null;
        Throwable thrown = null;
        try {
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16 * 1024);
                    map = (Map<String, Object>) XmlUtils.readMapXml(str);
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            // An errno exception means the stat failed. Treat as empty/non-existing by
            // ignoring.
        } catch (Throwable t) {
            thrown = t;
        }

        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();
            }
        }
    }

读取xml文件完成之后获取mLock对象锁,然后将mLoaded置为true,最后将WaitSet队列中的阻塞线程唤醒。

笔者原创,转载请注明出处:https://blog.csdn.net/devnn/article/details/138086118

SharedPreferences的edit方法

//android.app.SharedPreferencesImpl
   @Override
    public Editor edit() {
        // TODO: remove the need to call awaitLoadedLocked() when
        // requesting an editor.  will require some work on the
        // Editor, but then we should be able to do:
        //
        //      context.getSharedPreferences(..).edit().putString(..).apply()
        //
        // ... all without blocking.
        synchronized (mLock) {
            awaitLoadedLocked();
        }

        return new EditorImpl();
    }

获取mLock的锁,这里如果子线程读取xml时未释放锁,这时就会阻塞等待,如果比子线程先获取锁,也会阻塞直到子线程读取完毕。看awaitLoadedLocked方法:

//android.app.SharedPreferencesImpl
   @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);
        }
    }

可以看到一个while循环,当mLoaded字段为false时就会阻塞当前线程,直到被唤醒。

从以上获取SharedPreferences对象然后调用edit方法的过程,一般我们都会在onCreate中或者控件onClick中获取sp数据,因为可以得出结论:
在主线程中获取SharedPreferences.Editor,必须等待xml文件所有数据读取完毕才会进行后续操作,如果xml中数据越多,那么主线程等待的时间就会越长。

Editor.apply方法
//android.app.SharedPreferencesImpl
 public void apply() {
            final long startTime = System.currentTimeMillis();

            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }

                        if (DEBUG && mcr.wasWritten) {
                            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                                    + " applied after " + (System.currentTimeMillis() - startTime)
                                    + " ms");
                        }
                    }
                };

            QueuedWork.addFinisher(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };

            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

            // Okay to notify the listeners before it's hit disk
            // because the listeners should always get the same
            // SharedPreferences instance back, which has the
            // changes reflected in memory.
            notifyListeners(mcr);
        }

先调用commitToMemory方法将需要修改的数据封装到了MemoryCommitResult类型的对象mcr中:

//android.app.SharedPreferencesImpl
       // 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) {
                    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);
        }

这个方法主要是更新之前从xml中加载到内存的mMap集合,哪些字段被修改了就更新一下。

回到apply方法。

apply方法将要修改的数据包装成mcr,然后将mcr.writtenToDiskLatch.await方法封装成了Runnable,并添加到了QueuedWork当中,这一步很关键,后面再分析。

然后又创建了一个Runnable用来执行前面这个Runnable…

然后调用SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable)将mcr和postWriteRunnable传给了enqueueDiskWrite方法。

//android.app.SharedPreferencesImpl
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

        // Typical #commit() path with fewer allocations, doing a write on
        // the current thread.
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }

        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

这个方法又创建了一个Runnable类型的writeToDiskRunnablewriteToDiskRunnable先执行写出操作再调用传进来的Runnable,然后判断是否是同步执行还是异步操作,同步执行直接在当前线程执行writeToDiskRunnable,异步的话将其丢进QueuedWork中,因为apply是异步,因此我们继承看QueuedWork.queue方法。

//android.app.QueuedWork
 public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();

        synchronized (sLock) {
            sWork.add(work);

            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }

该方法比较简单,主要是将传进来的任务放进了sWork队列中,然后发送消息将工作线程唤醒执行队列中的任务。

//android.app.QueuedWork
    private static class QueuedWorkHandler extends Handler {
        static final int MSG_RUN = 1;

        QueuedWorkHandler(Looper looper) {
            super(looper);
        }

        public void handleMessage(Message msg) {
            if (msg.what == MSG_RUN) {
                processPendingWork();
            }
        }
    }

继续看processPendingWork做了什么。

//android.app.QueuedWork
private static void processPendingWork() {
        long startTime = 0;

        if (DEBUG) {
            startTime = System.currentTimeMillis();
        }

        synchronized (sProcessingWork) {
            LinkedList<Runnable> work;

            synchronized (sLock) {
                work = (LinkedList<Runnable>) sWork.clone();
                sWork.clear();

                // Remove all msg-s as all work will be processed now
                getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
            }

            if (work.size() > 0) {
                for (Runnable w : work) {
                    w.run();
                }

                if (DEBUG) {
                    Log.d(LOG_TAG, "processing " + work.size() + " items took " +
                            +(System.currentTimeMillis() - startTime) + " ms");
                }
            }
        }
    }

很简单,就是将sWork克隆一份,将原来的sWork清空,for循环执行克隆队列中的任务。

到这里apply流程已经分析完毕,主要就是先更新mMap中的数据,然后放到工作线程中执行IO写出操作。

QueuedWork.waitToFinish方法

但是之前调用 QueuedWork.addFinisher(awaitCommit)是做什么的呢?看代码是要等待写出完毕操作,在哪里等待呢?

//android.app.QueuedWork
 public static void waitToFinish() {
        long startTime = System.currentTimeMillis();
        boolean hadMessages = false;

        Handler handler = getHandler();

        synchronized (sLock) {
            if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
                // Delayed work will be processed at processPendingWork() below
                handler.removeMessages(QueuedWorkHandler.MSG_RUN);

                if (DEBUG) {
                    hadMessages = true;
                    Log.d(LOG_TAG, "waiting");
                }
            }

            // We should not delay any work as this might delay the finishers
            sCanDelay = false;
        }

        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
        try {
            processPendingWork();
        } finally {
            StrictMode.setThreadPolicy(oldPolicy);
        }

        try {
            while (true) {
                Runnable finisher;

                synchronized (sLock) {
                    finisher = sFinishers.poll();
                }

                if (finisher == null) {
                    break;
                }

                finisher.run();
            }
        } finally {
            sCanDelay = true;
        }

        synchronized (sLock) {
            long waitTime = System.currentTimeMillis() - startTime;

            if (waitTime > 0 || hadMessages) {
                mWaitTimes.add(Long.valueOf(waitTime).intValue());
                mNumWaits++;

                if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
                    mWaitTimes.log(LOG_TAG, "waited: ");
                }
            }
        }
    }

QueuedWork.waitToFinish方法即是阻塞当前线程等待apply写出任务完毕。

这个方法先是清空handler的中的消息,然后直接在当前线程执行processPendingWork方法,接着遍历之前addFinisher添加进来的任务进行执行。看这情况,相当于如果之前调用apply方法在工作线程中执行的队列任务中还有未完成的就不让它执行,并且将这些任务拿到当前线程进行执行,同时阻塞当前线程等待工作线程中任务执行完毕!

好家伙,相当于强行接管了工作线程中的后续任务,自己亲自来执行,同时等待当前工作线程正在执行的任务执行完毕。

那么QueuedWork.waitToFinish在哪里调用的呢?经过分析是在ActivityThread中执行组件生命周期函数前后:
在这里插入图片描述
这个地方是先执行Activity的onPause方法,然后如果系统小于Android 11则执行QueuedWork.waitToFinish,否则不执行QueuedWork.waitToFinish。看来现在市面上的机型基本在onStop不执行这个方法。

在这里插入图片描述
这个地方先执行Activity的onStop方法,然后如果系统大于Android 11则执行QueuedWork.waitToFinish,否则不执行QueuedWork.waitToFinish。看来现在市面上的机型基本会在onStop之后执行这个方法。

在这里插入图片描述
这个地方是先执行Service的onStartCommand方法,然后执行QueuedWork.waitToFinish

在这里插入图片描述
这个地方是先执行Service的onDetroy方法,然后执行QueuedWork.waitToFinish

经过对SP的apply方法分析可以看出,它是一个异步操作,并且会将sp文件中所有数据一并写出,如果只有一个字段更新,它也会将这些数据写出到磁盘。另外,如果页面即将要关闭,还会阻塞主线程直到sp数据写出完毕。很显然,当sp中数据量很大或者apply操作频繁调用,很容易引发主线程长时间阻塞甚至ANR。

笔者原创,转载请注明出处:https://blog.csdn.net/devnn/article/details/138086118

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

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

相关文章

微信小程序使用echarts实现条形统计图功能

微信小程序使用echarts组件实现条形统计图功能 使用echarts实现在微信小程序中统计图的功能&#xff0c;其实很简单&#xff0c;只需要简单的两步就可以实现啦&#xff0c;具体思路如下&#xff1a; 引入echarts组件调用相应的函数方法 由于需要引入echarts组件&#xff0c;代…

.net报错异常及常用功能处理总结(持续更新)

.net报错异常及常用功能处理总结---持续更新 1. WebApi dynamic传参解析结果中ValueKind Object处理方法问题描述方案1&#xff1a;(推荐&#xff0c;改动很小)方案2&#xff1a; 2.C# .net多层循环嵌套结构数据对象如何写对象动态属性赋值问题描述JavaScript动态属性赋值.net…

WebSocket通信协议

WebSocket是一种网络通信协议.RFC6455定义了它的通信标准 WebSocket是HTML5开始提供的一种在单个TCP连接上进行全双向通信的协议 HTTP协议是一种无状态的,无连接的,单向的应用层协议.它采用了请求,响应的模式.通信请求只能由客户端发起,服务端对请求做出应答处理. 这种模型有…

PO框架【自动化测试】

对象&#xff1a;Tpshop商城 需求&#xff1a;更换头像 操作步骤&#xff1a; 个人信息–头像–上传图片–图片确认–确认保存 核心代码&#xff1a; # 进入frame框架[不熟] driver.switch_to.frame(driver.find_element_by_xpath(//*[id"layui-layer-iframe1"]))…

物联网实战--平台篇之(一)架构设计

本项目的交流QQ群:701889554 物联网实战--入门篇https://blog.csdn.net/ypp240124016/category_12609773.html 物联网实战--驱动篇https://blog.csdn.net/ypp240124016/category_12631333.html 一、平台简介 物联网平台这个概念比较宽&#xff0c;大致可以分为两大类&#x…

为什么要学音视频?

一直都在说“科技改变生活”&#xff0c;现实告诉我们这是真的。 随着通信技术和 5G 技术的不断发展和普及&#xff0c;不仅拉近了人与人之间的距离&#xff0c;还拉近了人与物&#xff0c;物与物之间的距离&#xff0c;万物互联也变得触手可及。 基于此背景下&#xff0c;音…

C++面经(简洁版)

1. 谈谈C和C的认识 C在C的基础上添加类&#xff0c;C是一种结构化语言&#xff0c;它的重点在于数据结构和算法。C语言的设计首要考虑的是如何通过一个过程&#xff0c;对输入进行运算处理得到输出&#xff0c;而对C&#xff0c;首先要考虑的是如何构造一个对象&#xff0c;通…

Node.js -- 包管理工具

文章目录 1. 概念介绍2. npm2.1 npm 下载2.2 npm 初始化包2.3 npm 包(1) npm 搜索包(2) npm 下载安装包(3) require 导入npm 包的基本流程 2.4 开发依赖和生产依赖2.5 npm 全局安装(1) 修改windows 执行策略(2) 环境变量Path 2.6 安装包依赖2.7 安装指定版本的包2.8 删除依赖2.…

jenkins教程

jenkins 一、简介二、下载安装三、配置jdk、maven和SSH四、部署微服务 一、简介 Jenkins是一个流行的开源自动化服务器&#xff0c;用于自动化软件开发过程中的构建、测试和部署任务。它提供了一个可扩展的插件生态系统&#xff0c;支持各种编程语言和工具。 Jenkins是一款开…

PotatoPie 4.0 实验教程(27) —— FPGA实现摄像头图像拉普拉斯边缘提取

拉普拉斯边缘提取有什么作用&#xff1f; 拉普拉斯边缘检测是一种常用的图像处理技术&#xff0c;用于检测图像中的边缘和边界。它的主要作用包括&#xff1a; 边缘检测&#xff1a;拉普拉斯算子可以帮助检测图像中的边缘&#xff0c;即图像中亮度快速变化的位置。这些边缘通常…

前端HTML5学习2(新增多媒体标签,H5的兼容性处理)

前端HTML5学习2新增多媒体标签&#xff0c;H5的兼容性处理&#xff09; 分清标签和属性新增多媒体标签新增视频标签新增音频标签新增全局属性 H5的兼容性处理 分清标签和属性 标签&#xff08;HTML元素&#xff09;和属性&#xff0c;标签定义了内容的类型或结构&#xff0c;而…

RocketMQ 消息重复消费

现象 触发消息后&#xff0c;在1s内收到了两次消息消费的日志。 消息消费日志重复&#xff0c;reconsumeTimes0&#xff0c;主机实例也不同&#xff0c;说明是同一条消息被消费了两次 分析 生产者发送消息的时候使用了重试机制&#xff0c;发送消息后由于网络原因没有收到MQ…

永磁同步电机PMSM负载状态估计simulink模型

永磁同步电机PMSM负载状态估计simulink模型&#xff0c;龙伯格观测器&#xff0c;各种卡尔曼滤波器&#xff0c;矢量控制&#xff0c;坐标变换&#xff0c;永磁同步电机负载转矩估计&#xff0c;pmsm负载转矩测量&#xff0c;负载预测&#xff0c;转矩预测的matlab/simulink仿真…

【C++】---STL容器适配器之queue

【C】---STL容器适配器之queue 一、队列1、队列的性质 二、队列类1、队列的构造2、empty()3、push()4、pop()5、size()6、front()7、back() 三、队列的模拟实现1、头文件&#xff08;底层&#xff1a;deque&#xff09;2、测试文件3、底层&#xff1a;list 一、队列 1、队列的…

【NR RedCap】Release 18标准中对5G RedCap的增强

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G技术研究。 博客内容主要围绕…

R语言贝叶斯方法在生态环境领域中的应用

贝叶斯统计已经被广泛应用到物理学、生态学、心理学、计算机、哲学等各个学术领域&#xff0c;其火爆程度已经跨越了学术圈&#xff0c;如促使其自成统计江湖一派的贝叶斯定理在热播美剧《The Big Bang Theory》中都要秀一把。贝叶斯统计学即贝叶斯学派是一门基本思想与传统基于…

使用微信开发者工具模拟微信小程序定位

哈喽&#xff0c;各位同僚们&#xff0c;我们平时在测试微信小程序的时候&#xff0c;如果小程序中有获取定位或者地图的功能&#xff0c;测试场景中常常需要去模拟不同的位置&#xff0c;例如我们模拟在电子围栏的外面、里面和边界区域等。那么&#xff0c;我们如何在模拟微信…

[笔试训练](八)

目录 022&#xff1a;求最小公倍数 023&#xff1a;数组中的最长连续子序列 024&#xff1a;字母收集 022&#xff1a;求最小公倍数 求最小公倍数_牛客题霸_牛客网 (nowcoder.com) 题目&#xff1a; 题解&#xff1a; 求最小公倍数公式&#xff1a;lcm(a,b)a*b/gcd(a,b)&am…

创建springboot项目的问题

IDEA搭建spring boot时报错Error: Request failed with status code 400 Could not find artifact org.springframework.boot:spring-boot-starter-parent:pom:3.2.5.RELEASE in alimaven (http://maven.aliyun.com/nexus/content/repositories/central/) 原因是父级依赖的版本…

Web前端开发 小实训(一) 成绩分类统计

用于学生web前端开发课程实训练习&#xff0c;掌握基本语法和数据类型 实训目的 使用分支语句&#xff0c;完成分数统计与等级对比,通过输入框输入分数&#xff0c;可以根据分数多少划分等级。 参考思路&#xff1a; 分析题目&#xff1a;根据输入分数进行等级划分。 操作过…
最新文章