小说阅读器开发笔记(一)文件的读写

标签(空格分隔): 安卓开发 小说阅读器 文件读写


  借着空闲的时间,想要开发一款小说阅读器,一是满足自己的需求,二是锻炼下自己的能力。思索了良久,并没有太好的思路,于是决定从最简单的功能入手,一步步完善,最终达成自己的目标。如今各种阅读器可谓种类繁多,功能各异,想要一一实现绝非易事。但窥其本质,不过是文本数据的读取、分析和显示。因此,我将从数据的读写开始,一步步摸索一个简易小说阅读器的开发与实现。
  读写文件,需要系统从外存中读取文件数据,送到内存处理,这就绕不开了JAVA的输入输出流,即JAVA中的I/O操作。事实上,JAVA的绝大部分I/O机制都是基于数据流进行输入输出的,任何对数据源的操作都离不开数据流的支持。那么数据流究竟是什么样的概念呢,我也是有些模糊。

数据流的基本概念

  JAVA中将输入输出抽象为流,输入输出所传递的对象是数据,而其移动的状态也就称之为数据流。数据流是一串连续不断的数据集合。就像水管里的水流,在水管的一端一点一点地供水,而在水管的另一端看到的是一股连续不断的水流。编写程序也可以使之一段一段的向数据流中输入数据段,这些数据段就会按照特定的规则排好顺序,组成一个有序的数据长序列。在输出端看来,这些数据是完整的连续的。
  数据流的种类很多,应用的领域也很广泛,例如标准输入输出、文件的操作、网络上的数据流、字符串流、对象流、zip文件流等等。流是一个很形象的概念,当程序需要读取数据的时候,就会开启一个通向数据源的流,这个数据源可以是文件,内存,或是网络连接。类似的,当程序需要写入数据的时候,就会开启一个通向目的地的流。

按照流向可以分为两种:

  • 输入流:程序从输入流读取数据源。数据源包括外界(键盘、文件、网络…),即是将数据源读入到程序的通信通道。
  • 输出流:程序向输出流写入数据。将程序中的数据输出到外界(显示器、打印机、文件、网络…)的通信通道。

按照操作类型分为两种:

  • 字节流 :字节流可以操作任何数据,因为在计算机中任何数据都是以字节的形式存储的。
  • 字符流 : 字符流只能操作纯字符数据,比较方便。

  但是数据流也同样存在着缺点,虽然其能够读取任意长度的数据,却只能依照顺序先读取前面的数据,再读取后面的数据。这对于需要读取完整数据的应用显然没有问题,但对于小说阅读器来说,内容的翻阅往往不是按照先后的顺序,这会造成内存资源的极大浪费,而且在读取大文件时,还会影响软件的使用性能。显然,对于这样的情况,JAVA也给出了优异的解决方案。JAVA的I/O层次体系结构中还存在一些非流式部分,主要涵盖了一些辅助流式部分的类。RandomAccessFile(随机文件操作)就是其中一类,可以用来解决该电子书阅读器文件读取的问题。

RandomAccessFile的简单认识

android tbs 小说阅读器 android小说阅读器开发_阅读器


  查阅相关资料,从官方的文档中,可以了解到:

  此类的实例支持对随机访问文件的读取和写入。随机访问文件的行为类似存储在文件系统中的一个大型 byte 数组。存在指向该隐含数组的光标或索引,称为文件指针;输入操作从文件指针开始读取字节,并随着对字节的读取而前移此文件指针。如果随机访问文件以读取/写入模式创建,则输出操作也可用;输出操作从文件指针开始写入字节,并随着对字节的写入而前移此文件指针。写入隐含数组的当前末尾之后的输出操作导致该数组扩展。该文件指针可以通过 getFilePointer 方法读取,并通过 seek 方法设置。
  通常,如果此类中的所有读取例程在读取所需数量的字节之前已到达文件末尾,则抛出 EOFException(是一种 IOException)。如果由于某些原因无法读取任何字节,而不是在读取所需数量的字节之前已到达文件末尾,则抛出 IOException,而不是 EOFException。需要特别指出的是,如果流已被关闭,则可能抛出 IOException。

  从JDK文档的截图中,我们可以清楚的看到,RandomAccessFile直接继承于Object类,而不是字节流、字符流等众多数据流家族中的任何一个类。RandomAccessFile一般直译为随机访问文件,因为RandomAccessFile不属于IO流,支持对文件读取和写入的随机访问,又被译为随机流。事实上,翻译成随机也并不准确,这种操作更像一种任意。与IO流不同,随机流可以自由的对文件任意位置进行访问,极大的方便了用来读写有数据记录的文件。
  这时,一个想法在我心中慢慢成型,利用随机流可以访问文件任意位置的特性,可以将文件的章节信息全部解析出来,以目录的方式读取任意章节,也可以通过跳转显示任意片断的信息,基本上满足了小说阅读器显示的需要。接下来我将通过编程实践,来验证我心中的想法是否可行,并以此为契机,学会随机访问文件的应用。

