Android性能优化之SharedPreference卡顿优化

news2025/1/18 14:46:37

下面的源码都是基于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的file
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            //如果file为null,创建file
            file = getSharedPreferencesPath(name);
            mSharedPrefsPaths.put(name, file);
        }
    }
    //通过file获取sharepref
    return 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-spImpl
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        //先从cache中获取,
        sp = cache.get(file);
        if (sp == null) {
            //创建一个spImpl
            sp = 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) {
        //多进程,会执行reload
        sp.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;

    @Override
    public 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加1
        
        synchronized (mEditorLock) {
            boolean changesMade = false;
              //判断是否清除sp中的数据
            if (mClear) {
                if (!mapToWriteToDisk.isEmpty()) {
                    changesMade = true;
                    mapToWriteToDisk.clear();
                    //如果清除sp中的数据将mapToWirteToDisk的数据清空。
                }
                keysCleared = true;
                mClear = false;
            }
            //遍历mModified map
            for (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-v
                    mapToWriteToDisk.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加1
                mCurrentMemoryStateGeneration++;
            }

            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() {
            @Override
            public void run() {
                synchronized (mWritingToDiskLock) {
                    //往磁盘中写
                    writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                    //减1
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    //运行postWriteRunnable
                    postWriteRunnable.run();
                }
            }
        };


    if (isFromSyncCommit) {
        //如果通过commit提交的直接执行writeToDiskRunnable
        boolean 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() {
            @Override
            public void run() {
                try {
                ///创建awaitCommit,用于等待写磁盘完成后调用
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
            }
        };
    //添加awaitCommit
    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                //执行awaitCommit
                awaitCommit.run();
                ///移除awaitCommit
                QueuedWork.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.waitToFinish
            QueuedWork.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() {
            @Override
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
            }
        };
    //添加await
    QueuedWork.addFinisher(awaitCommit);
    //写磁盘后再调用
    Runnable postWriteRunnable = new Runnable() {
            @Override
            public 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.coloradmin.cn/o/809369.html

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

相关文章

Flink CEP(二) 运行源码解析

通过DemoApp学习一下&#xff0c;CEP的源码执行逻辑。为下一篇实现CEP动态Pattern奠定理论基础。 1. Pattern的定义 Pattern<Tuple3<String, Long, String>,?> pattern Pattern.<Tuple3<String, Long, String>>begin("begin").where(new…

MySQL检索数据和排序数据

目录 一、select语句 1.检索单个列&#xff08;SELECT 列名 FROM 表名;&#xff09; 2.检索多个列&#xff08;SELECT 列名1&#xff0c;列名2&#xff0c;列名3 FROM 表名;&#xff09; 3.检索所有的列&#xff08;SELECT * FROM 表名;&#xff09; 4.检索不同的行&#x…

【小白必看】Python图片合成示例之使用PIL库实现多张图片按行列合成

文章目录 前言效果图1. 导入必要的库2. 打开文件并获取大小3. 设置生成图片的行数和列数4. 获取所有图片的名称列表5. 创建新的画布6. 遍历每个位置并粘贴图片7. 保存合成的图片完整代码图片来源结束语 前言 本文介绍了一个用于图片合成的 Python 代码示例。该代码使用了PIL库来…

一篇文章搞定Java泛型

目录 介绍 优点 泛型类 语法定义 代码示例 泛型类注意事项 抽奖示例 泛型类派生子类 定义 代码示例 子类是泛型 子类不是泛型 泛型接口 定义 泛型方法 定义 代码示例 泛型方法与可变参数 泛型方法总结 ​编辑类型通配符 定义 代码示例 通配符的上限 定义 …

leetcode743. 网络延迟时间 floyd

https://leetcode.cn/problems/network-delay-time/ 有 n 个网络节点&#xff0c;标记为 1 到 n。 给你一个列表 times&#xff0c;表示信号经过 有向 边的传递时间。 times[i] (ui, vi, wi)&#xff0c;其中 ui 是源节点&#xff0c;vi 是目标节点&#xff0c; wi 是一个信…

详细介绍 React 中如何使用 redux

在使用之前要先了解它的配套插件&#xff1a; 在React中使用redux&#xff0c;官方要求安装其他插件 Redux Toolkit 和 react-redux Redux Toolkit&#xff1a;它是一个官方推荐的工具集&#xff0c;旨在简化 Redux 的使用和管理。Redux Toolkit 提供了一些提高开发效率的工具…

F5 LTM 知识点和实验 5-健康检测

第五章:健康检测 监控的分类: 地址监控(3层)服务监控(4层)内容监控(7层)应用监控(7层)性能监控(7层)路径监控(3、4、7层)三层监控: 三层监控可以帮助bipip系统通过检查网络是否可达监视资源。比如使用icmp echo,向监控节点发送icmp_echo报文,如果接收到响应…

求分享如何批量压缩视频的容量的方法

视频内存过大&#xff0c;不但特别占内存&#xff0c;而且还会使手机电脑出现卡顿的现象&#xff0c;除此之外&#xff0c;如果我们想发送这些视频文件可能还会因为内存太大无法发送。因此&#xff0c;我们可以批量地压缩视频文件的内存大小&#xff0c;今天小编要来分享一招&a…

车载软件架构 —— 信息安全与基础软件

车载软件架构 —— 信息安全与基础软件 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 没有人关注你。也无需有人关注你。你必须承认自己的价值,你不能站在他人的角度来反对自己。人生在世,最怕…

郑州多域名https证书

多域名https证书是https证书中比较特殊的一款&#xff0c;它保护的域名记录是众多https证书中最灵活的。不管是DV基础型的多域名https证书还是OV企业型和EV增强型的多域名https证书既可以保护多个主域名或者子域名&#xff0c;还可以主域名子域名随意组合&#xff0c;只要申请者…

matlab--solve函数的用法

目录 1.用法结构 2.解单变量方程 3.解多变量方程 4.解带参方程 5.解不等式 6.总结 1.用法结构 solve函数是MATLAB中的一个符号计算函数&#xff0c;用于求解方程组或方程的符号解。 它的用法如下&#xff1a; 定义符号变量&#xff1a;使用syms函数定义符号变量&#…

CSDN博客置顶操作

目录 背景: 过程: 第一步: 第二步&#xff1a; 总结&#xff1a; 背景: 对于文章博主都想把优质的好文放在第一位与大家分享&#xff0c;让更多的人看到自己的文章以便更好的展现出来&#xff0c;那么就是置顶。 过程: 第一步: 打开CSDN主页文章&#xff0c;将鼠标放在…

css定义超级链接a标签里面的title的样式

效果: 代码: 总结:此css 使用于任何元素,不仅仅是a标签!

找不到mfc140u.dll怎么解决

第一&#xff1a;mfc140u.dll有什么用途&#xff1f; mfc140u.dll是Windows操作系统中的一个动态链接库文件&#xff0c;它是Microsoft Foundation Class (MFC)库的一部分。MFC是 C中的一个框架&#xff0c;用于构建Windows应用程序的用户界面和功能。mfc140u.dll包含了MFC库中…

12. Mybatis 多表查询 动态 SQL

目录 1. 数据库字段和 Java 对象不一致 2. 多表查询 3. 动态 SQL 使用 4. 标签 5. 标签 6. 标签 7. 标签 8. 标签 9. 通过注解实现 9.1 查找所有数据 9.2 通过 id 查找 1. 数据库字段和 Java 对象不一致 我们先来看一下数据库中的数据&#xff1a; 接下来&#…

冠达管理:股指预计维持震荡格局 关注汽车、酿酒等板块

冠达管理指出&#xff0c;周四A股商场冲高遇阻、小幅震动整理&#xff0c;早盘股指高开后震动上行&#xff0c;沪指盘中在3245点邻近遭遇阻力&#xff0c;午后股指逐级回落&#xff0c;轿车、金融、酿酒以及军工等职业轮番领涨&#xff0c;互联网、软件、半导体以及证券等职业震…

Git克隆文件不显示绿色勾、红色感叹号等图标

1、问题 Git和TorToiseGit安装后&#xff0c;Git克隆的文件不会显示绿色勾、红色感叹号等图标。 2、检查注册表 2.1、打开注册表 (1)WinR打开运行窗口&#xff0c;输入regedit&#xff0c;点击确定&#xff0c;打开注册表编辑器。 2.2、找如下路径 (1)找到路径 计算机\HKEY_…

Unity 性能优化四:UI耗时函数、资源加载、卸载API

UI耗时函数 1.1 Canvas.SendWillRenderCanvases 这个函数是由于自身UI的更新&#xff0c;产生的耗时 1. 这里更新的是vertex 属性&#xff0c;比如 color、tangent、position、uv&#xff0c;修改recttransform的position、scale&#xff0c;rotation并不会导致顶点属性改变…

想测试入门就必须要懂的软件开发流程

从事软件测试行业&#xff0c;每天面对的被测对象都是软件。如果想要更好的去完成测试工作&#xff0c;首先需要对被测对象&#xff0c;也就是对软件要有基本的了解。 软件 与计算机系统操作有关的计算机程序、可能有的文件、文档及数据。 程序好理解&#xff0c;就是可以操…

JS正则表达式:常用正则手册/RegExp/正则积累

一、正则基础语法 JavaScript 正则表达式 | 菜鸟教程 JS正则表达式语法大全&#xff08;非常详细&#xff09; 二、使用场景 2.1、校验中国大陆手机号的正则表达式 正则 /^1[3456789]\d{9}$/解释 序号正则解释1^1以数字 1 开头2[3456789]第二位可以是 3、4、5、6、7、8、…