Unity

UnityNDK开发之通过ndk加载ExternalTexture

在游戏中有一些需求,例如相册,或者是聊天的自定义表情,这些时候我们通常需要直接从文件而不是从AssetBundle中读取图片,这个时候Unity提供了一个比较方便的方法:LoadImage

但是这个方法有意者巨大的缺陷,需要传入Texture大小相同的bytes,导致我们每加载一张图片就需要申请相对应的bytes数组,导致大量的GC出现。这个时候,我们想到上一篇文章中我们通过指针的方式来读取数组……Unity是否提供了这样的方法呢,答案是肯定的:

还有另外一种方案,就是直接把图片的指针(id)传给Unity:

这一次我使用的是第二种方案:

实际上也有人这么干过:hecomi/UnityNativeTextureLoader​github.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/libpng​github.com图标

可以直接通过autoconf或者cmake进行编译,对编译不熟悉的同学可以直接使用另外一个已经写好了给安卓的libpng的工程:julienr/libpng-android​github.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;

发表评论

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