小說閱讀器開發筆記(一)文件的讀寫


小說閱讀器開發筆記(一)文件的讀寫

標簽(空格分隔): 安卓開發 小說閱讀器 文件讀寫


  借着空閑的時間,想要開發一款小說閱讀器,一是滿足自己的需求,二是鍛煉下自己的能力。思索了良久,並沒有太好的思路,於是決定從最簡單的功能入手,一步步完善,最終達成自己的目標。如今各種閱讀器可謂種類繁多,功能各異,想要一一實現絕非易事。但窺其本質,不過是文本數據的讀取、分析和顯示。因此,我將從數據的讀寫開始,一步步摸索一個簡易小說閱讀器的開發與實現。
  讀寫文件,需要系統從外存中讀取文件數據,送到內存處理,這就繞不開了JAVA的輸入輸出流,即JAVA中的I/O操作。事實上,JAVA的絕大部分I/O機制都是基於數據流進行輸入輸出的,任何對數據源的操作都離不開數據流的支持。那么數據流究竟是什么樣的概念呢,我也是有些模糊。

數據流的基本概念

  JAVA中將輸入輸出抽象為流,輸入輸出所傳遞的對象是數據,而其移動的狀態也就稱之為數據流。數據流是一串連續不斷的數據集合。就像水管里的水流,在水管的一端一點一點地供水,而在水管的另一端看到的是一股連續不斷的水流。編寫程序也可以使之一段一段的向數據流中輸入數據段,這些數據段就會按照特定的規則排好順序,組成一個有序的數據長序列。在輸出端看來,這些數據是完整的連續的。
  數據流的種類很多,應用的領域也很廣泛,例如標准輸入輸出、文件的操作、網絡上的數據流、字符串流、對象流、zip文件流等等。流是一個很形象的概念,當程序需要讀取數據的時候,就會開啟一個通向數據源的流,這個數據源可以是文件,內存,或是網絡連接。類似的,當程序需要寫入數據的時候,就會開啟一個通向目的地的流。

按照流向可以分為兩種:

  • 輸入流:程序從輸入流讀取數據源。數據源包括外界(鍵盤、文件、網絡…),即是將數據源讀入到程序的通信通道。
  • 輸出流:程序向輸出流寫入數據。將程序中的數據輸出到外界(顯示器、打印機、文件、網絡…)的通信通道。

按照操作類型分為兩種:

  • 字節流 :字節流可以操作任何數據,因為在計算機中任何數據都是以字節的形式存儲的。
  • 字符流 : 字符流只能操作純字符數據,比較方便。

  但是數據流也同樣存在着缺點,雖然其能夠讀取任意長度的數據,卻只能依照順序先讀取前面的數據,再讀取后面的數據。這對於需要讀取完整數據的應用顯然沒有問題,但對於小說閱讀器來說,內容的翻閱往往不是按照先后的順序,這會造成內存資源的極大浪費,而且在讀取大文件時,還會影響軟件的使用性能。顯然,對於這樣的情況,JAVA也給出了優異的解決方案。JAVA的I/O層次體系結構中還存在一些非流式部分,主要涵蓋了一些輔助流式部分的類。RandomAccessFile(隨機文件操作)就是其中一類,可以用來解決該電子書閱讀器文件讀取的問題。

RandomAccessFile的簡單認識

RandomAccessFile
  查閱相關資料,從官方的文檔中,可以了解到:

  此類的實例支持對隨機訪問文件的讀取和寫入。隨機訪問文件的行為類似存儲在文件系統中的一個大型 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,項目地址為:小說閱讀器文件讀寫。項目效果圖如下所示:
RandomAccessFile


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM