android app深度优化—Shared Preference源码详解
创始人
2024-06-02 23:25:26
0

前言

SharedPreferences是Android提供的数据持久化的一种手段,适合单进程、小批量的数据存储与访问。因为SharedPreferences的实现是基于单个xml文件实现的,并且,所有持久化数据都是一次性加载到内存,如果数据过大,是不合适采用SharedPreferences存放的。而适用的场景是单进程的原因同样如此,由于Android原生的文件访问并不支持多进程互斥,所以SharePreferences也不支持,如果多个进程更新同一个xml文件,就可能存在同不互斥问题。

SharedPreferences简单使用

创建

第一个参数是储存的xml文件名称,第二个是打开方式,一般就用Context.MODE_PRIVATE;

SharedPreferences sp=context.getSharedPreferences("名称", Context.MODE_PRIVATE);
复制代码

写入

//可以创建一个新的SharedPreference来对储存的文件进行操作
​
SharedPreferences sp=context.getSharedPreferences("名称", Context.MODE_PRIVATE);
​
//像SharedPreference中写入数据需要使用Editor
​
SharedPreference.Editor editor = sp.edit();
​
//类似键值对
​
editor.putString("name", "string");
​
editor.putInt("age", 0);
​
editor.putBoolean("read", true);
​
//editor.apply();
​
editor.commit();
复制代码
  • apply和commit都是提交保存,区别在于apply是异步执行的,不需要等待。不论删除,修改,增加都必须调用apply或者commit提交保存;
  • 关于更新:如果已经插入的key已经存在。那么将更新原来的key;
  • 应用程序一旦卸载,SharedPreference也会被删除;

读取

SharedPreference sp=context.getSharedPreferences("名称", Context.MODE_PRIVATE);
​
//第一个参数是键名,第二个是默认值
​
String name=sp.getString("name", "暂无");
​
int age=sp.getInt("age", 0);
​
boolean read=sp.getBoolean("isRead", false);
复制代码

4、检索

SharedPreferences sp=context.getSharedPreferences("名称", Context.MODE_PRIVATE);
​
//检查当前键是否存在
​
boolean isContains=sp.contains("key");
​
//使用getAll可以返回所有可用的键值
​
//Map allMaps=sp.getAll();
复制代码

删除

当我们要清除SharedPreferences中的数据的时候一定要先clear()、再commit(),不能直接删除xml文件;

SharedPreference sp=getSharedPreferences("名称", Context.MODE_PRIVATE);
​
SharedPrefence.Editor editor=sp.edit();
​
editor.clear();
​
editor.commit();
复制代码
  • getSharedPreference() 不会生成文件,这个大家都知道;
  • 删除掉文件后,再次执行commit(),删除的文件会重生,重生文件的数据和删除之前的数据相同;
  • 删除掉文件后,程序在没有完全退出停止运行的情况下,Preferences对象所存储的内容是不变的,虽然文件没有了,但数据依然存在;程序完全退出停止之后,数据才会丢失;
  • 清除SharedPreferences数据一定要执行editor.clear(),editor.commit(),不能只是简单的删除文件,这也就是最后的结论,需要注意的地方

源码分析

获取SharedPreferences对象的过程

Context是一个抽象类,它的子类中有ContextImpl和ContextWrapper

