Android 源码解析: SharedPreferences的解析

news2024/11/16 19:55:31

Android源码解析:SharedPreferences的解析

导言

SharedPreferences是Android中的一种轻量的数据持久化手段,可能也是我们在学习Android时接触到的第一种特殊的本地数据持久化手段,本篇文章就将从源码角度分析SharedPreferences的原理。

源码分析

一般我们使用SharedPreferences是这样使用的:

//sp的使用--写入数据
val sp = getPreferences(Context.MODE_PRIVATE)
val editor = sp.edit()
editor.putString("cc","123")
editor.apply()
//读取数据
val ans = sp.getString("cc","null")
Toast.makeText(this, ans, Toast.LENGTH_SHORT).show()

我们接下来就以这段程序为例分析SharedPreferences的原理。

获取Preferences对象

我们可以有多种方法可以获得Preferences对象:

  • getPreferences(int mode)
  • getDefaultSharedPreferences(context context)
  • getSharedPreferences(String key,int mode)

这段示例中我们以getPreferences方法为例,实际上这个方法的完整显示应该是getActivity().getPreferences(),也就是说必须在Activity上调用该方法,我们来看该方法:

public SharedPreferences getPreferences(@Context.PreferencesMode int mode) {
    return getSharedPreferences(getLocalClassName(), mode);
}

可以看到该方法最终还是会调用到getSharedPreferences(String key,int mode)方法,只不过此处以本Activity的类名为关键字传递到第一个参数中,接下来我们继续看跳转到的第二个方法中:

public SharedPreferences getSharedPreferences(File file, int mode) {
    return mBase.getSharedPreferences(file, mode);
}

这最终就调用到了与Activity相关联的Context的方法中,这个mBase不出所料应该是ContextImpl,我们来看这个方法:

    public SharedPreferences getSharedPreferences(String name, int mode) {
		......
        File file;
        //同步代码块,以ContextImpl类为锁进行锁定
        synchronized (ContextImpl.class) {
            //当文件路径还没加载时
            if (mSharedPrefsPaths == null) { 
                //创建一个Map来存储文件路径
                mSharedPrefsPaths = new ArrayMap<>();
            }
            //尝试从文件路径存储中查找路径
            file = mSharedPrefsPaths.get(name);
            //若查找不到具体的文件,说明文件还没有被创建
            if (file == null) {
            	//调用getSharedPreferencesPath方法
                file = getSharedPreferencesPath(name);
                //将新创建出来的文件路径放入Map中
                mSharedPrefsPaths.put(name, file);
            }
        }
        // 跳转到另一个重载的方法中
        return getSharedPreferences(file, mode);
    }

重要的代码部分我已经加上了注释,此处的方法就是创建出一个Map来存储同一个Context下的Sp对象(路径),若目标的Sp对象不存在还要创建一个Sp对象然后将其存储到Map中,我们具体先来看getSharedPreferencesPath方法:

public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}

可以看到这个方法实际上就是创建出来了一个新的文件,父路径为getPreferencesDir()的值,子路径为name+xml,意思就是创建出来的是一个xml文件,也就是说Sp实际上是通过xml文件来存储具体数据的。

然后我们来看最后跳转到的另一个方法中:

public SharedPreferences getSharedPreferences(File file, int mode) {
	//实际的Sp实现类
    SharedPreferencesImpl sp;
    //仍然是以ContextImpl为锁进行同步
    synchronized (ContextImpl.class) {
    	//获取Sp缓存
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        //获得Sp的具体实例
        sp = cache.get(file);
        //当不能成功从缓存中获取Sp时
        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实例
            sp = new SharedPreferencesImpl(file, mode);
            //加入到缓存中
            cache.put(file, sp);
            //返回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;
}

这段代码的重要逻辑我也已经标注出来了,我们需要额外看的可能就是getSharedPreferencesCacheLocked()获取缓存的过程:

private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
     if (sSharedPrefsCache == null) {
         sSharedPrefsCache = new ArrayMap<>();
     }

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

     return packagePrefs;
 }

可以看到它获取当前Sp缓存还是通过另一个缓存获取的,也就是说是通过两级缓存来存取数据的。上级缓存是用来缓存同一个包名下的缓存,下级缓存是用来获得具体的Sp实例的。

