偷得浮生半桶水(半日闲), 好记性不如抄下来(烂笔头). 信息爆炸的时代, 学习是一项持续的工作.
全部博文(1748)
分类: Android平台
2016-04-14 18:52:09
前段时间开发Unity Android项目的时候需要在Unity C# Script、Native Code和Java三个模块之间相互调用,因此总结了一下这几种代码之间的调用方法,以防忘记。这篇先看看Unity C#与Native Code之间的相互调用。
一、Unity C#调用Native Code
最常见的应该是在游戏的C#脚本中调用Native Code中的方法,从层级来看可以视为游戏上层调用下层封装的函数,具体实现如下:
1、编写C、C++代码,导出相应函数或变量,下面代码是.h和.cpp中对应的代码:
//In NativeCode.h
#ifndef __NATIVE_CODE__
#define __NATIVE_CODE__
extern "C"
{
float AddFun(float x, float y);
}
#endif
//In NativeCode.cpp
#include "NativeCode.h"
extern "C"
{
float AddFun(float x, float y)
{
return x+y;
}
}
2、编写Android.mk,将mk文件和c、cpp文件放到jni文件夹下,使用NDK(ndk-build命令)将Native Code编译成.so文件。开发过Android项目的同学应该都非常熟悉Android.mk和Application.mk,这里就不详细介绍了,以下是Android.mk中的内容:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := NativeEngine
LOCAL_C_INCLUDES := $(LOCAL_PATH)
LOCAL_SRC_FILES := NativeCode.cpp
LOCAL_LDLIBS := -llog -landroid
include $(BUILD_SHARED_LIBRARY)
其中LOCAL_MODULE为模块的名字,生成文件的名字会自动带上lib前缀和.so后缀,比如上面使用ndk编译出的动态库的名字就是libNativeEngine.so;LOCAL_C_INCLUDES和LOCAL_SRC_FILES为头文件和源文件列表,注意路径;LOCAL_LDLIBS为编译模块要使用的附加连接器选项;最后BUILD_SHARED_LIBRARY表示编译成动态库(.so),相反使用LOCAL_STATIC_LIBRARY表示编译成静态库(.a)。
这应该是最简单的Android.mk了,实际项目中的Android.mk可能远比这个复杂,例如Native code中引用了其他库文件或者启用一些编译优化选项等等。
3、将编译好的libNativeEngine.so文件放入Unity工程Asset\Plugins\Android目录下,如果在Eclipse中调试则放入Android项目下的libs\armeabi-v7a目录下。
4、在Unity中编写C#脚本,引用so文件,声明导出的接口,代码如下:
//In Unity C# Script
using UnityEngine;
using System.Collections;
using System.Runtime.InteropServices;
public class CallNativeCode : MonoBehaviour
{
[DllImport("NativeEngine")]
public static extern floatAddFun(float x, float y);
void Start()
{
float addResult =AddFun(2.5f, 8.8f);
Debug.Log("Addresult is: " + addResult);
}
void Update()
{
}
}
通过DllImport引用Native模块的名字,注意需要去掉lib前缀和.so后缀,当然DllImport还包含一些缺省参数,这里就不一一介绍了,从字面上也很好理解:
5、通过Unity编译打包生成apk,安装并运行,使用adb logcat命令或者Eclipse 的LogCat查看结果,下图可以看到Unity C#调进了Native Code的函数:
方法很简单,但是也有一些需要注意的地方:
(1)如果程序提示找不到函数入口点,请查看导出的函数名字是否和声明的名字一致,如果不使用extern “C”,g++会对导出的函数进行签名,导出的接口除了函数名字还包含一些其他的字符。此时可以将DllImport的EntryPoint设置为包含其他字符的函数名字,或者在Native code导出时加上extern“C”,使导出的函数名和声明的函数名一致。Tips:extern “C”告诉链接器在链接的时候用C函数规范来链接,主要原因是C++和C程序编译完成后在目标代码中命名规则不同。
(2)最常见的问题应该是函数参数的传递上了,传值的还好,native层不会去修改传入的参数,但如果需要native层修改C#传入的参数就应该设置为传引用:
//In NativeCode.cpp
#include "NativeCode.h"
extern "C"
{
float AddRefFun(float* x, float* y)
{
*x = 3.2;
*y = 2.4;
return *x + *y;
}
}
//In Unity C# Script
using UnityEngine;
using System.Collections;
using System.Runtime.InteropServices
public class CallNativeCode : MonoBehaviour
{
[DllImport("NativeEngine")]
public static extern floatAddRefFun(ref float x, ref float y);
void Start ()
{
float x = 2.5f;
float y = 8.8f;
float addResult =AddRefFun(ref x, ref y);
Debug.Log("X is:" + x);
Debug.Log("Y is:" + y);
Debug.Log("Addresult is: " + addResult);
}
void Update ()
{
}
}
结果可以看出,Native code修改了C#传入的参数:
最后总结了一下C#和Native code之间常见不同参数类型的调用方法:
a、参数为基本类型,如int, float, char等:
//In Native Code
void Fun(int value);
void Fun(float value);
void Fun(char ch);
//In Unity C# Script
[DllImport("xxx")]
public static extern void Fun(Int32 value);
[DllImport("xxx")]
public static extern void Fun(float value);
[DllImport("xxx")]
public static extern void Fun(char ch);
b、参数为基本指针类型,如int*, float*, char*等:
//In Native Code
void Fun(int* value);
void Fun(float* value);
void Fun(char* ch);
//In Unity C# Script
[DllImport("xxx")]
public static extern void Fun(ref Int32 value);
[DllImport("xxx")]
public static extern void Fun(ref float value);
Native code参数为char*,C#有多种声明方式:
[DllImport("xxx")]
public static extern void Fun(string ch);//ch的内容不会被更改
[DllImport("xxx")]
public static extern void Fun(StringBuilder ch);//ch的内容可以被更改
c、参数为结构体,C#中一般使用MarshalAs属性来指示如何在托管代码和非托管代码之间传递数据(注意:定义结构体时应该考虑字节对其的问题):
//In Native Code
struct stEvent
{
int value; //基本类型
char ch; //基本类型
int number[100]; //数组
char buffer[100]; //字符串数组
};
void Fun(stEvent tEvent);
//In Unity C# Script
[StructLayout(LayoutKind.Sequential)]
public struct stEvent
{
public Int32 value;
public char ch;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 100)]
public Int32[] number;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 100)]
public char[] buffer;
}
[DllImport("xxx")]
public static extern void Fun(stEvent tEvent);
d、参数为结构体指针
//In Native Code
void Fun(stEvent* ptEvent);
//In Unity C# Script
[DllImport("xxx")]
public static extern void Fun(ref stEvent ptEvent);
二、Native Code调用Unity C#
通常在Native Code完成了某个任务后需要及时通知给游戏,从层级来看可以视为下层封装的函数同步回调至游戏上层,实现思路如下:
我们知道C++中有函数指针回调的概念,通过函数指针可以实现函数回调,C#是托管语言,没有指针的概念,但是有个和函数指针很类似的委托(delegate),我们可以在C#中定义一个委托,并将这个委托设置进Native Code,这样调用Native的函数指针就相当于直接调用上层C#的函数了,直接看代码:
//In NativeCode.h
#ifndef __NATIVE_CODE__
#define __NATIVE_CODE__
extern "C"
{
typedef void(*CALLBACK)(const char*);
staticCALLBACK callback; //Native层的全局回调指针
voidSetCallBackAndRun(CALLBACK cb);
}
#endif
//In NativeCode.cpp
#include "NativeCode.h"
extern "C"
{
voidSetCallBackAndRun(CALLBACK cb)
{
callback = cb; //cb相当于C#中的函数地址
callback("Hello World!");
}
}
//In Unity C# Script
using UnityEngine;
using System.Collections;
using System.Runtime.InteropServices;
public class CallNativeCode : MonoBehaviour
{
public delegate voidcallbackDelegate(string str);
[DllImport("NativeEngine")]
public static extern voidSetCallBackAndRun(callbackDelegate cb);
void Start ()
{
SetCallBackAndRun(newcallbackDelegate(PrintString));
}
void PrintString(string str)
{
Debug.Log("CallPrintString");
Debug.Log(str);
}
void Update ()
{
}
}
从LogCat看到,Native Code通过callback成功回调到了C#中的PrintString方法中:
先写到这,下次再来总结UnityC#和Java之间的调用吧。
hagendashao2016-05-02 11:57:44
楼主你好~!请问Android.mk的几个问题:
LOCAL_LDLIBS := -llog -landroid 编译选项是必备的吗,我用该选项提示没有找到android的lib。
如果不是必备的,我去掉这个选项后,编译出了armeabi版本的so库。在Android调用运行提示!error: No JNI_OnLoad() found.
如果是必备的,那android的lib应该是在哪里呢?