开源电子书项目FBReader初探(四)

栏目: IOS · Android · 发布时间: 7年前

内容简介:首先,我们回顾一下上一篇的一些知识点,针对一个可识别的有效电子书文件来说:查看清单文件,我们可以看到FBReader的启动模式:那么图书信息界面,点击“阅读”再次打开FBReader时,其onNewIntent将被触发:

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

  • 手机存储中的电子书文件会通过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资源文件。

开源电子书项目FBReader初探(四)

中文系统资源文件:

开源电子书项目FBReader初探(四)

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

开源电子书项目FBReader初探(四)

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:

开源电子书项目FBReader初探(四)
开源电子书项目FBReader初探(四)

紧接着我们来看这个方法,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内容:

开源电子书项目FBReader初探(四)

解析后的BookMode内容:

开源电子书项目FBReader初探(四)

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

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;
    }
}
复制代码

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

开源电子书项目FBReader初探(四)

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

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


以上所述就是小编给大家介绍的《开源电子书项目FBReader初探(四)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Algorithms

Algorithms

Robert Sedgewick、Kevin Wayne / Addison-Wesley Professional / 2011-3-19 / USD 89.99

Essential Information about Algorithms and Data Structures A Classic Reference The latest version of Sedgewick,s best-selling series, reflecting an indispensable body of knowledge developed over the ......一起来看看 《Algorithms》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具