所以总结下来就是当缓存中没有对应的Sp实例时创建一个Sp实例塞入缓存中,如果缓存中有就直接返回对应Sp实例。

Commit提交修改

首先我们要找到这个方法需要来到Sp的具体实现类SharedPreferencesImpl中的内部类EditorImpl,不过在分析该方法之前我们还需要先看一下另一个方法commitToMemory,它也是EditorImpl中的方法:

private MemoryCommitResult commitToMemory() {
    long memoryStateGeneration;
    boolean keysCleared = false;
    List<String> keysModified = null;
    Set<OnSharedPreferenceChangeListener> listeners = null;
    Map<String, Object> mapToWriteToDisk;
	//以当前的Sp实例(持有的外部类实例)为锁来同步
    synchronized (SharedPreferencesImpl.this.mLock) {
    	//当还有未完成的磁盘写入时
        if (mDiskWritesInFlight > 0) {
        	//更新Map,将之前的Map内容也写入到当前Map中
            mMap = new HashMap<String, Object>(mMap);
        }
        //更新要写入磁盘的Map
        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;
			//如果Clear位为true
            if (mClear) {
            	//写入磁盘的map不为空
                if (!mapToWriteToDisk.isEmpty()) {
                	//修改位置为true
                    changesMade = true;
                    mapToWriteToDisk.clear();
                }
                keysCleared = true;
                mClear = false;
            }
			
			//遍历需要修改的Map中的数据
            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                // v == this 时 或者 v 为空时
                // v == this 对应的是 remove方法
                if (v == this || v == null) {
                	// 需要写入磁盘的map中不包含当前key时,直接跳过本次循环
                    if (!mapToWriteToDisk.containsKey(k)) {
                        continue;
                    }
                    //将其从需要写入磁盘的map中移除
                    mapToWriteToDisk.remove(k);
                } else {
                	//当无修改的时候直接跳过
                    if (mapToWriteToDisk.containsKey(k)) {
                        Object existingValue = mapToWriteToDisk.get(k);
                        if (existingValue != null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    //否则将其写入需要写入磁盘的map
                    mapToWriteToDisk.put(k, v);
                }
				//将changesMade标志位置为true
                changesMade = true;
                //如果有监听器的话
                if (hasListeners) {
                	//将需要修改的键值对的key值写入keysModified中
                    keysModified.add(k);
                }
            }
			//清除mModified这个map
            mModified.clear();
			//如果有修改要提交到磁盘中去
            if (changesMade) {
            	//自增相当于是一个版本号
                mCurrentMemoryStateGeneration++;
            }
            memoryStateGeneration = mCurrentMemoryStateGeneration;
        }
    }
    //返回一个对象,这个对象描述的就是需要写入磁盘中的数据的相关信息
    return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
            listeners, mapToWriteToDisk);
}

该方法的一些注释已经写在上面了,这主要是将Editor之前的操作,比如在执行commit之前调用到的putString等操作封装成一个MemoryCommitResult对象,这个对象就是用来描述需要写入磁盘中的数据的相关信息。

看完了commitToMemory方法,我们接下来再来看commit方法:

public boolean commit() {
    long startTime = 0;

    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }
	//将之前的操作提交形成一个提交对象
    MemoryCommitResult mcr = commitToMemory();
	// 加入到外部类的磁盘写队列中
    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
    try {
    	//等待写入完成
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
        if (DEBUG) {
            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                    + " committed after " + (System.currentTimeMillis() - startTime)
                    + " ms");
        }
    }
    //写入完成之后唤醒监听器
    notifyListeners(mcr);
    //返回是否写入成功
    return mcr.writeToDiskResult;
}

主要的注释也已经写入在代码中了,commit的整个流程还是很好懂的,首先就是通过我们之前介绍过的commitToMemory方法将之前的操作封装成一个提交信息,然后将其添加到SP的任务队列中,等待其写入完成,最后返回结果即可。

