第十八章:自定义AssetLocator
前文已经分析了 AssetLocator 的工作原理,本章将介绍如何实现自定义AssetLocator。
jME3 提供了很多不同类型的 AssetLoactor,诸如:
如果感兴趣,读者可以直接查阅它们的源代码,阅读源码是学习的最好方式。
压缩文件格式
在游戏发布时,开发者经常需要把美术资产打成加密、压缩资源包。每个工作室都可能有自己的加密算法,但通用的压缩算法只有几种:zip, rar, tar, gz, 7z ...
Java语言原生支持 zip 压缩算法,相关类位于 java.util.zip
包中,jME3提供的ZipLocator 即是使用 zip 算法实现的资源查找和解压。
本文将选择 7-zip 压缩格式作为自定义 AssetLocator 的目标,将开发一个 SevenZAssetLocator
类,用于从 7z 格式的压缩包中提取文件内容。
当开发完成后,应该能够通过下面的方式来使用它:
public static void main(String[] args) {
AssetManager assetManager = JmeSystem.newAssetManager();
assetManager.registerLocator("assets.7z", SevenZAssetLocator.class);
AssetInfo info = assetManager.locateAsset(new AssetKey("assets/hello.txt"));
System.out.println(info == null? "加载成功": "加载失败");
}
第一步:下载7ZipJBind
使用Java程序来解压 7z 文件,需要一些第三方类库。 7-zip Java Binding 是一个开源Java实现,通过调用底层的 C/C++ 代码,实现对 7z 文件的解压。
下载地址: https://sourceforge.net/projects/sevenzipjbind/files/
因为我用的是Windows系统,下载得到了 sevenzipjbinding-9.20-2.00beta-AllWindows.zip
。解压后,在 lib
目录中找到了 sevenzipjbinding.jar
和 sevenzipjbinding-AllWindows.jar
,把这两个 jar 文件添加到项目的依赖中即可。
第二步:初始化7ZipJBind
根据 SevenZipJBind 官方网站的介绍,先使用下面的代码来初始化SevenZipJBind。
import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.SevenZipNativeInitializationException;
public class SevenZipJBindingInitCheck {
public static void main(String[] args) {
try {
SevenZip.initSevenZipFromPlatformJAR();
System.out.println("7-Zip-JBinding library was initialized");
} catch (SevenZipNativeInitializationException e) {
e.printStackTrace();
}
}
}
运行的结果如下:
7-Zip-JBinding library was initialized
如果输出不是这个结果,可能你需要检查自己下载的jar文件是否完整。
第三步:打开压缩文件
7-zip可以解压很多不同压缩格式的文件,其中就包括 zip 格式。下面尝试用它来打开我刚下载的 sevenzipjbinding-9.20-2.00beta-AllWindows.zip
文件。
SevenZipJBind 官网提供了很多代码,参考样例,编写测试代码如下:
package net.jmecn.assets;
import java.io.IOException;
import java.io.RandomAccessFile;
import net.sf.sevenzipjbinding.IInArchive;
import net.sf.sevenzipjbinding.PropID;
import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.SevenZipException;
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;
public class ListItems {
public static void main(String[] args) {
String archiveFilename = "sevenzipjbinding-9.20-2.00beta-AllWindows.zip";
RandomAccessFile randomAccessFile = null;
IInArchive inArchive = null;
try {
// 打开压缩文件
randomAccessFile = new RandomAccessFile(archiveFilename, "r");
inArchive = SevenZip.openInArchive(null, // 自动检测压缩格式
new RandomAccessFileInStream(randomAccessFile));
// 读取压缩文件中的文件数
System.out.println("Count of items in archive: " + inArchive.getNumberOfItems());
// 遍历目录,查询文件内容
System.out.println(" Size | Compr.Sz. | Filename");
System.out.println("----------+-----------+---------");
int itemCount = inArchive.getNumberOfItems();
for (int i = 0; i < itemCount; i++) {
System.out.println(String.format("%9s | %9s | %s", //
inArchive.getProperty(i, PropID.SIZE), // 原文件大小
inArchive.getProperty(i, PropID.PACKED_SIZE), // 压缩后大小
inArchive.getProperty(i, PropID.PATH))); // 文件路径
}
} catch (Exception e) {
System.err.println("Error occurs: " + e);
} finally {
// 关闭压缩文件
if (inArchive != null) {
try {
inArchive.close();
} catch (SevenZipException e) {
System.err.println("Error closing archive: " + e);
}
}
if (randomAccessFile != null) {
try {
randomAccessFile.close();
} catch (IOException e) {
System.err.println("Error closing file: " + e);
}
}
}
}
}
运行结果:
Count of items in archive: 16
Size | Compr.Sz. | Filename
-----------+-----------+---------
0 | 0 | sevenzipjbinding-9.20-2.00beta-AllWindows
153 | 125 | sevenzipjbinding-9.20-2.00beta-AllWindows\AUTHORS
141 | 129 | sevenzipjbinding-9.20-2.00beta-AllWindows\CMakeLists.txt
26444 | 9085 | sevenzipjbinding-9.20-2.00beta-AllWindows\COPYING
8940 | 3232 | sevenzipjbinding-9.20-2.00beta-AllWindows\ChangeLog
26444 | 9085 | sevenzipjbinding-9.20-2.00beta-AllWindows\LGPL
7441 | 2826 | sevenzipjbinding-9.20-2.00beta-AllWindows\README
1918 | 786 | sevenzipjbinding-9.20-2.00beta-AllWindows\ReleaseNotes.txt
984 | 590 | sevenzipjbinding-9.20-2.00beta-AllWindows\THANKS
72665 | 66013 | sevenzipjbinding-9.20-2.00beta-AllWindows\cpp-src.zip
91407 | 80833 | sevenzipjbinding-9.20-2.00beta-AllWindows\java-src.zip
278020 | 264548 | sevenzipjbinding-9.20-2.00beta-AllWindows\javadoc.zip
0 | 0 | sevenzipjbinding-9.20-2.00beta-AllWindows\lib
2297195 | 2289323 | sevenzipjbinding-9.20-2.00beta-AllWindows\lib\sevenzipjbinding-AllWindows.jar
71015 | 58970 | sevenzipjbinding-9.20-2.00beta-AllWindows\lib\sevenzipjbinding.jar
468405 | 451108 | sevenzipjbinding-9.20-2.00beta-AllWindows\website.zip
根据上面的例子,可以得知下列信息:
SevenZip.openInArchive()
方法可以打开压缩文件。IInArchive
表示压缩文件,使用完毕应该调用close()
方法关闭它。IInArchive#getNumberOfItems()
方法可以拿到文件数量。IInArchive
是根据下标来访问每一个文件的。IInArchive#getProperty(i, PropID.PATH)
方法可以访问下标为i
的文件属性。PropID
是一个枚举类型;PropID.SIZE
表示源文件大小;PropID.PATH
表示文件路径,可用于资源定位。
第四步:解压文件
上一步得到的信息,足够进行资源定位了,但是还无法拿到具体的文件数据,更别提 InputStream 了。通过查看IInArchive接口的源代码,发现了三个解压接口。
接口一:
public void extract(int[] indices, boolean testMode, IArchiveExtractCallback extractCallback)
throws SevenZipException;
第一个参数是一个int[] 数组。根据注释来看,IInArchive支持批量解压缩,只要把被解压的文件下标以数组形式传递给 extract
接口即可。
第二个参数testMode
是测试模型。当值为 true
时并不会真的解压数据,只是测试一下文件是否可用;当值为 false
时才会解压数据。
第三个参数 extractCallback
是一个回调接口。extract方法实际上是通过接口回调来获得解压后的数据的。通过实现回调接口,可以自己决定如何处理解压后的数据。
看来免不了要自己实现回调接口了。
接口二:
public ExtractOperationResult extractSlow(int index, ISequentialOutStream outStream) throws SevenZipException;
这个接口可以解压缩单个文件。第一个参数 index 表示文件的下标(从0开始),第二个参数是一个回调接口,输出的数据通过这个接口来处理。
模式跟前一个接口差不多,只是少了一个参数,并且只解压单个文件。但似乎刚好适合在 AssetLocator 中实现。
接口三:
public ExtractOperationResult extractSlow(int index, ISequentialOutStream outStream, String password)
throws SevenZipException;
这个接口与前一个接口几乎一样,只是多了第三个参数 password
。如果压缩包中的数据是被加密过的,就可以用 password
参数来解密。当然,前提是你知道解压密码。
IArchiveExtractCallback接口:
这个接口中一共有5个方法要实现。
有两个方法比较简单,它们是用于统计解压进度的。setTotal()
用于记录文件的总字节数, setCompleted
则用于记录当前已解压的字节数。如果不需要统计进度,就不用管这两个方法。
public void setTotal(long total) throws SevenZipException;
public void setCompleted(long complete) throws SevenZipException;
另外三个方法如下:
public ISequentialOutStream getStream(int index, ExtractAskMode extractAskMode) throws SevenZipException;
public void prepareOperation(ExtractAskMode extractAskMode) throws SevenZipException;
public void setOperationResult(ExtractOperationResult extractOperationResult) throws SevenZipException;
根据注释来看,SevenZipJBind 的实际解压功能是由 C/C++ 代码实现的。解压算法在执行时,会调用 getStream()
方法来获得一个输出流,用来输出数据。
prepareOperation()
方法会在解压之前被调用,用来设置解压请求模式。 ExtractAskMode 是一个枚举类型,它的值包括 EXTRACT、TEST、SKIP、UNKNOWN_ASK_MODE。除了第一个EXTRACT请求外,另外三个都表示并不实际解压。
setOperationResult()
会在解压结束后被调用,设置解压缩的结果。ExtractOperationResult 也是一个枚举类型,当它的值为 OK 是表示解压成功,其他值表示解压过程中发生了异常。
看来,不管是 extract 还是 extractSlow 方法,都需要实现 ISequentialOutStream
接口。
ISequentialOutStream接口:
这个接口中只定义了一个方法:
public int write(byte[] data) throws SevenZipException;
根据该接口的注释来看,这个接口将会被底层的 C/C++ 代码调用。如果需要解压缩的文件比较大,那么这个 write
方法会被调用多次,每次只写入一小段数据。
测试解压接口
在大致读完这些接口的源代码和注释之后,已经了解了这些接口的用法。但是还应该写一个测试程序,实际验证一下这些接口的工作机制。
验证思路很简单:用最简单的方式实现这些接口,直接打印方法的参数。然后用接口的实现类调用 extract()
方法,看看运行时会发生什么事情。为了验证write方法是否真的会被调用多次,应该选择解压一个体积较大的文件。
具体代码如下:
package net.jmecn.assets;
import java.io.IOException;
import java.io.RandomAccessFile;
import net.sf.sevenzipjbinding.ExtractAskMode;
import net.sf.sevenzipjbinding.ExtractOperationResult;
import net.sf.sevenzipjbinding.IArchiveExtractCallback;
import net.sf.sevenzipjbinding.IInArchive;
import net.sf.sevenzipjbinding.ISequentialOutStream;
import net.sf.sevenzipjbinding.PropID;
import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.SevenZipException;
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;
/**
* 测试解压接口
* @author yanmaoyuan
*
*/
public class TestExtract {
public static void main(String[] args) throws IOException {
TestExtract app = new TestExtract("sevenzipjbinding-9.20-2.00beta-AllWindows.zip");
app.tryExtract("sevenzipjbinding-9.20-2.00beta-AllWindows\\website.zip");
}
private String archiveFilename;
/**
* 构造方法,初始化压缩文件路径
* @param filename
*/
public TestExtract(String filename) {
this.archiveFilename = filename;
}
/**
* 尝试解压某个文件
* @param filepath
* @throws IOException
*/
public void tryExtract(String filepath) throws IOException{
// 打开压缩文件
RandomAccessFile randomAccessFile = new RandomAccessFile(archiveFilename, "r");
IInArchive inArchive = SevenZip.openInArchive(null, // 自动选择解压格式
new RandomAccessFileInStream(randomAccessFile));
// 查询文件
int count = inArchive.getNumberOfItems();
for(int i=0; i<count; i++) {
String path = (String)inArchive.getProperty(i, PropID.PATH);
Boolean isFolder = (Boolean)inArchive.getProperty(i, PropID.IS_FOLDER);
if (isFolder) {
// 文件夹勿需解压,跳过
continue;
}
if (path.equals(filepath)) {
// 记录开始时间
long start = System.currentTimeMillis();
// 解压
inArchive.extract(new int[]{i}, false, callback);
// 显示解压用时
long time = System.currentTimeMillis() - start;
System.out.println("time: " + time);
// 终止循环
break;
}
}
// 关闭压缩文件
inArchive.close();
}
// 实现 ISequentialOutStream 接口,打印方法参数。
ISequentialOutStream out = new ISequentialOutStream() {
@Override
public int write(byte[] data) throws SevenZipException {
System.out.println("write:" + data.length);
return data.length;
}
};
// 实现 IArchiveExtractCallback 接口,打印方法参数。
IArchiveExtractCallback callback = new IArchiveExtractCallback() {
@Override
public void setTotal(long total) throws SevenZipException {
System.out.println("total:" + total);
}
@Override
public void setCompleted(long complete) throws SevenZipException {
System.out.println("complete:" + complete);
}
@Override
public ISequentialOutStream getStream(int index, ExtractAskMode extractAskMode) throws SevenZipException {
System.out.println("getStream:" + index + ", " + extractAskMode);
return out;
}
@Override
public void prepareOperation(ExtractAskMode extractAskMode) throws SevenZipException {
System.out.println("prepare:" + extractAskMode);
}
@Override
public void setOperationResult(ExtractOperationResult extractOperationResult) throws SevenZipException {
System.out.println("result:" + extractOperationResult);
}
};
}
运行结果如下:
total:468405
complete:0
getStream:15, EXTRACT
prepare:EXTRACT
write:32768
write:32768
write:32768
write:32768
write:32768
write:32768
write:32768
write:32768
complete:262144
write:32768
write:32768
write:32768
write:32768
write:32768
write:32768
complete:468405
write:9653
result:OK
time: 12
根据这个运行结果,可以验证很多信息:
total
和complete
用于统计解压进度,其中total
可以拿到文件的原始大小;- 另外几个方法的调用顺序是
getStream
>prepare
>write
>write
> ... >write
>result
。 complete
和write
可能处于两个不同的线程中并行执行。extract
方法是同步阻塞的。当数据解压完毕后,才会继续执行后面的代码,显示运行时间。
把 getStream() 方法中的代码稍微改一下,直接返回null,看看会发生什么。
public ISequentialOutStream getStream(int index, ExtractAskMode extractAskMode) throws SevenZipException {
System.out.println("getStream:" + index + ", " + extractAskMode);
// return out;
return null;// <--------------
}
运行结果如下:
total:468405
complete:0
getStream:15, EXTRACT
time: 2
可以看到,在没有提供 ISequentialOutStream
实例的情况下,prepare
、write
、result
等步骤根本就不会执行。
解压文件
既然已经了解 SevenZipJBind 的接口,那么就可以尝试实际解压一个文件了。在 ISequentialOutStream
的实现中,使用 FileOutputStream
来输出数据,把解压后的文件保存到磁盘上。
代码如下:
package net.jmecn.assets;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import net.sf.sevenzipjbinding.ExtractAskMode;
import net.sf.sevenzipjbinding.ExtractOperationResult;
import net.sf.sevenzipjbinding.IArchiveExtractCallback;
import net.sf.sevenzipjbinding.IInArchive;
import net.sf.sevenzipjbinding.ISequentialOutStream;
import net.sf.sevenzipjbinding.PropID;
import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.SevenZipException;
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;
/**
* 测试解压接口
* @author yanmaoyuan
*
*/
public class TestExtract {
public static void main(String[] args) throws IOException {
TestExtract app = new TestExtract("sevenzipjbinding-9.20-2.00beta-AllWindows.zip");
app.tryExtract("sevenzipjbinding-9.20-2.00beta-AllWindows\\website.zip");
}
private String archiveFilename;
// 文件输出流
private FileOutputStream output;// <------------------
/**
* 构造方法,初始化压缩文件路径
* @param filename
*/
public TestExtract(String filename) {
this.archiveFilename = filename;
}
/**
* 尝试解压某个文件
* @param filepath
* @throws IOException
*/
public void tryExtract(String filepath) throws IOException{
// 打开压缩文件
RandomAccessFile randomAccessFile = new RandomAccessFile(archiveFilename, "r");
IInArchive inArchive = SevenZip.openInArchive(null, // 自动选择解压格式
new RandomAccessFileInStream(randomAccessFile));
// 查询文件
int count = inArchive.getNumberOfItems();
for(int i=0; i<count; i++) {
String path = (String)inArchive.getProperty(i, PropID.PATH);
Boolean isFolder = (Boolean)inArchive.getProperty(i, PropID.IS_FOLDER);
if (isFolder) {
// 文件夹勿需解压,跳过
continue;
}
if (path.equals(filepath)) {
// 记录开始时间
long start = System.currentTimeMillis();
// 检查文件夹是否存在,不存在则创建
int n = filepath.lastIndexOf("\\");
String folder = filepath.substring(0, n);
File dir = new File(folder);
if(!dir.exists())
dir.mkdirs();
// 打开文件流
File file = new File(filepath);
output = new FileOutputStream(file);//<------------
// 解压
inArchive.extract(new int[]{i}, false, callback);
// 关闭文件流
output.flush();
output.close();
output = null;
// 显示解压用时
long time = System.currentTimeMillis() - start;
System.out.println("time: " + time);
// 终止循环
break;
}
}
// 关闭压缩文件
inArchive.close();
}
// 实现 ISequentialOutStream 接口,打印方法参数。
ISequentialOutStream out = new ISequentialOutStream() {
@Override
public int write(byte[] data) throws SevenZipException {
try {
if (output != null) {
// 写文件
output.write(data);//<----------
}
} catch (IOException e) {
throw new SevenZipException(e);
}
return data.length;
}
};
// 实现 IArchiveExtractCallback 接口,打印方法参数。
IArchiveExtractCallback callback = new IArchiveExtractCallback() {
@Override
public void setTotal(long total) throws SevenZipException {
System.out.println("total:" + total);
}
@Override
public void setCompleted(long complete) throws SevenZipException {
System.out.println("complete:" + complete);
}
@Override
public ISequentialOutStream getStream(int index, ExtractAskMode extractAskMode) throws SevenZipException {
System.out.println("getStream:" + index + ", " + extractAskMode);
return out;
}
@Override
public void prepareOperation(ExtractAskMode extractAskMode) throws SevenZipException {
System.out.println("prepare:" + extractAskMode);
}
@Override
public void setOperationResult(ExtractOperationResult extractOperationResult) throws SevenZipException {
System.out.println("result:" + extractOperationResult);
}
};
}
运行结果。
第五步:自定义AssetLocator
通过前四步,对SevenZipJBind如何查询、如何解压文件已经有所了解。下面定义一个 SevenZAssetLocator,使用SevenZipJBind 来定位压缩包中的文件。
实现AssetLocator
首先定义SevenZAssetLocator,实现 AssetLocator 接口。
package net.jmecn.assets;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetLocator;
import com.jme3.asset.AssetManager;
/**
* 7z 资产定位器
* @author yanmaoyuan
*
*/
public class SevenZAssetLocator implements AssetLocator {
@Override
public void setRootPath(String rootPath) {
}
@Override
public AssetInfo locate(AssetManager manager, AssetKey key) {
return null;
}
}
AssetLocator 接口中有两个方法:
public void setRootPath(String rootPath)
用于定义查找资产的根目录。对于 SevenZAssetLocator 来说,这个方法将传入压缩文件所在的路径。public AssetInfo locate(AssetManager manager, AssetKey key)
用于查找资源,返回AssetInfo 对象。
根据前文的介绍,可以很容易地实现这两个方法。
package net.jmecn.assets;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.HashMap;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetLoadException;
import com.jme3.asset.AssetLocator;
import com.jme3.asset.AssetManager;
import net.sf.sevenzipjbinding.IInArchive;
import net.sf.sevenzipjbinding.PropID;
import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;
/**
* 7z 资产定位器
* @author yanmaoyuan
*
*/
public class SevenZAssetLocator implements AssetLocator {
// 压缩文件
private IInArchive inArchive;
// 文件路径缓存,用来加速查询时间。
private HashMap<String, Integer> cache;
@Override
public void setRootPath(String rootPath) {
try {
// 以只读模式打开压缩包
RandomAccessFile randomAccessFile = new RandomAccessFile(rootPath, "r");
inArchive = SevenZip.openInArchive(null,// 自动选择解压格式
new RandomAccessFileInStream(randomAccessFile));
// 统计文件数量,并缓存文件路径
cache = new HashMap<String, Integer>();
int count = inArchive.getNumberOfItems();
for(int i=0; i<count; i++) {
Boolean isFolder = (Boolean)inArchive.getProperty(i, PropID.IS_FOLDER);
if (isFolder) {
continue;// 文件夹勿需解压,跳过
}
String path = (String)inArchive.getProperty(i, PropID.PATH);
// 统一文件分隔符
path = path.replaceAll("\\\\", "/");
// 缓存文件路径
cache.put(path, i);
}
} catch (IOException e) {
throw new AssetLoadException("Failed to open archive file: " + rootPath, e);
}
}
@Override
public AssetInfo locate(AssetManager manager, AssetKey key) {
// 获得文件路径
String name = key.getName();
// 统一文件分隔符
name = name.replace("\\\\", "/");
if(name.startsWith("/"))
name = name.substring(1);
// 查找文件索引
Integer index = cache.get(name);
System.out.println(index);
return null;
}
}
在上面的代码中,setRootPath
方法的主要作用是打开压缩包,并使用HashMap来缓存压缩包中的文件路径。这样在 locate
方法中就可以直接查询文件的索引。
实现AssetInfo
目前 locate
方法直接返回了 null。想要正常使用,必须返回一个AssetInfo对象才行。
用户不需要关心这个 AssetInfo 对象的具体实现,因此我直接在 SevenZAssetLocator 类中定义了一个私有内部类 SevenZipAssetInfo,并让它实现 AssetInfo 中的 openStream()
方法。
SevenZipAssetInfo 类还需要负责解压文件数据。当用户调用 openStream()
方法时,需要返回一个 InputStream 对象,把解压后的数据交给客户。为了实现文件解压,这个类还需要实现 ISequentialOutStream
、IArchiveExtractCallback
这两个接口。
@Override
public AssetInfo locate(AssetManager manager, AssetKey key) {
// 获得文件路径
String name = key.getName();
// 统一文件分隔符
name = name.replace("\\\\", "/");
if(name.startsWith("/"))
name = name.substring(1);
// 查找文件索引
Integer index = cache.get(name);
if (index != null) {
return new SevenZipAssetInfo(manager, key, index);
} else {
return null;
}
}
private class SevenZipAssetInfo extends AssetInfo implements ISequentialOutStream, IArchiveExtractCallback {
private int index = -1;// 待解压的文件索引
public SevenZipAssetInfo(AssetManager manager, AssetKey key, int index) {
super(manager, key);
this.index = index;
}
@Override
public InputStream openStream() {
return null;
}
@Override
public int write(byte[] data) throws SevenZipException {
return 0;
}
@Override
public void setTotal(long total) throws SevenZipException {}
@Override
public void setCompleted(long complete) throws SevenZipException {}
@Override
public ISequentialOutStream getStream(int index, ExtractAskMode extractAskMode) throws SevenZipException {
return null;
}
@Override
public void prepareOperation(ExtractAskMode extractAskMode) throws SevenZipException {}
@Override
public void setOperationResult(ExtractOperationResult result) throws SevenZipException {}
}
前文已经实现了文件解压,做法是通过 FileOutputStream
把数据写到磁盘文件中。但现在需要的是一个 InputStream 对象,又应该怎么做呢?
一种比较简单的办法,是创建一个字节数组 byte[] data
,把解压后的数据保存到内存中。然后再用 ByteArrayInputStream
来包装这个数组,提供给客户使用。按照同样的思路,还可以使用 Java nio 中的 ByteBuffer 来做到同样的事。
这么做的风险在于,如果被解压的文件较大,就可能发生内存溢出的错误。因为文件数据是直接缓存在Java虚拟机的堆内存中的。不过考虑到单个游戏资产文件一般不会太大,应该还是可以接受的。
完整的实现代码如下:
package net.jmecn.assets;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.HashMap;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetLoadException;
import com.jme3.asset.AssetLocator;
import com.jme3.asset.AssetManager;
import net.sf.sevenzipjbinding.ExtractAskMode;
import net.sf.sevenzipjbinding.ExtractOperationResult;
import net.sf.sevenzipjbinding.IArchiveExtractCallback;
import net.sf.sevenzipjbinding.IInArchive;
import net.sf.sevenzipjbinding.ISequentialOutStream;
import net.sf.sevenzipjbinding.PropID;
import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.SevenZipException;
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;
/**
* 7z 资产定位器
* @author yanmaoyuan
*
*/
public class SevenZAssetLocator implements AssetLocator {
// 压缩文件
private IInArchive inArchive;
// 文件路径缓存,用来加速查询时间。
private HashMap<String, Integer> cache;
@Override
public void setRootPath(String rootPath) {
try {
// 以只读模式打开压缩包
RandomAccessFile randomAccessFile = new RandomAccessFile(rootPath, "r");
inArchive = SevenZip.openInArchive(null,// 自动选择解压格式
new RandomAccessFileInStream(randomAccessFile));
// 统计文件数量,并缓存文件路径
cache = new HashMap<String, Integer>();
int count = inArchive.getNumberOfItems();
for(int i=0; i<count; i++) {
Boolean isFolder = (Boolean)inArchive.getProperty(i, PropID.IS_FOLDER);
if (isFolder) {
continue;// 文件夹勿需解压,跳过
}
String path = (String)inArchive.getProperty(i, PropID.PATH);
// 统一文件分隔符
path = path.replaceAll("\\\\", "/");
// 缓存文件路径
cache.put(path, i);
}
} catch (IOException e) {
throw new AssetLoadException("Failed to open archive file: " + rootPath, e);
}
}
@Override
public AssetInfo locate(AssetManager manager, AssetKey key) {
// 获得文件路径
String name = key.getName();
// 统一文件分隔符
name = name.replace("\\\\", "/");
if(name.startsWith("/"))
name = name.substring(1);
// 查找文件索引
Integer index = cache.get(name);
if (index != null) {
return new SevenZipAssetInfo(manager, key, index);
} else {
return null;
}
}
private class SevenZipAssetInfo extends AssetInfo implements ISequentialOutStream, IArchiveExtractCallback {
private ByteArrayOutputStream out;
private ExtractOperationResult result;
private int index = -1;// 待解压的文件索引
public SevenZipAssetInfo(AssetManager manager, AssetKey key, int index) {
super(manager, key);
this.index = index;
}
@Override
public InputStream openStream() {
try {
result = null;
// 解压数据
inArchive.extract(new int[]{index}, false, this);
// 解压失败
if (result != ExtractOperationResult.OK) {
throw new AssetLoadException("Extracting operation error: " + result);
}
// 解压成功,获得解压后的数据。
byte[] data = out.toByteArray();
out = null;
// 返回一个内存字节输入流
return new ByteArrayInputStream(data);
} catch (IOException e) {
throw new AssetLoadException("Failed to extract file: " + index, e);
}
}
@Override
public int write(byte[] data) throws SevenZipException {
try {
// 把数据写到内存字节流中。
out.write(data);
} catch (IOException e) {
e.printStackTrace();
}
return data.length;
}
@Override
public void setTotal(long total) throws SevenZipException {
System.out.println(total);
// 根据数据总长度,初始化内存字节输出流。
// FIXME 若内存不足,可能发生内存溢出。
out = new ByteArrayOutputStream((int)total);
}
@Override
public void setCompleted(long complete) throws SevenZipException {}
@Override
public ISequentialOutStream getStream(int index, ExtractAskMode extractAskMode) throws SevenZipException {
// 非解压模式下,不解压数据。
if (extractAskMode != ExtractAskMode.EXTRACT)
return null;
return this;
}
@Override
public void prepareOperation(ExtractAskMode extractAskMode) throws SevenZipException {}
@Override
public void setOperationResult(ExtractOperationResult result) throws SevenZipException {
// 记录解压结果
this.result = result;
}
}
}
测试程序
编写一个测试程序:
package net.jmecn.assets;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetManager;
import com.jme3.system.JmeSystem;
/**
* 测试SevenZAssetLocator
* @author yanmaoyuan
*
*/
public class TestSevenZ {
public static void main(String[] args) {
AssetManager assetManager = JmeSystem.newAssetManager();
assetManager.registerLocator("sevenzipjbinding-9.20-2.00beta-AllWindows.zip", SevenZAssetLocator.class);
AssetInfo info = assetManager.locateAsset(new AssetKey("sevenzipjbinding-9.20-2.00beta-AllWindows/AUTHORS"));
System.out.println(info != null? "加载成功": "加载失败");
if (info != null) {
// 读取文件内容
InputStream in = info.openStream();
String line = null;
// 逐行读取,并打印到控制台。
Scanner scanner = new Scanner(in);
while(scanner.hasNextLine()) {
line = scanner.nextLine();
System.out.println(line);
}
// 关闭输入流
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
运行结果:
加载成功
153
Authors of SevenZipJBinding.
See also the files THANKS, ChangeLog and git history.
Boris Brodski has initially designed and implemented 7-Zip-JBinding.