热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

AndroidSharedPreferences原理分析

目录访问SharedPreferencesSharedPreferences的创建与初始化Editor介绍异步操作apply同步操作commit写入文件writeToFileQue

目录

  • 访问SharedPreferences
  • SharedPreferences的创建与初始化
  • Editor介绍
  • 异步操作apply
  • 同步操作commit
  • 写入文件writeToFile
  • QueuedWork介绍
  • 系统组件中对于SharedPreferences的处理
  • 总结

注:本文基于Android 8.1。

1. 访问SharedPreferences

SharedPreferences是Android系统提供的一种轻量级的数据存取方式,数据存取是通过键值对的形式,存放到xml中。
xml文件的存放路径为:/data/data/packageName/shared_prefs/目录
在应用中可以调用context.getSharedPreferences(fileName, mode)使用获取到SharedPreferences,如果操作比较简单,或者希望异步操作,不需要等待结果,可以在存数据之后调用apply();
如果是批量操作,并且需要知道是否写入文件成功,则可以存数据之后,调用commit();
对Context有了解的都知道,context中的方法调用最终会调用到ContextImpl中实现,其中关于getSharedPreferences的public方法有以下几个:

// 传入文件以及打开文件的模式获取SharedPreferences
public SharedPreferences getSharedPreferences(File file, int mode)
// 传入文件名称以及打开文件的模式获取SharedPreferences
public SharedPreferences getSharedPreferences(String name, int mode)
// 根据文件名称获取相应的SharedPreferences对应的文件
public File getSharedPreferencesPath(String name)

看上去有三个方法,但是其实传递文件名称的方法在ContextImpl中也是调用传递文件的方法,所以就从getSharedPreferences(String name, int mode)开始介绍。
方便起见,以下SharedPreferences简称为sp

2. SharedPreferences的创建与初始化

2.1 getSharedPreferences

public SharedPreferences getSharedPreferences(String name, int mode) {
...
File file;
synchronized (ContextImpl.class) {
// mSharedPrefsPaths是ContextImpl中的一个ArrayMap
// key是文件名,value是存放该文件名对应的sp的File对象
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
//对于每一个创建的sp,如果不存在则创建对应的文件
file = mSharedPrefsPaths.get(name);
if (file == null) {
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
// 还是调用到了另外一个参数为File的重载方法
return getSharedPreferences(file, mode);
}
// 下面这两个方法可以看出,sp存放在App数据目录下的shared_prefs目录中
// 对应文件的名称就是在获取sp的时候传入的name,结尾添加一个 ".xml"
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
private File getPreferencesDir() {
synchronized (mSync) {
if (mPreferencesDir == null) {
mPreferencesDir = new File(getDataDir(), "shared_prefs");
}
return ensurePrivateDirExists(mPreferencesDir);
}
}

getSharedPreferences(file, mode)

public SharedPreferences getSharedPreferences(File file, int mode) {
// SharedPreferencesImpl类是核心,sp中数据的存取包括写入到文件都是由这个类来实现的
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
// getSharedPreferencesCacheLocked去获取sp缓存,下面有对这个方法的介绍
// 这里获取到当前进程所有的存放的sp的map
// key是存放sp的File,value是这个sp对应的SharedPreferencesImpl对象
final ArrayMap cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
// 对传入的mode进行校验
// 在N以及之上的版本,不再支持MODE_WORLD_READABLE和MODE_WORLD_READABLE
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");
}
}
// 如果不存在则创建并存入map缓存中
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion // 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;
}
private ArrayMap getSharedPreferencesCacheLocked() {
// sSharedPrefsCache是一个二级map
// 一集key是packageName,二级key是sp对应的File,value是sp对应的SharedPreferencesImpl对象
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
// 先以packageName为key,value是这个package对应的SharedPreferencesImpl的Map
// 如果不存在则创建map
ArrayMap packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}

小结:
这里需要注意的是:
mSharedPrefsPaths:这个map是ContextImpl的成员变量,也就是说对于每一个ContextImpl而言,都有这样一个map,其中的key是存放sp的name,value是存放sp对应的文件。
sSharedPrefsCache:这个map是静态的,属于ContextImpl这个类,也就是说每个进程中都只有一份,是一个二级map,根据pakcgaeName以及sp的文件存放sp对应的SharedPreferencesImpl对象。
也就是说,对于每一个sp,以文件名(或者说是文件)为区分,都拥有不同的SharedPreferencesImpl来进行管理。
所以这个SharedPreferencesImpl中的实现才是整个sp的核心。

