FBReader如何打开一个指定的电子书,以及一些阅读操作的实现

首先,我们回顾一下上一篇的一些知识点,针对一个可识别的有效电子书文件来说:

  • 手机存储中的电子书文件会通过ZLFile.createFileByPath被创建成一个ZLPhysicalFile类型的文件对象
  • BookCollectionShadow的大部分方法其实是由BookCollection来实现的
  • BookCollection中有一个非常重要的方法getBookByFile(ZLFile),其中会校验文件的扩展名,如果是支持的电子书格式时,那么就会获取到相应的解析插件
  • 随后在BookCollection中创建一个DbBook对象,DbBook在初始化时会读取book的基本信息,这里主要是通过传入的plugin,调用plugin的native方法读取到的
  • 图书信息页打开FBReader进行阅读时,通过FBReaderIntents.putBookExtra(intent, book),传递的一个有效参数的Book对象

一、FBReader是如何获取book,又是如何获取并显示图书内容的

FBReader如何获取Book,以及如何更简便的打开一本电子书

查看清单文件,我们可以看到FBReader的启动模式:

android:launchMode="singleTask"
复制代码

那么图书信息界面,点击“阅读”再次打开FBReader时,其onNewIntent将被触发:

@Override
protected void onNewIntent(final Intent intent) {final String action = intent.getAction();final Uri data = intent.getData();if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) {super.onNewIntent(intent);} else if (Intent.ACTION_VIEW.equals(action)&& data != null && "fbreader-action".equals(data.getScheme())) {//忽略部分代码...} else if (Intent.ACTION_VIEW.equals(action) || FBReaderIntents.Action.VIEW.equals(action)) {//为myOpenBookIntent赋值myOpenBookIntent = intent;//忽略部分代码...} else if (FBReaderIntents.Action.PLUGIN.equals(action)) {//忽略部分代码...} else if (Intent.ACTION_SEARCH.equals(action)) {//忽略部分代码...} else if (FBReaderIntents.Action.CLOSE.equals(intent.getAction())) {//忽略部分代码...} else if (FBReaderIntents.Action.PLUGIN_CRASH.equals(intent.getAction())) {//忽略部分代码...} else {super.onNewIntent(intent);}
}
复制代码

发现校验了action,那么我们的之前的Intent其action是什么呢?这里要回看一下打开阅读页面的时候调用的代码:

FBReader.openBookActivity(BookInfoActivity.this, myBook, null);public static void openBookActivity(Context context, Book book, Bookmark bookmark) {final Intent intent = defaultIntent(context);FBReaderIntents.putBookExtra(intent, book);FBReaderIntents.putBookmarkExtra(intent, bookmark);context.startActivity(intent);
}public static Intent defaultIntent(Context context) {return new Intent(context, FBReader.class).setAction(FBReaderIntents.Action.VIEW)//设置action.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
}
复制代码

默认的Intent其action被设置为了FBReaderIntents.Action.VIEW,那么在onNewIntent方法,经过断点可以知道,针对当前我们从图书信息跳转过来阅读的情况,这里只是对myOpenBookIntent进行了赋值,并没有其他多余的操作。

这样的话,我们就要继续往下看,在FBReader的onResume中:

@Override
protected void onResume() {super.onResume();//忽略部分代码...if (myCancelIntent != null) {//忽略部分代码...} else if (myOpenBookIntent != null) {final Intent intent = myOpenBookIntent;myOpenBookIntent = null;getCollection().bindToService(this, new Runnable() {public void run() {openBook(intent, null, true);}});} else if (myFBReaderApp.getCurrentServerBook(null) != null) {//忽略部分代码...} else if (myFBReaderApp.Model == null && myFBReaderApp.ExternalBook != null) {//忽略部分代码...} else {//忽略部分代码...}
}
复制代码

当myOpenBookIntent != null时,会执行getCollection().bindToService,这个好像我们在那见过啊,看看getCollection:

private BookCollectionShadow getCollection() {return (BookCollectionShadow)myFBReaderApp.Collection;
}
复制代码

老朋友BookCollectionShadow,之前的分析来看,下面就会执行runnable了,也就是openBook:

