其实发文章只是想发图(???
上一篇文章中我们知道了如何简单编写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的内容。