随机访问文件的应用

  新建一个类,命名为ListModel.java,其作为章节信息的数据模型。类中定义了6个基本数据类型,chapterName是章节的名称,chapterNum是章节的编号,chapterSize是章节的大小,isRead是布尔型,代表该章节是否被阅读过,已读为真,chapterIndex则是该章节在文件中的位置。代码如下:

package com.example.wxz.myapplication.modle;

import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * Created by wxz on 2018/6/3.
 *
 */

public class ListModel {
    private int chapterSize;
    private String chapterName;
    private int chapterNum;
    private boolean isRead;
    private long chapterIndex;

    public ListModel(int listId, String chapterName, int chapterNum, boolean isRead, long chapterIndex) {
        this.listId = listId;
        this.chapterName = chapterName;
        this.chapterNum = chapterNum;
        this.isRead = isRead;
        this.chapterIndex = chapterIndex;
    }

    public int getListId() {
        return listId;
    }

    public void setListId(int listId) {
        this.listId = listId;
    }

    public String getChapterName() {
        return chapterName;
    }

    public void setChapterName(String chapterName) {
        this.chapterName = chapterName;
    }

    public int getChapterNum() {
        return chapterNum;
    }

    public void setChapterNum(int chapterNum) {
        this.chapterNum = chapterNum;
    }

    public boolean isRead() {
        return isRead;
    }

    public void setRead(boolean read) {
        isRead = read;
    }

    public long getChapterIndex() {
        return chapterIndex;
    }

    public void setChapterIndex(long chapterIndex) {
        this.chapterIndex = chapterIndex;
    }

    public void write(RandomAccessFile raf) throws IOException {
        raf.writeInt(listId);
        raf.writeUTF(chapterName);
        raf.writeInt(chapterNum);
        raf.writeBoolean(isRead);
        raf.writeLong(chapterIndex);
    }

    public void read(RandomAccessFile raf) throws IOException
    {
        this.listId =raf.readInt();
        this.chapterName = raf.readUTF();
        this.chapterNum = raf.readInt();
        this.isRead = raf.readBoolean();
        this.chapterIndex = raf.readLong();
    }
}

  新建一个类,命名为StrModel.java,其作为电子书信息的数据模型。类中定义了6个基本数据类型,listSize是章节列表的大小,暂时没有用到。listNum是统计章节的总数量,readIndex是当前文件指针的偏移量,用来记录用户的阅读记录。listContent是保存章节列表的详细信息,startIndex是记录文件正文数据的开始偏移量。strContent是小说正文的内容,暂时用字符串类型来使用。代码如下:

package com.example.wxz.myapplication.modle;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by wxz on 2018/6/2.
 *
 */

public class StrModel {
    private long listSize;
    private int listNum;
    private long readIndex;
    private List<ListModel> listContent;
    private long startIndex;
    private String strContent;

    public long getListSize() {
        return listSize;
    }

    public void setListSize(long listSize) {
        this.listSize = listSize;
    }

    public int getListNum() {
        return listNum;
    }

    public void setListNum(int listNum) {
        this.listNum = listNum;
    }

    public long getReadIndex() {
        return readIndex;
    }

    public void setReadIndex(long readIndex) {
        this.readIndex = readIndex;
    }

    public List<ListModel> getListContent() {
        return listContent;
    }

    public void setListContent(List<ListModel> listContent) {
        this.listContent = listContent;
    }

    public long getStartIndex() {
        return startIndex;
    }

    public void setStartIndex(long startIndex) {
        this.startIndex = startIndex;
    }

    public String getStrContent() {
        return strContent;
    }

    public void setStrContent(String strContent) {
        this.strContent = strContent;
    }

    public void write(RandomAccessFile raf) throws IOException {
        raf.writeLong(listSize);
        raf.writeInt(listNum);
        raf.writeLong(readIndex);
        for(int i=0;i<listContent.size();i++) {
            ListModel listModel =listContent.get(i);
            listModel.write(raf);
        }
        this.startIndex = raf.getFilePointer()+10;
        raf.writeLong(startIndex);
        raf.writeUTF(strContent);
    }
    public void read(RandomAccessFile raf) throws IOException
    {
        this.listSize = raf.readLong();
        this.listNum = raf.readInt();
        this.readIndex = raf.readLong();
        List<ListModel> listModels =new ArrayList<>();
        for(int i=0;i<listNum;i++) {
            ListModel listModel =new ListModel("",0,false,0,0);
            listModel.read(raf);
            listModels.add(listModel);
        }
        this.listContent = listModels;
        this.startIndex = raf.readLong();
        this.strContent = raf.readUTF();
    }
}

  在MainActivity类中加入如下代码,其作用是向文件写入对应数据,然后读取并解析出来,利用随机访问文件的特性,通过文件指针把相应的章节数据读取,并显示在文本框内。在本程序中,我试图自定义了一个文件类型,将其后缀命名为xkr,使用方法如同打开其他文件类型一样,RandomAccessFile(this.getFilesDir()+ "test.xkr","rw");,根据自定义数据格式,能正常读写。

package com.example.wxz.myapplication;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
import com.example.wxz.myapplication.modle.ListModel;
import com.example.wxz.myapplication.modle.StrModel;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity  {

    TextView readView;
    String chapter1 =  "    随机流(RandomAccessFile)不属于IO流,支持对文件的读取和写入随机访问。\n" ;
    String chapter2 =  "    首先把随机访问的文件对象看作存储在文件系统中的一个大型 byte 数组,然后通过指向该 byte 数组的光标或索引" +
            "(即:文件指针 FilePointer)在该数组任意位置读取或写入任意数据。\n" ;
    String chapter3 =  "    1、对象声明:RandomAccessFile raf = newRandomAccessFile(File file, String mode);\n" +
            "       其中参数 mode 的值可选 \"r\":可读,\"w\" :可写,\"rw\":可读性;\n" +
            "    2、获取当前文件指针位置:int RandowAccessFile.getFilePointer();\n" +
            "    3、改变文件指针位置(相对位置、绝对位置):\n" +
            "        1> 绝对位置:RandowAccessFile.seek(int index);\n" +
            "        2> 相对位置:RandowAccessFile.skipByte(int step); 相对当前位置\n" +
            "    4、给写入文件预留空间:RandowAccessFile.setLength(long len);";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        readView = (TextView) findViewById(R.id.read_view);
        setSupportActionBar(toolbar);

        try {
            readStr();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public void readStr() throws IOException {
        RandomAccessFile file = new RandomAccessFile(this.getFilesDir()+ "test.xkr","rw");
        StrModel strModel = new StrModel();
        strModel = eBookInit(strModel);
        strModel.write(file);

        file.seek(0);
        StrModel strModel1 = new StrModel();
        strModel1.read(file);
        String ebook = "";
        for (int i=0;i<strModel1.getListContent().size();i++){
            ebook = ebook + "第" + strModel1.getListContent().get(i).getChapterNum() + "节"
                    + ": " + strModel1.getListContent().get(i).getChapterName() + "\n"
                    + getChapterStr(file,strModel1,i) +"\n";
        }
        readView.setText(ebook);
    }

    public StrModel eBookInit(StrModel strModel){
        long strIndex = 0;
        strModel.setReadIndex(0);
        ListModel listModel1 = new ListModel("作用",1,false,strIndex,chapter1.getBytes().length);
        strIndex += chapter1.getBytes().length;
        ListModel listModel2 = new ListModel("随机访问文件原理",2,false,strIndex,chapter2.getBytes().length);
        strIndex += chapter2.getBytes().length;
        ListModel listModel3 = new ListModel("相关方法说明",3,false,strIndex,chapter3.getBytes().length);
        List<ListModel> listModels = new ArrayList<>();
        listModels.add(listModel1);
        listModels.add(listModel2);
        listModels.add(listModel3);
        strModel.setListNum(listModels.size());
        strModel.setListContent(listModels);
        strModel.setListSize(listModels.size());
        strModel.setStrContent(chapter1+chapter2+chapter3);
        return strModel;
    }

    public String getChapterStr (RandomAccessFile file,StrModel strModel,int i) throws IOException {
        file.seek(strModel.getStartIndex()+strModel.getListContent().get(i).getChapterIndex());
        byte[] buff = new byte[1024];
        file.read(buff,0,(int)strModel.getListContent().get(i).getChapterSize());
        return new  String(buff);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
}

  项目的布局文件如下,十分的简洁,只有一个文本控件,用来显示文件读取出来的文本数据,没有做任何花俏的处理。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="com.example.wxz.myapplication.MainActivity"
    tools:showIn="@layout/activity_main">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="Hello World!"
        android:id="@+id/read_view"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

  该部分代码经过了真机测试,期间虽然出了些小差错,经过调整,结果也颇为满意。最初时,由于没有加入文件路径,无法打开文件,查看日志,open failed: EROFS (Read-only file system),出现了系统只读文件的错误,后来加入getFilesDir()得以解决。再来,因为对章节大小和文件偏移量计算的错误,导致读取信息乱码和不完整,后来改进计算方法,最终修正了这些比较低级的错误。该项目的完整代码以上传github,项目地址为:小说阅读器文件读写。项目效果图如下所示:

android tbs 小说阅读器 android小说阅读器开发_RandomAccessFile_02