private synchronized void openBook(Intent intent, final Runnable action, boolean force) {if (!force && myBook != null) {return;}//取出bookmyBook = FBReaderIntents.getBookExtra(intent, myFBReaderApp.Collection);final Bookmark bookmark = FBReaderIntents.getBookmarkExtra(intent);if (myBook == null) {final Uri data = intent.getData();if (data != null) {myBook = createBookForFile(ZLFile.createFileByPath(data.getPath()));}}//忽略部分代码...Config.Instance().runOnConnect(new Runnable() {public void run() {myFBReaderApp.openBook(myBook, bookmark, action, myNotifier);AndroidFontUtil.clearFontCache();}});
}
复制代码

在openBook方法中,发现取出了我们之前传递过来的book。而且,仔细阅读下面的判断,可以分析出,如果Intent中没有传递book,但是有传递的Uri,那么就回去调用方法createBookForFile:

private Book createBookForFile(ZLFile file) {if (file == null) {return null;}Book book = myFBReaderApp.Collection.getBookByFile(file.getPath());if (book != null) {return book;}if (file.isArchive()) {for (ZLFile child : file.children()) {book = myFBReaderApp.Collection.getBookByFile(child.getPath());if (book != null) {return book;}}}return null;
}
复制代码

熟悉的方法,去创建了一个Book。那么这样的话,我们就还可以通过这种方式去打开一本电子书:

//path电子书绝对路径
public static void openBookActivity(Context context, String path) {final Intent intent = FBReader.defaultIntent(context);intent.setData(Uri.parse(path));context.startActivity(intent);
}
复制代码

关于Book和DbBook

在这里,不知大家有没有发现一个问题,那就是我们熟悉的BookCollectionShadow和BookCollection,我们知道他们都是继承于AbstractBookCollection,但是BookCollectionShadow是使用的Book,而BookCollection是使用的DbBook:

public class BookCollectionShadow extends AbstractBookCollection<Book> implements ServiceConnectionpublic class BookCollection extends AbstractBookCollection<DbBook>
复制代码

再来看一下Book和DbBook这两个类的定义:

public final class DbBook extends AbstractBookpublic final class Book extends AbstractBook
复制代码

很明显这两个类,是均继承于AbstractBook的不同子类,但是我们之前有分析过BookCollectionShadow中有关于IBookCollection的实现,实际最终是BookCollection来操作的,但是他们是基于两个不同数据类型的,比如我们查看getBookByFile:

BookCollectionShadow中:
public synchronized Book getBookByFile(String path) {if (myInterface == null) {return null;}try {return SerializerUtil.deserializeBook(myInterface.getBookByFile(path), this);} catch (RemoteException e) {return null;}
}BookCollection中:
public DbBook getBookByFile(String path) {return getBookByFile(ZLFile.createFileByPath(path));
}
复制代码

调用者BookCollectionShadow,调用getBookByFile期望得到Book类型的数据,而最终实现者调用getBookByFile却返回了DbBook类型的数据,这是怎么一回事?

在BookCollectionShadow中,我们可以发现,最终return的是SerializerUtil.deserializeBook方法返回的数据。那这个方法又是做什么的呢?点进去看一下:

SerializerUtil.class private static final AbstractSerializer defaultSerializer = new XMLSerializer();public static <B extends AbstractBook> B deserializeBook(String xml, AbstractSerializer.BookCreator<B> creator) {return xml != null ? defaultSerializer.deserializeBook(xml, creator) : null;
}XMLSerializer.class
@Override
public <B extends AbstractBook> B deserializeBook(String xml, BookCreator<B> creator) {try {final BookDeserializer<B> deserializer = new BookDeserializer<B>(creator);Xml.parse(xml, deserializer);return deserializer.getBook();} catch (SAXException e) {System.err.println(xml);e.printStackTrace();return null;}
}
复制代码

不难看出,在调用BookCollectionShadow的getBookByFile方法时,会调用LibraryService的getBookByFile,而后者会返回一段xml数据,BookCollectionShadow会根据这段xml数据,将其解析成对应的Book对象。我们知道,虽然BookCollection是最终实施人,但是在他和BookCollectionShadow之间,还有一个LibraryService中的LibraryImplementation作为中间人,那么我们就看看中间人的这个方法是做了些什么:

public String getBookByFile(String path) {//这里myCollection是BookCollection实例,返回结果为DbBookreturn SerializerUtil.serialize(myCollection.getBookByFile(path));
}
复制代码

同样进入了SerializerUtil中:

