Unity支持的Android插件

Unity的Android插件可以通过以下方式提供。

  • AAR:一个压缩包

  • Android Library项目,使用Android Studio创建的一个代码目录。Unity会自动扫描Assets下面的目录,如果一个目录下面有mylibrary.androidlib文件,则Unity将其视为一个AndroidLibrary项目。

  • jar插件:jar插件就是一个jar包,放在Assets目录下任意文件夹即可。

  • 直接把Java源文件放在Plugins里面

C#调用Java

Unity调用JNI的原理:实际上Unity是无法直接调用Java的,它只能直接调用C++。但是在Android平台上调用Java是一个非常强烈的需求。而JNI接口又几乎是不怎么变化的,因此Unity团队把JNI(C++)的头文件用C#封装了一遍,从而可以在C#里面调用JNI。JNI不够好用,又对它使用AndroidJavaObject系列包装了一层。

Unity C#调用Java插件的有三层封装,对应三种级别的API。

第一层:AndroidJNI是调用原始JNI接口的包装器。这个类底层实现是C语言,这个类的所有方法都是静态的,并且与Java原生接口一一对应。可以认为,它就是JNI的原始封装,它所有的方法都是静态方法。

第二层:AndroidJNIHelper是AndroidJNI的一些便利封装。

第三层:AndroidJavaObject和AndroidJavaClass是对AndroidJNI和AndroidJNIHelper的封装。在使用JNI调用时自动执行许多任务,并且使用缓存加快Java的调用速度。AndroidJavaObject对应java.lang.Object,AndroidJavaClass对应java.lang.Class,它们的封装也是一一映射的。它们基本上提供与Java端的三种交互:

  • 调用一个方法

  • 获取字段的值

  • 设置字段的值

这三种交互每种都包含两种调用:操作实例和操作类的静态成员。

总结一下,C#调用Java的三种方式

  • 裸用AndroidJNI,比较底层

  • AndroidJNIHelper+AndroidJNI

  • AndroidJavaObject和AndroidJavaClass,高级API。

实例一:实例化对象

AndroidJavaObject jo = new AndroidJavaObject("java.lang.String", "some_string"); 
  // jni.FindClass("java.lang.String");
  // jni.GetMethodID(classID, "<init>", "(Ljava/lang/String;)V");
  // jni.NewStringUTF("some_string");
  // jni.NewObject(classID, methodID, javaString);
  int hash = jo.Call<int>("hashCode"); 
  // jni.GetMethodID(classID, "hashCode", "()I");
  // jni.CallIntMethod(objectID, methodID);

实例化内部类的时候,使用$

内部类必须使用 $ 分隔符。请使用 android.view.ViewGroup$LayoutParams

实例二:获取应用程序的缓存目录

AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); 
 // jni.FindClass("com.unity3d.player.UnityPlayer"); 
 AndroidJavaObject jo = jc.GetStatic <AndroidJavaObject>("currentActivity"); 
 // jni.GetStaticFieldID(classID, "Ljava/lang/Object;");  
 // jni.GetStaticObjectField(classID, fieldID); 
 // jni.FindClass("java.lang.Object"); 

 Debug.Log(jo.Call <AndroidJavaObject>("getCacheDir").Call<string>("getCanonicalPath")); 
 // jni.GetMethodID(classID, "getCacheDir", "()Ljava/io/File;"); // 或其任何基类!
 // jni.CallObjectMethod(objectID, methodID); 
 // jni.FindClass("java.io.File"); 
 // jni.GetMethodID(classID, "getCanonicalPath", "()Ljava/lang/String;"); 
 // jni.CallObjectMethod(objectID, methodID); 
 // jni.GetStringUTFChars(javaString);

实例三:Java将数据传递到Unity

UnityActivity有一个UnitySendMessage方法,用于将消息发送给Unity。

UnitySendMessage(gameObjectName,gameObjectCallbackFunctionName,realMessage)

using UnityEngine;

public class NewBehaviourScript : MonoBehaviour { 

    void Start () { 
        AndroidJNIHelper.debug = true; 
        using (AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) { 
        jc.CallStatic("UnitySendMessage", "Main Camera", "JavaMessage", "NewMessage");
        } 
    } 

    void JavaMessage(string message) { 
        Debug.Log("message from java: " + message); 
    }
}