接着我们来看加入到任务队列中的过程,具体来说就是SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null );这一句:

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    //判断是否是同步提交的(postWriteRunnable == null 时说明是同步提交的)
    final boolean isFromSyncCommit = (postWriteRunnable == null);
	//将写任务包装成一个Runnable
    final Runnable writeToDiskRunnable = new Runnable() {
            @Override
            public void run() {
                synchronized (mWritingToDiskLock) {
                	//同步执行writeToFile方法,也就是写入磁盘中,具体来说是每个Sp对应的xml文件
                    writeToFile(mcr, isFromSyncCommit);
                }
                //将正在写入的任务数--
                synchronized (mLock) {
                    mDiskWritesInFlight--;
                }
                //异步写入的时候会添加一个postWriteRunnable任务,在此处执行
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };

    //当操作为同步提交
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        //当只有当前这一个任务需要提交的时候
        if (wasEmpty) {
        	//当empyt标志位为true,直接执行我们上面包装好的Runnable
            writeToDiskRunnable.run();
            return;
        }
    }
	//若是异步或者同步提交前有其他任务才会将其添加到工作队列中执行,第二个参数为shouldDelay
	//标志位,即需不需要进行延时100ms,可以看到当同步时不需要延时而异步时需要延时
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

这里相关的代码逻辑也已经在方法中标注出来了。此处的特殊处理在于同步异步时的处理,当为同步提交且当前任务是唯一的任务时将直接执行当前任务而不需要经过任务队列,否则将通过任务队列处理。同步时提交到任务队列执行时不需要进行延时,而异步提交时需要进行100ms的延时。为什么是100ms的延时呢?我们等等再来看这一部分的源码。

Apply提交修改

看完了同步提交,我们接下来再来看异步提交。首先我们紧接着上面关于任务队列的操作,紧接上面的QueuedWork.queue方法:

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

可以看到这个方法很短,其实具体还是通过Handler机制来提交任务的,那其对应的Thread在哪里?可以在getHandler方法中看到:

private static Handler getHandler() {
    synchronized (sLock) {
        if (sHandler == null) {
        	//创建一个HandlerThread用作工作线程
            HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                    Process.THREAD_PRIORITY_FOREGROUND);
            //启动工作线程
            handlerThread.start();
			//创建出Handler
            sHandler = new QueuedWorkHandler(handlerThread.getLooper());
        }
        return sHandler;
    }
}

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

由于写磁盘也是耗时操作,所以说SharedPreferences在执行写任务的时候是会创建一个HandlerThread线程作为工作线程,并且将其与Handler关联起来,通过Handler处理任务队列。我们之前所说的延时100ms具体是通过queue方法体现的:

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

这里通过handler将任务发送到MessageQueue中。如果shouldDelaysCanDelay标志位均为true就会通过Handler的sendMessageDelay方法,第二个参数即为延时的毫秒数,我们可以看到它的具体取值:
在这里插入图片描述
可以看到只是一个100ms的延时。

好了,现在言归正传,我们来看apply方法的源码:

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

可以看到这整个apply方法和commit是差不多的,区别之一是commit方法的mcr.writtenToDiskLatch.await()这一句是直接在commit方法中执行的,该方法就是用来等待写入完成的;而apply方法是将该方法封装进入一个Runnable对象中再塞入工作队列中执行,所以就不会在调用处引起阻塞。除此之外我们还可以在apply中发现的不同点是调用到了addFinisher方法,这个就和具体的工作队列类QueuedWork有关了。

工作队列QueuedWork

该特殊的工作队列和其他的工作队列的不同之处应该就在于其持有的Finisher队列,具体来说这个队列是保证该队列中的任务一定会被执行,什么叫一定被执行呢?众所周知诸如Activity等组件是存在其生命周期的,如果当其生命周期终结时任务队列中的剩余任务自然也不会被执行了,该队列的存在保证剩余的任务一定会被处理,具体我们可以在waitToFinish方法的注释中看出来:

Trigger queued work to be processed immediately. The queued work is processed on a separate thread asynchronous. While doing that run and process all finishers on this thread. The finishers can be implemented in a way to check weather the queued work is finished. Is called from the Activity base class’s onPause(), after BroadcastReceiver’s onReceive, after Service command handling, etc. (so async work is never lost)

这个方法将在Activity的onPause方法中执行来确保Finisher队列中的任务一定会被执行。

读取数据的过程

读取数据的过程我们就以getString方法为例:

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