上面已经针对与sp创建了SharedPreferencesImpl对象,那接下来就看看这个SharedPreferencesImpl的构造方法中做了什么。

2.2 SharedPreferencesImpl初始化

SharedPreferencesImpl(File file, int mode) {
// 每一个sp对应的文件都对应一个SharedPreferencesImpl,mFile存放的正是这个File
mFile = file;
mBackupFile = makeBackupFile(file);
// sp的mode
mMode = mode;
// 是否从disk中读取了数据
mLoaded = false;
// 因为sp中的数据是以key-value的形式存放的,装载到内存中就是存放在这个map中
mMap = null;
// 从disk读取数据并装载到上面的map
startLoadFromDisk();
}

startLoadFromDisk()中会开启一个线程去从disk上读取File中的内容,装载到内存

private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
// 开启新的线程读写文件,线程名称是"SharedPreferencesImpl-load"
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}

loadFromDisk这个方法的实现这里就步不贴了,其中主要是先把文件中的内容读取出来,然后通过pull解析的方式,将数据装到map中,有兴趣可以自己去看一下实现。

android-8.1.0_r15/frameworks/base/core/java/com/android/internal/util/XmlUtils.java

经过了上面的初始化,如果是刚刚创建的sp,则只是初始化了一些变量,对于已经有数据的sp,则已经将sp再磁盘上的数据写入到了mMap中。

3. Editor介绍

获取到sp之后,就可以进行数据的读写,取数据的实现都很简单,因为前面已经将数据放到了map中,所以直接从mMap中根据key去拿就行了,但是写数据的话还需要调用edit()方法获取一个Editor进行操作。

public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
// 这里要注意一下,每次调用edit都会创建出一个新的EditorImpl对象
return new EditorImpl();
}

EditorImpl中主要就是一些put操作,把要写入的数据存放在一个内部的map(mModified)中,然后当调用apply或者commit的时候,统一提交到内存/disk。
最终要的还是其中的apply和commit方法。

4. 异步操作apply

apply是异步操作,commit是同步操作,这个应该很多人都知道,这里就具体看看是怎么实现的。

public void apply() {
// 记录开始时间,debug的才用到
final long startTime = System.currentTimeMillis();
// commitToMemory,将前面写入到mModified这个map中的数据提交到sp的内存中
final MemoryCommitResult mcr = commitToMemory();
// 调用这个runnable中是CountDownLatch的await方法,等待锁被释放
// 这个释放的地方其实是在写入到文件之后
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
// 将上面的runnable添加到queuedwork
// QueuedWork中有一个线程管理所有添加到其中的work,专门为sp的调度使用的
// Finisher是QueuedWork在o上新加的,目前的调用只有在这里
// 是为了确保在调度了sp相关的work之后,已经将内存中的sp数据树立完毕,写到了disk中
QueuedWork.addFinisher(awaitCommit);
// 如果提前执行了postWriteRunnable,则就不在QueuedWork中等待,换做当前线程等待
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
// 前面已经将数据提交到了内存,这里是将数据写入到disk
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// 通知所有的listener, listener是已经注册的监听sp变化的listener
notifyListeners(mcr);
}

4.1 commitToMemory

