開源電子書項目FBReader初探(四)

FBReader如何打開一個指定的電子書,以及一些閱讀操做的實現

首先,咱們回顧一下上一篇的一些知識點,針對一個可識別的有效電子書文件來講:android

  • 手機存儲中的電子書文件會經過ZLFile.createFileByPath被建立成一個ZLPhysicalFile類型的文件對象
  • BookCollectionShadow的大部分方法實際上是由BookCollection來實現的
  • BookCollection中有一個很是重要的方法getBookByFile(ZLFile),其中會校驗文件的擴展名,若是是支持的電子書格式時,那麼就會獲取到相應的解析插件
  • 隨後在BookCollection中建立一個DbBook對象,DbBook在初始化時會讀取book的基本信息,這裏主要是經過傳入的plugin,調用plugin的native方法讀取到的
  • 圖書信息頁打開FBReader進行閱讀時,經過FBReaderIntents.putBookExtra(intent, book),傳遞的一個有效參數的Book對象

1、FBReader是如何獲取book,又是如何獲取並顯示圖書內容的

FBReader如何獲取Book,以及如何更簡便的打開一本電子書

查看清單文件,咱們能夠看到FBReader的啓動模式:緩存

android:launchMode="singleTask"
複製代碼

那麼圖書信息界面,點擊「閱讀」再次打開FBReader時,其onNewIntent將被觸發:app

@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是什麼呢?這裏要回看一下打開閱讀頁面的時候調用的代碼:ide

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進行了賦值,並無其餘多餘的操做。post

這樣的話,咱們就要繼續往下看,在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:ui

private BookCollectionShadow getCollection() {
    return (BookCollectionShadow)myFBReaderApp.Collection;
}
複製代碼

老朋友BookCollectionShadow,以前的分析來看,下面就會執行runnable了,也就是openBook:this

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:spa

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方法時,會調用LibraryService的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開發至關火熱...」。看到這句話,眼中盡是淚水啊!青春!怎麼這麼快就過去了!......

相關文章
相關標籤/搜索