SharedPrefrences的源码分析

SharedPrefrences的分析

  • 源码分析
    • 文件操作
    • 缓存机制
    • SharedPreferencesImpl实现类
    • 值操作
      • 取值
      • 修改
    • 提交操作
      • commit
        • commitToMemory
        • MemoryCommitResult
        • enqueueDiskWrite
        • mcr.writtenToDiskLatch.await()
        • mcr.writeToDiskResult
      • apply
        • commitToMemory
        • enqueueDiskWrite
        • writeToFile
    • 注意点
      • ANR
      • null命名规范
      • 序列化对象

源码分析

SharedPrefrences是开发中常用的类,作用是持久化本地的一些基础数据,使用简单易封装的特性,相比数据库、内容提供者更为实用。
SharedPreferences的本质是一个文件操作的接口类。先看下源码的注释部分:
SharedPrefrences的源码分析_第1张图片
大致的传达的意思是:

  1. 这是用于访问或者修改preference的接口
  2. Editor是修改的操作类对象
  3. 进程不安全
  4. 只有一个实例,客户端共享

文件操作

接着上面的说,竟然是一个文件操作的类,必然会涉及到本地的文件操作,牵扯到IO流和文件。那具体操作的文件是啥了?
SharedPreferences是一个接口,而他的具体实现类是SharedPreferencesImpl,在writeToFile方法中就用了XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str)去实现将数据写入xml文件中。一般保存在/data/data//shared_prefs下,需要root权限。
所以,SharedPreferences实际就是一个xml文件和IO操作的集合。

缓存机制

如果重复对文件进行操作,会是耗时操作。所以SharedPreferences是有缓存机制的,将磁盘内容读取到内存中,然后直接对内存进行操作,实现缓存。怎样实现缓存机制的了?

 @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        // At least one application in the world actually passes in a null
        // name.  This happened to work because when we generated the file name
        // we would stringify it to "null.xml".  Nice.
        if (mPackageInfo.getApplicationInfo().targetSdkVersion <
                Build.VERSION_CODES.KITKAT) {
            if (name == null) {
                name = "null";
            }
        }

        File file;
        //synchronized保证线程安全
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            //拿到第一个缓存 string,file的map
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
            	//如果当前name的为空,新建一个命名为/data/data//shared_prefs下的.xml文件存放入mSharedPrefsPaths中
                file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
        //根据file拿第二个缓存
        return getSharedPreferences(file, mode);
    }

我们每次调用context.getSharedPreferences(String name,int mode)会通过name创建一个对应的file,然后会相应的有IO流的操作,为了避免重复多次进行getSharedPreferences创建,导致文件流操作过多,这里创建了个map的cache[name,file]缓存,使用了synchronized保证了线程安全,并且保证只存在一个这样的缓存。上面拿到了对应的file后调用getSharedPreferences(File file, int mode)拿到最终的sp.

public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        //同上面一样,保证线程安全
        synchronized (ContextImpl.class) {
        	//getSharedPreferencesCacheLocked就是拿到对应包名的map缓存,看下面源代码,会发现跟上面的大同小异步骤
            final ArrayMap cache = getSharedPreferencesCacheLocked();
            //拿到file对应的sp
            sp = cache.get(file);
            if (sp == null) {
            //从android N 开始,系统不在支持APP访问另一个APP的sp,也就是不在支持MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE
                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;
    }
private ArrayMap getSharedPreferencesCacheLocked() {
        if (sSharedPrefsCache == null) {
            sSharedPrefsCache = new ArrayMap<>();
        }

        final String packageName = getPackageName();
        //获取包名对应的缓存ArrayMap>
        //所以sSharedPrefsCache跟包名对应 每一应用默认会分配一个进程,每个进程维护唯一份sSharedPrefsCache,并且由ContextImpl.class的锁保护
        ArrayMap packagePrefs = sSharedPrefsCache.get(packageName);
        if (packagePrefs == null) {
            packagePrefs = new ArrayMap<>();
            sSharedPrefsCache.put(packageName, packagePrefs);
        }

        return packagePrefs;
    }

第二个缓存cache是将file映射到sharedPreferences的关键,其中重要部分就是getSharedPreferencesCacheLocked拿到的ArrayMap>。具体的流程图如下:
SharedPrefrences的源码分析_第2张图片

SharedPreferencesImpl实现类

上面看到缓存机制中,当sp==null时,会去通过SharedPreferencesImpl的构造函数去创建sp

SharedPreferencesImpl(File file, int mode) {
		//name映射的file,即sp对应的文件对象,所有的K-V值都存放与次
        mFile = file;
        //容灾文件的file 后缀为.bak
        mBackupFile = makeBackupFile(file);
        //创建模式
        mMode = mode;
        //文件的读取完成进度boolean值
        mLoaded = false;
        //sp中所有K-V数据,从xml文件获取到的
        mMap = null;
        //开启线程异步加载文件的内容
        startLoadFromDisk();
    }
    private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
            //开启线程去加载
                loadFromDisk();
            }
        }.start();
    }
