第十七章: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文件,这种路径叫做 classpathclasspath 不止一个。在开发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。