显然是通过一个map来取数据的,不过在这之前会执行一个awaitLoadedLocked,顾名思义就是等待Sp读取磁盘文件的过程,这个过程我们就不再深入了,不过在读取的过程中也是加锁的。所以我们可以说SharedPerferences是线程安全的工具。

总结

最后我们来对SharedPreferences的工作流程进行一下总结,首先是它的创建:
在这里插入图片描述
接着是它的写入过程:
在这里插入图片描述

读取过程很简单就不写了。

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

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

相关文章

2023年中国烹饪机器人市场发展概况分析:整体规模较小,市场仍处于培育期[图]

烹饪机器人仍属于家用电器范畴&#xff0c;是烹饪小家电的进一步细分&#xff0c;它是烹饪小家电、人工智能和服务机器在厨房领域的融合。烹饪机器人是一种智能化厨房设备&#xff0c;可以根据预设的程序实现自动翻炒和烹饪&#xff0c;是多功能料理机和炒菜机结合的产物。 烹…

【轻松玩转MacOS】更新升级篇

引言 我们都知道&#xff0c;一个运行良好的操作系统就像是一台高速运转的机器。而操作系统的更新和升级&#xff0c;就像是给这台机器进行定期的维护和检查。通过更新和升级&#xff0c;我们可以获得新的功能&#xff0c;修复已知的问题&#xff0c;甚至提高系统的性能和稳定…

vc课堂发票

在这个页面 在控制台中执行&#xff1a; // 获取需要存储的元素值 var 销货单位名称 document.querySelector("body > section > div.table_middle > table > tbody > tr:nth-child(5) >td:nth-child(2) > ul > li:nth-child(1) > span"…

监控搭建-Prometheus

监控搭建-Prometheus 1、背景2、目标3、选型4、Prometheus4.1、介绍4.2、架构4.3、构件4.4、运行机制4.5、环境介绍4.6、数据准备4.7、网络策略4.7.1、主机端口放行4.7.2、设备端口放行 4.8、部署4.9、验证4.10、配置 1、背景 随着项目信息化进程的推进&#xff0c;操作系统、…

基于Springboot实现房屋租赁租房平台系统项目【项目源码+论文说明】分享

基于Springboot实现房屋租赁租房平台系统演示 摘要 在网络高速发展的时代&#xff0c;众多的软件被开发出来&#xff0c;给用户带来了很大的选择余地&#xff0c;而且人们越来越追求更个性的需求。在这种时代背景下&#xff0c;房东只能以用户为导向&#xff0c;所以开发租房网…

Python图形界面框架PyQt5使用详解

概要 使用Python开发图形界面的软件其实并不多&#xff0c;相对于GUI界面&#xff0c;可能Web方式的应用更受人欢迎。但对于像我一样对其他编程语言比如C#或WPF并不熟悉的人来说&#xff0c;未必不是一个好的工具。 常见GUI框架 PyQt5&#xff1a;Qt是一个跨平台的 C图形用户界…

养生产品商城小程序的作用是什么

养生除了食用产品外&#xff0c;还有外用的辅助用品&#xff0c;比如按摩椅、足疗桶等&#xff0c;相应的市场中养生按摩足疗店也非常多&#xff0c;并且有较高的市场需求&#xff0c;除此之外&#xff0c;不少家庭也是购买相关产品在家养生。对厂家或经销商来说&#xff0c;市…

【智能家居项目】裸机版本——认识esp8266 | 网络子系统

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《智能家居项目》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 如上图整个智能家居程序总体框架图&#xff0c;还剩下网络子系统没有实现&#xff0c;以及最终…

使用GitLab CI/CD 定时运行Playwright自动化测试用例

创建项目并上传到GitLab npm init playwright@latest test-playwright # 一路enter cd test-playwright # 运行测试用例 npx playwright test常用指令 # Runs the end-to-end tests. npx playwright test# Starts the interactive UI mode. npx playwright

MySQL案例详解 三:MMM高可用架构及其故障切换

1. MMM高可用概述 1.1 简介 MMM&#xff08;Master-Master replication manager for MvSQL&#xff0c;MySQL主主复制管理器&#xff09;是一套支持双主故障切换和双主日常管理的脚本程序。 MMM提供了自动和手动两种方式移除一组服务器中复制延迟较高的服务器的虚拟ip&#xf…