实例四:尽量使用using来管理AndroidJavaClass和AndroidJavaObject的生命周期。

如果不使用using,则回收AndroidJavaClass、AndroidJavaObject的时候自动释放。

如果将 AndroidJNIHelper.debug 设置为 true,您将在调试输出中看到垃圾回收器的活动记录。

//安全地获取系统语言
void Start () { 
    using (AndroidJavaClass cls = new AndroidJavaClass("java.util.Locale")) { 
        using(AndroidJavaObject locale = cls.CallStatic<AndroidJavaObject>("getDefault")) { 
            Debug.Log("current lang = " + locale.Call<string>("getDisplayLanguage")); 

        } 
    } 
}

实例五:liangdong的例子,isLogin(activity)

Java侧

    
    public void GetUserInfo()
    {
        AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        AndroidJavaObject activityObject = jc.GetStatic<AndroidJavaObject>("currentActivity");
        Debug.Log("Login  update activityObject " + activityObject);

        AndroidJavaClass jc2 = new AndroidJavaClass("com.bytedance.picovr.sdk.usercenter.utils.UserCenterUtils");
        Boolean isLogin = jc2.CallStatic<Boolean>("isLogin", activityObject);
        Debug.Log("Login  update isLogin " + isLogin);
    }

C#侧


    public class LoginCallBack : MonoBehaviour
    {

        public void LoginSuccess(string loginInfo)
        {
            Debug.Log("LoginCallBack LoginSuccess :" + loginInfo);
            Text PhoneNumtext = GameObject.Find("PhoneNumText").GetComponent<Text>();
            Text GenderText = GameObject.Find("GenderText").GetComponent<Text>();
            Text UniqidText = GameObject.Find("UniqidText").GetComponent<Text>();

            if (loginInfo != null)
            {
                JsonData jsrr = JsonMapper.ToObject(loginInfo);
                jsrr["phone"].ToString();
                PhoneNumtext.text = "用户手机号码:" + jsrr["phone"].ToString();
                GenderText.text = "用户性别:" + jsrr["gender"].ToString();
                UniqidText.text = "用户ID:" + jsrr["uniqid"].ToString();
            } else {

                PhoneNumtext.text = "用户未登录";
            }
        }

        public void UnLogin(string message) {
            Text PhoneNumtext = GameObject.Find("PhoneNumText").GetComponent<Text>();
            PhoneNumtext.text = "用户未登录";
        }

    }

实例六:XR旧代码中的支付和登录

登录支付部分的代码,它会通过调用Java的方式来完成一些动作。

// Copyright © 2015-2021 Pico Technology Co., Ltd. All Rights Reserved.

#if !UNITY_EDITOR
#if UNITY_ANDROID
#define ANDROID_DEVICE
#elif UNITY_IPHONE
#define IOS_DEVICE
#elif UNITY_STANDALONE_WIN
#define WIN_DEVICE
#endif
#endif

using UnityEngine;

namespace Unity.XR.PXR
{
    public class PicoPaymentSDK
    {
        private static AndroidJavaObject _jo = new AndroidJavaObject("com.pico.loginpaysdk.UnityInterface");

        public static AndroidJavaObject jo
        {
            get { return _jo; }
            set { _jo = value; }
        }

        public static void Login()
        {
            AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
            AndroidJavaObject mJo = jc.GetStatic<AndroidJavaObject>("currentActivity");
            jo.Call("init", mJo);
            jo.Call("authSSO");


        }

        public static void Pay(string payOrderJson)
        {
            AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
            AndroidJavaObject mJo = jc.GetStatic<AndroidJavaObject>("currentActivity");
            jo.Call("init", mJo);
            jo.Call("pay", payOrderJson);

        }

        public static void QueryOrder(string orderId)
        {
            AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
            AndroidJavaObject mJo = jc.GetStatic<AndroidJavaObject>("currentActivity");
            jo.Call("init", mJo);
            jo.Call("queryOrder", orderId);

        }

        public static void GetUserAPI()
        {
            AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
            AndroidJavaObject mJo = jc.GetStatic<AndroidJavaObject>("currentActivity");
            jo.Call("init", mJo);
            jo.Call("getUserAPI");
        }
    }
}
AndroidJavaObject jo = new AndroidJavaObject("com.pico.loginpaysdk.UnityInterface");
public static void Login(){
    ...
    jo.Call("init", mJo);
    jo.Call("authSSO");
    ...
}