public static String serialize(AbstractBook book) {return book != null ? defaultSerializer.serialize(book) : null;
}XMLSerializer.class
@Override
public String serialize(AbstractBook book) {final StringBuilder buffer = builder();serialize(buffer, book);return buffer.toString();
}
复制代码

细节我们就不再深入去看了,这里流程已经比较清晰,就拿getBookByFile这个方法来说:

  • 客户端通过BookCollectionShadow实例调用此方法,意图得到Book类型的数据
  • BookCollectionShadow调用到中间人LibraryImplementation的getBookByFile方法
  • LibraryImplementation调用最终实施人BookCollection的getBookByFile方法,后者返回DbBook数据
  • LibraryImplementation对返回的DbBook,通过SerializerUtil转换成对应xml数据
  • 转换后的xml返回客户端BookCollectionShadow中,再次通过SerializerUtil转为Book对象

这里也就实现了Book的跨进程传输,由于AbstractBook及其父类,均没有实现Serializable或者Parcelable,所以是不能夸进程传输的。通过跨进程传输,把Book的一些核心信息传递给客户端,同时使客户端可以忽略DbBook中其他的关于dataBase的操作行为。

Book获取内容及显示前的准备工作

经过上面简单的分析,FBReader已经拿到了book,那么接下来,FBReader又分别做了些什么呢?

这就要从openBook方法中的,最后一段代码来开始接下来的分析了:

private synchronized void openBook(Intent intent, final Runnable action, boolean force) {//忽略部分代码...Config.Instance().runOnConnect(new Runnable() {public void run() {myFBReaderApp.openBook(myBook, bookmark, action, myNotifier);AndroidFontUtil.clearFontCache();}});
}
复制代码

runOnConnect这个方法我们之前已经分析过了,接下来会执行runnable。这里,我们发现了一个新的角色登场了,就是FBReaderApp。

先看看这个FBReaderApp在FBReader的中,是什么时候初始化的吧:

@Override
protected void onCreate(Bundle icicle) {super.onCreate(icicle);//忽略部分代码...myFBReaderApp = (FBReaderApp)FBReaderApp.Instance();if (myFBReaderApp == null) {myFBReaderApp = new FBReaderApp(Paths.systemInfo(this), new BookCollectionShadow());}myFBReaderApp.setWindow(this);//忽略部分代码...
}
复制代码

首次进入FBReader时,FBReaderApp.Instance()为null,就会通过new创建,之后会被重用。看下它的构造方法:

public FBReaderApp(SystemInfo systemInfo, final IBookCollection<Book> collection)
复制代码

BookCollectionShadow我们已经很熟了,这个SystemInfo是个啥呢?进去看看:

public interface SystemInfo {String tempDirectory();String networkCacheDirectory();
}
复制代码

在看看onCreate创建FBReaderApp时传入的Paths.systemInfo:

public static SystemInfo systemInfo(Context context) {final Context appContext = context.getApplicationContext();return new SystemInfo() {public String tempDirectory() {final String value = ourTempDirectoryOption.getValue();if (!"".equals(value)) {return value;}return internalTempDirectoryValue(appContext);}public String networkCacheDirectory() {return tempDirectory() + "/cache";}};
}
复制代码

看来是获取文件存储和缓存路径的。

接下来,我们就进入FBReaderApp,去看一下它的openBook方法:

public void openBook(Book book, final Bookmark bookmark, Runnable postAction, Notifier notifier) {//忽略部分代码..final SynchronousExecutor executor = createExecutor("loadingBook");executor.execute(new Runnable() {public void run() {openBookInternal(bookToOpen, bookmark, false);}}, postAction);
}
复制代码

逐步分析:

1.createExecutor:

protected SynchronousExecutor createExecutor(String key) {if (myWindow != null) {return myWindow.createExecutor(key);} else {return myDummyExecutor;}
}
复制代码

FBReader在onCreate生成FBReaderApp之后,就调用了FBReaderApp.setWindow(this),那么当前的myWindow就是FBReader,其createExecutor方法:

@Override
public FBReaderApp.SynchronousExecutor createExecutor(String key) {return UIUtil.createExecutor(this, key);
}
复制代码

接着进入了UIUtil:

