通用编程

UnityNDK开发小试牛刀之NDK实现同步读取Android下的StreamingAssets资源

其实发文章只是想发图(???

上一篇文章中我们知道了如何简单编写NDK代码,这时我们就小试牛刀,看看NDK在什么情况下可以为我们所用!

在Unity开发当中StreamingAssets始终是一个比较重要的目录,里面一般会放一些重要的资源,配置等等,在PC以及iOS等平台,我们能够直接通过文件的形式访问到StreamingAssets文件夹下的文件,但是在安卓平台下,这些文件是经过压缩的。

实际上这些资源在android下是通过AssetManager进行管理的。我们可以通过Android的JavaAPI进行直接的读取。如下代码:

public byte[] LoadByAndroid(String path, long offset, long length) {

        try {

            assetManagerInputStream = assetManager.open(path);
            if (assetManagerInputStream == null)
                SDKManager.GetInstance().ULogError("LoadError:" + path);
            return readTextBytes(assetManagerInputStream, offset, length);

        } catch (IOException e) {

            SDKManager.GetInstance().ULogError(e.getMessage());

        } finally {
            try {
                assetManagerInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.gc();
        }
        return null;
    }

    private byte[] readTextBytes(InputStream inputStream, long offset, long length) {

        if (outputStream == null)
            outputStream = new ByteArrayOutputStream();

        //长度这里暂时先写成1024 * 64
        byte[] result = null;

        int len = 0;
        long needToRead = length;//已读长度

        try {
            long at = offset;
            //反复skip以防止失败
            while (at > 0) {
                long amt = inputStream.skip(at);
                if (amt < 0) {
                    SDKManager.GetInstance().ULogError("Android ReadBytes unexpected EOF");
                    throw new RuntimeException("Android ReadBytes unexpected EOF");
                }
                at -= amt;
            }

            //读取所需长度
            while (needToRead > 0) {
                len = inputStream.read(buf, 0, (int) Math.min(needToRead, buf.length));
                if (len <= 0) {
                    SDKManager.GetInstance().ULogError("Android ReadBytes unexpected EOF When Reading");
                    throw new RuntimeException("Android ReadBytes unexpected EOF When Reading");
                }

                outputStream.write(buf, 0, len);
                needToRead -= len;
            }

            result = outputStream.toByteArray();
            return result;

        } catch (IOException e) {
            SDKManager.GetInstance().ULogError(e.getMessage());
        } finally {
            outputStream.reset();
        }
        return null;
    }

但是在实际的使用过程当中我们会发现,如果我们将Java堆当中的bytes直接通过Unity提供的CallFunc的方式调用的话每一次调用都会带来相应bytes大小的GCAlloc,导致大量的GC出现,所以这个时候NDK就需要出来帮忙了。

实际上NDK中的C++代码不仅仅是java虚拟机可以通过jni的方式调用,C#也可以直接通过PInvoke的方式进行调用。

使用ndk读取assetmanager的代码如下:

JNIEXPORT int32_t JNICALL ReadAssetsBytes(char* fileName, unsigned char** result){

    if(assetManager == nullptr){
        return -1;
    }
    AAsset* asset = AAssetManager_open(assetManager, fileName, AASSET_MODE_UNKNOWN);
    if(asset == nullptr){
        return -1;
    }
    off_t size = AAsset_getLength(asset);
    if(size > 0){
        *result = new unsigned char[size];
        AAsset_read(asset, *result, size);
    }
    AAsset_close(asset);

    return (int32_t)size;
}

JNIEXPORT int32_t JNICALL ReadAssetsBytesWithOffset(char* fileName, unsigned char** result, int32_t offset, int32_t length){

    if(assetManager == nullptr){
        return -1;
    }
    AAsset* asset = AAssetManager_open(assetManager, fileName, AASSET_MODE_UNKNOWN);
    if(asset == nullptr){
        return -1;
    }
    off_t size = AAsset_getLength(asset);
    if(size > 0){
        try {
            *result = new unsigned char[length];
            AAsset_seek(asset, offset, SEEK_SET);
            AAsset_read(asset, *result, length);
        }catch (std::bad_alloc){
            *result = nullptr;
            return -1;
        }
    }
    AAsset_close(asset);

    return (int32_t)length;
}

JNIEXPORT int32_t JNICALL ReadRawBytes(char* fileName, unsigned char** result){
    if(fileName == nullptr){
        return -1;
    }
    FILE* file = fopen(fileName, "r");
    if(file == nullptr){
        return -2;
    }
    fseek(file, 0L, SEEK_END);
    int32_t size = ftell(file);
    if(size <= 0){
        return -3;
    }
    *result = new uint8_t[size];
    fseek(file, 0, SEEK_SET);
    fread(*result, sizeof(uint8_t), static_cast<size_t>(size), file);
    fclose(file);
    return size;
}


JNIEXPORT void JNICALL ReleaseBytes(unsigned char* bytes){
    delete[] bytes;
}

上述的方法第一个直接通过AssetManager读取文件,另外一个则以offset的形式进行读取AssetManager的文件,第三个方法则是直接读取文件,例如沙盒目录的文件就可以使用该方法读取,最后一个则是释放bytes。

接下来我们需要将bytes读取到C#当中:

我们先将方法导出到C#当中:

public class ReadNativeByte
{

	public const string libName = "NativeLib";


	[DllImport(libName)]
	public static extern int ReadAssetsBytes(string name, ref IntPtr ptr);
	
	[DllImport(libName)]
	public static extern int ReadAssetsBytesWithOffset(string name, ref IntPtr ptr, int offset, int length);
	
	
    [DllImport(libName)]
    public static extern int ReadRawBytes(string name, ref IntPtr ptr);

    [DllImport(libName)]
	public static extern void ReleaseBytes(IntPtr ptr);
	
}

这样我们就可以直接调用C++的方法了。

然后我们编写读取数据的代码:

var ptr = IntPtr.Zero;
int size = ReadNativeByte.ReadAssetsBytesWithOffset("Test.txt", ref ptr, sizeof(int), sizeof(int));
Debug.Log("Size:" + size.ToString());
if (size > 0)
{
	if (ptr == IntPtr.Zero)
	{
           Debug.LogError("Read Failed!!!");
	}
	stream.SetLength(size);
	stream.Position = 0;
	Marshal.Copy(ptr, stream.GetBuffer(), 0, size);
	var reader = new BinaryReader(stream);
	Debug.Log(reader.ReadInt32().ToString());
	ReadNativeByte.ReleaseBytes(ptr);
}

可以看到我们这里使用了流作为缓冲区,流里面的buffer可以反复为我们利用,这样我们就可以通过Marshal.Copy不断将数据写入到流中,当我们需要使用的时候我们就可以直接读取数据。

下面同样的,是直接读取文件的方法:

var ptr = IntPtr.Zero;
var size = ReadNativeByte.ReadRawBytes(rawBytesPath, ref ptr);
if (size <= 0)
{
	Debug.LogError(string.Format("read error errcode={0}", size.ToString()));
	return;
}
stream.SetLength(size);
stream.Position = 0;
Marshal.Copy(ptr, stream.GetBuffer(), 0, size);
var reader = new BinaryReader(stream);
Debug.Log(reader.ReadString());
ReadNativeByte.ReleaseBytes(ptr);

以上,通过这样的方法,我们可以毫无GC地同步读取Android下StreamingAssets的代码。如果不使用Java也不使用NDK的情况下,如果我们使用WWW去加载,不仅仅无法同步读取,其带来的GC也是不可小觑的。

接下去我们可能会提升一些难度,加入UnityNativePlugin以及OpenGLES的内容,有关于NDK调用libpng的内容。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注