Unity 与 NDK开发

Unity 与 C++

C#是Unity的官方推荐的开发语言。如果某些逻辑需要C++支持以提供高性能特性,或者需要C++去跟底层硬件或者操作系统级别的接口交互,那么就需要用C#去调用C++的接口。

这往往依赖动态链接。即用C++开发动态链接库,然后C#调用动态链接库里暴露的接口。

而在Android上开发动态链接库(.so文件)的方法,就是NDK开发

NDK开发动态链接库

NDK

NDK 全称原生开发工具包(Native Development Kit),是Google提供的在安卓上直接使用C++/C之类的底层语言进行原生开发的方式。原生开发意味着你的代码不会产生类似java字节码那样需要被虚拟机代理解释的代码,而是直接被编译成本机处理器能够识别的机器码。为此你需要先下载这整个工具包:下载地址

注意需要下载的版本。如果你的NDK同时也为Unity的IL2cpp服务,那就需要注意Unity对NDK的版本有硬性要求,比如2018的版本要求 NDK r16b的版本(我不清楚如果仅仅用作NDK开发,用其他版本的NDK会不会有影响)。

下载下来的压缩包,解压之后会得到如下目录:
目录
最重要的是那个叫做 ndk-build.cmd 的脚本文件。它是一个基于make的构建脚本。查看它的源码你会发现它实际上执行的是:

make.exe -f ./build/core/build-local.mk SHELL=cmd ...

其中,build-local.mk可以在上述的路径中找到,...表示从命令行中传过来的所有参数。为了能够在全局使用这个脚本,你最好将这个目录加入到环境变量中。

利用NDK构建.so

现在开始利用NDK构建.so文件,目的是在.so文件中暴露一个函数给C#调用。这个函数是自定义的加法函数,它会将两个整型相加,然后乘以10,再返回结果。新建一个 main.cppmain.h。内容分别如下:

// main.cpp
extern "C"
{
	int MyAdd(int a, int b)
	{
		return (a+b)*10;
	}
}
// main.h
extern "C"
{
	__declspec(dllexport) int MyAdd(int a, int b);
}

上面代码的关键点有:

  1. extern “C”:指示编译器将这段代码按照C语言的方式进行编译。由于C++支持重载的缘故,C++的编译器编译代码之后,其函数的名称与其在符号表中对应的符号名一般是不相等的。
  2. __declspec(dllexport):在动态链接库中,需要导出的符号(变量,函数,结构体,等)都需要使用这个修饰前缀。

我们将在这两个文件所在的目录下运行ndk-build.cmd,在此之前,我们还需要两个文件:Android.mkApplication.mk。这两个文件将包含 ndk-build 所需要的所有配置。两个文件的内容如下:

// Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE     :=  NDKTest
LOCAL_C_INCLUDES := $(LOCAL_PATH)
LOCAL_SRC_FILES  := main.cpp
LOCAL_CFLAGS    := -DANDROID_NDK

include $(BUILD_SHARED_LIBRARY)
// Application.mk
APP_STL := gnustl_static
APP_CPPFLAGS := -frtti -std=c++11
APP_PLATFORM := android-19
APP_CFLAGS += -Wno-error=format-security
APP_BUILD_SCRIPT := Android.mk
APP_ABI := armeabi-v7a,,arm64-v8a

Android.mk 为 ndk-build描述了构建所需的源代码,链接库等信息。