private void loadFromDisk() {
//mLoaded是读取完成的标志
        synchronized (mLock) {
        //如果加载完成,直接返回
            if (mLoaded) {
                return;
            }
            //如果灾备文件存在,则直接使用灾被文件,将mFile删除,并修改灾备为新的name映射的file。
            //可以理解成回滚
            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 map = null;
        StructStat stat = null;
        try {
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                //创建io流
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16*1024);
                     //读取xml内容,并将转换成map的k-v数据
                     //深入了解readThisValueXml()方法,你可以了解到sp支持的数据类型和解析方法
                    map = XmlUtils.readMapXml(str);
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            /* ignore */
        }

        synchronized (mLock) {
        //保证线程安全,加载完成mLoaded置为true
            mLoaded = true;
            if (map != null) {
            //这个map就是sp的k-v值
                mMap = map;
                mStatTimestamp = stat.st_mtim;
                mStatSize = stat.st_size;
            } else {
                mMap = new HashMap<>();
            }
            //释放锁
            mLock.notifyAll();
        }
    }

值操作

取值

前面得到的mMap对象了,取值直接调用mMap.get(key)就行了。需要注意的是每次的读取操作需要根据上面提到的mLoaded字段去判断文件的内容是否加载到内存里面了。源码如下

 public Map getAll() {
 //保证线程安全
        synchronized (mLock) {
        	//判断是否加载到内存
            awaitLoadedLocked();
            //noinspection unchecked
            return new HashMap(mMap);
        }
    }

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

上面就是熟悉的get**操作,最主要的要看下awaitLoadedLocked();方法怎么判断的

private void awaitLoadedLocked() {
        if (!mLoaded) {
        	//去监控磁盘的读取操作
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
            //锁等待
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
    }

由代码可知:如果文件加载还未完成,则mLoaded为false,则方法get**就会锁等待;如果加载完成会调用mLock.notifyAll()并且mLoaded会被置为true,锁解决掉,直接从缓存获取到值。由于直接从缓存中取值,除去第一次的新建操作外,大部分取值情况不用等待。

修改

值的修改是需要Editor对象去操作的,里面包含了所以的put**接口方法,跟SharedPreferences一样是一个抽象接口类,最终的实现是由EditorImpl去完成的。先说明一点的是:Editor的每次操作都是先修改内存中的数据,最终写入磁盘的操作要通过commit()apply() 同步或者异步去写入与磁盘的。

  public Editor putString(String key, @Nullable String value) {
 			 //保证线程安全
            synchronized (mLock) {
                mModified.put(key, value);
                return this;
            }
        }

这里的mModified不是mMap,设置值的操作也不是直接对SharedPrefrences的mMap进行处理。而是操作mModified,在提交磁盘时会与mMap进行合并,生成新的mMap,然后再写入磁盘的。相应的remove(String key)clear()操作也并非真正的执行了移除和清空操作。

		public Editor remove(String key) {
            synchronized (mLock) {
            //记住这个this,后面提交时会判断这个this,其实设置为null就行了
                mModified.put(key, this);
                return this;
            }
        }

        public Editor clear() {
            synchronized (mLock) {
            //设置标志位,并没有实际清空
                mClear = true;
                return this;
            }
        }

所以,一般情况下,只有第一次创建SharedPrefrences是一定的开销的,后面的读取值操作开销很小,是在内存上操作的。但是写入或者修改值操作,在没有提交情况下也是基本没有开销的(也没有起到修改新增效果),所以推荐尽量批量设置值后一次性提交commit()或者apply(),来减少开销,毕竟一次次修改提交会有大量的io流操作。

SharedPrefrences的源码分析_第3张图片

提交操作

前面提到的sp的数据的新增或者修改需要提交操作,但是提交的方法有commit()apply()两种提交方式。主要的区别就是同步和异步的区别

commit

commit是直接写入磁盘的,同步进行的操作,会返回一个写入成功的结果值。相比apply优势就是,能得到准确的返回结果,对一些重要的值操作,得知写入结果后去做一些其他处理或者对失败情况做一些补救工作。源码如下

 public boolean commit() {
            long startTime = 0;

            if (DEBUG) {
                startTime = System.currentTimeMillis();
            }
			//commitToMemory就是merg操作,将mModified和mMap进行比较
            MemoryCommitResult mcr = commitToMemory();
			//执行写操作 传入mcr对象,再写入操作完成后,释放mcr的锁
            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;
        }

commitToMemory

无论commit()还是apply()都会先去merge下,`commitToMemory()`都要去新旧比较更新出最终的值。源码如下:
// Returns true if any changes were made
        private MemoryCommitResult commitToMemory() {
            long memoryStateGeneration;
            List keysModified = null;
            Set listeners = null;
            Map 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.
                //mDiskWritesInFlight当前的操作磁盘的线程数
                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(mMap);
                }
                mapToWriteToDisk = mMap;
                mDiskWritesInFlight++;

                boolean hasListeners = mListeners.size() > 0;
                if (hasListeners) {
                    keysModified = new ArrayList();
                    listeners = new HashSet(mListeners.keySet());
                }

                synchronized (mLock) {
                //合并是否发生改变的标志位
                    boolean changesMade = false;
					//清空map的标志位
                    if (mClear) {
                        if (!mMap.isEmpty()) {
                            changesMade = true;
                            mMap.clear();
                        }
                        mClear = false;
                    }
					//mModified的遍历
                    for (Map.Entry 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.
                        //remove的标志位
                        if (v == this || v == null) {
                            if (!mMap.containsKey(k)) {
                                continue;
                            }
                            mMap.remove(k);
                        } else {
                        //如果包含该K值,替换新的Value值
                            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();

                    if (changesMade) {
                        mCurrentMemoryStateGeneration++;
                    }

                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
            //创建MemoryCommitResult
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
        }

ps-源码并不是所有的都要看

MemoryCommitResult

里面定义了一个计数器闭锁,计数为1,它会等待一个线程结束后解锁,而这个线程干的事情就是写入磁盘的操作。


   final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
private static class MemoryCommitResult {
        final long memoryStateGeneration;
        @Nullable final List keysModified;
        @Nullable final Set listeners;
        final Map mapToWriteToDisk;
        //创建的计数器闭锁
        final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

        @GuardedBy("mWritingToDiskLock")
        //volatile关键字修饰,保证writeToDiskResult的可见性、有序性 保证commit返回的结果的准确性
        volatile boolean writeToDiskResult = false;
        boolean wasWritten = false;

        private MemoryCommitResult(long memoryStateGeneration, @Nullable List keysModified,
                @Nullable Set listeners,
                Map mapToWriteToDisk) {
            this.memoryStateGeneration = memoryStateGeneration;
            this.keysModified = keysModified;
            this.listeners = listeners;
            this.mapToWriteToDisk = mapToWriteToDisk;
        }

        void setDiskWriteResult(boolean wasWritten, boolean result) {
            this.wasWritten = wasWritten;
            //写入磁盘成功是否的结果
            writeToDiskResult = result;
            //计数减1 ,这里计数数量为1,即为解锁。计数结束,所有线程并行
            writtenToDiskLatch.countDown();
        }
    }

enqueueDiskWrite

commit操作实际不一定都是在主线程里面执行的,但是利用了闭锁能保证返回正确的结果,即使不在同一线程,commit的执行也能其他线程执行时不影响写入顺序。(假设操作是在主线程,如果有大量的commit操作,也会导致主线程卡顿,闭锁)

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        //isFromSyncCommit   是否 是同步提交   apply的参数中会传递一个Runnable对象,commit为null
        final boolean isFromSyncCommit = (postWriteRunnable == null);
		//创建一个线程 执行 写盘操作 这里还没执行 只是创建
        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    synchronized (mWritingToDiskLock) {
                    //写入操作
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                    //执行完毕后,减少一个线程个数
                        mDiskWritesInFlight--;
                    }
                    //commit不会走这里
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

        // Typical #commit() path with fewer allocations, doing a write on
        // the current thread.
        if (isFromSyncCommit) {
        	//commit走这里
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            //当前只有一个线程操作磁盘,空闲状态
            if (wasEmpty) {
                writeToDiskRunnable.run();
                //return 不在执行 QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
                return;
            }
        }
		//非空闲状态,执行writeToDiskRunnable线程 无延迟
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
        //放到LinkedList中再执行run,也是LinkedList遍历顺序去执行Runnable.run
    }

mcr.writtenToDiskLatch.await()

MemoryCommitResult中mcr闭锁等待,如果写入耗时,闭锁可能造成anr

mcr.writeToDiskResult

返回MemoryCommitResult的writeToDiskResult的结果,为commit返回的写入成功是否值。

apply

commit()相比,直接开启一个线程去执行写操作,不用关系操作是否成功是否;对于一些非关键变量或者不需要写入失败补救的值,最好使用apply()执行值修改。

public void apply() {
            final long startTime = System.currentTimeMillis();
			//同commit一样
            final MemoryCommitResult mcr = commitToMemory();
            //创建提交的等待线程
            final Runnable awaitCommit = new Runnable() {
                    public void run() {
                        try {
                    		//等待解锁后才能执行下一个runnable,writeToFile会解锁
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }

                        if (DEBUG && mcr.wasWritten) {
                            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                                    + " applied after " + (System.currentTimeMillis() - startTime)
                                    + " ms");
                        }
                    }
                };
			//添加到waitToFinish去执行 , finishers 通过这种方式实现检查排队任务是否完成。
			//waitToFinish触发排队任务马上执行
            QueuedWork.addFinisher(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    public void run() {
                    	//awaitCommit去闭锁,阻塞线程。  所以大文件的sp存储也会造成主线程的卡顿
                        awaitCommit.run();
                        //移除掉
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };
			//开始执行apply的enqueueDiskWrite
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
            notifyListeners(mcr);
        }

commitToMemory

同commit一样去merge生成新的mMap

enqueueDiskWrite

apply和commit用的是同一个方法,多的是postWriteRunnable不为null

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        //  isFromSyncCommit  为false      
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    synchronized (mWritingToDiskLock) {
                    //writeToFile完成后会开锁
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                    //写完线程数减一
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

      	//下面不会执行
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }
       //执行异步任务writeToDiskRunnable,也会执行 postWriteRunnable.run();
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

writeToFile

writeToFile的关键是写入操作,还有setDiskWriteResult计数闭锁的关闭。怎么写入不需要太去了接。setDiskWriteResult(boolean wasWritten, boolean result)是开锁操作。删除计时源码如下:

private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        boolean fileExists = mFile.exists();
        //重命名当前文件,以便在下次读取时将其用作备份
        if (fileExists) {
            boolean needsWrite = false;

            // 只需要在磁盘状态比此提交更早时写入
            if (mDiskStateGeneration < mcr.memoryStateGeneration) {
                if (isFromSyncCommit) {
                //commit 是即时写入
                    needsWrite = true;
                } else {
                    synchronized (mLock) {
                    //需要等待最新的状态
                        // No need to persist intermediate states. Just wait for the latest state to
                        // be persisted.
                        if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                            needsWrite = true;
                        }
                    }
                }
            }

            if (!needsWrite) {
            //不需要写入,此处不是异常,可能是合并状态未更新。分析commitToMemory()里面的参数changesMade的值
                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();
            }
        }

        // Attempt to write the file, delete the backup and return true as atomically as
        // possible.  If any exception occurs, delete the new file; next time we will restore
        // from the backup.
        try {
            FileOutputStream str = createFileOutputStream(mFile);
            if (str == null) {
                mcr.setDiskWriteResult(false, false);
                return;
            }
            //开始写入
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
           
            FileUtils.sync(str);
            
            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
            }
            //写入成功,删除灾备文件
            mBackupFile.delete();
            mDiskStateGeneration = mcr.memoryStateGeneration;
            mcr.setDiskWriteResult(true, true);
            return;
        } catch (XmlPullParserException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        } catch (IOException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        }

        // Clean up an unsuccessfully written file
        if (mFile.exists()) {
            if (!mFile.delete()) {
                Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
            }
        }
        mcr.setDiskWriteResult(false, false);
    }

注意点

ANR

无论是apply还是commit方法去提交修改值操作,如果文件过大,涉及到io操作,也会出现anr情况,如果日志中出现countDownLatch.await waitToFinish()关键字段大概率就是存在耗时操作并且没有执行完。

null命名规范

4.4之前版本中,如果name为null,系统会以字符串“null”为名称,但是这不符合命名规范,为了避免这些和整理使用,建议存放同一类中集中管理,保证命名不重名。

序列化对象

如需要使用将对象序列化存入sp,如果原序列化对象发生改变了,那么serialVersionUID可能发生改变了,就不是同一个类了。虽然可以手动指定serialVersionUID去保证,但是还是建议不要序列化大的的数据类型到sp里面。

你可能感兴趣的:(工作的学习)