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对象,而不是在使用的时候再去创建。

相关内容

热门资讯

赵孟頫滕王阁序书法 蓬莱阁景点导游词推荐度:硬笔书法书法社团活动计划推荐度:俄狄浦斯王读后感推荐度:书法班招生方案推荐度...
用“通感”手法赏析欧阳修的《... 用“通感”手法赏析欧阳修的《醉翁亭记》《醉翁亭记》写于庆历六年,欧阳修因支持韩琦、范仲淹等的政治改革...
小学语文第三册《苹果落地》课... 北师大版小学语文第三册《苹果落地》课堂实录一、创设情境,激趣导入,揭示课题师:瞧!老师今天为小朋友们...
《浣溪沙》译文及赏析 《浣溪沙》译文及赏析  赏析,是一个汉语词汇,意思是欣赏并分析(诗文等),通过鉴赏与分析得出理性的认...
李白《梦游天姥吟留别》赏析 李白《梦游天姥吟留别》赏析(5篇)李白《梦游天姥吟留别》赏析1  梦游天姥(mǔ)吟留别  (也作《...
手机怎么用百度翻译文言文 手机怎么用百度翻译文言文  第一步:  下载“手机百度翻译”软件,打开百度翻译软件,在源语言一栏中点...
《都市歌手》第十七集 《都市歌手》第十七集都市歌手第十七集我开始喜欢你(上)人物:金紫燕、穆双毅、隋世兵、霍智广、屠天信、...
《假如生活欺骗了你》2 《假如生活欺骗了你》2教学设计:诗歌导入人生道路,不可能一帆风顺,有时真的不遂人愿。假如你觉得生活欺...
周易是什么东西 周易是什么东西  《周易》这部特殊的经典,在我国历史上地位非常高。儒家认为它是第一经典,所谓的五经之...
《般若波罗蜜多心经》浅释:四... 《般若波罗蜜多心经》浅释:四圣谛  我们都知道,佛法分为「世间」和「出世间」两种,而出世间又有大乘、...
虞卿谓春申君原文及翻译 虞卿谓春申君原文及翻译  在日常学习、工作或生活中,大家一定避免不了阅读课文,阅读有利于提升我们的文...
苏轼:题西林壁 苏轼:题西林壁  题西林壁  横看成岭侧成峰,  远近高低各不同。  不识庐山真面目,  只缘身在此...
《论语·季氏》文言文阅读题 《论语·季氏》文言文阅读题  陈亢问于伯鱼①曰:“子亦有异闻乎?”对曰:“未也。尝独立,鲤趋而过庭。...
孔子见罗雀者文言文翻译 孔子见罗雀者文言文翻译  孔子见罗雀者是选自《孔子家语》的,那么,下面是小编给大家整理收集的孔子见罗...
乐羊子妻翻译和原文 乐羊子妻翻译和原文  《乐羊子妻》(yuèyángzǐqī),是一篇人物传记。它通过两个小故事,赞扬...
爱情是什么的排比句 爱情是什么的排比句  1、爱情是一杯茶,香醇可口,伴着甜味,夹着苦味;爱情是一把钥匙,打开心灵,享受...
游白水书付过原文及赏析 游白水书付过原文及赏析  原文:  游白水书付过  [宋代]苏轼  绍圣元年十月十二日,与幼子过游白...
平凡的世界电视剧 平凡的世界电视剧  平凡的世界剧情介绍  上世纪70年代,自尊好强的农家子弟孙少平在原西县高中读书,...
苏轼《西江月》原文翻译及赏析 苏轼《西江月》原文翻译及赏析  西江月·顷在黄州 苏轼  顷在黄州,春夜行蕲水中,过酒家饮,酒醉,乘...
管鲍之交文言文翻译 管鲍之交文言文翻译  文言文是中国古代的一种书面语言,主要包括以先秦时期的口语为基础而形成的书面语。...