Context mBase;
public SharedPreferences getSharedPreferences(String name, int mode) {
//这里其实调用的是ContextImpl的getSharedpreferences方法return mBase.getSharedPreferences(name, mode);}

最终还是要看看ContextImpl中的getSharedPreferences的具体实现:

public SharedPreferences getSharedPreferences(String name, int mode) {//当SDK< 19时,如果文件名为null则会初始化名字为"null"if (mPackageInfo.getApplicationInfo().targetSdkVersion ();}file = mSharedPrefsPaths.get(name);if (file == null) {//在相应的sharedPreference下新建File,保存文件名为XXX.xmlfile = getSharedPreferencesPath(name);mSharedPrefsPaths.put(name, file);}}return getSharedPreferences(file, mode);}

新建File的缓存,使用Map建立了文件名和File文件的一一对应,如果没有文件则新建,并放入Map中,如果Map中有需要的文件,则直接使用get方法取出。最终调用了ContextImpl的getSharedPreferences方法。

 public SharedPreferences getSharedPreferences(File file, int mode) {SharedPreferencesImpl sp;synchronized (ContextImpl.class) {//在每个包下面建立了一个File-SharedPreferencesImpl对应关系的ArrayMap//getSharedPreferencesCacheLocked()根据包名返回相对应包下的ArrayMapfinal ArrayMap cache = getSharedPreferencesCacheLocked();//从file-SharedPreferencesImpl键值对中根据当前file找到SharedPreferencesImpl实例sp = cache.get(file);//如果没有对应的SharedPreferencesImpl,就新建一个if (sp == null) {// 检查mode,如果是MODE_WORLD_WRITEABLE或者MODE_MULTI_PROCESS则直接抛异常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;}

getSharedPreferences()就是返回了File文件对应的SharedPreferenceImpl对象。

整个获取sharedPreferences的过程:首先建立文件名和File文件的一一对应关系,将数据存储在mSharedPrefsPaths中,如果mSharedPrefsPaths中有文件了,则直接使用不需要重复创建,根据File文件找到对应的SharedPreferenceImpl对象并返回。

SharedPreferencesImpl SharedPreferencesImpl是SharedPreferences的子类,首先看一下SharedPreferences接口中的方法。

public interface SharedPreferences {public interface OnSharedPreferenceChangeListener {void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);}public interface Editor {Editor putString(String key, @Nullable String value);Editor putStringSet(String key, @Nullable Set values);Editor putInt(String key, int value);Editor putLong(String key, long value);Editor putFloat(String key, float value);Editor putBoolean(String key, boolean value);Editor remove(String key);Editor clear();boolean commit();void apply();}Map getAll();@NullableString getString(String key, @Nullable String defValue);@NullableSet getStringSet(String key, @Nullable Set defValues);int getInt(String key, int defValue);long getLong(String key, long defValue);float getFloat(String key, float defValue);boolean getBoolean(String key, boolean defValue);boolean contains(String key);Editor edit();void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
}
​

具体方法的实现都在SharedPreferencesImpl中,返回的是SharedPreferencesImpl的对象,首先是构造方法:

SharedPreferencesImpl(File file, int mode) {mFile = file;//File的备份文件 【fileName.bak】mBackupFile = makeBackupFile(file);mMode = mode;mLoaded = false;//真正保存的数据mMap = null;mThrowable = null;//第一次创建时,会去磁盘中加载数据startLoadFromDisk();}

构造函数中初始化一下数据之后,就会去磁盘中记载数据。

    private void startLoadFromDisk() {synchronized (mLock) {mLoaded = false;}//在子线程中加载new Thread("SharedPreferencesImpl-load") {public void run() {loadFromDisk();}}.start();}*-----------------------------------------------------*private void loadFromDisk() {synchronized (mLock) {//如果数据已经加载过了,就直接返回。if (mLoaded) {return;}//如果备份文件存在,则将原来的文件删除,并将备份文件重命名为mFileif (mBackupFile.exists()) {mFile.delete();mBackupFile.renameTo(mFile);}}// Debuggingif (mFile.exists() && !mFile.canRead()) {Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");}Map map = null;StructStat stat = null;Throwable thrown = null;try {stat = Os.stat(mFile.getPath());if (mFile.canRead()) {BufferedInputStream str = null;try {//获取文件流str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);//将文件流转化为mapmap = (Map) XmlUtils.readMapXml(str);} catch (Exception e) {Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);} finally {IoUtils.closeQuietly(str);}}} catch (ErrnoException e) {} catch (Throwable t) {thrown = t;}synchronized (mLock) {mLoaded = true;mThrowable = thrown;try {if (thrown == null) {if (map != null) {//mMap是我们真正取数据的mapmMap = map;mStatTimestamp = stat.st_mtim;mStatSize = stat.st_size;} else {mMap = new HashMap<>();}}} catch (Throwable t) {mThrowable = t;} finally {mLock.notifyAll();}}}
​

这里有一个mLock锁,这个锁很重要,这里获取到了锁,当SharedPreferences去操作数据的时候就会先去判断这个锁是否释放了,如果没有,那么就会等待锁释放了在执行,执行到就将数据加载到了内存中,最终保存在mMap中。

SharedPreferencesImpl的构建过程其实就是将数据从磁盘加载进内存,并保存在mMap中。

获取数据

那么接下来看他是怎样获取数据的,这里就看下它的getString()方法:

public String getString(String key, @Nullable String defValue) {
/*这里一进来就加了一个锁,这个锁是为了防止数据还没加载就执行到这里,那这时就需要将锁释放掉,
但又不对数据进行操作,那这个awaitLoadedLocked()就起到了这个作用,
这也就是说,数据没加载完是不能对数据进行操作的,这时就阻塞在这里*/synchronized (mLock) {awaitLoadedLocked();String v = (String)mMap.get(key);return v != null ? v : defValue;}}
*------------------------------*
private void awaitLoadedLocked() {if (!mLoaded) {BlockGuard.getThreadPolicy().onReadFromDisk();}while (!mLoaded) {try {mLock.wait(); //将锁释放掉,等待加载完成} catch (InterruptedException unused) {}}if (mThrowable != null) {throw new IllegalStateException(mThrowable);}}

读数据的过程就是如果数据已经加载完成就直接在mMap中找到Key中对应的数据,如果没有加载则等待加载完成。

更改数据(Editor)
public Editor edit() {synchronized (mLock) {awaitLoadedLocked();}return new EditorImpl();}

调用putXXX()方法,将所有需要修改的数据先保存在一个临时的集合中;

存放完数据,会调用apply()或者commit()方法,看一下这两个方法的实现。

commitToMemory() 在apply方法中首先会将数据同步到内存,调用的方法就是commitToMemory(),所以先分析这个方法。 这个方法主要做的就是将保存在临时集合中的数据合并到mMap(之前的数据集合)中,同时返回一个MemoryCommitResult对象,将数据保存到磁盘中用到的就是这个类;

private MemoryCommitResult commitToMemory() {//当前Memory的状态,其实也就是当需要提交数据到内存的时候,他的值就加一long memoryStateGeneration;List keysModified = null;Set listeners = null;Map mapToWriteToDisk;
​synchronized (SharedPreferencesImpl.this.mLock) {if (mDiskWritesInFlight > 0) {mMap = new HashMap(mMap);}//将需要保存到磁盘中的数据保存到这个集合中mapToWriteToDisk = mMap;mDiskWritesInFlight++;boolean hasListeners = mListeners.size() > 0;if (hasListeners) {keysModified = new ArrayList();listeners = new HashSet(mListeners.keySet());}synchronized (mEditorLock) {boolean changesMade = false;// 如果调用了clear(),那么就会将集合中原先的数据清除掉if (mClear) {if (!mapToWriteToDisk.isEmpty()) {changesMade = true;mapToWriteToDisk.clear();}mClear = false;}// 遍历存储在临时集合中的数据,然后合并到原先的数据集合mMap中for (Map.Entry e : mModified.entrySet()) {String k = e.getKey();Object v = e.getValue(); if (v == this || v == null) {if (!mapToWriteToDisk.containsKey(k)) {continue;}mapToWriteToDisk.remove(k);} else {if (mapToWriteToDisk.containsKey(k)) {Object existingValue = mapToWriteToDisk.get(k);if (existingValue != null && existingValue.equals(v)) {continue;}}mapToWriteToDisk.put(k, v);}changesMade = true;if (hasListeners) {keysModified.add(k);}}mModified.clear();if (changesMade) {mCurrentMemoryStateGeneration++;}memoryStateGeneration = mCurrentMemoryStateGeneration;}}return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,mapToWriteToDisk);}

接下来看一下apply方法

CountDownLatch
1、CountDownLatch end = new CountDownLatch(N); //构造对象时候 需要传入参数N
​
2、end.await() 能够阻塞线程 直到调用N次end.countDown() 方法才释放线程
​
3、end.countDown() 可以在多个线程中调用 计算调用次数是所有线程调用次数的总和
​
apply()
//构造函数中传入1,也就说明调用一次countDown()会唤醒线程final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);public void apply() {final long startTime = System.currentTimeMillis();//先将数据同步到内存,返回的是MermoryCommitResult对象final MemoryCommitResult mcr = commitToMemory();final Runnable awaitCommit = new Runnable() {@Overridepublic void run() {try {// writtenToDiskLatch是一个CountDownLatch对象,除非数据全部同步到磁盘,否则这里就一直阻塞mcr.writtenToDiskLatch.await();} catch (InterruptedException ignored) {}
​if (DEBUG && mcr.wasWritten) {Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration+ " applied after " + (System.currentTimeMillis() - startTime)+ " ms");}}};
// 这里将awaitCommit添加到QueuedWork中的目的是为了退出activity时,确保所有数据已经全部同步到磁盘中了QueuedWork.addFinisher(awaitCommit);
​Runnable postWriteRunnable = new Runnable() {@Overridepublic void run() {awaitCommit.run();// 数据同步完这里就会将awaitCommit从QueuedWork移除QueuedWork.removeFinisher(awaitCommit);}};
// 开始执行将数据同步到磁盘中,会新开线程,后面会说到SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);// 这里是唤醒回调,前提是你注册了registerOnSharedPreferenceChangeListener()回调// 还有一个条件就是这个key是新添加的才会触发notifyListeners(mcr);}
​

apply的整个过程:首先将数据同步到内存,将内存中的数据全部同步到磁盘,如果没有同步完成,就会阻塞线程。

commit()@Overridepublic boolean commit() {long startTime = 0;
​if (DEBUG) {startTime = System.currentTimeMillis();}//先将数据同步到内存,返回的是MermoryCommitResult对象MemoryCommitResult mcr = commitToMemory();// 开始执行将数据同步到磁盘中,会新开线程,这和前面的apply调用的相同的方法,参数不同。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");}}//最终会调用onSharedPreferenceChanged方法notifyListeners(mcr);// 返回文件操作的结果return mcr.writeToDiskResult;}
​

这里会先判断是不是commit()执行的,如果是commit(),那么就在当前线程中同步数据,这也就是为什么android现在不推荐使用这个方法了,如果是apply()方法,那就还得往下看了,这里调用的是QueueWork这个类queue(),并将同步数据的任务传递进去,这里同步数据的工作就封装在writerDiskRunnable中,接下来看看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,然后将同步数据的任务添加到了sWork这个集合中,那后面处理肯定也是去这个集合里面拿了,重点看这个getHandler():

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

这里就是构建一个handler,如果已经存在就直接返回了,不过这个handler处理消息是在子线程中执行的,要看明白这里,首先你得知道Handler和HandlerThread的原理,构建Handler在子线中处理消息时,一定要为他准备一个looper对象,否则是会有异常的,看这里的handleMessage(),将执行的逻辑交给了processPendingWork(),来看看:

private static void processPendingWork() {long startTime = 0;
​if (DEBUG) {startTime = System.currentTimeMillis();}synchronized (sProcessingWork) {LinkedList work;synchronized (sLock) {// 将前面添加的任务全部clone出来,然后清除work = (LinkedList) sWork.clone();sWork.clear();// Remove all msg-s as all work will be processed now// 将任务取出来后,就可以取消后面执行任务的消息了,这里取消的只是上面clone出来的任务      getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);}
​if (work.size() > 0) {for (Runnable w : work) {w.run();}if (DEBUG) {Log.d(LOG_TAG, "processing " + work.size() + " items took " ++(System.currentTimeMillis() - startTime) + " ms");}}}}

这里执行的逻辑就是将前面添加的数据clone出来作为局部变量,然后将clone的集合清空,再移除发送任务的消息(这个任务已经添加到集合中了),这样数据同步就在子线程中完成了。到这,主要的逻辑都捋了一遍,但是还有一个点,退出activity时,数据没同步完是会阻塞线程的,现在就看看为什么会阻塞线程?这里要先知道一点,Activity生命周期都是通过ActivityThread来控制的,在ActivityThread的handleStopActivity方法中,这里控制的就是Activity的stop,在这个方法中有调用到:

QueuedWork.waitToFinish();

这里就是查看QueuedWork中的任务是否都执行完了,如果没有执行完,那么就会处于等待状态,看下它里面是一个怎样的逻辑:

    public static void waitToFinish() {long startTime = System.currentTimeMillis();boolean hadMessages = false;Handler handler = getHandler();// 先看下Handler中是否还有等待的消息,如果有那么就移除synchronized (sLock) {if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {// Delayed work will be processed at processPendingWork() belowhandler.removeMessages(QueuedWorkHandler.MSG_RUN);if (DEBUG) {hadMessages = true;Log.d(LOG_TAG, "waiting");}}// We should not delay any work as this might delay the finisherssCanDelay = false;}StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();try {// 这里就是将本该在子线程中执行的任务直接拿到主线程中来执行了processPendingWork();} finally {StrictMode.setThreadPolicy(oldPolicy);}try {// 这里是为了确保所有的任务确实已经执行完了while (true) {Runnable finisher;synchronized (sLock) {finisher = sFinishers.poll();}if (finisher == null) {break;}finisher.run();}} finally {sCanDelay = true;}synchronized (sLock) {long waitTime = System.currentTimeMillis() - startTime;if (waitTime > 0 || hadMessages) {mWaitTimes.add(Long.valueOf(waitTime).intValue());mNumWaits++;if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {mWaitTimes.log(LOG_TAG, "waited: ");}}}}
这里会判断添加的任务是否全部执行完了,如果没有执行完,那么就会将本该在子线程中执行的任务全部移到主线程中来执行。

总结:

1.sp数据都保存在内存缓存mMap中,缓存路径为data/data/packagename/shared_prefs,这样也就说明sp文件不适合存储大数据,会十分浪费内存。

2.apply()写入文件操作是异步执行的,不会占用主线程资源。先保存到内存,再通过异步线程保存sp文件到磁盘,commit保存到内存跟磁盘是同步保存,所以,如果频繁保存数据的话,apply肯定要高效,优先推荐使用apply。commit()有返回值,apply()没有返回值,apply()失败了是不会报错的。保存数据时,apply()是先将数据保存到内存中,然后在子线程中将数据更新到磁盘,commit()是将数据保存到内存中,之后立即将数据同步到磁盘,这个操作是在当前线程中完成的,所以会阻塞线程,所以现在android建议使用apply(),但是使用apply()时也有需要注意的地方,如果保存数据时这个过程还没有结束,但是这时退出了activity,这时activity会先确保数据是否已经全部同步到磁盘了,如果没有,这时会两种情况,一是保存的过程正在子线中执行,这时等待就好,如果这时还没分发给子线程,那么就直接切换到主线程执行了,所以这时提交数据时也有需要注意的地方,使用SharedPreferences.Editor提交数据时,尽量在所有数据都提交后再调用apply()方法;

3.从sp中读取数据,需要等sp构造方法调用从磁盘读取数据成功后才继续往下执行,所以为了不造成阻塞,我们可以提前创建出SharedPreferences对象,而不是在使用的时候再去创建。

相关内容

热门资讯

文艺节目主持词 文艺节目主持词四篇  主持词要把握好吸引观众、导入主题、创设情境等环节以吸引观众。在一步步向前发展的...
幼儿园六一儿童节主持词 幼儿园六一儿童节主持词尊敬的各位来宾、各位朋友大家下午好!沐浴着和风丽日,我们即将迎来花团锦簇、芳香...
教师节表彰大会校长的致辞 教师节表彰大会校长的致辞范文(精选6篇)  在平平淡淡的日常中,要用到致辞的地方还是很多的,致辞讲求...
婚礼开场白主持词 婚礼开场白主持词  利用在中国拥有几千年文化的诗词能够有效提高主持词的感染力。随着社会一步步向前发展...
会主持人开场白台词 会主持人开场白台词2013年会主持人开场白台词    甲:新年的钟声即将敲响,时光的车轮又留下了一道...
领导主持词 领导主持词三篇  主持词已成为各种演出活动和集会中不可或缺的一部分。在现今人们越来越重视活动氛围的社...
升学宴致辞 升学宴致辞(精选15篇)  在现实生活或工作学习中,大家一定都接触过致辞吧,致辞具有“礼仪性”或“仪...
农村白事的主持词开场白 农村白事的主持词开场白(精选10篇)  在发展不断提速的社会中,越来越多的人会用到开场白,好的开场白...
生日主持词的开场白   生日主持词开场白(一)  各位同事和寿星们,各人晚顶好!在这天高气爽、丹桂飘喷鼻的夸姣季候,咱们...
旅游文化节主持词 旅游文化节主持词  主持词的写作需要将主题贯穿于所有节目之中。现今社会在不断向前发展,主持人参与的事...
主持人串词 主持人串词  一、串词的语言特征  (串词的语言,可以说是用尽了所有的修辞手法,我们不可能去全讲,因...
浪漫婚礼司仪主持词 浪漫婚礼司仪主持词  主持词是主持人在台上表演的灵魂之所在。在现在的社会生活中,很多场合都需要主持人...
公司迎春晚会的主持词 公司迎春晚会的主持词  主持词的写作需要将主题贯穿于所有节目之中。在当今不断发展的世界,主持人在活动...
少儿节目主持词 精选少儿节目主持词4篇  主持词已成为各种演出活动和集会中不可或缺的一部分。随着社会一步步向前发展,...
王家卫电影经典台词 王家卫电影经典台词(精选50句)  我们爱看王家卫的电影,不止爱他所创造的那个光影世界,更爱他电影中...
演唱会主持台词 演唱会主持台词  (甲)尊敬的各位领导,  (乙)各位来宾,  (甲)敬爱的老师,  (乙)亲爱的同...
《你的名字》经典台词 《你的名字》经典台词  你的名字,是谁的心事,还记得你的名字里面的经典台词吗?以下是小编为你精心整理...
教研活动主持词 教研活动主持词  主持人在台上表演的灵魂就表现在主持词中。在当下的中国社会,主持成为很多活动不可或缺...
艺术节主持词开场白 艺术节主持词开场白  什么是艺术节  艺术节是文艺工作者及艺术家、艺术爱好者之间学术交流与学习的重要...
老板在公司年会致辞 老板在公司年会致辞15篇  在平平淡淡的学习、工作、生活中,大家最不陌生的就是致辞了吧,致辞具有针对...