public static void Pay(string payOrderJson){
    ...
    jo.Call("init", mJo);
    jo.Call("pay", payOrderJson);
    ...
}

实例七:

string packageName = "com.unity3d.player.UnityPlayer";
              if (unityAcvity == null)
              {
                  unityAcvity = new AndroidJavaClass(packageName).GetStatic<AndroidJavaObject>("currentActivity");
              }
  //直接获取主Activity的类名
  AndroidJavaObject a = new AndroidJavaObject("packagename.classname");
  //获取指定包名下面的类
  classname.Call<returntype>(string methodname,params object[] args);
  // 通过AndroidJavaObject 中封装的方法获取调用类中的方法,同时传递参数,并获得返回类型
  这边只是其中的一种调用,在Api中还包含其他很多种可自行查阅

Java调用C#

Java要想调用C#,就需要引入Unity的clasess.jar,这样Java调用C#的时候才能找到一些关键类。

在项目的libs目录中添加Unity 安装目录中的classes.jar(该jar文件中的UnityPlayer类中的UnitySendMessage方法可以实现Java方法调用Unity GameObject上绑定的C#脚本中的方法)。同时在build.gradle文件中的dependencies添加compileOnly files('libs/classes.jar') 。

此处compileOnly表示只用于编译,不要把classes.jar打包进aar里面去。

Java调用C#有两种方式:

  1. UnitySendMessage:直接根据GameObject的名称找到GameObject,调用GameObject的某个函数。

  2. 定义一个C#类,定义一个Java类,直接调用Unity

UnitySendMessage

UnityPlayer.UnitySendMessage(String GameObjectName, String MethodName, String param)

这个函数只能向C#侧的某个GameObject发送消息,并且函数参数只能是string类型,C#侧可能需要解析string类型的消息。

// 需要先引入Unity的Jar包
import com.unity3d.player.UnityPlayer;
// 调用 object上 C# 脚本中的方法
UnityPlayer.UnitySendMessage("objectName", "methodName", "data");

示例一:loginpay中的实现

Android端,调用Java 方法进行数据处理 , 在回调中通知Unity端

public void init(Activity activity) {
    // 初始化
    ...
}

// 授权登录    
public void authSSO(){
   // 调用Java登录入口
   Login mLogin = new Login();
   mLogin.login(new Callback(){
       public void loginCallback(boolean isSuccess,String mesage){
           ...
           // 向 Unity 发送结果
           UnityPlayer.UnitySendMessage("PicoPayment", "LoginCallback", message);
           ...
       }
   });
}
// 支付
public void pay(String payOrderJson) {
    // 调用Java支付入口
    PayOrder order = PayOrder.parse(payOrderJson);
    PicoPay.getInstance(mUnityPlayerActivity).pay(order, new PaySDKCallBack(){
        @Override
        public void callback(String code, String msg) {
            ...
            // 向 Unity 发送结果
            UnityPlayer.UnitySendMessage("PicoPayment", "QueryOrPayCallback", msg);
            ...
        }
        ...
        ...
        
    });
}

示例二:Android改变画布颜色

Unity添加一个MonoBehavior

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UnityAndroidTest: MonoBehaviour
{   
    // 0. 该成员就是 Canvas 内部的 Text 控件.
    public Text text1;

    void Start()
    {
        // 1. 页面初始化时,初始化 Android 通信对象
        AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity");

        // 2.定义一个int变量
        int res = 0;

        try
        {
            // 3.调用Android平台的 increment 自增方法,该方法会返回一个 int 值
            res = jo.Call<int>("increment", 2);
            // 4.将返回值更新到 Text 上
            text1.text = res.ToString();
        }
        catch (Exception e)
        {
            text1.text = "error";
        }
    }

    void Update()
    {
        
    }

    // 5.这里我们向Android平台暴露一个方法,调用后可以改变Text的颜色
    public void ChangeColor()
    {
        text1.color = Color.red;
    }
}

Android调用Unity

public class UnityPlayerActivity extends Activity implements IUnityPlayerLifecycleEvents {
  // ...  
  // 省略其它代码
  // ...
  
  private int count = 1;
  
  // 1.Unity项目中的 UnityAndroidTest.cs 脚本,会主动唤起该方法
  public int increment(int value) {
      count += value;
      // 2.在这个方法中,我们通过发送消息,让 Canvas 执行 ChangeColor 方法
      //   将文字从黑色变成红色
      UnityPlayer.UnitySendMessage("Canvas", "ChangeColor", "");
      
      // 3.最终,将 int 类型的 count = 3 作为桥接方法的返回值返回
      return count;
  }
}

示例三:UnitySendMessageExtension

UnityPlayer.UnitySendMessage只能用来Java向C#发送一个通知,无法得到C#侧的Response。

基于UnitySendMessage可以实现带返回值的调用,原理就是C#侧主动把返回值推送给Java侧。

UnitySendMessage是一个同步函数,会等待C#侧函数执行结束。

Java侧的封装

public final class MyPlugin
{
    //Make class static variable so that the callback function is sent to one instance of this class.public static MyPlugin testInstance;

     public static MyPlugin instance()
     {
         if(testInstance == null)
         {
             testInstance = new MyPlugin();
         }
         return testInstance;
     }

    string result = "";


    public string UnitySendMessageExtension(string gameObject, string functionName, string funcParam)
    {
        UnityPlayer.UnitySendMessage(gameObject, functionName, funcParam);
        string tempResult = result;
        return tempResult;
    }

    //Receives result from C# and saves it to result  variablevoid receiveResult(string value)
    {
        result = "";//Clear old data
        result = value; //Get new one
    }
}

C#侧的配合

class TestScript: MonoBehaviour
{
    //Sends the data from PlayerPrefs to the receiveResult function in Javavoid sendResultToJava(float value)
    {
        using(AndroidJavaClass javaPlugin = new AndroidJavaClass("com.company.product.MyPlugin"))
        {
             AndroidJavaObject pluginInstance = javaPlugin.CallStatic("instance");
             pluginInstance.Call("receiveResult",value.ToString());
        }
    }

    //Called from Java to get the saved PlayerPrefsvoid testFunction(string key)
    {
        float value = PlayerPrefs.GetFloat(key) //Get the saved value from keysendResultToJava(value); //Send the value to Java
    }
}

在Java侧使用带返回值的UnitySendMessage

String str = UnitySendMessageExtension("ProfileLoad", "testFunction","highScore");

使用类

Unity
public class A :AndroidJavaProxy
{
    private const string interfaceName = "packageName.IB";
    public A() : base(interfaceName)
    {
    }
    func()
    {
    }
}

Android
public interface IB
{
    func();
}
public void setA(AscanMatcher) {
    func();
}
利用接口的方式,做回调函数。


Unity中C#与Java高频互调产生ANR

现象

ANR:Application Not Respond,应用无响应。与stackOverlow、segmentFault、nullPointerReference等著名错误类似,是一种错误的程序状态。

Unity中,C#与Java层的互相调用一般情况下没问题,但是在高频、互调的情况下会产生死锁,进而导致ANR问题。

首先,如果程序只涉及到C#调用Java,因为这个调用发生在主线程中,所以不会发生死锁问题。

其次,如果程序只是低频的出现Java调用C#,那么发生死锁的概率极低。

最后,如果程序高频出现C#调用Java和Java调用C#,那么出现死锁的概率就挺大。

C#调用Java一般都是在主线程中进行,Java调用C#则有可能是其它的线程。

当Java调用C#的时候,有一个加锁语句。如果C#和Java互调的时候,线程出现竞争,导致了死锁,那么就会出现程序卡住的情况。

解决方案

ANR产生的根本原因在于线程死锁。解决思路就是避免从Java侧主动调用C#。

实现方式是:保证Java调用C#的时候,统一收敛到UnityMain线程去执行。

在Java侧维护一个队列,Java想调用C#的时候,往Java的任务队列里面塞任务即可。

在C#侧创建一个MonoBehavior,每一帧调用Java,Java受到调用的时候,从队列里面弹出一个Runnable并执行之。

本文中第二部分介绍了很多Java调用C#的内容,其实这一部分内容是很危险的,只能在主线程中使用,不然就容易出现死锁。