開源電子書專案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; } //取出book myBook = 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 ServiceConnection public class BookCollection extends AbstractBookCollection<DbBook> 複製程式碼
再來看一下Book和DbBook這兩個類的定義:
public final class DbBook extends AbstractBook public 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方法時,會呼叫service的getBookByFile,而後者會返回一段xml資料,BookCollectionShadow會根據這段xml資料,將其解析成對應的Book物件。我們知道,雖然BookCollection是最終實施人,但是在他和BookCollectionShadow還有一個LibraryService中的LibraryImplementation作為中間人,那麼我們就看看中間的這個方法是做了些什麼:
public String getBookByFile(String path) { //這裡myCollection是BookCollection例項,返回結果為DbBook return 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 method for (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開發相當火熱...”。看到這句話,眼中滿是淚水啊!青春!怎麼這麼快就過去了!......