public static ZLApplication.SynchronousExecutor createExecutor(final Activity activity, final String key) {return new ZLApplication.SynchronousExecutor() {private final ZLResource myResource =ZLResource.resource("dialog").getResource("waitMessage");private final String myMessage = myResource.getResource(key).getValue();private volatile ProgressDialog myProgress;public void execute(final Runnable action, final Runnable uiPostAction) {activity.runOnUiThread(new Runnable() {public void run() {myProgress = ProgressDialog.show(activity, null, myMessage, true, false);final Thread runner = new Thread() {public void run() {action.run();activity.runOnUiThread(new Runnable() {public void run() {try {myProgress.dismiss();myProgress = null;} catch (Exception e) {e.printStackTrace();}if (uiPostAction != null) {uiPostAction.run();}}});}};runner.setPriority(Thread.MAX_PRIORITY);runner.start();}});}//忽略部分代码...};
}
复制代码

简单分析一下,这段代码做了什么:

  • 加载资源ZLResource
  • 根据传入的key获取resouce下对应的资源信息msg
  • 实现execute(action,uiaction)
  • 执行execute时创建ProgressDialog,并设置其提示信息为msg
  • 随后创建子线程执行action,执行完毕后通过activity调度到主线程关闭ProgressDialog,然后执行uiaction

那么资源信息都是有哪些?又存储在什么地方呢?要想了解这两个问题的答案,我们就需要去看一下ZLResource:

static void buildTree() {synchronized (ourLock) {if (ourRoot == null) {ourRoot = new ZLTreeResource("", null);ourLanguage = "en";ourCountry = "UK";loadData();}}
}private static void loadData() {ResourceTreeReader reader = new ResourceTreeReader();loadData(reader, ourLanguage + ".xml");loadData(reader, ourLanguage + "_" + ourCountry + ".xml");
}private static void loadData(ResourceTreeReader reader, String fileName) {reader.readDocument(ourRoot, ZLResourceFile.createResourceFile("resources/zlibrary/" + fileName));reader.readDocument(ourRoot, ZLResourceFile.createResourceFile("resources/application/" + fileName));reader.readDocument(ourRoot, ZLResourceFile.createResourceFile("resources/lang.xml"));reader.readDocument(ourRoot, ZLResourceFile.createResourceFile("resources/application/neutral.xml"));
}
复制代码

ZLResource在加载资源前,会首先buildTree,在首次buildTree的时会调用loadData方法,最终加载了资源目录下当前系统语言的资源文件,分别是zlibrary下的相应语言资源文件,application下的相应语言资源文件,lang资源文件,application/neutral.xml资源文件。

中文系统资源文件:

上面分析处调用的UIUtil,分别加载了"dialog"-"waitMessage"-"loadingBook"

2.openBookInternal(bookToOpen, bookmark, false)

通过第一步的分析,在调用execute时,首先会执行第一个runnable,也就是其中的openBookInternal方法:

private synchronized void openBookInternal(final Book book, Bookmark bookmark, boolean force) {//忽略部分代码...final PluginCollection pluginCollection = PluginCollection.Instance(SystemInfo);final FormatPlugin plugin;try {plugin = BookUtil.getPlugin(pluginCollection, book);} catch (BookReadingException e) {processException(e);return;}//忽略部分代码...try {Model = BookModel.createModel(book, plugin);Collection.saveBook(book);ZLTextHyphenator.Instance().load(book.getLanguage());BookTextView.setModel(Model.getTextModel());setBookmarkHighlightings(BookTextView, null);gotoStoredPosition();if (bookmark == null) {setView(BookTextView);} else {gotoBookmark(bookmark, false);}Collection.addToRecentlyOpened(book);final StringBuilder title = new StringBuilder(book.getTitle());if (!book.authors().isEmpty()) {boolean first = true;for (Author a : book.authors()) {title.append(first ? " (" : ", ");title.append(a.DisplayName);first = false;}title.append(")");}setTitle(title.toString());} catch (BookReadingException e) {processException(e);}getViewWidget().reset();getViewWidget().repaint();//忽略部分代码...
}
复制代码

关于PluginCollection.Instance(SystemInfo):

public static PluginCollection Instance(SystemInfo systemInfo) {if (ourInstance == null) {createInstance(systemInfo);}return ourInstance;
}private static synchronized void createInstance(SystemInfo systemInfo) {if (ourInstance == null) {ourInstance = new PluginCollection(systemInfo);// This code cannot be moved to constructor// because nativePlugins() is a native methodfor (NativeFormatPlugin p : ourInstance.nativePlugins(systemInfo)) {ourInstance.myBuiltinPlugins.add(p);System.err.println("native plugin: " + p);}}
}private native NativeFormatPlugin[] nativePlugins(SystemInfo systemInfo);
复制代码

PluginCollection初始化之后,会调用native的nativePlugins去获取一个图书解析插件集合,返回的结果就是可解析的各电子书类型对应的解析插件。这里我打开的电子书格式为epub,获取到的插件是OEBNativePlugin:

紧接着我们来看这个方法,BookUtil.getPlugin(pluginCollection, book),在上一篇已经分析过,这里最终会通过对Book文件类型的区分,获取该电子书格式对应的解析插件。

随后,一个超级核心的方法出现了!那就是解析电子书内容的方法:

BookModel.createModel(book, plugin);BookModel.class
public static BookModel createModel(Book book, FormatPlugin plugin) throws BookReadingException {if (plugin instanceof BuiltinFormatPlugin) {final BookModel model = new BookModel(book);((BuiltinFormatPlugin)plugin).readModel(model);return model;}throw new BookReadingException("unknownPluginType", null, new String[] { String.valueOf(plugin) });
}对于我测试使用的书来说,最终解析图书内容会调用NativeFormatPlugin的readModel
synchronized public void readModel(BookModel model) throws BookReadingException {final int code;final String tempDirectory = SystemInfo.tempDirectory();synchronized (ourNativeLock) {//这里返回解析结果code,为0时则正确解析code = readModelNative(model, tempDirectory);}switch (code) {case 0:return;case 3:throw new CachedCharStorageException("Cannot write file from native code to " + tempDirectory);default:throw new BookReadingException("nativeCodeFailure",BookUtil.fileByBook(model.Book),new String[] { String.valueOf(code), model.Book.getPath() });}
}private native int readModelNative(BookModel model, String cacheDir);
复制代码

解析前的BookMode内容:

解析后的BookMode内容:

最后,我们再看一下最后两句:

getViewWidget().reset();
getViewWidget().repaint();public final ZLViewWidget getViewWidget() {return myWindow != null ? myWindow.getViewWidget() : null;
}我们知道myWindow为FBReader,那么就去看一下FBReader中的getViewWidget:
@Override
public ZLViewWidget getViewWidget() {return myMainView;
}在FBReader的onCreate中:
myMainView = (ZLAndroidWidget)findViewById(R.id.main_view);
复制代码

进入ZLAndroidWidget看一下对应的方法:

@Override
public void reset() {myBitmapManager.reset();
}@Override
public void repaint() {postInvalidate();
}BitmapManagerImpl.class
void reset() {for (int i = 0; i < SIZE; ++i) {myIndexes[i] = null;}
}
复制代码

最终,页面绘制出了电子书的内容。

当然,由于本人接触此项目时间有限,而且书写技术文章的经验实在欠缺,过程中难免会有存在错误或描述不清或语言累赘等等一些问题,还望大家能够谅解,同时也希望大家继续给予指正。最后,感谢大家对我的支持,让我有了强大的动力坚持下去。

PS:《Android开发艺术探索》,前言中的第一行“从目前形势来看,Android开发相当火热...”。看到这句话,眼中满是泪水啊!青春!怎么这么快就过去了!......

转载于:https://juejin.im/post/5bfe2d7cf265da615b712a2a

开源电子书项目FBReader初探(四)相关推荐

  1. 开源电子书项目FBReader初探(六)

    FBReader是如何读取缓存文件内容,并生成每一页Bitmap内容的呢? 经过上一篇的分析,我们已经知道,FBRreader在绘制时是获取每一页对应的bitmap,然后再进行绘制的.同时,在绘制完当 ...

  2. 并联四足机器人项目开源教程(五) --- 四足机器人相关书籍论文研读

    这个是本人在大三期间做的项目 ---- 基于MIT的Cheetah方案设计的十二自由度并联四足机器人,这个项目获得过两个国家级奖项和一个省级奖项.接下来我会将这个机器人的控制部分所有代码进行开源,并配 ...

  3. C++开源代码项目汇总

    Google的C++开源代码项目 v8  -  V8 JavaScript Engine V8 是 Google 的开源 JavaScript 引擎. V8 采用 C++ 编写,可在谷歌浏览器(来自 ...

  4. 看一下基于ASP.NET MVC的开源社区项目Orchard

    昨天介绍了基于ASP.NET MVC的框架Catharsis,今天给大家介绍的是基于ASP.NET MVC的一个开源社区项目Orchard,本篇主要介绍一下Orchard是什么,如何下载安装以及安装过 ...

  5. Google开源OCR项目Tesseract训练(自己训练的记录,未成功)

    图像处理开发需求.图像处理接私活挣零花钱,请加微信/QQ 2487872782 图像处理开发资料.图像处理技术交流请加QQ群,群号 271891601 本文训练Tesseract用的方法主要参考文章  ...

  6. ssm架构 开源项目_6个开源架构项目签出

    ssm架构 开源项目 架构世界的变化没有软件那样快,但是架构师仍在寻找共享创新设计和思想的新方法. 开源架构运动旨在免费提供架构设计,工程图,3D渲染和文档,以根据开源许可将其集成到其他项目中. 它的 ...

  7. 最新C#开源资源项目

    一.AOP框架 Encase 是C#编写开发的为.NET平台提供的AOP框架.Encase 独特的提供了把方面(aspects)部署到运行时代码,而其它AOP框架依赖配置文件的方式.这种部署方面(as ...

  8. 一些知名的J2me优秀开源UI项目

    一些知名的J2me优秀开源UI项目 源文地址:http://www.open-open.com/73.htm  J2ME Polish   J2ME Polish是用于开发J2ME应用的工具集:  从 ...

  9. 《安富莱嵌入式周报》第285期:电子技术更新换代太快,我要躺平,Linux内核6.1已经并入RUST,一夜161个网站密码遭泄,Matlab精选课件,开源电子书

    往期周报汇总地址:嵌入式周报 - uCOS & uCGUI & emWin & embOS & TouchGFX & ThreadX - 硬汉嵌入式论坛 - P ...

  10. 28款GitHub最流行的开源机器学习项目,推荐GitHub上10 个开源深度学习框架

    20 个顶尖的 Python 机器学习开源项目 机器学习 2015-06-08 22:44:30 发布 您的评价: 0.0 收藏 1收藏 我们在Github上的贡献者和提交者之中检查了用Python语 ...

最新文章

  1. C#事件的发送方和接收方(订阅方)
  2. 亿级流量架构之服务器扩容思路及问题分析
  3. 获取自定义组件的宽度和高度
  4. 【数学基础】算法工程师必备的机器学习--线性模型(上)
  5. Ubantu使用笔记
  6. mysql数据库入门教程(10):标识列和事务
  7. 平面方程(Plane Equation)
  8. python指数运算是不是有问题_为什么在Python 3中复指数运算如此之快?
  9. 机器学习、深度学习概念术语的理解
  10. Power BI for Office 365 概览
  11. Adobe DPS解决方案工作流程及其收费情况介绍
  12. Android MVVM框架搭建(一)ViewModel + LiveData + DataBinding
  13. 校园招聘数电模电笔试题
  14. 计算机打字测试,打字测试
  15. 费马大定理与费马小定理
  16. 云计算 码率适配限速_【省带宽、压成本专题】码率适配限速大揭秘,带你认识这款视频网站节流大杀器...
  17. 中国大学MOOC-陈越、何钦铭-数据结构-起步能力自测题
  18. LeetCode - 1175 - 质数排列(prime-arrangements)
  19. 计算机检测不到双显示器,win10系统双屏幕检测不到第二屏幕怎么办 解决双屏幕不显示的方法步骤...
  20. 二层板的射频RF信号如何控阻抗 四层板的射频RF信号如何控阻抗  射频信号是否可以不控阻抗,射频差分需要控阻抗吗?为什么射频信号需要挖空隔层参考?射频信号为什么要加粗?

热门文章

  1. 快速入门nebula graph
  2. 妇科宫颈细胞学计算机检查,宫颈细胞学检查是怎么回事?
  3. 实施(运维)工程师 笔试选择题
  4. python使用作为转义符的开始符号_python转义符的使用
  5. 软件需求的薛定谔之猫
  6. Internet协议的安全性
  7. redis 结合 spring
  8. C语言统计多个闰年,C语言统计闰年
  9. Mybatis OGNL表达式报错
  10. wps office 2013 利用wps文字制作一张漂亮的座位表