在游戏中有一些需求,例如相册,或者是聊天的自定义表情,这些时候我们通常需要直接从文件而不是从AssetBundle中读取图片,这个时候Unity提供了一个比较方便的方法:LoadImage
但是这个方法有意者巨大的缺陷,需要传入Texture大小相同的bytes,导致我们每加载一张图片就需要申请相对应的bytes数组,导致大量的GC出现。这个时候,我们想到上一篇文章中我们通过指针的方式来读取数组……Unity是否提供了这样的方法呢,答案是肯定的:
还有另外一种方案,就是直接把图片的指针(id)传给Unity:
这一次我使用的是第二种方案:
实际上也有人这么干过:hecomi/UnityNativeTextureLoadergithub.com
很可惜他使用了WWW去加载bytes导致GC出现,我们会在他的基础上写的更好,实际上在最后的工程中我也实现了iOS版本的加载。
那么这个时候NDK大现身手的时候又到了。
在这之前,我们其实知道,Android使用的图形API是OpenGLES,我们可以调用Android的图形API来创建图片。
实际上Java也可以直接调用OpenGLES的API,但是这个地方有个坑,Android在多线程渲染的时候Java层是无法直接拿到OpenGL的上下文的,导致在开启多线程渲染的时候无法正常渲染图片。(然而iOS和PC都不存在这个问题)How to use unity CreateExternalTexture on Android?stackoverflow.com
所以我们选择了UnityNativePlugin配合NDK的方式来编写我们需要的代码。
在此之前,我们需要知道如何加载图片,这个时候我用到了一个比较知名的第三方库:libpng。glennrp/libpnggithub.com
可以直接通过autoconf或者cmake进行编译,对编译不熟悉的同学可以直接使用另外一个已经写好了给安卓的libpng的工程:julienr/libpng-androidgithub.com
然后我们进行jni的编写:
工程目录如下:
这里我们使用的是静态库,随后跟着ndk一起打入我们的.so文件。
首先看看我们的CMake文件:
cmake_minimum_required(VERSION 3.4.1)
include_directories(src/main/jni)
include_directories(src/main/jni/include/libpng)
file(GLOB_RECURSE *NDK_SRC*src/main/jni/*.cpp)
link_directories(src/main/jni/thirdpart/${ANDROID_ABI})
add_library( # Sets the name of the library.
NativeLib
SHARED
${NDK_SRC})
find_library(
android
log
GLESv1_CM
GLESv2)
target_link_libraries(
NativeLib
log
android
png
GLESv1_CM
GLESv2)
在普通的ndk开发之外我们需要引入GLES的库,所以我们就加入了GLESv1以及v2,详细库我们可以看官方文档。
Gradle中我们使用的c++14的库并且libpng需要链接libz所以加上了-lz的参数。(虽然这个写在cmake里面也行
我们编写了一个PngLoader进行图片的加载:
//
// Created by 董宸 on 2018/11/7.
//
#ifndef ANDROIDLIB_PNGLOADER_H
#define ANDROIDLIB_PNGLOADER_H
#include <cstddef>
#include <png.h>
#include <memory>
#include <GLES2/gl2.h>
namespace RONDK{
class PngLoader {
public:
void Load(const void *pData, size_t size);
void SetTexture(GLuint texture) { m_texture = texture; }
void UpdateTexture();
bool HasLoaded() const { return m_hasLoaded; }
int GetWidth() const { return m_width; }
int GetHeight() const { return m_height; };
private:
std::unique_ptr<unsigned char[]> m_data;
bool m_hasLoaded = false;
GLuint m_texture = 0;
GLenum m_format = 0;
GLint m_alignment = 1;
size_t m_dataSize = 0;
uint32_t m_width = 0;
uint32_t m_height = 0;
};
}
#endif //ANDROIDLIB_PNGLOADER_H
下面是实现
#include "PngLoader.h"
#include <functional>
#include <png.h>
#include <cstring>
namespace RONDK{
class ScopeReleaser
{
public:
using Func = std::function<void()>;
ScopeReleaser(const Func &func) : m_func(func) {}
~ScopeReleaser() { if (m_func) m_func(); }
private:
const Func m_func;
};
void PngLoader::Load(const void *pData, size_t size){
if(size < 8){
return;
}
const auto *pHeader = reinterpret_cast<const png_byte*>(pData);
if(png_sig_cmp(pHeader, 0, 8)){
return;
}
auto png = png_create_read_struct(
PNG_LIBPNG_VER_STRING,
nullptr,
nullptr,
nullptr);
if(!png){
return;
}
auto info = png_create_info_struct(png);
if(!info){
return;
}
ScopeReleaser releaser([&]{
png_destroy_read_struct(&png, &info, nullptr);
});
struct Data
{
const unsigned char *m_pData;
unsigned long m_offset;
};
Data data
{
static_cast<const unsigned char*>(pData),
8,
};
png_set_read_fn(
png,
&data,
[](png_structp png, png_bytep buf, png_size_t size)
{
auto &data = *static_cast<Data*>(png_get_io_ptr(png));
memcpy(buf, data.m_pData + data.m_offset, size);
data.m_offset += size;
});
png_set_sig_bytes(png, 8);
png_read_png(
png,
info,
PNG_TRANSFORM_STRIP_16 | PNG_TRANSFORM_PACKING | PNG_TRANSFORM_EXPAND,
nullptr);
const auto type = png_get_color_type(png, info);
switch (type)
{
case PNG_COLOR_TYPE_PALETTE:
png_set_palette_to_rgb(png);
m_format = GL_RGB;
m_alignment = 1;
break;
case PNG_COLOR_TYPE_RGB:
m_format = GL_RGB;
m_alignment = 1;
break;
case PNG_COLOR_TYPE_RGBA:
m_format = GL_RGBA;
m_alignment = 4;
break;
default:
return;
}
const size_t rowBytes = png_get_rowbytes(png, info);
m_width = png_get_image_width(png, info);
m_height = png_get_image_height(png, info);
m_data = std::make_unique<unsigned char[]>(rowBytes * m_height);
const auto rows = png_get_rows(png, info);
for (int i = 0; i < m_height; ++i)
{
const size_t offset = rowBytes * i;
memcpy(m_data.get() + offset, rows[i], rowBytes);
}
m_hasLoaded = true;
}
void PngLoader::UpdateTexture() {
if(!HasLoaded() || m_texture == 0)
return;
glBindTexture(GL_TEXTURE_2D, m_texture);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glPixelStorei(GL_UNPACK_ALIGNMENT, m_alignment);
glTexSubImage2D(
GL_TEXTURE_2D,
0,
0,
0,
m_width,
m_height,
m_format,
GL_UNSIGNED_BYTE,
m_data.get());
}
}
在pngloader中我们已经可以把图片创建出来了。
然后我们需要提供NativePlugin相应的事件方法:
这个方法会帮我们把渲染Texture的代码插入到渲染管线之中以保障我们的Texture能够正常加载出来,其中的callback是我们提供的C++的函数指针,在c++中的代码如下:
//
// Created by 董宸 on 2018/11/7.
//
#include <queue>
#include <jni.h>
#include "PngLoader.h"
using namespace RONDK;
using UnityRenderEvent = void(*)(int);
std::queue<PngLoader*> g_updateQueue;
extern "C"{
JNIEXPORT void* JNICALL CreateLoader(){
return new PngLoader();
}
JNIEXPORT void JNICALL DestroyLoader(PngLoader *pPngLoader){
if(pPngLoader == nullptr){
return;
}
delete pPngLoader;
}
JNIEXPORT bool JNICALL Load(PngLoader *pPngLoader, const void *pData, size_t dataSize){
if(pPngLoader == nullptr){
return false;
}
pPngLoader->Load(pData, dataSize);
return true;
}
JNIEXPORT bool JNICALL LoadWithPath(PngLoader *pPngLoader, char* fileName){
if(fileName == nullptr){
return false;
}
FILE* file = fopen(fileName, "r");
if(file == nullptr){
return false;
}
fseek(file, 0L, SEEK_END);
int32_t size = ftell(file);
if(size <= 0){
return false;
}
uint8_t* pData = new uint8_t[size];
fseek(file, 0, SEEK_SET);
fread(pData, sizeof(uint8_t), static_cast<size_t>(size), file);
fclose(file);
Load(pPngLoader, pData, static_cast<size_t>(size));
delete[] pData;
return true;
}
JNIEXPORT void JNICALL SetTexture(PngLoader *pPngLoader, GLuint texture){
if(pPngLoader == nullptr){
return;
}
pPngLoader->SetTexture(texture);
g_updateQueue.push(pPngLoader);
}
JNIEXPORT void JNICALL UpdateTextureImmediate(PngLoader *pPngLoader){
if(pPngLoader == nullptr){
return;
}
pPngLoader->UpdateTexture();
}
JNIEXPORT int32_t JNICALL GetWidth(PngLoader *pPngLoader){
if(pPngLoader == nullptr){
return 0;
}
return pPngLoader->GetWidth();
}
JNIEXPORT int32_t JNICALL GetHeight(PngLoader *pPngLoader){
if(pPngLoader == nullptr){
return 0;
}
return pPngLoader->GetHeight();
}
void OnRenderEvent(int eventId){
while(!g_updateQueue.empty()){
g_updateQueue.front()->UpdateTexture();
g_updateQueue.pop();
}
}
JNIEXPORT UnityRenderEvent JNICALL GetPngRenderEventFunc(){
return OnRenderEvent;
}
}
我们可以看到方法GetPngRenderEventFunc返回了一个函数指针,当我们加载一个贴图的时候我们会将其加入到队列之中,当我们调用了OnRenderEvent的时候,队列里的所有贴图都会进行更新。
然后我们就可以将这些方法进行导出:(我们可以先忽略iOS的代码)
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
public static class PngLib
{
#if UNITY_IOS
public const string libName = "__Internal";
#else
public const string libName = "NativeLib";
#endif
#if UNITY_ANDROID
[DllImport(libName)]
public static extern IntPtr CreateLoader();
[DllImport(libName)]
public static extern void DestroyLoader(IntPtr loader);
[DllImport(libName)]
public static extern bool Load(IntPtr loader, IntPtr data, int size);
[DllImport(libName)]
public static extern bool LoadWithPath(IntPtr loader, string fileName);
[DllImport(libName)]
public static extern void SetTexture(IntPtr loader, IntPtr texture);
[DllImport(libName)]
public static extern void UpdateTextureImmediate(IntPtr loader);
[DllImport(libName)]
public static extern int GetWidth(IntPtr loader);
[DllImport(libName)]
public static extern int GetHeight(IntPtr loader);
[DllImport(libName)]
public static extern IntPtr GetPngRenderEventFunc();
#elif UNITY_IOS
[DllImport(libName)]
public static extern IntPtr CreateLoader();
[DllImport(libName)]
public static extern bool Load(IntPtr loader, string fileName);
[DllImport(libName)]
public static extern void DestroyLoader(IntPtr loader);
[DllImport(libName)]
public static extern int GetWidth(IntPtr loader);
[DllImport(libName)]
public static extern int GetHeight(IntPtr loader);
[DllImport(libName)]
public static extern IntPtr GetTexturePtr(IntPtr loader);
[DllImport(libName)]
public static extern System.IntPtr CreateNativeTexture(string filename);
[DllImport(libName)]
public static extern System.IntPtr CreateNativeTextureWithFullPath(string filePath);
[DllImport(libName)]
public static extern void DestroyNativeTexture(System.IntPtr tex);
#endif
}
然后我们可以对这些方法进行封装了,并且保证在卸载的时候能够及时释放图片的内存:(同样地先忽略iOS的代码)
using System;
using System.Runtime.InteropServices.ComTypes;
using UnityEngine;
public class NativeTexture : IDisposable
{
public Texture2D Tex { get; private set; }
private IntPtr _loader;
public int Width { get; private set; }
public int Height { get; private set; }
public NativeTexture(string fileName)
{
#if UNITY_ANDROID
_loader = PngLib.CreateLoader();
if (!PngLib.LoadWithPath(_loader, fileName))
{
Debug.LogError("Load With Path Error");
return;
}
Width = PngLib.GetWidth(_loader);
Height = PngLib.GetHeight(_loader);
Tex = new Texture2D(Width, Height, TextureFormat.RGBA32, false);
PngLib.SetTexture(_loader, Tex.GetNativeTexturePtr());
NativeTextureManager.Instance.UpdateTexture();
#elif UNITY_IOS
_loader = PngLib.CreateLoader();
PngLib.Load(_loader, fileName);
Width = PngLib.GetWidth(_loader);
Height = PngLib.GetHeight(_loader);
Tex = Texture2D.CreateExternalTexture(Width, Height, TextureFormat.ARGB32, false, true,
PngLib.GetTexturePtr(_loader));
#endif
}
public void Dispose()
{
#if UNITY_ANDROID
PngLib.DestroyLoader(_loader);
UnityEngine.Object.Destroy(Tex);
Tex = null;
_loader = IntPtr.Zero;
Width = 0;
Height = 0;
GC.SuppressFinalize(this);
#elif UNITY_IOS
PngLib.DestroyLoader(_loader);
UnityEngine.Object.Destroy(Tex);
Tex = null;
#endif
}
}
我们使用了IDisposable的方式去写这个Loader,以保证我们的图片能够及时释放,需要注意的是Disposable的写法要标准,GC.SuppressFinalize保证了我们在手动Dispose之后GC的时候不会再次调用Dispose,否则指针释放两次可能会造成致命错误。(详细可以看Effective C#中对IDisposable的描述)
using System.Collections;
using UnityEngine;
public class NativeTextureManager : MonoBehaviour
{
#if UNITY_ANDROID
public static NativeTextureManager Instance { get; private set; }
private void Awake()
{
Instance = this;
}
private bool isWaitingToUpdate = false;
public void UpdateTexture()
{
if (isWaitingToUpdate)
{
return;
}
isWaitingToUpdate = true;
StartCoroutine(IssuePluginEvent());
}
private IEnumerator IssuePluginEvent()
{
yield return new WaitForEndOfFrame();
GL.IssuePluginEvent(PngLib.GetPngRenderEventFunc(), 0);
isWaitingToUpdate = false;
}
#endif
}
注意我们需要在帧的末尾对我们的回调进行调用,这样才能够帮助我们及时更新图片。
最后是Demo代码的呈现:
Debug.Log("Load " + imgpath);
if (!File.Exists(imgpath))
{
Debug.LogError(imgpath + " not exist!!!");
}
currentRawTexture = new NativeTexture(imgpath);
img.texture = currentRawTexture.Tex;