变量描述
LOCAL_PATH这个变量描述了构建所需的源代码在当前工程中的相对位置。如果你是使用Android Studio进行开发,那源代码一般位于工程的 app/src/main/cpp 目录下。$(call my-dir) 返回Android.mk所在的目录。
CLEAR_VARS将一个特殊的Makefile文件包含进来。CLEAR_VARS定义了这个文件的位置。这个Makefile文件会清除所有之前定义过的Android.mk变量,一般都以LOCAL_开头
LOCAL_MODULEndk-build构建出来的库的名称,如果设定的名称没有以lib开头,则ndk-build会自动为你加这个前缀。例如,上面定义的名称将构建出来一个叫libNDKTest.so的库
LOCAL_C_INCLUDES当你的代码中include一些文件不在默认的搜索路径时,你可以使用这个变量去指定。一般来说,标准c头文件,和一些常见的Android头文件,都是在默认的搜索路径中的,他们都可以在你下载的NDK工具包中找得到
LOCAL_SRC_FILES需要构建的源文件(不包含头文件)都需要放到这里面
LOCAL_CFLAGS这个变量定义一些ndk-build的编译选项,一般来说,不需要额外的设置
BUILD_SHARED_LIBRARY将一个特殊的构建文件包含进来。BUILD_SHARED_LIBRARY定义了这个文件的位置。该文件会收集你在定义的所有上述LOCAL_开头的变量所包含的信息,然后确定如何从源文件中构建动态链接库。这个构建脚本要求你至少定义了LOCAL_MODULELOCAL_SRC_FILES两个变量。

Application.mk 为ndk-build描述了项目的构建性质。

变量描述
APP_STL需要使用哪些C++标准库
APP_CPPFLAGS项目中,C++部分使用的编译选项
APP_PLATFORM构建的Android API级别,对应于应用程序的min SDK Version
APP_CFLAGS项目中,C/C++部分使用的编译选项
APP_BUILD_SCRIPTAndroid.mk 文件所在的位置
APP_ABI需要为哪些abi机器构建共享库

然后我们在终端输入以下命令:

ndk-build.cmd NDK_PROJECT_PATH=. NDK_APPLICATION_MK=Application.mk

前者指定ndk-build的工程的根目录,后者指定配置文件的位置。输出如下:
在这里插入图片描述
你会得到一个libs目录和一个obj中间文件暂存目录。libs目录里面就是对应你所设置的abi平台对应的共享库
在这里插入图片描述

在Unity中使用.so

只有将.so文件放在/Assets/Plugins/Android/libs下,Unity才会将.so文件识别为共享库,并在打包时将之拷贝到合适的地方。

将我们打包出来的.so文件放到上述的目录下。因为不同abi的共享库名称都一样,为避免冲突,我们拷贝它的任意父目录即可。无论.so存放到哪儿,Unity都会识别出这个共享库属于哪个abi平台的。项目目录结构如下:
在这里插入图片描述
在场景中新建一个空物体GameMgr,为其挂上一个脚本GameMgr.cs,脚本加载动态链接库,引用函数,计算MyAdd(233, 2)的结果,然后在一个Text UI上显示出结果(记得加上平台判定宏,IOS不支持装载动态链接库):

using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.UI;

public class GameMgr : MonoBehaviour
{
    public Text text;

#if UNITY_ANDROID
    [DllImport("NDKTest")]
    static extern int MyAdd(int a, int b);
#endif
    void Start()
    {
#if UNITY_ANDROID
        text.text = MyAdd(233, 2).ToString();
#endif
    }
}

因为我们做了加法之后还要乘以10,所以预期结果应该是 2350。
打包apk,安装,运行结果如下:
在这里插入图片描述

其他返回值

除了int的对应之外,还可以返回其他类型的变量:
字符串

//******** C **********
char* ReturnString() 
{
	char* a = (char*)malloc(100 * sizeof(char));
    strncpy(a, "Hello World From ReturnString", 100);
    return a;
}

//******** C# *********
string ReturnString(); 

整型数组

//******** C **********
void *ReturnIntPtr(int size)
{
    int *a = (int*)malloc(size * sizeof(int));
    for (int i = 0; i < size; i++)
    {
        a[i] = i;
    }
    return (void*)a;
}

//******** C# *********
IntPtr ReturnIntPtr(int size);
void Start() // C# 读取数组
{
	int size = 4;
	IntPtr p = ReturnIntPtr(size);
	for(int i=0;i<size;i++)
    {
        int r = Marshal.ReadInt32(ret, i*sizeof(int));
        Debug.Log("r = " + r);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值