vue3前端开发-pinia小菠萝使用详细说明

文章目录 1. 介绍1.1 Pinia介绍1.2 pinia的属性说明 2. 安装3. 初步使用4. store具体使用4.1 值修改4.2.1 直接修改4.2.2 通过$patch整体修改4.2.3 通过$patch函数式4.2.4 通过$state整体修改4.2.5 通过actions修改 4.2 解构store 5 actions使用6. getters使用6.1 通过this获取…

nacos初步学习

Nacos初步学习 Nacos 是一个开源的服务注册和配置中心&#xff0c;它允许您注册、注销和发现服务实例&#xff0c;并提供了配置管理的功能。下面是Nacos的最基础用法&#xff1a; 1. 服务注册和发现&#xff1a; 首先&#xff0c;您需要将您的应用程序或服务注册到Nacos中。…

基于FPGA的视频接口之千兆网口(四配置)

简介 相信网络上对于FPGA驱动网口的开发板、博客、论坛数不胜数,为何博主需要重新手敲一遍呢,而不是做一个文抄君呢!因为目前博主感觉网络上描述的多为应用层上的开发,非从底层开始说明,本博主的思虑还是按照老规矩,按照硬件、底层、应用等关系,使用三~四篇文章,来详细…

MacOS安装conda

下载conda 地址https://repo.anaconda.com/miniconda/ 选择合适的安装文件下载 运行安装 执行命令安装 bash Miniconda3-latest-MacOSX-arm64.sh 设置环境变量 echo export PATH"/Users/your_user_name/miniconda3/bin:$PATH" >> ~/.zshrc source ~/.zsh…

智慧安防AI视频智能分析云平台EasyCVR加密机授权小tips

视频云存储/安防监控EasyCVR视频汇聚平台基于云边端智能协同&#xff0c;支持海量视频的轻量化接入与汇聚、转码与处理、全网智能分发、视频集中存储等。音视频流媒体视频平台EasyCVR拓展性强&#xff0c;视频能力丰富&#xff0c;具体可实现视频监控直播、视频轮播、视频录像、…

口袋参谋:批量下载优质买家秀超实用工具!

​说到买家秀&#xff0c;我相信99.9999%的卖家都不陌生&#xff0c;好看的买家秀是可以帮助店铺吸引大批消费者&#xff0c;是可以提升商品销量的。 我们先来看看买家秀的好处有哪些&#xff1f; 买家秀可以更好地获得流量 买家秀可以通过在微淘上发布&#xff0c;或者淘宝头…

接口自动化测试详解,看完不会我退出测试界

一、自动化分类 &#xff08;1&#xff09;接口自动化 python/javarequestsunittest框架来实现 python/javaRF&#xff08;RobotFramework&#xff09;框架来实现——对于编程要求不高 &#xff08;2&#xff09;Web UI功能自动化 python/javaseleniumunittestddtPO框架来实…

nginx版本号隐藏(405 not allowed解决办法)

背景 项目安全测试发现405页面暴露了nginx版本&#xff0c;其相关版本号泄露时攻击者会利用相应软件版本的当前漏洞&#xff0c;进行有效的相应攻击。所以需要我们做好版本号的隐藏。 解决办法 1.nginx版本隐藏 只需要我们在nginx.conf文件的server中添加server_tokens off&…

k8s修改集群IP--不重置集群

正常在用集群想要更换ip master 节点ip192.168.10.138 改为192.168.10.148 node1节点ip192.168.10.139 改为192.168.10.149 node2节点ip192.168.10.140 改为192.168.10.150 master 节点 1)执行脚本1233.sh 1233.sh 内容如下&#xff1a; # master 节点 export oldip1192.168.…

Outlook屏蔽Jira AI提醒

前言&#xff1a;最近不知道为什么jira上的ai小助手抽风&#xff0c;一周发个几千封邮件…导致我现在都不想在邮箱里面跟找垃圾一样找消息了。实在忍无可忍&#xff0c;决定屏蔽AI小助手&#xff0c;方法很简单&#xff0c;follow me~~ 第一步&#xff1a;双击打开电脑版Outloo…