第十七章:AssetLocator原理
先总结一下前面几章中讲过的概念:
用Java I/O读取文件,分为三步:
- 打开输入流
- 读取数据
- 关闭输入流
在jME3中,“打开输入流”这一步是由资产定位器(AssetLocator)来实现的。
通过调用 AssetManager 中的 registerLocator(String rootPath, Class locatorClass)
方法,可以配置一对 资产根目录 和 资产定位器 的关系。接下来再使用 AssetManager 来加载资产,就会自动去这些目录下搜索文件。
本文会进一步分析AssetLocator的工作原理,以及 AssetManager 中的相关接口。
AssetInfo
在分析AssetLocator之前,请容许我再啰嗦一次,先分析 AssetInfo 类。
在传统的Java I/O操作中,打开输入流这一步的结果一般会得到InputStream对象。由于在解析数据时会需要一些额外的信息,jME3 把 InputStream 包装到了 AssetInfo 类中,并利用它来传递其他数据,诸如AssetKey和AssetManager对象。
即是说,AssetLocator 在定位到某个资产文件后,并不是直接返回一个InputStream对象,而是返回一个AssetInfo对象。通过AssetInfo对象,就可以拿到InputStream。
需要注意的是, jME3 中原始的 AssetInfo 是一个抽象类,是不能直接实例化的。openStream()
方法是一个抽象方法,需要由不同的子类来实现。
public abstract InputStream openStream();
这样做很好理解。因为对于不同来源的数据,Java I/O 中有不同的InputStream实现类。例如:
- 文件输入流 new FileInputStream("/var/assets/example.png");
- Socket输入流 socket.getInputStream();
- Zip解压输入流 new ZipInputStream();
- Class加载输入流 class.getResourceAsStream("/net/jmecn/assets/Main.class");
基本上每个 AssetLocator 都会定义不同的 AssetInfo 实现类。例如 FileLocator,它使用私有内部类 AssetInfoFile 来打开FileInputStream ,供 AssetLoader 使用。
private static class AssetInfoFile extends AssetInfo {
private File file;
public AssetInfoFile(AssetManager manager, AssetKey key, File file){
super(manager, key);
this.file = file;
}
@Override
public InputStream openStream() {
try{
return new FileInputStream(file);
}catch (FileNotFoundException ex){
// NOTE: Can still happen even if file.exists() is true, e.g.
// permissions issue and similar
throw new AssetLoadException("Failed to open file: " + file, ex);
}
}
}
实际使用时不需要关心 AssetInfo 的实现,只需要调用 openStream()
获得 InputStream
即可。
示例
下面我将创建一个JME3工程,并编写测试类 net.jmecn.assets.TestAssetLocator
来演示它的作用。
package net.jmecn.assets;
import java.io.IOException;
import java.io.InputStream;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetManager;
import com.jme3.asset.plugins.ClasspathLocator;
import com.jme3.system.JmeSystem;
/**
* 演示 AssetInfo 的作用
* @author yanmaoyuan
*
*/
public class TestAssetLocator {
public final static String URL = "net/jmecn/assets/TestAssetLocator.class";
public static void main(String[] args) {
// 创建AssetManager
AssetManager assetManager = JmeSystem.newAssetManager();
// 注册ClasspathLocator
assetManager.registerLocator("/", ClasspathLocator.class);
// 定位资产
AssetInfo info = assetManager.locateAsset(new AssetKey(URL));
if (info == null) {
// 没有找到资产
System.out.println("Asset not found!");
} else {
AssetKey key = info.getKey();
AssetManager manager = info.getManager();
System.out.println("info:" + info);
System.out.println("key:" + key);
System.out.println("manager:" + manager);
InputStream in = info.openStream();
try {
// TODO 读取数据
int len = in.available();
System.out.println("len:" + len);
// 关闭输入流
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
上面的代码通过 AssetManager 的 locateAsset()
方法查找一个 .class 文件,查询结果为一个 AssetInfo 对象。通过 AssetInfo 的 openStream()
方法获得了InputStream,显示了 .class 文件的字节数。
运行结果:
info:com.jme3.asset.plugins.UrlAssetInfo[key=net/jmecn/assets/TestAssetLocator.class]
key:net/jmecn/assets/TestAssetLocator.class
manager:com.jme3.asset.DesktopAssetManager@2ff4acd0
len:2135
创建AssetManager
在上面的代码中,我并没有让 TestAssetLocator 继承 SimpleApplication 类,因为用不着3D渲染。但是不继承 SimpleApplication,就不能直接使用 JME3 系统创建的 AssetManager。所以我通过这行代码自己创建了一个:
// 创建AssetManager
AssetManager assetManager = JmeSystem.newAssetManager();
当然,也可以这样做:
// 创建PC专用AssetManager
AssetManager assetManager = new DesktopAssetManager();
或者这样做:
// 创建Android专用AssetManager
AssetManager assetManager = new AndroidAssetManager();
后两种方法也完全可以做到同样的事,只是 JmeSystem
使用“代理模式”来创建 AssetManager
,可以提供更好的平台兼容性。
ClasspathLocator
示例代码中定义了如下的资源路径:
public final static String URL = "net/jmecn/assets/TestAssetLocator.class";
这个class文件是由JDK编译生成的,源文件就是测试类 net.jmecn.assets.TestAssetLocator.java
。在不同的开发环境中,该class文件生成的位置是不一样的:
- Eclipse中的Java工程,默认会把class文件编译到工程的
bin
目录下; - IDEA 中的Java工程,默认会把class文件输出到
out/production
目录下; - Maven 和 Gradle项目一般会把class文件生成到
build/target
目录下; - Java EE项目,通常会把class文件生成到
WebContent/WEB-INF/classes
目录下;
Java虚拟机在工作时,会去这些路径下加载已编译的class文件,这种路径叫做 classpath
。classpath
不止一个。在开发Java项目时,通常不会只使用自己编写的类。项目所依赖的各种 jar 文件中保存了其他人开发的 class,这些 jar 文件也会被添加到 classpath
中。这些 classpath
都是由Java虚拟机管理的。
ClasspathLocator
的主要作用就是在 classpath
中查找所需的文件。不管使用什么IDE,不管文件是被打包成jar、还是直接放在目录中,都可以被 ClasspathLocator
识别,因为它是依靠Java虚拟机的类加载机制工作的。
下面这行代码的作用是告知 AssetManager,可以在 classpath
的根目录下查找资源。
// 注册ClasspathLocator
assetManager.registerLocator("/", ClasspathLocator.class);
如果我想只查找 net.jmecn
包下的资源,就应该这么做:
// 注册ClasspathLocator
assetManager.registerLocator("/net/jmecn", ClasspathLocator.class);
相对的,资源的URL也得缩短:
public final static String URL = "assets/TestAssetLocator.class";
locateAsset
有了 AssetManager,也注册好了 AssetLocator,就可以使用 AssetManager 来定位资产了。
// 定位资产
AssetInfo info = assetManager.locateAsset(new AssetKey(URL));
AssetInfo 保存了 AssetManager 搜索资源的结果。如果 info == null
,就说明没有找到任何东西,正常流程下会产生 AssetNotFoundException
;如果 info != null
,就可以通过它打开 InputSteam 了。
if (info == null) {
// 没有找到资产
System.out.println("Asset not found!");
} else {
AssetKey key = info.getKey();
AssetManager manager = info.getManager();
System.out.println("info:" + info);
System.out.println("key:" + key);
System.out.println("manager:" + manager);
InputStream in = info.openStream();
try {
// TODO 读取数据
int len = in.available();
System.out.println("len:" + len);
// 关闭输入流
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
在jME3默认的资产加载流程中,开发者是不需要自己去调用 locateAsset()
的,最常使用的是 loadAsset()
、loadModel()
、loadTexture()
等方法。但如果你需要自己管理资产加载流程,或者根据资产是否存在来做一些处理, locateAsset()
方法的作用就很大了。
另外,AssetManager
需要通过 AssetLoader
才能解析 InputStream
中的数据。如果没有对应的AssetLoader,可以通过 locateAsset()
方法拿到 InputStream
后自己解析数据。
在jME3中读取文件
问:如何在jME3程序中读文件?
创建一个JME3工程,并在 assets 目录中创建一个文本文件 hello.txt
,内容就一行文字:
Hello jMonkeyEngine!
下面通过多个例子,演示如何读取这个文件。
使用JavaIO
很多Java程序员在接触 “框架” 后会养成一种思维定式,认为什么事都应该按照 “框架” 的方式来做。完全忘记了 “框架” 也是用Java开发的,既然是Java的类和对象,自然就可以用Java I/O来读写文件。
事实上,你完全可以不使用 AssetManager
来读文件。
编写一个测试类,读取这个文件:
package net.jmecn.assets;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
public class TestReadFile {
public static void main(String[] args) {
try {
// 打开文件
File file = new File("assets/hello.txt");
InputStream in = new FileInputStream(file);
// 读取文件
Scanner scanner = new Scanner(in);
String line = scanner.nextLine();
System.out.println(line);
// 关闭文件
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输入结果:
Hello jMonkeyEngine!
那么,如果让 TestReadFile 类继承 SimpleApplication 类,代码又应该怎么写呢?
这么写:
package net.jmecn.assets;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
import com.jme3.app.SimpleApplication;
public class TestReadFile extends SimpleApplication {
@Override
public void simpleInitApp() {
try {
// 打开文件
File file = new File("assets/hello.txt");
InputStream in = new FileInputStream(file);
// 读取文件
Scanner scanner = new Scanner(in);
String line = scanner.nextLine();
System.out.println(line);
// 关闭文件
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
TestReadFile app = new TestReadFile();
app.start();
}
}
运行结果:
一月 11, 2018 12:32:06 下午 com.jme3.system.JmeDesktopSystem initialize
信息: Running on jMonkeyEngine 3.2-stable
* Branch: HEAD
* Git Hash: 95d33e6
* Build Date: 2018-01-05
一月 11, 2018 12:32:11 下午 com.jme3.system.lwjgl.LwjglContext printContextInitInfo
信息: LWJGL 2.9.3 context running on thread jME3 Main
* Graphics Adapter: igdumd64
* Driver Version: 9.17.10.4229
* Scaling Factor: 1
一月 11, 2018 12:32:11 下午 com.jme3.renderer.opengl.GLRenderer loadCapabilitiesCommon
信息: OpenGL Renderer Information
* Vendor: Intel
* Renderer: Intel(R) HD Graphics
* OpenGL Version: 3.1.0 - Build 9.17.10.4229
* GLSL Version: 1.40 - Intel Build 9.17.10.4229
* Profile: Compatibility
一月 11, 2018 12:32:11 下午 com.jme3.asset.AssetConfig loadText
一月 11, 2018 12:32:12 下午 com.jme3.audio.openal.ALAudioRenderer initOpenAL
信息: Audio Renderer Information
* Device: OpenAL Soft
* Vendor: OpenAL Community
* Renderer: OpenAL Soft
* Version: 1.1 ALSOFT 1.15.1
* Supported channels: 64
* ALC extensions: ALC_ENUMERATE_ALL_EXT ALC_ENUMERATION_EXT ALC_EXT_CAPTURE ALC_EXT_DEDICATED ALC_EXT_disconnect ALC_EXT_EFX ALC_EXT_thread_local_context ALC_SOFT_loopback
* AL extensions: AL_EXT_ALAW AL_EXT_DOUBLE AL_EXT_EXPONENT_DISTANCE AL_EXT_FLOAT32 AL_EXT_IMA4 AL_EXT_LINEAR_DISTANCE AL_EXT_MCFORMATS AL_EXT_MULAW AL_EXT_MULAW_MCFORMATS AL_EXT_OFFSET AL_EXT_source_distance_model AL_LOKI_quadriphonic AL_SOFT_buffer_samples AL_SOFT_buffer_sub_data AL_SOFTX_deferred_updates AL_SOFT_direct_channels AL_SOFT_loop_points AL_SOFT_source_latency
一月 11, 2018 12:32:12 下午 com.jme3.audio.openal.ALAudioRenderer initOpenAL
信息: Audio effect extension version: 1.0
一月 11, 2018 12:32:12 下午 com.jme3.audio.openal.ALAudioRenderer initOpenAL
信息: Audio max auxiliary sends: 4
Hello jMonkeyEngine!
相比之下,继承SimpleApplication之后,控制台只是额外多输出了一些启动信息。最终还是打印了一行 Hello jMonkeyEngine!
,证明文件读取成功了。
前后两个代码没有实质区别。只是从 main()
方法挪到了 simpleInitApp()
里面。如果你愿意,还可以定义一个类来专门进行文件操作,然后在 TestReadFile 类中调用它。与普通的Java程序没有任何区别。
使用AssetManager
使用 AssetManager,代码稍微有一点变化:
@Override
public void simpleInitApp() {
// 注册运行时当前目录
assetManager.registerLocator("./", FileLocator.class);
try {
// 打开文件
AssetInfo info = assetManager.locateAsset(new AssetKey("assets/hello.txt"));
InputStream in = info.openStream();
// 读取文件
Scanner scanner = new Scanner(in);
String line = scanner.nextLine();
System.out.println(line);
// 关闭文件
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
因为要从当前目录中加载资源,所以把 "./"
目录和 FileLocator.class
注册到了 AssetManager 中。然后通过 locateAsset()
方法得到 AssetInfo
对象。
如果把 assets
目录注册到 AssetManager 中,那么文件的路径需要从 assets/hello.txt
缩短为 hello.txt
。
@Override
public void simpleInitApp() {
// 注册运行时当前目录
assetManager.registerLocator("assets", FileLocator.class);
try {
// 打开文件
AssetInfo info = assetManager.locateAsset(new AssetKey("hello.txt"));
InputStream in = info.openStream();
// 读取文件
Scanner scanner = new Scanner(in);
String line = scanner.nextLine();
System.out.println(line);
// 关闭文件
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
小结
本章进一步分析了 AssetLocator 的原理及用法,下一章我们来尝试实现自定义 AssetLocator。