设计应用的二进制存储格式有什么要点?
分享一下我设计实现的过程
总结一下就是
- 最开始有 文件头 包括 魔数 版本号 md5校验值
- 文件体分段 每段有 类型 偏移值(数据部分的字节数) 数据 三部份组成
这篇文章就是分享一下我为自己的矢量绘图程序实现二进制保存的过程
总的来说我需要使用这个文件来使程序中的一些类回复到保存时的状态
目前有4个类
public class Camera {
private float mwidth;
private float mheight;
private float moffsetX = 0;
private float moffsetY = 0;
.
.
.
}
public class PaintManager {
private static PaintStyle mPaintStyle;
.
.
.
}
public class PaintStyle{
private int color = Color.BLACK;
private int alpha = 255;
private float blur = 0;
private float size = 10;
.
.
.
}
public class PathCollection {
LinkedList<VectorPath> theWrold = new LinkedList<>();
private int size = 0;
.
.
.
}
所谓的使类恢复到某个状态 其实就是保存并设置类中的属性罢了,毕竟类是属性和方法的集合.
很明显只有这个类自身才知道如何保存和恢复他自身. 所以每个需要恢复的类都最起码有 init(初始化) preserve(保存) 这两个方法 为了获得类型 还必须有一个 byte getByte() 也就是说他们都必须 实现一个reincarnation(重生)接口
public interface reincarnation {
void init(InputStream in);
int preserve(OutputStream out);
byte getByte();
}
现在假设我们所有的类都已经实现了这个接口 那么该如何使用哪?
要保存文件和打开文件
public class SaveTool {
public static void save(File f) {
}
public static void recover(File data){
}
}
在保存文件时 我们应该先计算文件体 这样才能获得到其的md5
private static void save(File f) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Camera.getInstance().preserve(bos);
PaintManager.getInstance().preserve(bos);
PathCollection.getInstance().preserve(bos);
byte[] data = bos.toByteArray();
byte[] md5 = calMD5(data);
ByteArrayOutputStream out = new ByteArrayOutputStream();
String magic = "vdg";//魔数
int majorVersion = 0;//主要版本号
int minorVersion = 1;//次要版本号
//文件头 魔数(3) 主版本号(1) 次版本号(1) md5(16)
byte[] magbs = ConvertData.getANSCII(magic);
byte mav = ConvertData.getByte(majorVersion);
byte minv = ConvertData.getByte(minorVersion);
//head
out.write(magbs);
out.write(mav);
out.write(minv);
out.write(md5);
out.write(data);
writeBytesToFile(out.toByteArray(), f);
}
我们使用 ByteArrayOutputStream(Java自带)来写进byte值 最后通过 writeBytesToFile(byte[]data,File f) 来将byte数组写入文件中
public static void recover(File vdata){
byte[] data = readBytesToFile(vdata);//获取到全部的byte
ByteArrayInputStream in = new ByteArrayInputStream(data);
checkHead(data);
in.read(new byte[21]);//流出头
Camera.getInstance().init(in);
PaintManager.getInstance().init(in);
PathCollection.getInstance().init(in);
}
恢复时同样简单 首先检查头部信息(魔数 版本 MD5)
这样我们的二进制文件就保存成功了
笑.
-------------------------------------------------------------
好了现在让我们回到Camera 中来 Camera有4个float 类型的属性 所以他的preserve 应该是
@Override
public int preserve(OutputStream out){
try {
ByteArrayOutputStream data = new ByteArrayOutputStream();
byte[] v = ConvertData.tobytes(new float[]{mheight, mwidth, moffsetX, moffsetY});
data.write(getByte());
data.write(ConvertData.tobytes(v.length));
data.write(v);
out.write(data.toByteArray());
data.close();
return data.size();
} catch (IOException e) {
throw new RuntimeException("保存出错");
}
}
相应的init 应该是
@Override
public void init(InputStream in) {
try {
byte t = (byte) in.read();
if (t != getByte()) {
throw new RuntimeException("检测到的类型" + t + "与此处(Camera)冲突 ");
}
float[] v = ConvertData.tofloats(in,getInt(in));
float height = v[0];
float width = v[1];
moffsetX = v[2];
moffsetY = v[3];
} catch (IOException e) {
throw new RuntimeException("IO 错误");
}
}
getByte()则可以很简当的在每个类中定义一个常量 返回即可
public static final byte type = 3;
@Override
public byte getByte() {
return type;
}
属性只有基本数据类型的 可以这样写 如果属性有类呢? 下面我们看PaintManager 他就持有一个PaintStyle (PaintStyle只有基本数据类型 同Camera)
同上 PaintManager的preserver
@Override
public int preserve(OutputStream out) {
try {
ByteArrayOutputStream data = new ByteArrayOutputStream();
ByteArrayOutputStream sub = new ByteArrayOutputStream();
int len = mPaintStyle.preserve(sub);
data.write(getByte());
data.write(ConvertData.tobytes(len));
data.write(sub.toByteArray());
out.write(data.toByteArray());
return data.size();
} catch (IOException e) {
throw new RuntimeException("保存出错");
}
}
因为我们要获得的偏移值是数据部分的字节数 而PaintManager持有PaintStyle 所以 我们要新定义一个ByteArrayOutputStream sub 来放置这个类持有的对象 之后 再将sub加入到data中 最后放到总的out中
init 则很简单 只要剥去头部信息再转交给PaintStyle就行了
public void init(InputStream in) {
if (in.read()!=getByte()) {
throw new RecoverFailException("类型错误(PaintManager)");
}
int len = ConvertData.getInt(in);//流出长度
mPaintStyle.init(in);
}
如此就能保存和恢复程序
--------------------------------------------------------------------------------------------
这么做是可行的 简单纯朴粗暴 但是很丑陋 很多重复的代码
ByteArrayOutputStream data = new ByteArrayOutputStream();
ByteArrayOutputStream sub = new ByteArrayOutputStream();
int len = mPaintStyle.preserve(sub);
data.write(getByte());
data.write(ConvertData.tobytes(len));
data.write(sub.toByteArray());
out.write(data.toByteArray());
return data.size();
这段代码只有 mPaintStyle.preserve(sub); 是有意义的 其他的每个类都要写 都是重复
这个函数的理想状态是 我们不用管 type len add 什么的 每个需要保存的都会设置这些
所以 应该把它们提升到父类中
另外 write(XX)和ConvertData.XX 我也写烦了 就保存来说 是不需要返回值的 所以我们可以写成流畅界面的感觉
add(XX).add(XX).add(XX)的feel
我们先来解决流畅界面 很明显可以封装一下 ByteArrayOutputStream
public class ReOutputStream extends ByteArrayOutputStream {
public ReOutputStream add(byte b) {
super.write(b);
return this;
}
public ReOutputStream add(byte[] bs) {
try {
super.write(bs);
return this;
} catch (IOException e) {
throw new RuntimeException("写入出错");
}
}
public ReOutputStream add(int data) {
add(tobytes(data));
return this;
}
public ReOutputStream add(int[] data) {
add(tobytes(data));
return this;
}
public ReOutputStream add(float data) {
add(tobytes(data));
return this;
}
public ReOutputStream add(float[] data) {
add(tobytes(data));
return this;
}
public ReOutputStream add(ReOutputStream data) {
add(data.toByteArray());
return this;
}
}
ByteArrayInputStream 也是同样
public class ReInputStream extends ByteArrayInputStream {
public ReInputStream(byte[] buf) {
super(buf);
}
public ReInputStream(byte[] buf, int offset, int length) {
super(buf, offset, length);
}
public int toInt() {
try {
byte[] data = new byte[4];
read(data);
return ConvertData.bytesToInt(data, 0);
} catch (IOException e) {
throw new RuntimeException("toInt出错");
}
}
/**
* 读取n字节 转换成int[]
*
* @param n byte 数
* @return
*/
public int[] toInts(int n) {
try {
byte[] data = new byte[n];
read(data);
return ConvertData.getIntArray(data);
} catch (IOException e) {
throw new RuntimeException("toInts出错");
}
}
public int[] toNInts(int n) {
return toInts(n * 4);
}
public float toFloat(int n) {
try {
byte[] data = new byte[4];
read(data);
return ConvertData.bytesToFloat(data, 0);
} catch (IOException e) {
throw new RuntimeException("toFloat出错");
}
}
public float[] toFloats(int n) {
try {
byte[] data = new byte[n];
read(data);
return ConvertData.tofloats(data);
} catch (IOException e) {
throw new RuntimeException("toInts出错");
}
}
public float[] toNFloas(int n) {
return toFloats(n * 4);
}
public ReInputStream get(int[] d) {
try {
ConvertData.bytesToInts(this,d);
return this;
} catch (IOException e) {
throw new RuntimeException("出错");
}
}
public ReInputStream get(float[] d) {
try {
ConvertData.bytesTofloats(this,d);
return this;
} catch (IOException e) {
throw new RuntimeException("出错");
}
}
}
这样我们在使用时就可以直接用
out.add(new float[]{mheight, mwidth, moffsetX, moffsetY, horizon.right, horizon.bottom, mratio});
最终我们来设计一个抽象父类ReLoad
在继承他之后 只要在preserver中放入数据 在init中获得并设置数据即可
@Override
public ReOutputStream preserve(ReOutputStream out) {
return out.add(new float[]{mheight, mwidth, moffsetX, moffsetY, horizon.right, horizon.bottom, mratio});
}
@Override
public void init(ReInputStream in, int len) {
float[]v=new float[4];
in.get(v);
height = v[0];
width = v[1];
moffsetX = v[2];
moffsetY = v[3];
}
public abstract class ReLoad {
private abstract byte getByte();
public ReOutputStream save(ReOutputStream out) {
try {
ReOutputStream data = new ReOutputStream();
preserve(data);
out.add(getByte())
.add(data.size())
.add(data);
data.close();
return out;
} catch (IOException e) {
throw new RuntimeException("保存错误");
}
}
public void recover(ReInputStream in) {
if (in.read() != getByte()) {
throw new RuntimeException("类型错误 " + getByte());
}
L.d(TAG, getByte());
int len[] = new int[1];
in.get(len);
byte[] data = new byte[len[0]];//不能污染了
try {
in.read(data);
} catch (IOException e) {
e.printStackTrace();
}
init(new ReInputStream(data), len[0]);
}
public abstract void init(ReInputStream in, int len);
public abstract ReOutputStream preserve(ReOutputStream out);
}
---------------------------------------------------------------------------------------------------------------
还能给给力一点吗?
现在每个类都要实现getByte()来获得代表此类的byte 然而自己自动加一个byte全局变量是一件很痛苦的事
我们可以直接在ReLoad中实现
public abstract class ReLoad {
private byte getByte(){
return TypeMap.get(this);
}
。。。。
}
public class TypeMap {
//TODO 应该是类名的hash的
private static HashMap<String,Byte> map=new HashMap<>();
public static byte get(Object obj) {
String name=obj.getClass().getName();
Object v= map.get(name);
if (v==null){
int type=map.size();
if (type>255){
throw new RuntimeException("类过多 无法再使用byte");
}
map.put(name,(byte)type);
return (byte)type;
}else {
return (byte) v;
}
}
}
(T_T) 大致就是如此 只是做了一些微小的工作