private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
// 只有在mDiskWritesInFlight > 0的时候才会去做深拷贝,创建出一个新的map
// mDiskWritesInFlight初始化的时候是0,只有在需要提交到内存的时候会++,也就是下面的++
// 当写入文件完毕后,会执行--
// 所以这里的意思是如果只有单次操作,则一定是0
// 如果是多次操作,且前面的任务还没有及时写入到disk,就会出现大于0的情况
// 那么此时因为有新的数据,不能影响到上一次的写入任务,需要新创建一个map
if (mDiskWritesInFlight > 0) {
// 看上去是mMap指向了一个新的对象,但事实上原先的那个对象被上一次的写入disk的任务所持有
// 这样就保证不同的写入disk的任务之间的数据互不干扰
mMap = new HashMap<String, Object>(mMap);
}
// 记录要写入内存的数据的map以及次数
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
// 获取所有的listener
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet(mListeners.keySet());
}
// 下面就是提交到内存的实现
// 将mModified中存储的数据放入到mMap中,并清空mModified
synchronized (mLock) {
boolean changesMade = false;
// 如果设置了mClear,则需要先把mMap中的所有数据清空,再写入
// mClear是在editor的clear()方法中被置为true的
// 也就是说,调用了clear()方法后,会先清空原有的所有数据,再写入新数据
if (mClear) {
if (!mMap.isEmpty()) {
changesMade = true;
mMap.clear();
}
mClear = false;
}
// for循环取出mModified中的key和value写入mMap
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
if (v == this || v == null) {
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mMap.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
// 清空mModified
mModified.clear();
// 如果有新的数据需要写入,通过mCurrentMemoryStateGeneration标记
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
// 保存本地调用提交到内存的结果
private MemoryCommitResult(long memoryStateGeneration, @Nullable List<String> keysModified,
@Nullable Set listeners,
Map<String, Object> mapToWriteToDisk) {
this.memoryStateGeneration = memoryStateGeneration;
this.keysModified = keysModified;
this.listeners = listeners;
this.mapToWriteToDisk = mapToWriteToDisk;
}

4.2 enqueueDiskWrite

private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
// 通过判断postWriteRunnable是否为null,判断调用方式是apply还是commit
// 这个方法只有在apply和commit中调用,apply上面已经写了,commit调用的时候传的是null
final boolean isFromSyncCommit = (postWriteRunnable == null);
// 这个runnable就是将数据写入到disk中
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
// 数据写入文件之后,mDiskWritesInFlight--
synchronized (mLock) {
mDiskWritesInFlight--;
}
// 在数据写入文件之后,调用apply传递过来的runnable
// 这个runnable前面已经看过了,就是CountDownLatch的等待释放
// 而这个释放是在写入文件之后的setDiskWriteResult
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// 如果是commit调用过来的,则直接调用调用写入文件的runnable
// 即commit调用过来的话,就在当前线程执行写入文件
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
// 提交到内存的时候++,所以对于单次操作,这里是成立的
// 但是只要前面有写入文件的任务没有执行完,则这里就不成立
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
// 将写入文件的runnable入队到QueuedWork中去调度,并且说明了这个任务是apply还是commit
// 后面介绍QueuedWork的调度方式
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

小结:apply操作确实是异步的,它只是先把数据提交到了内存中,然后post一个写入disk的任务到QueuedWork中,QueuedWork中会根据queue的时候是否是需要同步执行而决定是否延迟执行任务,这点后面介绍QueuedWork的时候会说明。

5. 同步操作commit

public boolean commit() {
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}

commit的调用的方法跟apply一致,都是先写入到内存中,再写入到文件中,不同之处在于:会在调用者线程中等待文件的写入结果。
这里可以再回去看一下enqueueDiskWrite方法,如果只有一个commit任务,则就用不到QueuedWork,会在调用线程中直接执行写入文件的runnable,但是假设前面有一个apply操作,其写入文件的runnable还没有被执行,那么本次commit任务就也会被QueuedWork.queue。即会将commit的写入文件的任务放到QueuedWork线程中。
或者说如果两个线程都调用同一个sp的commit,第一个线程的commit还没有执行完毕,mDiskWritesInFlight还没有–,那么第二个线程的commit同样会被放入到QueuedWork。
且写入文件的时候需要持锁,也就是说同时只能有一个线程去写,后面的任务必须要等待第一个任务写入完毕。

6. 写入文件writeToFile

// Note: must hold mWritingToDiskLock
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
...
boolean fileExists = mFile.exists();
// 当前文件如果存在,则需要创建一个backup,用于读取
if (fileExists) {
boolean needsWrite = false;
// 前面保存了写入文件的state,这里进行判断,满足条件才需要写入
if (mDiskStateGeneration if (isFromSyncCommit) {
needsWrite = true;
} else {
synchronized (mLock) {
// 不需要写入
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
needsWrite = true;
}
}
}
}
if (!needsWrite) {
// setDiskWriteResult就是前台提到多次的写入文件的结果
// 在这个方法中CountDownLatch执行的countDown
mcr.setDiskWriteResult(false, true);
return;
}
boolean backupFileExists = mBackupFile.exists();
if (!backupFileExists) {
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
mcr.setDiskWriteResult(false, false);
return;
}
} else {
mFile.delete();
}
}
// 写入xml文件
try {
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
// 可以自己看下XmlUtils中的具体实现
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
writeTime = System.currentTimeMillis();
FileUtils.sync(str);
fsyncTime = System.currentTimeMillis();
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
try {
final StructStat stat = Os.stat(mFile.getPath());
synchronized (mLock) {
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
// Do nothing
}
// 写入成功之后删除backup
mBackupFile.delete();
mDiskStateGeneration = mcr.memoryStateGeneration;
// 写入文件成功
mcr.setDiskWriteResult(true, true);
long fsyncDuration = fsyncTime - writeTime;
mSyncTimes.add((int) fsyncDuration);
mNumSync++;
if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) {
mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": ");
}
return;
} catch (XmlPullParserException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
} catch (IOException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
}
// 写入失败则删除
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false, false);
}
void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}

7. QueuedWork介绍

这里介绍一下QueuedWork这个类,因为sp的初始化之后就是使用,前面看到,无论是apply还是commit方法都是通过QueuedWork来实现的。
QueuedWork是一个管理类,顾名思义,其中有一个队列,对所有入队的work进行管理调度。
其中最重要的就是有一个HandlerThread

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

7.1 入队queue

// 如果是commit,则不能delay,如果是apply,则可以delay
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
if (shouldDelay && sCanDelay) {
// 默认delay的时间是100ms
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}

7.2 消息的处理

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();
}
}
}
private static void processPendingWork() {
synchronized (sProcessingWork) {
LinkedList work;
synchronized (sLock) {
work = (LinkedList) sWork.clone();
sWork.clear();
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}
if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
}
}
}

可以看到,调度非常简单,内部有一个sWork,需要执行的时候遍历所有的runnable执行。
对于apply操作,会有一定的延迟再去执行work,但是对于commit操作,则会马上触发调度,而且并不仅仅是调度commit传过来的那个任务,而是马上就调度队列中所有的work。

7.3 waitToFinish

系统中很多地方会等待sp的写入文件完成,等待方式是通过调用QueuedWork.waitToFinish();

public static void waitToFinish() {
Handler handler = getHandler();
synchronized (sLock) {
// 移除所有消息,直接开始调度所有work
if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
handler.removeMessages(QueuedWorkHandler.MSG_RUN);
}
sCanDelay = false;
}
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
try {
// 如果是waitToFinish调用过来,则马上执行所有的work
processPendingWork();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
try {
// 在所有的work执行完毕之后,还需要执行Finisher
// 前面在apply的时候有一步是QueuedWork.addFinisher(awaitCommit);
// 其中的实现是等待sp文件的写入完成
// 如果没有通过msg去调度而是通过waitToFinish,则那个runnable就会在这里被执行
while (true) {
Runnable finisher;
synchronized (sLock) {
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
finisher.run();
}
} finally {
sCanDelay = true;
}
...
}

8. 系统组件中对于SharedPreferences的处理

系统中对于四大组件的处理逻辑都在ActivityThread中实现,看源码就会知道,在service/activity的生命周期的执行中都会等待sp的写入完成,正是通过调用QueuedWork.waitToFinish(),确保app的数据正确的写入到disk。

对于broadcast而言,在执行完了onReceive方法之后,如果需要发送结果到AMS,则也会等待QueuedWork中的任务执行完毕,具体实现方式是将发送到AMS消息的这个runnable post到QueuedWork中。
注意:这也就造成app的主线程可能会因为sp被block。

9. 总结

SharedPreferences的本身实现就是分为两步,一步是内存,一部是磁盘,而主线程又依赖SharedPreferences的写入,所以可能当io成为瓶颈的时候,App会因为SharedPreferences变的卡顿,严重情况下会ANR,总结下来有以下几点:

  1. 存放在xml文件中的数据会被装在到内存中,所以获取数据很快
  2. apply是异步操作,提交数据到内存,并不会马上提交到磁盘
  3. commit是同步操作,会等待数据写入到磁盘,并返回结果
  4. 如果有同一个线程多次commit,则后面的要等待前面执行结束
  5. 如果多个线程对同一个sp并发commit,后面的所有任务会进入到QueuedWork中排队执行,且都要等第一个执行完毕

建议:

  1. 对数据实时性要求不高,尽量使用apply
  2. 如果业务要求必须数据成功写入,使用commit
  3. 减少sp操作频次,尽量一次commit把所有的数据都写入完毕
  4. 可以适当考虑不要在主线程访问sp
  5. 写入sp的数据尽量轻量级

推荐阅读
  • 本文介绍了Android 7的学习笔记总结,包括最新的移动架构视频、大厂安卓面试真题和项目实战源码讲义。同时还分享了开源的完整内容,并提醒读者在使用FileProvider适配时要注意不同模块的AndroidManfiest.xml中配置的xml文件名必须不同,否则会出现问题。 ... [详细]
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • Mac OS 升级到11.2.2 Eclipse打不开了,报错Failed to create the Java Virtual Machine
    本文介绍了在Mac OS升级到11.2.2版本后,使用Eclipse打开时出现报错Failed to create the Java Virtual Machine的问题,并提供了解决方法。 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • Linux服务器密码过期策略、登录次数限制、私钥登录等配置方法
    本文介绍了在Linux服务器上进行密码过期策略、登录次数限制、私钥登录等配置的方法。通过修改配置文件中的参数,可以设置密码的有效期、最小间隔时间、最小长度,并在密码过期前进行提示。同时还介绍了如何进行公钥登录和修改默认账户用户名的操作。详细步骤和注意事项可参考本文内容。 ... [详细]
  • Python实现变声器功能(萝莉音御姐音)的方法及步骤
    本文介绍了使用Python实现变声器功能(萝莉音御姐音)的方法及步骤。首先登录百度AL开发平台,选择语音合成,创建应用并填写应用信息,获取Appid、API Key和Secret Key。然后安装pythonsdk,可以通过pip install baidu-aip或python setup.py install进行安装。最后,书写代码实现变声器功能,使用AipSpeech库进行语音合成,可以设置音量等参数。 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • Centos7.6安装Gitlab教程及注意事项
    本文介绍了在Centos7.6系统下安装Gitlab的详细教程,并提供了一些注意事项。教程包括查看系统版本、安装必要的软件包、配置防火墙等步骤。同时,还强调了使用阿里云服务器时的特殊配置需求,以及建议至少4GB的可用RAM来运行GitLab。 ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • Android系统移植与调试之如何修改Android设备状态条上音量加减键在横竖屏切换的时候的显示于隐藏
    本文介绍了如何修改Android设备状态条上音量加减键在横竖屏切换时的显示与隐藏。通过修改系统文件system_bar.xml实现了该功能,并分享了解决思路和经验。 ... [详细]
  • Android开发实现的计时器功能示例
    本文分享了Android开发实现的计时器功能示例,包括效果图、布局和按钮的使用。通过使用Chronometer控件,可以实现计时器功能。该示例适用于Android平台,供开发者参考。 ... [详细]
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • 在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板
    本文介绍了在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板的方法和步骤,包括将ResourceDictionary添加到页面中以及在ResourceDictionary中实现模板的构建。通过本文的阅读,读者可以了解到在Xamarin XAML语言中构建控件模板的具体操作步骤和语法形式。 ... [详细]
  • 本文讨论了在openwrt-17.01版本中,mt7628设备上初始化启动时eth0的mac地址总是随机生成的问题。每次随机生成的eth0的mac地址都会写到/sys/class/net/eth0/address目录下,而openwrt-17.01原版的SDK会根据随机生成的eth0的mac地址再生成eth0.1、eth0.2等,生成后的mac地址会保存在/etc/config/network下。 ... [详细]
  • r2dbc配置多数据源
    R2dbc配置多数据源问题根据官网配置r2dbc连接mysql多数据源所遇到的问题pom配置可以参考官网,不过我这样配置会报错我并没有这样配置将以下内容添加到pom.xml文件d ... [详细]
author-avatar
ccM保佑加琳诺爱儿1984f
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有