開源電子書專案FBReader初探(三)
通過上一篇分析,我們已經知道如何響應並開啟選單,而且選單中第一項是開啟本地書櫃,這一篇我們就以此為入口,去探究FBReader的書櫃是怎麼實現,以及是如何分辨一本書並且能開啟一本書的。
一、開啟FBReader本地書櫃時,首頁內容顯示都做了些什麼

開啟本地書櫃action:ShowLibraryAction,直接看其run方法:
@Override protected void run(Object ... params) { final Intent externalIntent = new Intent(FBReaderIntents.Action.EXTERNAL_LIBRARY); final Intent internalIntent = new Intent(BaseActivity.getApplicationContext(), LibraryActivity.class); //查詢是否有滿足條件的外掛書櫃,有則開啟外掛中的書櫃,沒有就開啟本地書櫃 LibraryActivity if (PackageUtil.canBeStarted(BaseActivity, externalIntent, true)) { try { startLibraryActivity(externalIntent); } catch (ActivityNotFoundException e) { startLibraryActivity(internalIntent); } } else { startLibraryActivity(internalIntent); } } 複製程式碼
該Activity為ListActivity:

檢視其onCreate:
private final BookCollectionShadow myCollection = new BookCollectionShadow(); @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); // 忽略部分程式碼... new LibraryTreeAdapter(this); myCollection.bindToService(this, new Runnable() { public void run() { setProgressBarIndeterminateVisibility(!myCollection.status().IsComplete); myRootTree = new RootTree(myCollection, PluginCollection.Instance(Paths.systemInfo(LibraryActivity.this))); myCollection.addListener(LibraryActivity.this); init(getIntent()); } }); } 複製程式碼
可以發現,這裡設定了adapter為 LibraryTreeAdapter ,那麼這個頁面顯示的內容資料是從哪來的呢?彆著急,這裡我們先去看一下myCollection是個什麼東東,我們就順著onCreate中呼叫的bindToService看起:
public synchronized boolean bindToService(Context context, Runnable onBindAction) { if (myInterface != null && myContext == context) { if (onBindAction != null) { Config.Instance().runOnConnect(onBindAction); } return true; } else { if (onBindAction != null) { synchronized (myOnBindActions) { myOnBindActions.add(onBindAction); } } final boolean result = context.bindService( FBReaderIntents.internalIntent(FBReaderIntents.Action.LIBRARY_SERVICE), this, Service.BIND_AUTO_CREATE ); if (result) { myContext = context; } return result; } } 複製程式碼
可以看出,這個collection會繫結LibraryService,而且傳遞的ServiceConnection為this,那麼說明這個collection實現了ServiceConnection這個介面,順著這個邏輯去看一下onServiceConnected:
public void onServiceConnected(ComponentName name, IBinder service) { synchronized (this) { myInterface = LibraryInterface.Stub.asInterface(service); } final List<Runnable> actions; synchronized (myOnBindActions) { actions = new ArrayList<Runnable>(myOnBindActions); myOnBindActions.clear(); } for (Runnable a : actions) { Config.Instance().runOnConnect(a); } // 忽略部分程式碼... } 複製程式碼
通過分析這兩個方法可以得知,bindToService(context,runnable)是當已繫結service時,則直接通過Config來執行runnable,否則先繫結service,然後再通過Config來執行runnable。那麼這個Config又是何方神聖呢?我們就順著其runOnConnect來一探究竟:
public abstract void runOnConnect(Runnable runnable); 複製程式碼
向下查詢,可以找到其唯一實現在子類ConfigShadow中:
@Override public void runOnConnect(Runnable runnable) { if (myInterface != null) {//當前已成功繫結service,則直接執行run runnable.run(); } else { synchronized (myDeferredActions) { myDeferredActions.add(runnable);//未成功繫結放入集合中,待執行 } } } 複製程式碼
再來看一下ConfigShadow的構造方法:
public ConfigShadow(Context context) { myContext = context; context.bindService( FBReaderIntents.internalIntent(FBReaderIntents.Action.CONFIG_SERVICE), this, Service.BIND_AUTO_CREATE ); } 複製程式碼
可以看出該類綁定了ConfigService,而且自身實現了ServiceConnection介面:
public void onServiceConnected(ComponentName name, IBinder service) { synchronized (this) { myInterface = ConfigInterface.Stub.asInterface(service); myContext.registerReceiver( myReceiver, new IntentFilter(FBReaderIntents.Event.CONFIG_OPTION_CHANGE) ); } final List<Runnable> actions; synchronized (myDeferredActions) { actions = new ArrayList<Runnable>(myDeferredActions); myDeferredActions.clear(); } for (Runnable a : actions) { a.run(); } } 複製程式碼
可以看出,在成功連結ConfigService之後,首先綁定了Config_Option_Change的廣播接收者,並且將待執行集合中的runnable依次取出並執行。
那麼ConfigService是做啥的呢?直接進去看看:
public class ConfigService extends Service { private ConfigInterface.Stub myConfig; @Override public IBinder onBind(Intent intent) { return myConfig; } @Override public void onCreate() { super.onCreate(); myConfig = new SQLiteConfig(this); } // 忽略部分程式碼 } 複製程式碼
可以看到onBind返回的是SQLiteConfig,這裡也就比較明確了,是操作Config資料庫的。那麼ConfigShadow又是在哪被建立的呢,通過find可以查到其唯一建立在ZLAndroidApplication:
public abstract class ZLAndroidApplication extends Application { // 忽略部分程式碼 private ConfigShadow myConfig; @Override public void onCreate() { super.onCreate(); myConfig = new ConfigShadow(this); } } 複製程式碼
到此,我們先簡單總結一下:
- 應用在啟動時通過建立 ConfigShadow 綁定了 ConfigService (程序:configService)
- LibraryActivity在啟動時通過 BookCollectionShadow 綁定了 LibraryService (程序:libraryService)
- BookCollectionShadow的 bindToService(context,runnable) 方法,在繫結LibraryService成功後,會通過Config的 runOnConnect 方法處理runnable
通過以上簡單的總結,我們可以知道,下一步一般會執行runnable中的方法:
myRootTree = new RootTree(myCollection, PluginCollection.Instance(Paths.systemInfo(LibraryActivity.this))); myCollection.addListener(LibraryActivity.this); init(getIntent()); 複製程式碼
那麼我們就一步步來分析,這些操作都做了些什麼:
1.RootTree的建立:

public RootTree(IBookCollection collection, PluginCollection pluginCollection) { super(collection, pluginCollection); // 收藏 new FavoritesTree(this); // 最近讀過 new RecentBooksTree(this); // 作者 new AuthorListTree(this); // 按書名 new TitleListTree(this); // 按系列 new SeriesListTree(this); // 標籤 new TagListTree(this); // 同步 if (new SyncOptions().Enabled.getValue()) { new SyncTree(this); } // 資料夾 new FileFirstLevelTree(this); } 複製程式碼
FBReader對於樹形結構通過ZLTree來定義。
看到這裡,我們應該可以判斷,書庫首頁開啟時顯示的資料就來自於此,那麼這些資料又是怎麼樣傳遞給Adapter的呢?看樣還得繼續看。
2.BookCollectionShadow新增監聽,這個我們後面再分析。
3.呼叫init(intent)方法:
//TreeActivity protected void init(Intent intent) { //首次開啟書庫時intent中沒有資料,key為null final FBTree.Key key = (FBTree.Key)intent.getSerializableExtra(TREE_KEY_KEY); final FBTree.Key selectedKey = (FBTree.Key)intent.getSerializableExtra(SELECTED_TREE_KEY_KEY); //根據指定的key獲取對應的tree myCurrentTree = getTreeByKey(key); myCurrentKey = myCurrentTree.getUniqueKey(); final TreeAdapter adapter = getTreeAdapter(); adapter.replaceAll(myCurrentTree.subtrees(), false); // 忽略部分程式碼... } //LibraryActivity @Override protected LibraryTree getTreeByKey(FBTree.Key key) { return key != null ? myRootTree.getLibraryTree(key) : myRootTree; } //TreeAdapterLibraryTreeAdapter父類 public void replaceAll(final Collection<FBTree> items, final boolean invalidateViews) { myActivity.runOnUiThread(new Runnable() { public void run() { synchronized (myItems) { myItems.clear(); myItems.addAll(items); } notifyDataSetChanged(); if (invalidateViews) { myActivity.getListView().invalidateViews(); } } }); } 複製程式碼
通過上面三個方法的,我們可以知道,在起初進入LibraryAcivity時,由於intent中沒有資料,故取出的key為null,在呼叫getTreeByKey時返回RootTree,並通過Adapter的replaceAll方法,修改資料並nofity。
二、揭開BookCollectionShadow的神祕面紗
通過上面的分析,我們已經知道BookCollectionShadow是用來繫結LibraryService的。通過它,可以跟LibraryService進行通訊。另外,上面還有提到註冊監聽者,這個監聽者又是怎麼一回事,LibraryService又是怎麼一回事呢?我們接下來就一步步去弄清這些問題。
先來看一下它的繼承關係圖:

由於BookCollectionShadow實現了IBookCollection介面,而該介面定義行為與LibraryService.aidl一致,並且BookCollectionShadow是用來繫結LibraryService的,細看其對故其對IBookCollection的實現均會通過LibraryInterface例項由程序間通訊,呼叫LibraryService的對應方法。
那麼我們就去看一下,這個LibraryService到底是個做啥的:
public class LibraryService extends Service 複製程式碼
1.onCreate:
@Override public void onCreate() { super.onCreate(); synchronized (ourDatabaseLock) { if (ourDatabase == null) { ourDatabase = new SQLiteBooksDatabase(LibraryService.this); } } myLibrary = new LibraryImplementation(ourDatabase); bindService( new Intent(this, DataService.class), DataConnection, DataService.BIND_AUTO_CREATE ); } 複製程式碼
可以看出這裡做了三件事:
- 建立了 SQLiteBooksDatabase ,該資料庫為Book資料庫
- 建立了 LibraryImplementation 例項myLibrary,並把資料庫傳遞進去
- 綁定了 DataService (程序:dataService)
2.onBind:
@Override public IBinder onBind(Intent intent) { return myLibrary; } 複製程式碼
從這裡可以看出,返回的IBinder物件為myLibrary,那麼也就意味著LibraryImplementation繼承自LibraryService.Stub,也就具體實現了LibraryInterface。
3.LibraryImplementation:
public final class LibraryImplementation extends LibraryInterface.Stub { private final BooksDatabase myDatabase; private final List<FileObserver> myFileObservers = new LinkedList<FileObserver>(); private BookCollection myCollection; LibraryImplementation(BooksDatabase db) { myDatabase = db; myCollection = new BookCollection( Paths.systemInfo(LibraryService.this), myDatabase, Paths.bookPath() ); reset(true); } //忽略部分程式碼... } 複製程式碼
這裡發現例項化了一個名字不帶Shadow的 BookCollection ,這個BookCollection又是幹啥的呢?
4.BookCollection:
public class BookCollection extends AbstractBookCollection<DbBook> 對比看一下 BookCollectionShadow public class BookCollectionShadow extends AbstractBookCollection<Book> implements ServiceConnection 複製程式碼
通過繼承關係,我們可以得知BookCollection跟BookCollectionShadow一樣是繼承自AbstractBookCollection,只不過泛型的AbstractBook型別不同。而且通過上面BookCollectionShadow的繼承圖來看,可以推出BookCollection同樣實現了IBookCollection介面。
5.由於LibraryService在onBind時返回的例項為LibraryImplementation,所以在BookCollectionShadow中:
public void onServiceConnected(ComponentName name, IBinder service) { synchronized (this) { // LibraryInterface的例項myInterface,實際為LibraryService中的LibraryImplementation myInterface = LibraryInterface.Stub.asInterface(service); } // 忽略部分程式碼... } 複製程式碼
通過檢視程式碼,可以看出BookCollectionShadow對介面IBookCollection的實現具體均是通過myInterface呼叫同樣的方法跨程序完成,我們隨便挑一個實現方法看一下:
public synchronized void rescan(String path) { if (myInterface != null) { try { myInterface.rescan(path); } catch (RemoteException e) { // ignore } } } 複製程式碼
6.myInterface的真身LibraryImplementation對LibraryInterface的具體實現,同5我們還是檢視rescan:
public void rescan(String path) { // myCollection為BookCollection的例項 myCollection.rescan(path); } 複製程式碼
這裡就很明顯了,LibraryImplementation對LibraryInterface的具體實現,最後的執行主體就是BookCollection。
到此,我們就可以簡單對BookCollectionShadow做一個總結:
- 繫結服務 LibraryService
- 實現 IBookCollection ,與服務LibraryService有同樣的功能
- 繫結LibraryService成功時,獲取到的LibraryInterface例項為 LibraryImplementation
- 呼叫IBookCollection定義的方法時,實際是由LibraryImplementation呼叫內部的 BookCollection 例項來最終完成
- BookCollection行為基本以通過 SQLiteBooksDatabase 操作Book.db為主
BookCollectionShadow是BookCollection的“影子”,真正的實現是BookCollection。
三、書櫃頁面LibraryActivity,是如何響應不同型別item點選的
上面我們已經知道,書櫃首頁的資料來自於RootTree,接下來我們就從“資料夾”這個item,繼續往下分析。
既然要檢視item的點選處理,那就檢視點選處理方法onListItemClick:
@Override protected void onListItemClick(ListView listView, View view, int position, long rowId) { final LibraryTree tree = (LibraryTree)getTreeAdapter().getItem(position); if (tree instanceof ExternalViewTree) { runOrInstallExternalView(true); } else { final Book book = tree.getBook(); if (book != null) { showBookInfo(book); } else { openTree(tree); } } } 複製程式碼
一個判斷book != null,非空開啟書籍詳情,為空就開啟子樹。從RootTree初始化中,我們知道“資料夾”對應的Tree為FileFirstLevelTree:

其getBook方法通過追溯可以發現是在LibraryTree中,而且return null。那麼就是開啟子tree,具體是通過方法openTree:
private void openTree(final FBTree tree, final FBTree treeToSelect, final boolean storeInHistory) { switch (tree.getOpeningStatus()) { case WAIT_FOR_OPEN: case ALWAYS_RELOAD_BEFORE_OPENING: final String messageKey = tree.getOpeningStatusMessage(); if (messageKey != null) { UIUtil.createExecutor(TreeActivity.this, messageKey).execute( new Runnable() { public void run() { tree.waitForOpening(); } }, new Runnable() { public void run() { openTreeInternal(tree, treeToSelect, storeInHistory); } } ); } else { // 對於FileFirstLevelTree來說會執行這塊 tree.waitForOpening(); openTreeInternal(tree, treeToSelect, storeInHistory); } break; default: openTreeInternal(tree, treeToSelect, storeInHistory); break; } } 複製程式碼
對於FileFirstLevelTree來說:
@Override public Status getOpeningStatus() { return Status.ALWAYS_RELOAD_BEFORE_OPENING; } public String getOpeningStatusMessage() { return null; } 複製程式碼
那麼上面的openTree會執行標記處的程式碼,也就是會執行waitForOpening方法:
@Override public void waitForOpening() { clear(); for (String dir : Paths.BookPathOption.getValue()) { addChild(dir, resource().getResource("fileTreeLibrary").getValue(), dir); } addChild("/", "fileTreeRoot"); final List<String> cards = Paths.allCardDirectories(); if (cards.size() == 1) { addChild(cards.get(0), "fileTreeCard"); } else { final ZLResource res = resource().getResource("fileTreeCard"); final String title = res.getResource("withIndex").getValue(); final String summary = res.getResource("summary").getValue(); int index = 0; for (String dir : cards) { addChild(dir, title.replaceAll("%s", String.valueOf(++index)), summary); } } } 複製程式碼
這段程式碼可以暫時不去關心具體都是做了哪些,我們只需要知道最終經過waitForOpening這個方法,他生成了當前tree的子tree,那麼子tree又是如何生成的呢?這個我們就要去著重看一下方法addChild:
private void addChild(String path, String title, String summary) { final ZLFile file = ZLFile.createFileByPath(path); if (file != null) { new FileTree(this, file, title, summary); } } 複製程式碼
這裡我們不免會有個疑惑,既然是add打頭的方法,為什麼跟到最後,發現,並沒有哪裡進行了相關add操作呢?其實,這個add是發生在了FileTree建立的時候:
FileTree(LibraryTree parent, ZLFile file, String name, String summary) { super(parent); //忽略部分程式碼 } 一直往上追溯,檢視super構造方法,最終在ZLTree: protected ZLTree(T parent, int position) { //忽略部分程式碼... Parent = parent; if (parent != null) { Level = parent.Level + 1; //此處執行add操作,將this當前tree,新增到parent中 parent.addSubtree((T)this, position); } else { Level = 0; } } 複製程式碼
通過執行waitForOpening,已經準備好了當前tree的子tree,那麼就進入到後續顯示子tree的處理了:
private void openTreeInternal(FBTree tree, FBTree treeToSelect, boolean storeInHistory) { switch (tree.getOpeningStatus()) { case READY_TO_OPEN: case ALWAYS_RELOAD_BEFORE_OPENING: if (storeInHistory && !myCurrentKey.equals(tree.getUniqueKey())) { myHistory.add(myCurrentKey); } onNewIntent(new Intent(this, getClass()) .setAction(OPEN_TREE_ACTION) .putExtra(TREE_KEY_KEY, tree.getUniqueKey()) .putExtra( SELECTED_TREE_KEY_KEY, treeToSelect != null ? treeToSelect.getUniqueKey() : null ) .putExtra(HISTORY_KEY, new ArrayList<FBTree.Key>(myHistory)) ); break; case CANNOT_OPEN: UIMessageUtil.showErrorMessage(TreeActivity.this, tree.getOpeningStatusMessage()); break; } } 複製程式碼
這裡同樣的會執行ALWAYS_RELOAD_BEFORE_OPENING這個case,也就是說會呼叫onNewIntent,並且在intent中傳遞了key為TREE_KEY_KEY,value為tree.getUniqueKey()的引數。接著我們來看一下onNewIntent:
@Override protected void onNewIntent(final Intent intent) { OrientationUtil.setOrientation(this, intent); if (OPEN_TREE_ACTION.equals(intent.getAction())) { runOnUiThread(new Runnable() { public void run() { init(intent); } }); } else { super.onNewIntent(intent); } } 複製程式碼
上面分析時,我們知道在呼叫方法init(intent)時,會讀取intent中的資料,這時key不為空,可以根據key獲得對應的tree,並且再次呼叫adapter的replaceAll替換資料並notify。
到這裡,我們來個簡單的總結。
當點選的item不是book時:
- 呼叫該tree的 waitForOpening 方法,準備子tree
- 在生成子tree時,將 父tree 作為 構造引數 傳入子tree,會將子tree新增到父tree的subtree中
- 呼叫 onNewIntent 方法,並將點選的 tree.getgetUniqueKey 傳入intent中
- Activity的onNewIntent方法出發,會執行 init(intent)
- init(intent)方法會讀取傳遞過來的 key ,根據此key獲取到對應的tree
- 呼叫adapter的 replcaAll 方法,將adapter的資料替換為獲取到的 tree.subTrees
當點選的item為book時,開啟圖書詳情。
四、FBReader在掃描本地檔案時,是如何識別出可閱讀電子書的
通過上面的分析,我們已經知道,當我們點選非圖書item時,都是經歷了些什麼。那麼當我們繼續往下點,進入SD卡目錄去掃描其中檔案時,可以發現當FBReader掃描到某個item是單子書時會顯示出來電子書的圖示,那麼FBReader是如何識別的呢?

由書櫃首頁“電子書”,點選進入子tree時,通過上面的分析,我們知道其準備子tree時呼叫了addChild方法,而其中生成的子tree為FileTree:
private void addChild(String path, String title, String summary) { final ZLFile file = ZLFile.createFileByPath(path); if (file != null) { new FileTree(this, file, title, summary); } } 複製程式碼
這裡呼叫了一個非常重要的方法ZLFile.createFileByPath(path):
public static ZLFile createFileByPath(String path) { if (path == null) { return null; } ZLFile cached = ourCachedFiles.get(path); if (cached != null) { //快取中有,則直接返回快取中的ZLFile return cached; } int len = path.length(); char first = len == 0 ? '*' : path.charAt(0); if (first != '/') { //路徑為資源路徑時 while (len > 1 && first == '.' && path.charAt(1) == '/') { path = path.substring(2); len -= 2; first = len == 0 ? '*' : path.charAt(0); } return ZLResourceFile.createResourceFile(path); } int index = path.lastIndexOf(':'); if (index > 1) { //路徑中包含:時 final ZLFile archive = createFileByPath(path.substring(0, index)); if (archive != null && archive.myArchiveType != 0) { return ZLArchiveEntryFile.createArchiveEntryFile( archive, path.substring(index + 1) ); } } return new ZLPhysicalFile(path); } 複製程式碼
我們知道SD卡中的資料夾或檔案路徑以“/”開頭,而且不包含“:”,那麼在生成子tree時,就會對應生成ZLPhysicalFile。這裡我們也就知道了ZLPhysicalFile是用來描述物理檔案的,而且FBReader中對檔案的描述,統一是基於ZLFile。接著,我們就進入ZLPhysicalFile,去了解一下這個類具體是做啥的:
private final File myFile;//路徑對應的檔案實體 ZLPhysicalFile(String path) { this(new File(path)); } public ZLPhysicalFile(File file) { myFile = file; init(); } protected void init() { final String name = getLongName(); final int index = name.lastIndexOf('.'); //獲取檔案拓展名 myExtension = (index > 0) ? name.substring(index + 1).toLowerCase().intern() : ""; myShortName = name.substring(name.lastIndexOf('/') + 1); int archiveType = ArchiveType.NONE; //根據特定副檔名,給檔案設定archiveType if (myExtension == "zip") { archiveType |= ArchiveType.ZIP; } else if (myExtension == "oebzip") { archiveType |= ArchiveType.ZIP; } else if (myExtension == "epub") { archiveType |= ArchiveType.ZIP; } else if (myExtension == "tar") { archiveType |= ArchiveType.TAR; } else if (lowerCaseName.endsWith(".tgz")) { //nothing to-do myNameWithoutExtension = myNameWithoutExtension.substr(0, myNameWithoutExtension.length() - 3) + "tar"; //myArchiveType = myArchiveType | ArchiveType.TAR | ArchiveType.GZIP; } myArchiveType = archiveType; } 複製程式碼
這裡ZLPhysicalFile在初始化時做了兩件事:
- 儲存路徑對應的真實File
- 對檔案進行識別,看其是否屬於支援的電子書格式型別,如果是的話,則將其archiveType設定為對應值
這樣我們也就能夠看出,如果遍歷到的檔案為特定格式的電子書檔案時,其archiveType是會有對應值的。那麼就可以猜測是不是在LibraryActivity的Adapter中應該是有對該值的判斷?進去看一下:
public View getView(int position, View convertView, final ViewGroup parent) { final LibraryTree tree = (LibraryTree)getItem(position); //忽略部分程式碼... if (myCoverManager == null) { view.measure(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); final int coverHeight = view.getMeasuredHeight(); final TreeActivity activity = getActivity(); myCoverManager = new CoverManager(activity, activity.ImageSynchronizer, coverHeight * 15 / 32, coverHeight); view.requestLayout(); } final ImageView coverView = ViewUtil.findImageView(view, R.id.library_tree_item_icon); if (!myCoverManager.trySetCoverImage(coverView, tree)) { coverView.setImageResource(getCoverResourceId(tree)); } return view; } private int getCoverResourceId(LibraryTree tree) { if (tree.getBook() != null) { return R.drawable.ic_list_library_book; } else if (tree instanceof ExternalViewTree) { return R.drawable.plugin_bookshelf; } else if (tree instanceof FavoritesTree) { return R.drawable.ic_list_library_favorites; } else if (tree instanceof FileTree) { final ZLFile file = ((FileTree)tree).getFile(); if (file.isArchive()) { return R.drawable.ic_list_library_zip; } else if (file.isDirectory() && file.isReadable()) { return R.drawable.ic_list_library_folder; } else { return R.drawable.ic_list_library_permission_denied; } }... } 複製程式碼
發現在Adapter的getView方法中,並沒有對archiveType的判斷,但是卻發現:
- 這裡會建立封面管理器CoverManager
- 並且當呼叫myCoverManager.trySetCoverImage返回false的時候,才會給imageview設定圖片資源
- getCoverResourceId方法是根據當前tree的型別來給它設定對應圖片資源的
那麼這個trySetCoverImage方法又做了什麼呢?
public boolean trySetCoverImage(ImageView coverView, FBTree tree) { final CoverHolder holder = getHolder(coverView, tree); Bitmap coverBitmap; try { //取快取中的封面bitmap coverBitmap = Cache.getBitmap(holder.Key); } catch (CoverCache.NullObjectException e) { return false; } if (coverBitmap == null) { final ZLImage cover = tree.getCover(); if (cover instanceof ZLImageProxy) { final ZLImageProxy img = (ZLImageProxy)cover; if (img.isSynchronized()) { setCoverForView(holder, img); } else { img.startSynchronization( myImageSynchronizer, holder.new CoverSyncRunnable(img) ); } } else if (cover != null) { coverBitmap = getBitmap(cover); } } if (coverBitmap != null) { //如果封面coverBitmap存在,就設定給Imageview holder.CoverView.setImageBitmap(coverBitmap); return true; } return false; } 複製程式碼
其中對是否有封面的一個核心方法是tree.getCover():
public final ZLImage getCover() { if (!myCoverRequested) { //生成封面 myCover = createCover(); //忽略部分程式碼... } return myCover; } @Override public ZLImage createCover() { return CoverUtil.getCover(getBook(), PluginCollection); } 複製程式碼
最終是呼叫了CoverUtil的getConver的方法:
public static ZLImage getCover(AbstractBook book, IFormatPluginCollection collection) { if (book == null) { return null; } synchronized (book) { return getCover(ZLFile.createFileByPath(book.getPath()), collection); } } 複製程式碼
所以當book不為空時,才會執行獲取封面的方法,那麼就需要看一下getBook獲取的book物件是否為空:
@Override public Book getBook() { if (myBook == null) { //根據檔案路徑去嘗試獲取book myBook = Collection.getBookByFile(myFile.getPath()); if (myBook == null) { myBook = NULL_BOOK; } } return myBook instanceof Book ? (Book)myBook : null; } 複製程式碼
這裡又出現了Collection,我們知道其最終執行者是BookCollection,那我們就直接進入BookCollection的getBookByFile(path)方法看一下:
public DbBook getBookByFile(String path) { //熟悉的方法,根據路徑生成不同的檔案型別,我們知道SD卡中的檔案最終會生成ZLPhysicalFile return getBookByFile(ZLFile.createFileByPath(path)); } private DbBook getBookByFile(ZLFile bookFile) { if (bookFile == null) { return null; } return getBookByFile(bookFile, PluginCollection.getPlugin(bookFile)); } private DbBook getBookByFile(ZLFile bookFile, final FormatPlugin plugin) { if (plugin == null || !isFormatActive(plugin)) { return null; } //忽略部分程式碼... } 複製程式碼
至此,我們可以知道,如果plugin為空的話,那麼就會直接返回null,所以plugin不為空是至關重要的,那麼我們就去看一下這個plugin是怎麼獲取的:
public FormatPlugin getPlugin(ZLFile file) { final FileType fileType = FileTypeCollection.Instance.typeForFile(file); final FormatPlugin plugin = getPlugin(fileType); if (plugin instanceof ExternalFormatPlugin) { return file == file.getPhysicalFile() ? plugin : null; } return plugin; } 複製程式碼
通過FileTypeCollection來獲取當前file的檔案型別,那麼FileTypeCollection中定義了哪些可以識別的電子書檔案型別呢?可以從其構造方法中看出:
private FileTypeCollection() { //fb2 addType(new FileTypeFB2()); //epub oebzip opf addType(new FileTypeEpub()); //mobi azw azw3 addType(new FileTypeMobipocket()); //html htm addType(new FileTypeHtml()); //txt addType(new SimpleFileType("txt", "txt", MimeType.TYPES_TXT)); //rtf addType(new SimpleFileType("RTF", "rtf", MimeType.TYPES_RTF)); //pdf addType(new SimpleFileType("PDF", "pdf", MimeType.TYPES_PDF)); //djvu djv addType(new FileTypeDjVu()); //cbz cbr addType(new FileTypeCBZ()); //zip addType(new SimpleFileType("ZIP archive", "zip", Collections.singletonList(MimeType.APP_ZIP))); //msdoc addType(new SimpleFileType("msdoc", "doc", MimeType.TYPES_DOC)); } 複製程式碼
FileTypeCollection是怎麼根據ZLFile獲取其檔案型別的呢?拿epub格式為例,其實現在FileTypeEpub類中:
@Override public boolean acceptsFile(ZLFile file) { //獲取副檔名 final String extension = file.getExtension(); return //比對副檔名 "epub".equalsIgnoreCase(extension) || "oebzip".equalsIgnoreCase(extension) || ("opf".equalsIgnoreCase(extension) && file != file.getPhysicalFile()); } 複製程式碼
很明顯,是根據副檔名來獲取檔案型別,如果能獲取到檔案型別FileType物件,則說明當前檔案為可支援的電子書檔案,進而呼叫後面的getPlugin(fileType),就能夠取到對應的檔案解析外掛。這個時候我們再回頭去看BookCollection的getBookByFile方法:
private DbBook getBookByFile(ZLFile bookFile, final FormatPlugin plugin) { //忽略部分程式碼... book = new DbBook(bookFile, plugin); //儲存識別出來的book到資料庫 saveBook(book); return book; } 複製程式碼
這裡會生成DbBook,而且DbBook在生成的時候會通過外掛plugin去讀取Book中的內容:
DbBook(ZLFile file, FormatPlugin plugin) throws BookReadingException { this(-1, plugin.realBookFile(file), null, null, null); BookUtil.readMetainfo(this, plugin); mySaveState = SaveState.NotSaved; } static void readMetainfo(AbstractBook book, FormatPlugin plugin) throws BookReadingException { book.myEncoding = null; book.myLanguage = null; book.setTitle(null); book.myAuthors = null; book.myTags = null; book.mySeriesInfo = null; book.myUids = null; book.mySaveState = AbstractBook.SaveState.NotSaved; //讀取book內容 plugin.readMetainfo(book); if (book.myUids == null || book.myUids.isEmpty()) { plugin.readUids(book); } //忽略部分程式碼... } 複製程式碼
plugin讀取Book內容的方法,最終會呼叫native方法:
private native int readMetainfoNative(AbstractBook book); 複製程式碼
針對這部分內容,我們來做一個總結:
- 在生成子tree時, ZLFile.createFileByPath 會首先被呼叫,並且會根據對 檔案路徑 的判別生成對應的檔案型別
- 如果路徑為手機儲存的路徑時,會生成 ZLPhysicalFile
- ZLPhysicalFile在被建立時,為根據檔案路徑例項化 真實的File ,並且會根據檔案的副檔名,設定當前檔案對應的 archiveType
- LibraryActivity的Adapter在getView方法中,會根據 CoverManager 對當前tree的封面獲取情況,來設定具體的圖示
- CoverManager在獲取當前tree的封面時,會觸發Tree的 getBook 方法,來嘗試獲取當前路徑對應的Book
- getBook方法會觸發 getPlugin 方法,而getPlugin則是會判斷 副檔名 ,當副檔名為支援的檔案型別時,才會返回對應的檔案解析外掛getPlugin,可支援的檔案型別定義再 FileTypeCollection
- 當檔案型別為支援電子書格式時,最終會呼叫 new DbBook(bookFile, plugin) 生成book
- DbBook在初始化時會呼叫plugin的 readMetainfo 並最終呼叫 native 方法 readMetainfoNative 去讀取book內容,如編碼、語言、標題、作者、標籤等等
五、開啟電子書,終於進入了閱讀頁面
還記得LibraryActivity的方法:
@Override protected void onListItemClick(ListView listView, View view, int position, long rowId) { //忽略部分程式碼... final LibraryTree tree = (LibraryTree)getTreeAdapter().getItem(position); final Book book = tree.getBook(); if (book != null) { //顯示圖書資訊 showBookInfo(book); } } private void showBookInfo(Book book) { final Intent intent = new Intent(getApplicationContext(), BookInfoActivity.class); FBReaderIntents.putBookExtra(intent, book); OrientationUtil.startActivity(this, intent); } 複製程式碼
進入圖書資訊Activity

廢話不多說,直接看閱讀按鈕:
setupButton(R.id.book_info_button_open, "openBook", new View.OnClickListener() { public void onClick(View view) { if (myDontReloadBook) { finish(); } else { //閱讀電子書,傳遞引數為book FBReader.openBookActivity(BookInfoActivity.this, myBook, null); } } }); 複製程式碼
終於,進入了閱讀頁面!

當然,由於本人接觸此專案時間有限,而且書寫技術文章的經驗實在欠缺,過程中難免會有存在錯誤或描述不清或語言累贅等等一些問題,還望大家能夠諒解,同時也希望大家繼續給予指正。最後,感謝大家對我的支援,讓我有了強大的動力堅持下去。