Android性能优化之SharedPreference卡顿优化

下面的源码都是基于Android api 31

1、SharedPreference使用

val sharePref = getPreferences(Context.MODE_PRIVATE)
with(sharePref.edit())
{
putBoolean("isLogin", true)putInt("age", 18)apply()
}
val isLogin = sharePref.getBoolean("isLogin", false)
val age = sharePref.getInt("age", -1)
Log.d(TAG, "isLogin $isLogin age $age")

1、获取SharedPreferencesImpl

获取sharedpreferences实现类是ContextImpl,下面是ContextImpl中获取sharedpreferencesImpl的源码

public SharedPreferences getSharedPreferences(String name, int mode) {......File file;synchronized (ContextImpl.class) {//如果mSharedPrefsPaths为null,先创建一个mSharedPrefsPaths的Map。sharePreferences存放的地址if (mSharedPrefsPaths == null) {mSharedPrefsPaths = new ArrayMap<>();}//获取sharedPrefs的filefile = mSharedPrefsPaths.get(name);if (file == null) {//如果file为null,创建filefile = getSharedPreferencesPath(name);mSharedPrefsPaths.put(name, file);}}//通过file获取shareprefreturn getSharedPreferences(file, mode);
}//通过名称来获取file
public File getSharedPreferencesPath(String name) {return makeFilename(getPreferencesDir(), name + ".xml");
}

在contextImpl中是先根据sharedpref的名称来获取对应的File文件,然后通过file来获取sharedprefImpl

public SharedPreferences getSharedPreferences(File file, int mode) {SharedPreferencesImpl sp;synchronized (ContextImpl.class) {//Map中存放file-spImplfinal ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();//先从cache中获取,sp = cache.get(file);if (sp == null) {//创建一个spImplsp = new SharedPreferencesImpl(file, mode);//放入到cache中cache.put(file, sp);return sp;}}if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {//多进程,会执行reloadsp.startReloadIfChangedUnexpectedly();}return sp;
}

由上面代码可知获取sharedPreferencesImpl实例分为两步:

1、通过sp的name来获取存放sp的File。

2、通过file来获取sharedPreferencesImpl实例。

name和file,file和sharedPreferencesImpl在创建过一次后会以key-value的形式保存在map中,方便后面再次获取。所以并不是每次获取spImpl都会去new。

2、SharedPrefrence存储

sp在磁盘中是以xml文件的形式存放的,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<defaultMap><entry><key>key</key><value>true</value></entry>    <entry><key>key</key><value>Hello World!</value></entry><entry><key>key</key><value>5</value></entry>
</defaultMap>

3、SharedPreference源码

SharedPreference本身是一个接口实现类是SharedPreferencesImpl。SharedPreferencesImpl构造方法如下:

SharedPreferencesImpl(File file, int mode) {mFile = file;mBackupFile = makeBackupFile(file);mMode = mode;mLoaded = false;mMap = null;mThrowable = null;startLoadFromDisk();
}

SharedPreferencesImpl的构建方法中会先通过传参中的file,创建一个mBackupFile,并且开始从磁盘中获取数据。

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

startLoadFromDisk()中会创建一个名称为SharedPreferencesImpl-load的线程来加载磁盘中的数据。并将加载后的数据以key-value的形式放在mMap中。

写入数据

public final class EditorImpl implements Editor {private final Object mEditorLock = new Object();@GuardedBy("mEditorLock")private final Map<String, Object> mModified = new HashMap<>();@GuardedBy("mEditorLock")private boolean mClear = false;@Overridepublic Editor putString(String key, @Nullable String value) {synchronized (mEditorLock) {mModified.put(key, value);return this;}}
}

写入数据时并不是直接往磁盘中写,如上的putString,会先将数据方法到mModified的map中,然后在commit或者apply的时候再进行写磁盘操作。

1、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方法中会创建一个mcr对象,然后执行enqueueDiskWrite,最后返回mcr.writeToDiskResult。

在这里插入图片描述

2、commitToMemory方法

private MemoryCommitResult commitToMemory() {long memoryStateGeneration;boolean keysCleared = false;List<String> keysModified = null;Set<OnSharedPreferenceChangeListener> listeners = null;Map<String, Object> mapToWriteToDisk;synchronized (SharedPreferencesImpl.this.mLock) {if (mDiskWritesInFlight > 0) {//先判断是不是有多个写磁盘的操作,如果有多个写磁盘的操作,先copy mMap的对象。mMap = new HashMap<String, Object>(mMap);}//把map的对象赋值给mapToWriteDisk,mapToWriteToDisk = mMap;mDiskWritesInFlight++;//mDiskWritesInFlight加1synchronized (mEditorLock) {boolean changesMade = false;//判断是否清除sp中的数据if (mClear) {if (!mapToWriteToDisk.isEmpty()) {changesMade = true;mapToWriteToDisk.clear();//如果清除sp中的数据将mapToWirteToDisk的数据清空。}keysCleared = true;mClear = false;}//遍历mModified mapfor (Map.Entry<String, Object> e : mModified.entrySet()) {String k = e.getKey();Object v = e.getValue();////当v==this,或者v为null时,也就是执行了remove()方法的if (v == this || v == null) {if (!mapToWriteToDisk.containsKey(k)) {//如果mapToWriteToDisk中不包含这个k,继续循环continue;}//否则从mapToWriteToDisk中移除这个k-vmapToWriteToDisk.remove(k);} else {if (mapToWriteToDisk.containsKey(k)) {Object existingValue = mapToWriteToDisk.get(k);if (existingValue != null && existingValue.equals(v)) {continue;}//如果mapTopWriteToDisk中的key-value和mModified中的相同不操作}//把k-v方法哦mapToWriteToDisk中mapToWriteToDisk.put(k, v);}changesMade = true;}//清除mModified中的数据mModified.clear();if (changesMade) {//Generation加1mCurrentMemoryStateGeneration++;}memoryStateGeneration = mCurrentMemoryStateGeneration;}return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,listeners, mapToWriteToDisk);}

对应的流程图如下

在这里插入图片描述

3、enqueueDiskWrite方法

private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {final boolean isFromSyncCommit = (postWriteRunnable == null);final Runnable writeToDiskRunnable = new Runnable() {@Overridepublic void run() {synchronized (mWritingToDiskLock) {//往磁盘中写writeToFile(mcr, isFromSyncCommit);}synchronized (mLock) {//减1mDiskWritesInFlight--;}if (postWriteRunnable != null) {//运行postWriteRunnablepostWriteRunnable.run();}}};if (isFromSyncCommit) {//如果通过commit提交的直接执行writeToDiskRunnableboolean wasEmpty = false;synchronized (mLock) {wasEmpty = mDiskWritesInFlight == 1;}if (wasEmpty) {writeToDiskRunnable.run();return;}}//先进入队列QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

在这里插入图片描述

4、queue代码

由上面可知mDiskWritesInFlight>1,或者是通过sp.apply的方法调用的,会执行到queue方法中

public static void queue(Runnable work, boolean shouldDelay) {Handler handler = getHandler();synchronized (sLock) {sWork.add(work);//添加到work中//shouldDelay和sCanDelay为true时Handler发一个Delayed消息,否则发送一个非延迟消息if (shouldDelay && sCanDelay) {handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);} else {handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);}}
}

上面的handler是通过HandlerThread创建的sHandler,源码如下

private static Handler getHandler() {synchronized (sLock) {if (sHandler == null) {HandlerThread handlerThread = new HandlerThread("queued-work-looper",Process.THREAD_PRIORITY_FOREGROUND);handlerThread.start();sHandler = new QueuedWorkHandler(handlerThread.getLooper());}return sHandler;}
}

由上面流程可以知道,只有执行commit操作并且mDiskWritesInFlight == 1时才会在主线程中执行,卡住主线程,当mDiskWritesInFlight > 1或者apply的时候不是在主线程中的。

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

5、apply()方法

在这里插入图片描述

public void apply() {final long startTime = System.currentTimeMillis();//创建mcr对象final MemoryCommitResult mcr = commitToMemory();final Runnable awaitCommit = new Runnable() {@Overridepublic void run() {try {///创建awaitCommit,用于等待写磁盘完成后调用mcr.writtenToDiskLatch.await();} catch (InterruptedException ignored) {}}};//添加awaitCommitQueuedWork.addFinisher(awaitCommit);Runnable postWriteRunnable = new Runnable() {@Overridepublic void run() {//执行awaitCommitawaitCommit.run();///移除awaitCommitQueuedWork.removeFinisher(awaitCommit);}};//进入队列SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);notifyListeners(mcr);
}

apply方法,首先创建了一个 awaitCommit 的 Runnable,然后加入到 QueuedWork 的 sPendingWorkFinishers 队列中,awaitCommit 中包含了一个等待锁,需要在真正对 SP 进行持久化的时候进行释放。

6、apply方法中为什么要QueuedWork.addFinisher(awaitCommit)?

先要了解QueuedWork.waitToFinish() 方法,QueuedWork.waitToFinish() 会等待所有的addFinisher的runnable执行完成后才会进行执行。在ActivityThread中下面的方法都会执行QueuedWork.waitToFinish()方法。

  1. handleServiceArgs -> Service.onStartCommand
  2. handleStopService -> Service.onDestroy
  3. handlePauseActivity -> Activity.onPause
  4. handleStopActivity -> Activity.onStop

上面的问题转换成了为什么ActivityThread中上面的方法为什么都要等待sharePref的apply方法执行完写磁盘后才能被调用。

应用可能被系统回收、被用户杀死,会Crash,这些都是不确定的。apply 提交的任务,是先放到队列中去依次执行,而不是立即执行,意味着可能该任务还没被执行,app就被杀死。所以为了尽可能保证数据能被持久化,就要找到一些重要的时机去卡住,来保证完成写入。

7、获取数据

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

获取数据时先要等数据从磁盘中加载完成,所以会先执行awaitLoadedLocked();方法,这里面的会通过mLoaded判断数据是否已经从磁盘中加载完成,如果没有加载完成则会等待。

private void awaitLoadedLocked() {while (!mLoaded) {try {mLock.wait();} catch (InterruptedException unused) {}}if (mThrowable != null) {throw new IllegalStateException(mThrowable);}
}

4、疑问

1、执行put不执行apply或者commit,再获取这个值能获取到吗?

val sharePref = getPreferences(Context.MODE_PRIVATE)
with(sharePref.edit())
{
putBoolean("isLogin", true)putInt("age", 18)val age = sharePref.getInt("age", -1)Log.d(TAG, "----before apply--- age $age")apply()val age1 = sharePref.getInt("age", -1)Log.d(TAG, "----after apply--- age $age1")
} 

打印的log如下,在执行apply之前获取到的age是默认值-1,执行apply后获取的值是set的值18.

 ----before apply--- age -1----after apply--- age 18

5、SharedPreferences跨进程

在使用SharedPreference 时,有如下一些模式: MODE_PRIVATE 私有模式,这是最常见的模式,一般情况下都使用该模式。 MODE_WORLD_READABLE,MODE_WORLD_WRITEABLE ,文件开放读写权限,不安全,已经被废弃了,google建议使用FileProvider共享文件。 MODE_MULTI_PROCESS,跨进程模式,如果项目有多个进程使用同一个Preference,需要使用该模式,但是也已经废弃了。使用contentProvider可以实现sp的跨进程,

在这里插入图片描述

参考:https://www.jianshu.com/p/875d13458538

6、SharedPreferences ANR

使用sp的项目中经常会出现下面的ANR

"main" prio=5 tid=1 Waiting| group="main" sCount=1 dsCount=0 obj=0x73f50268 self=0xefc05400| sysTid=13726 nice=0 cgrp=default sched=0/0 handle=0xf30cf534| state=S schedstat=( 1450815451 10991027618 7548 ) utm=40 stm=105 core=0 HZ=100| stack=0xff1f4000-0xff1f6000 stackSize=8MB| held mutexes=at java.lang.Object.wait!(Object.java)- waiting on <0x04386712> (a java.lang.Object)at java.lang.Thread.parkFor$(Thread.java:2127)- locked <0x04386712> (a java.lang.Object)at sun.misc.Unsafe.park(Unsafe.java:325)at java.util.concurrent.locks.LockSupport.park(LockSupport.java:161)at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:840)at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:994)at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1303)at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:203)at android.app.SharedPreferencesImpl$EditorImpl$ 1. run(SharedPreferencesImpl.java: 366 )  at android.app.QueuedWork.waitToFinish(QueuedWork.java:88)at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:3400)at android.app.ActivityThread.-wrap21(ActivityThread.java:-1)at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1632)at android.os.Handler.dispatchMessage(Handler.java:110)at android.os.Looper.loop(Looper.java:203)at android.app.ActivityThread.main(ActivityThread.java:6251)at java.lang.reflect.Method.invoke!(Method.java)at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1063)at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:924)

1、sp引起ANR的原因

由上面日志可以看出main线程在等待锁导致的ANR,具体原因是ActivityThread.handleServiceArgs,时执行QueueWork.waitToFinish。ActivityThread中handleServiceArgs的代码如下:

private void handleServiceArgs(ServiceArgsData data) {Service s = mServices.get(data.token);if (s != null) {try {if (data.args != null) {data.args.setExtrasClassLoader(s.getClassLoader());data.args.prepareToEnterProcess();}int res;if (!data.taskRemoved) {res = s.onStartCommand(data.args, data.flags, data.startId);} else {s.onTaskRemoved(data.args);res = Service.START_TASK_REMOVED_COMPLETE;}//QueuedWork.waitToFinishQueuedWork.waitToFinish();try {ActivityManager.getService().serviceDoneExecuting(data.token, SERVICE_DONE_EXECUTING_START, data.startId, res);} catch (RemoteException e) {throw e.rethrowFromSystemServer();}} catch (Exception e) {......}}
}

如上代码在handleServiceArgs中执行了QueueWork.waitFinish(),waitFinish()的源码如下:

public static void waitToFinish() {......try {//循环获取sFinishers队列中的任务,当任务为null时退出while (true) {Runnable finisher;synchronized (sLock) {finisher = sFinishers.poll();}if (finisher == null) {break;}//执行任务finisher.run();}} finally {sCanDelay = true;}......
}

在handleServiceArgs时执行QueueWork.waitFinish(),会等待QueueWork中的任务执行完成。在SharedPreferencesImpl的apply方法中如下:

public void apply() {final MemoryCommitResult mcr = commitToMemory();final Runnable awaitCommit = new Runnable() {@Overridepublic void run() {try {mcr.writtenToDiskLatch.await();} catch (InterruptedException ignored) {}}};//添加awaitQueuedWork.addFinisher(awaitCommit);//写磁盘后再调用Runnable postWriteRunnable = new Runnable() {@Overridepublic void run() {awaitCommit.run();QueuedWork.removeFinisher(awaitCommit);}};//添加到队列SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);notifyListeners(mcr);
}

postWriteRunnable执行时会调用awaitCommit.run();这里会执行mcr.writtenToDiskLatch.await();等待写磁盘完成,再调用,所以QueuedWork.removeFinisher(awaitCommit);是在写磁盘完成后移除的。

ActivityThread 中,下面的方法中都调用了QueuedWork.waitToFinish() :

  1. handleServiceArgs -> Service.onStartCommand
  2. handleStopService -> Service.onDestroy
  3. handlePauseActivity -> Activity.onPause
  4. handleStopActivity -> Activity.onStop

以上四处都存在着 SP 的等待锁问题所导致的 ANR 风险。

2、解决方法

1、8.0及以下版本

由于sPendingWorkFinishers是QueuedWork的静态集合对象,而且ConcurrentLinkedQueue这个集合类是可以继承的,所以可以直接重新定义一个集合类继承自ConcurrentLinkedQueue,覆盖ConcurrentLinkedQueue集合在QueuedWork中暴露出来的接口,将poll接口的返回值改为固定返回null,用这个自定义的集合动态代理之前的集合,这个时候ActivityThread的H在处理消息的时候就再也不用执行等待行为了。
在这里插入图片描述

如下方式hook住QueueWork替换sPendingWorkFinishers为ConcurrentLinkedQueueProxy

public static void replaceQueueWorkPendingWorkFinishers() {Log.d(TAG, "start hook, time stamp = " + System.currentTimeMillis());ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers = null;Field field = null;try {Class<?> atClass = Class.forName("android.app.QueuedWork");field = atClass.getDeclaredField("sPendingWorkFinishers");field.setAccessible(true);sPendingWorkFinishers = (ConcurrentLinkedQueue<Runnable>) field.get(null);if (sPendingWorkFinishers != null) {field.set(null, new ConcurrentLinkedQueueProxy<Runnable>(sPendingWorkFinishers));Log.d(TAG, "Below android 0,replaceQueueWorkPendingWorkFinishers success.");}} catch (Exception e) {// 出现异常try {if (sPendingWorkFinishers != null && field != null) {field.set(null, sPendingWorkFinishers);}} catch (Exception ex) {//ignore}Log.e(TAG, "Below android 0,,hook sPendingWorkFinishers fail.", e);}Log.d(TAG, "end hook, time stamp = " + System.currentTimeMillis());
}

7、SharedPreferences替代方案

MMKV

|len|key|len|value||len|key|len|value||len|key|len|value|...

文件中key value紧密排布。另有一个crc文件。

MMKV写策略是,增量kv对象序列化后,直接append到内存末尾。这样同一个key会有新旧若干份数据,最新的数据在最后。

不断 append 的话,文件大小会增长得不可控。例如同一个 key 不断更新的话,是可能耗尽几百 M 甚至上 G 空间,而事实上整个 kv 文件就这一个 key,不到 1k 空间就存得下。这明显是不可取的。因此MMKV在性能和空间上做了折中:以内存 pagesize 为单位申请空间,在空间用尽之前都是 append 模式;当 append 到文件末尾时,进行文件重整、key 排重,尝试序列化保存排重结果;排重后空间还是不够用的话,将文件扩大一倍,直到空间足够。

而这个重写策略,就是直接将内存中的hash表取出kv逐个写入文件,直接丢弃原先的文件内容。

存在问题:

  • 原先一直没有改变的值也需要跟着一起重新写入。
  • 由于写入的是同一个文件路径,在这期间如果重写中断或者失败,就会导致内容丢失。
  • append内容的时候如果被中断,会导致脏数据。
  • 多进程情况下,一旦有进程更新文件,另一进程在访问KV的时候就必须要校验crc,如不一致则需重新载入kv来构建新的hash表。
  • 无类型信息。一方面,无法根据已有文件来追踪所存内容。另一方面,这将导致已经迁移至MMKV的数据后续无法再迁移到其他方案,因为无法取出每个值的完整类型信息。
  • 每新开一个kv仓库,都需要新增一个fd,如果该仓库要支持多进程,需要再增加一个fd,从而容易导致fd超限问题。

链接:https://github.com/Tencent/MMKV

参考

1、https://stackoverflow.com/questions/45358761/how-to-save-variable-value-even-if-app-is-destroyed

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

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

相关文章

windows环境下docker数据迁移到其他盘

docker安装在C盘&#xff0c;使用一段时间后&#xff0c;C盘爆满。因此想把C盘中的数据迁移到其他盘&#xff0c;以释放C盘空间。分为以下步骤&#xff1a; 1、启动docker软件&#xff0c;打开PowerShell并切换到Docker Compose配置文件的目录。 Docker Compose配置文件的目录…

通过社区参与解锁早期增长:Maven 远程医疗平台概览

Maven通过用户导向的渐进式验证&#xff0c;找到了一个被忽视的巨大女性医疗服务市场&#xff0c;作为女性医疗保健的先行者&#xff0c;已服务超过1500万用户&#xff0c;目前估值已达$14亿。本文将深入探索Maven实现产品市场匹配的三个阶段&#xff0c;从如何验证初始的市场机…

Vue2基础十、Vuex

零、文章目录 Vue2基础十、Vuex 1、vuex概述 &#xff08;1&#xff09;vuex是什么 vuex 是一个 vue 的 状态管理工具&#xff0c;状态就是数据。大白话&#xff1a;vuex 是一个插件&#xff0c;可以帮我们管理 vue 通用的数据 (多组件共享的数据) 例如&#xff1a;购物车数…

Linux安装部署Nacos和sentinel

1.将nacos安装包下载到本地后上传到linux中 2.进入nacos的/bin目录,输入命令启动nacos [rootlocalhost bin]# sh startup.sh -m standalone注:使用第二种方式启动,同时增加日志记录的功能 2.2 startup.sh文件是不具备足够的权限,否则不能操作 给文件赋予执行权限 [rootlocalh…

【lesson5】linux vim介绍及使用

文章目录 vim的基本介绍vim的基本操作vim常见的命令命令模式下的命令yypnyynpuctrlrGggnG$^wbh,j,k,lddnddnddp~shiftrrnrxnx 底行模式下的命令set nuset nonuvs 源文件wq!command&#xff08;命令&#xff09; vim配置解决无法使用sudo问题 vim的基本介绍 首先vim是linux下的…

十、数据结构——链式队列

数据结构中的链式队列 目录 一、链式队列的定义 二、链式队列的实现 三、链式队列的基本操作 ①初始化 ②判空 ③入队 ④出队 ⑤获取长度 ⑥打印 四、循环队列的应用 五、总结 六、全部代码 七、结果 在数据结构中&#xff0c;队列&#xff08;Queue&#xff09;是一种常见…

react-router-dom和react-router的区别

react-router-dom和react-router的区别 前言 在使用react-router-dom的时候&#xff0c;经常会和react-router搞混了&#xff0c;搞不清楚它们哪个跟哪&#xff0c;到底有什么关系&#xff0c;今天来总结一下。 结论 react-router-dom是在react-router的基础上开发的&#…

变现:利用 chatgpt + midjourney 制作微信表情包

1、利用gpt生成提示词&#xff0c;当然也可以直接翻译 生成基础提示词&#xff0c; 比如&#xff1a; an anime image with a white kawaii character in it, in the style of light green and brown, minimalist detail, animated gifs, cranberrycore, 1860–1969, babyco…

C#实现数字验证码

开发环境&#xff1a;VS2019&#xff0c;.NET Core 3.1&#xff0c;ASP.NET Core API 1、建立一个验证码控制器 新建两个方法Create和Check&#xff0c;Create用于创建验证码&#xff0c;Check用于验证它是否有效。 声明一个静态类变量存放列表&#xff0c;列表中存放包含令…

python selenium爬虫自动登录实例

拷贝地址&#xff1a;python selenium爬虫自动登录实例_python selenium登录_Ustiniano的博客-CSDN博客 一、概述 我们要先安装selenium这个库&#xff0c;使用pip install selenium 命令安装&#xff0c;selenium这个库相当于机器模仿人的行为去点击浏览器上的元素&#xff0…

Android ANR触发机制之Service ANR

一、前言 在Service组件StartService()方式启动流程分析文章中&#xff0c;针对Context#startService()启动Service流程分析了源码&#xff0c;其实关于Service启动还有一个比较重要的点是Service启动的ANR&#xff0c;因为因为线上出现了上百例的"executing service &quo…

R-并行计算

本文介绍在计算机多核上通过parallel包进行并行计算。 并行计算运算步骤&#xff1a; 加载并行计算包&#xff0c;如library(parallel)。创建几个“workers”,通常一个workers一个核&#xff08;core&#xff09;&#xff1b;这些workers什么都不知道&#xff0c;它们的全局环…

c++学习(位图)[22]

位图 位图&#xff08;Bitmap&#xff09;是一种数据结构&#xff0c;用于表示一个固定范围的布尔值&#xff08;通常是0或1&#xff09;。它使用一个二进制位来表示一个布尔值&#xff0c;其中每个位的值表示对应位置的元素是否存在或满足某种条件。 位图可以用于解决一些特…

利用MATLAB制作DEM山体阴影

在地理绘图中&#xff0c;我们使用的DEM数据添加山体阴影使得绘制的图件显得更加的美观。 GIS中使用ArcGIS软件就可以达到这一目的&#xff0c;或者使用GMT&#xff0c;同样可以得到山体阴影的效果。 本文提供了一个MATLAB的函数&#xff0c;可以得到山体阴影。 clear all;c…

《面试1v1》如何能从Kafka得到准确的信息

&#x1f345; 作者简介&#xff1a;王哥&#xff0c;CSDN2022博客总榜Top100&#x1f3c6;、博客专家&#x1f4aa; &#x1f345; 技术交流&#xff1a;定期更新Java硬核干货&#xff0c;不定期送书活动 &#x1f345; 王哥多年工作总结&#xff1a;Java学习路线总结&#xf…

安防视频管理平台GB设备接入EasyCVR, 如何获取RTMP与RTSP视频流

安防视频监控平台EasyCVR可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等&#xff0c;以及支持厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等。平台既具备传统安防视频监控的能力&#xff0c;比…

基于粒子群优化算法的分布式电源选址与定容【多目标优化】【IEEE33节点】(Matlab代码实现)

目录 &#x1f4a5;1 概述 1.1 目标函数 2.2 约束条件 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f308;4 Matlab代码实现 &#x1f4a5;1 概述 分布式电源接入配电网&#xff0c;实现就地消纳&#xff0c;可以提高新能源的利用率、提高电能质量和降低系统网损…

出海周报|Temu在美状告shein、ChatGPT安卓版上线、小红书回应闪退

工程机械产业“出海”成绩喜人&#xff0c;山东相关企业全国最多Temu在美状告shein&#xff0c;跨境电商战事升级TikTok将在美国推出电子商务计划&#xff0c;售卖中国商品高德即将上线国际图服务&#xff0c;初期即可覆盖全球超200个国家和地区ChatGPT安卓版正式上线&#xff…

echarts遇到的问题

文章目录 折线图-区域面积图 areaStyley轴只有整数y轴不从0开始y轴数值不确定&#xff0c;有大有小&#xff0c;需要动态处理折线-显示label标线legend的格式化和默认选中状态x轴的lable超长处理x轴的相关设置 echarts各个场景遇到的问题 折线图-区域面积图 areaStyle areaStyl…

node.js的优点

提示&#xff1a;node.js的优点 文章目录 一、什么是node.js二、node.js的特性 一、什么是node.js 提示&#xff1a;什么是node.js? Node.js发布于2009年5月&#xff0c;由Ryan Dahl开发&#xff0c;是一个基于ChromeV8引擎的JavaScript运行环境&#xff0c;使用了一个事件驱…