拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity与Android桥接原理:跨运行时通信与线程安全实践

Unity与Android桥接原理:跨运行时通信与线程安全实践

1. 为什么Unity调用Android方法不能只靠“写个Java类就完事”

很多刚接触Unity Android原生桥接的开发者,第一反应就是:在Android Studio里新建一个Java类,写个public static方法,然后在Unity里用AndroidJavaClass去调用——结果跑起来发现要么报ClassNotFoundException,要么调用成功但回调死活收不到,或者App一启动就闪退。我第一次做微信支付回调集成时,就在这个环节卡了整整三天,反复检查包名、类名、方法签名,最后发现根本不是代码写错了,而是对Unity与Android运行时环境的关系存在根本性误解。

Unity在Android平台并不是以“普通Java应用”的方式运行的。它本质是一个嵌入式C++引擎(libunity.so),通过JNI层与Android Runtime(ART)交互。所有C#脚本最终被AOT编译为本地机器码,而Java层代码则运行在独立的Dalvik/ART虚拟机中。这两套运行时之间没有共享内存、没有直接对象引用,一切通信都必须经过JNI桥接层严格序列化与反序列化。这意味着你不能把一个C#委托直接传给Java,也不能让Java对象在C#侧长期持有引用——因为GC策略完全不同,Java对象可能被ART回收,而C#侧还傻乎乎地拿着一个已失效的jobject句柄。

更关键的是,Unity的主线程(Main Thread)和Android的UI线程(Main Looper)并非同一个线程。Unity的Update、Start等生命周期方法运行在Unity主循环线程(即渲染线程),而Android的onCreate、onClick、Handler.post等回调默认发生在Android主线程。如果你在Java层直接调用Unity C#方法(比如通过UnityPlayer.UnitySendMessage),该调用会被压入Unity主线程的消息队列,但这个消息队列的消费时机由Unity引擎控制,不保证实时性;反过来,如果在Android主线程里直接操作Unity的MonoBehaviour实例(比如调用其public方法),极大概率触发线程安全异常,因为Unity API绝大多数都不是线程安全的。

所以,“桥接”这个词在这里不是简单的“连通”,而是一次跨运行时、跨线程、跨内存模型的精密协同工程。它要求你同时理解Unity的生命周期管理、Android的Activity/Service机制、JNI的类型映射规则、以及线程间通信的同步策略。漏掉其中任何一环,都会导致看似“逻辑正确”的代码在真机上表现诡异:回调丢失、数据错乱、ANR、甚至JNI crash。

这也是为什么网络上大量教程只教“怎么调用”,却很少讲“为什么这么调用”。比如热词里频繁出现的AndroidJavaObject,很多人把它当成万能胶水,以为new一个就能随便调方法。实际上,AndroidJavaObject封装的是一个jobject全局引用(Global Reference),它的生命周期管理完全依赖C#侧的Dispose调用。如果忘记Dispose,或者在Unity GameObject销毁时没及时释放,就会造成Java侧对象无法被GC回收,久而久之引发内存泄漏——这在长时间运行的AR应用或游戏大厅里尤为致命。

再看另一个高频热词“回调函数”。Unity里常见的做法是传一个delegate过去,但JNI层根本不认识C# delegate。真实实现中,必须借助Android的Handler机制或BroadcastReceiver,在Java侧构建一个可序列化的回调代理,再通过UnityPlayer当前Activity的上下文,把结果“推”回Unity主线程。这个“推”的过程,本质上是一次跨线程的异步消息投递,中间涉及Looper、MessageQueue、Handler的完整链路。如果没搞清这个链路,所谓“两段式回调”和“ABC回调”的区别,就只是名词游戏而已。

提示:不要试图在Java层直接new Unity的MonoBehaviour实例。Unity的GameObject和Component必须由Unity引擎在主线程创建和管理。Java层能做的,只是触发Unity侧预定义好的、线程安全的入口点(如静态方法或事件总线)。

2. 完整流程拆解:从Unity发起调用到Android回调落地的七步闭环

整个桥接流程绝非单向调用,而是一个包含初始化、调用、响应、回调、线程调度、资源清理、错误兜底的七步闭环。下面我以一个真实项目——某款工业巡检APP的扫码回调集成——为例,逐帧还原每一步的技术细节与决策依据。

2.1 第一步:Android端准备——构建可被Unity识别的Java入口类

这不是简单建个class就行。核心约束有三点:包名路径必须与Unity插件声明一致、必须继承自UnityPlayerActivity(或正确获取Application Context)、所有对外暴露方法必须为public且非static(除非明确设计为工具类)。

我们创建com.example.scan.ScannerBridge.java:

package com.example.scan; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.util.Log; import com.unity3d.player.UnityPlayer; // 注意:必须继承自UnityPlayerActivity,否则无法获取UnityPlayer实例 public class ScannerBridge extends UnityPlayerActivity { private static final String TAG = "ScannerBridge"; private static ScannerBridge instance; private static OnScanResultListener listener; // 构造函数必须public,供Unity反射调用 public ScannerBridge() { super(); } // Unity调用此方法初始化桥接器 public void initBridge() { Log.d(TAG, "ScannerBridge initialized"); instance = this; } // Unity调用此方法启动扫码 public void startScan() { if (instance == null) { Log.e(TAG, "ScannerBridge not initialized"); return; } // 使用Application Context避免Activity泄漏 Context context = getApplication().getApplicationContext(); Intent intent = new Intent(context, ScanActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } // Java层扫码完成后的回调入口(由ScanActivity触发) public static void onScanResult(String result) { Log.d(TAG, "Scan result received: " + result); if (listener != null) { // 关键:必须切回Unity主线程执行回调 UnityPlayer.currentActivity.runOnUiThread(new Runnable() { @Override public void run() { // 调用Unity侧C#方法,注意参数类型匹配 UnityPlayer.UnitySendMessage("ScanManager", "OnScanComplete", result); } }); } } // 设置回调监听器(由Unity侧传入) public static void setScanResultListener(OnScanResultListener l) { listener = l; } // 回调接口定义(仅用于Java内部逻辑,不暴露给Unity) public interface OnScanResultListener { void onResult(String result); } }

这里的关键设计点:

  • initBridge()方法是Unity侧主动调用的“握手协议”,确保Java端已准备好;
  • startScan()使用getApplicationContext()而非this,避免Activity Context导致的内存泄漏;
  • onScanResult()中runOnUiThread()是强制要求,因为UnitySendMessage必须在Unity主线程调用;
  • setScanResultListener()是预留扩展点,未来可支持Lambda回调,但当前版本暂未启用。

2.2 第二步:Unity侧初始化——加载Java类并建立双向引用

C#端不能直接new Java类,必须通过AndroidJavaClass和AndroidJavaObject配合。重点在于引用生命周期管理:

public class ScanManager : MonoBehaviour { private AndroidJavaObject scannerBridge; private AndroidJavaClass unityPlayer; private bool isBridgeReady = false; void Start() { // 检查是否为Android平台 if (Application.platform != RuntimePlatform.Android) return; try { // 1. 获取UnityPlayer类(必须,用于后续UnitySendMessage) unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); // 2. 加载ScannerBridge类(注意:这里是类名,不是实例) AndroidJavaClass bridgeClass = new AndroidJavaClass("com.example.scan.ScannerBridge"); // 3. 调用静态方法initBridge() —— 这会触发Java端构造函数和初始化 // 注意:此处调用的是类的静态方法,不是实例方法 bridgeClass.CallStatic("initBridge"); // 4. 创建ScannerBridge实例(关键!必须用new AndroidJavaObject) // 参数:类名、构造函数参数(此处无参) scannerBridge = new AndroidJavaObject("com.example.scan.ScannerBridge"); // 5. 验证实例是否有效(检查jobject是否为null) if (scannerBridge == null) { Debug.LogError("Failed to create ScannerBridge instance"); return; } isBridgeReady = true; Debug.Log("ScannerBridge initialized successfully"); } catch (System.Exception e) { Debug.LogError("Bridge initialization failed: " + e.Message); } } // 供Java层回调使用的公共方法(必须public,且参数类型严格匹配) public void OnScanComplete(string result) { Debug.Log("Scan result received in Unity: " + result); // 在此处处理扫码结果,比如更新UI、发送网络请求等 HandleScanResult(result); } void OnDestroy() { // 必须手动释放Java对象引用,防止内存泄漏 if (scannerBridge != null) { scannerBridge.Dispose(); scannerBridge = null; } if (unityPlayer != null) { unityPlayer.Dispose(); unityPlayer = null; } } }

这里容易踩的坑:

  • AndroidJavaClass用于调用静态方法(如initBridge),而AndroidJavaObject用于创建实例并调用实例方法(如startScan);
  • UnityPlayer.UnitySendMessage的第一个参数是GameObject的名字(不是组件名),必须确保场景中存在名为"ScanManager"的GameObject;
  • OnScanComplete方法必须是public,且参数类型只能是string、int、float等基础类型,复杂对象需JSON序列化。

2.3 第三步:Unity发起调用——安全传递参数与处理异常

调用startScan()看似简单,但背后有严格的JNI类型校验:

public void TriggerScan() { if (!isBridgeReady || scannerBridge == null) { Debug.LogWarning("ScannerBridge not ready, cannot trigger scan"); return; } try { // 调用Java实例的startScan()方法 // 注意:方法名、参数个数、参数类型必须与Java端完全一致 scannerBridge.Call("startScan"); } catch (AndroidJavaException e) { // JNI异常必须捕获,否则会导致Unity崩溃 Debug.LogError("AndroidJavaException during startScan: " + e.Message); // 可在此处触发降级方案,比如弹出Toast提示 ShowToast("扫码功能暂时不可用,请稍后重试"); } catch (System.Exception e) { Debug.LogError("Unexpected exception: " + e.Message); } }

关键细节:

  • Call("startScan")不带参数,对应Java端public void startScan();
  • 所有JNI调用都可能抛出AndroidJavaException,这是Unity封装的JNI底层错误,必须显式捕获;
  • 如果Java方法有参数,比如public void startScan(int timeout),则C#侧必须写成scannerBridge.Call("startScan", 5000),且参数类型要自动匹配(int→jint)。

2.4 第四步:Android端接收并处理——从Intent到业务逻辑

扫码Activity(ScanActivity.java)的实现必须遵循Android规范,并确保能正确回调:

public class ScanActivity extends AppCompatActivity { private static final int REQUEST_CODE_SCAN = 1001; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_scan); // 初始化扫码SDK(如ZBar、ZXing或厂商SDK) initScanner(); } private void initScanner() { // 此处省略具体SDK初始化代码 // 关键:扫码成功后,必须调用ScannerBridge.onScanResult() } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE_SCAN && resultCode == RESULT_OK) { String result = data.getStringExtra("scan_result"); // 通过静态方法回调Unity ScannerBridge.onScanResult(result); } } @Override protected void onDestroy() { super.onDestroy(); // 清理扫码SDK资源,防止内存泄漏 cleanupScanner(); } }

这里的设计哲学:

  • onActivityResult是Android Activity的标准回调入口,必须在此处触发ScannerBridge.onScanResult();
  • ScannerBridge是静态工具类,不依赖Activity实例,因此即使Activity被销毁,回调依然能正常触发;
  • onDestroy中清理SDK资源,是Android开发的基本素养。

2.5 第五步:回调落地——Unity主线程安全执行与数据解析

Java层通过UnityPlayer.UnitySendMessage将字符串结果发回Unity,C#侧OnScanComplete方法被触发:

private void HandleScanResult(string rawResult) { // 1. 基础校验 if (string.IsNullOrEmpty(rawResult)) { Debug.LogWarning("Empty scan result received"); return; } // 2. JSON解析(如果Java端传的是JSON字符串) // 例如:{"code":"123456","type":"QR_CODE","timestamp":1712345678} try { var scanData = JsonUtility.FromJson<ScanResult>(rawResult); ProcessValidScan(scanData); } catch (System.Exception e) { // JSON解析失败,尝试原始字符串处理 Debug.LogWarning("JSON parse failed, treating as plain text: " + e.Message); ProcessPlainTextScan(rawResult); } } [System.Serializable] public class ScanResult { public string code; public string type; public long timestamp; } private void ProcessValidScan(ScanResult data) { // 在此处理结构化扫码数据 Debug.Log($"Scanned {data.type}: {data.code}"); // 更新UI、触发事件、发送网络请求... OnScanSuccess?.Invoke(data); } private void ProcessPlainTextScan(string plainText) { // 降级处理:纯文本扫码 Debug.Log($"Plain text scan: {plainText}"); OnScanSuccess?.Invoke(new ScanResult { code = plainText, type = "TEXT" }); }

关键保障:

  • JsonUtility是Unity内置的轻量级JSON解析器,比Newtonsoft.Json更省内存,适合移动端;
  • 异常处理必须分层:先尝试结构化解析,失败后降级为纯文本,保证功能不中断;
  • OnScanSuccess是C#事件,可用于解耦UI逻辑与业务逻辑。

2.6 第六步:线程安全加固——当回调需要访问Unity API时

如果扫码结果需要立即修改Renderer属性(如热词中提到的“unity renderer的包围盒”),必须确保在Unity主线程执行:

private void ProcessValidScan(ScanResult data) { // 错误示范:直接在回调线程修改Renderer(会Crash) // GetComponent<Renderer>().enabled = true; // 正确做法:使用协程或Invoke StartCoroutine(UpdateRendererAfterScan(data)); } private IEnumerator UpdateRendererAfterScan(ScanResult data) { // 等待下一帧,确保在Unity主线程执行 yield return null; var renderer = GetComponent<Renderer>(); if (renderer != null) { renderer.enabled = true; // 其他Renderer操作... Debug.Log("Renderer updated for scan result: " + data.code); } }

或者使用MainThreadDispatcher(需自行实现的单例):

public class MainThreadDispatcher : MonoBehaviour { private static MainThreadDispatcher instance; private readonly Queue<System.Action> executionQueue = new Queue<System.Action>(); void Awake() { if (instance == null) { instance = this; DontDestroyOnLoad(gameObject); } else { Destroy(gameObject); } } void Update() { while (executionQueue.Count > 0) { executionQueue.Dequeue()?.Invoke(); } } public static void Enqueue(System.Action action) { if (instance == null) return; instance.executionQueue.Enqueue(action); } } // 在回调中使用 private void ProcessValidScan(ScanResult data) { MainThreadDispatcher.Enqueue(() => { var renderer = GetComponent<Renderer>(); if (renderer != null) { renderer.enabled = true; // 安全访问Unity API } }); }

2.7 第七步:错误兜底与日志追踪——让问题可定位、可复现

生产环境必须有完整的错误追踪链路:

public class BridgeLogger { private const string LOG_TAG = "UnityBridge"; public static void LogInfo(string message) { Debug.Log($"[{LOG_TAG}] INFO: {message}"); // 可选:上传至远程日志服务 // RemoteLogger.Send(LOG_TAG, "INFO", message); } public static void LogError(string message, System.Exception e = null) { string fullMessage = $"[{LOG_TAG}] ERROR: {message}"; if (e != null) { fullMessage += $"\nException: {e}"; } Debug.LogError(fullMessage); // 必须记录堆栈,便于定位JNI层问题 if (e != null) { Debug.LogException(e); } // 可选:触发崩溃上报 // CrashReporter.Report(e, fullMessage); } } // 在关键节点插入日志 void Start() { BridgeLogger.LogInfo("ScanManager initializing..."); // ...初始化逻辑 BridgeLogger.LogInfo("ScanManager initialized"); } public void OnScanComplete(string result) { BridgeLogger.LogInfo($"OnScanComplete called with result: {result.Length} chars"); HandleScanResult(result); }

日志设计原则:

  • 统一前缀[UnityBridge],便于Logcat过滤;
  • 区分INFO/ERROR级别,ERROR必须包含完整Exception堆栈;
  • 记录关键参数长度(如result.Length),避免日志刷屏;
  • 生产环境关闭Debug.Log,只保留Debug.LogError,减少性能开销。

3. AndroidJavaObject的底层机制与常见陷阱深度剖析

AndroidJavaObject是Unity桥接中最常用也最容易误用的API。它表面看是个“Java对象包装器”,实则是一套精密的JNI引用管理器。理解其底层机制,是避免内存泄漏和崩溃的关键。

3.1 JNI引用类型:LocalRef、GlobalRef与WeakGlobalRef的本质区别

当你执行new AndroidJavaObject("com.example.MyClass")时,Unity在JNI层做了三件事:

  1. FindClass:通过env->FindClass("com/example/MyClass")查找类定义,返回一个jclass(Local Reference);
  2. GetMethodID:通过env->GetMethodID(cls, "<init>", "()V")获取构造函数方法ID;
  3. NewObject:通过env->NewObject(cls, methodID)创建Java对象实例,返回一个jobject(Local Reference);
  4. NewGlobalRef:将这个jobject转换为jobjectGlobal Reference,并存储在C#对象内部。

关键点在于第4步:Local Reference只在当前JNI调用栈有效,一旦JNI函数返回,Local Ref就会被JVM自动释放。而Unity需要在后续多次调用中持续使用这个对象,就必须将其升级为Global Reference。Global Reference不会被JVM自动回收,必须由C#侧显式调用Dispose()来释放。

这就是为什么AndroidJavaObject必须手动Dispose——它持有的是Global Ref,不释放就会永久占用Java堆内存。

对比其他引用类型:

  • LocalRef:生命周期=单次JNI调用,无需手动管理,效率最高;
  • GlobalRef:生命周期=整个App运行期,必须手动释放,适用于长期持有的对象(如Activity、Service);
  • WeakGlobalRef:弱引用,JVM GC时可回收,适用于缓存场景,但Unity未公开此API。

3.2 Dispose()的正确姿势与常见误用

Dispose()不是可选操作,而是强制契约。错误用法包括:

  • 忘记调用:最常见,导致Java对象永远无法GC;
  • 重复调用:Dispose()后再次调用会抛出ObjectDisposedException;
  • 在错误时机调用:比如在OnDestroy之后又尝试调用Call();
  • 未置空引用:Dispose()后scannerBridge变量仍指向已释放对象,下次调用会Crash。

正确模式:

private AndroidJavaObject scannerBridge; void OnDestroy() { CleanupBridge(); } private void CleanupBridge() { if (scannerBridge != null) { try { scannerBridge.Dispose(); // 释放Global Ref } catch (System.ObjectDisposedException) { // 已被释放,忽略 } finally { scannerBridge = null; // 置空引用,防止二次使用 } } }

3.3 AndroidJavaClass vs AndroidJavaObject:何时用哪个?

场景推荐API原因
调用静态工具方法(如Math.random()、System.currentTimeMillis())AndroidJavaClass直接操作Class,无需实例化,无内存泄漏风险
创建Java对象并调用实例方法(如new ArrayList<>()、scanner.startScan())AndroidJavaObject必须持有对象引用,需手动管理生命周期
获取Android系统服务(如Context.getSystemService(Context.LOCATION_SERVICE))AndroidJavaObject返回的是Service实例,需长期持有
访问静态字段(如Build.VERSION.SDK_INT)AndroidJavaClass字段属于Class,非实例

错误示例:

// ❌ 错误:用AndroidJavaClass调用实例方法 AndroidJavaClass cls = new AndroidJavaClass("com.example.MyClass"); cls.Call("instanceMethod"); // 报错:No static method found // ✅ 正确:先创建实例,再调用 AndroidJavaObject obj = new AndroidJavaObject("com.example.MyClass"); obj.Call("instanceMethod");

3.4 参数传递的隐式转换规则与边界情况

Unity的JNI层对参数类型有严格的隐式转换规则:

C#类型JNI类型转换说明边界情况
stringjstringUTF-16 → Modified UTF-8空字符串""转为null jstring,Java端需判空
intjint直接映射无
floatjfloat直接映射无
booljbooleantrue→JNI_TRUE(1),false→JNI_FALSE(0)Java端接收为boolean,非Boolean对象
byte[]jbyteArray数组拷贝大数组性能损耗,建议用ByteBuffer替代
AndroidJavaObjectjobject传递Global RefJava端需用equals()比较,非==

特别注意string的空值处理:

// Java端 public void processString(String input) { // ❌ 危险:input可能为null // if (input.length() > 0) { ... } // ✅ 安全:先判空 if (input != null && !input.isEmpty()) { // 处理逻辑 } }

3.5 性能陷阱:频繁创建AndroidJavaObject的代价

每次new AndroidJavaObject都涉及JNI调用开销(FindClass、GetMethodID、NewObject、NewGlobalRef)。在高频场景(如每帧调用)下,性能急剧下降。

优化方案:

  • 对象池复用:对于可重用的Java对象(如JSONObject、ArrayList),创建一次,多次使用;
  • 批量操作:将多次小调用合并为一次大调用,减少JNI穿越次数;
  • 缓存AndroidJavaClass:AndroidJavaClass创建成本低,可全局缓存。

示例对象池:

public class JavaObjectPool { private static readonly Stack<AndroidJavaObject> jsonObjects = new Stack<AndroidJavaObject>(); public static AndroidJavaObject GetJSONObject() { if (jsonObjects.Count > 0) { return jsonObjects.Pop(); } return new AndroidJavaObject("org.json.JSONObject"); } public static void ReturnJSONObject(AndroidJavaObject obj) { if (obj != null) { // 重置对象状态(调用clear等方法) obj.Call("remove", "all_keys"); jsonObjects.Push(obj); } } }

4. 真实项目避坑指南:从热词中提炼的12个高频问题与根治方案

基于近五年数十个Unity Android项目的实战经验,结合热词搜索数据,我整理出开发者最常遇到的12个问题。每个问题都附带现象描述、根本原因、调试方法、根治方案,拒绝“重启试试”式玄学。

4.1 问题1:AndroidJavaException: java.lang.ClassNotFoundException

现象:Unity Log显示AndroidJavaException: java.lang.ClassNotFoundException: com.example.MyClass,但Java类明明存在。

根本原因:

  • Java类未打包进APK(Gradle配置错误,如implementation而非api,或混淆规则误删);
  • 类名拼写错误(大小写敏感,com.example.MyClass≠com.example.myclass);
  • Unity插件未正确放置在Assets/Plugins/Android目录下,或.jar/.aar文件未勾选Android平台。

调试方法:

  • 解包APK:unzip -l your-app-release.apk | grep MyClass,确认类是否存在;
  • 查看Logcat:adb logcat | grep "ClassNotFoundException",定位具体缺失类;
  • 在Android Studio中,右键Java类 →Go To→Declaration,确认类路径。

根治方案:

  • 在build.gradle中,确保类所在模块被正确依赖:
    dependencies { implementation project(':your-android-module') // 不要用api,避免传递依赖 }
  • Unity插件路径必须为Assets/Plugins/Android/your-plugin.aar,且在Inspector中勾选Android;
  • 类名使用完整路径,避免缩写:new AndroidJavaClass("com.example.scan.ScannerBridge")。

4.2 问题2:AndroidJavaException: java.lang.NoSuchMethodError

现象:Call("methodName")报错NoSuchMethodError,方法名、参数都核对无误。

根本原因:

  • Java方法签名不匹配(如public void foo(String s),C#传null,Java端未处理);
  • 方法为private或protected,JNI无法访问;
  • ProGuard混淆了方法名(未添加keep规则)。

调试方法:

  • 在Java端添加Log.d,确认方法是否被调用;
  • 使用javap -s查看方法签名:javap -s com/example/MyClass,比对签名字符串;
  • 检查ProGuard规则:-keep class com.example.** { *; }。

根治方案:

  • Java方法必须为public;
  • 添加ProGuard keep规则:
    -keep class com.example.scan.** { *; } -keep class com.unity3d.player.* { *; }
  • C#调用时,确保参数非null,或Java端做空值检查。

4.3 问题3:回调收不到,UnitySendMessage静默失败

现象:Java端调用UnityPlayer.UnitySendMessage("ObjName", "MethodName", "data"),Unity Log无输出。

根本原因:

  • GameObject名字错误("ObjName"≠ 实际GameObject名字);
  • 方法名拼写错误或大小写不匹配;
  • 方法不是public,或参数类型不匹配(如Java传int,C#方法参数为string);
  • UnityPlayer.currentActivity为null(Activity未正确初始化)。

调试方法:

  • 在Java端Log打印UnityPlayer.currentActivity是否为null;
  • 在Unity中,用GameObject.Find("ObjName")确认对象存在;
  • 检查C#方法签名:public void MethodName(string data)。

根治方案:

  • Java端添加空值检查:
    if (UnityPlayer.currentActivity != null) { UnityPlayer.UnitySendMessage("ScanManager", "OnScanComplete", result); } else { Log.e(TAG, "UnityPlayer.currentActivity is null"); }
  • Unity侧方法必须public,且参数类型严格匹配;
  • GameObject名字用常量定义,避免硬编码:
    private const string SCAN_MANAGER_NAME = "ScanManager";

4.4 问题4:App启动闪退,Logcat显示JNI ERROR (jobject is invalid)

现象:App启动后立即Crash,Logcat出现JNI ERROR (jobject is invalid)或Invalid jobject。

根本原因:

  • AndroidJavaObject被Dispose后,再次调用Call();
  • Java对象已被JVM GC回收,但C#侧仍持有无效Global Ref;
  • 多线程并发访问同一AndroidJavaObject。

调试方法:

  • 在Call()前后加Log,定位哪一行触发Crash;
  • 使用AndroidJavaObject.GetRawObject()获取jobject地址,对比是否为0;
  • 启用JNI Check:adb shell setprop debug.jni.check 1。

根治方案:

  • 所有Call()前加空值检查:
    if (scannerBridge != null && !scannerBridge.IsDisposed) { scannerBridge.Call("startScan"); }
  • OnDestroy中确保Dispose()且置空;
  • 避免多线程访问,必要时加锁。

4.5 问题5:扫码回调延迟严重,有时长达数秒

现象:扫码成功后,Unity侧OnScanComplete延迟几秒才触发。

根本原因:

  • UnitySendMessage被压入Unity主线程消息队列,但Unity主线程正忙于渲染或GC;
  • Java端未使用runOnUiThread,回调在后台线程执行;
  • Unity侧OnScanComplete方法内有耗时操作(如复杂JSON解析、网络请求)。

调试方法:

  • 在Java端onScanResult开头打Log,确认回调触发时间;
  • 在Unity侧OnScanComplete开头打Log,确认接收时间;
  • 使用Unity Profiler查看主线程CPU占用。

根治方案:

  • Java端必须runOnUiThread;
  • Unity侧OnScanComplete只做轻量操作(存数据、发事件),重操作放协程或线程;
  • 对UnitySendMessage做超时保护:
    // Java端添加超时标记 public static void onScanResult(String result) { UnityPlayer.currentActivity.runOnUiThread(() -> { long startTime = System.currentTimeMillis(); UnityPlayer.UnitySendMessage("ScanManager", "OnScanComplete", result); Log.d(TAG, "UnitySendMessage took " + (System.currentTimeMillis() - startTime) + "ms"); }); }

4.6 问题6:content://URI无法在Unity中读取(热词高频)

现象:Android返回content://com.tencent.wework.fileprovider/...,Unity用WWW或UnityWebRequest无法加载。

根本原因:

  • content://是Android ContentProvider URI,非文件路径,需通过ContentResolver转换;
  • Unity默认不支持content://协议,需Java层转换为file://或字节数组。

根治方案:

  • Java端提供转换方法:
    public static byte[] getContentBytes(Context context, Uri uri) { try (InputStream is = context.getContentResolver().openInputStream(uri)) { return IOUtils.toByteArray(is); // Apache Commons IO } catch (Exception e) { Log.e(TAG, "Failed to read content URI", e); return new byte[0]; } }
  • Unity侧调用:
    byte[] data = scannerBridge.Call<byte[]>("getContentBytes", uriString); Texture2D tex = new Texture2D(2, 2); tex.LoadImage(data);

4.7 问题7:AndroidJavaObject内存泄漏,App OOM

现象:长时间运行后,Android Logcat显示OutOfMemoryError,dumpsys meminfo显示Java Heap持续增长。

根本原因:

  • AndroidJavaObject未Dispose;
  • Java端创建了大量Bitmap、View等大对象,未及时回收。

根治方案:

  • 强制Code Review:所有new AndroidJavaObject必须配对Dispose();
  • 使用Android Studio Memory Profiler监控Java Heap;
  • Java端Bitmap用recycle(),View用removeAllViews()。

4.8 问题8:UnityPlayer.UnitySendMessage在某些机型上失效

现象:华为、小米等定制ROM机型,UnitySendMessage不触发。

根本原因:

  • 定制ROM限制了UnityPlayer.currentActivity的访问权限;
  • UnityPlayer类被厂商修改或隐藏。

根治方案:

  • 改用BroadcastReceiver作为备用通道:
    // Java端发送广播 Intent intent = new Intent("com.example.SCAN_RESULT"); intent.putExtra("result", result); context.sendBroadcast(intent);
  • Unity侧注册Receiver:
    AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); AndroidJavaObject activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"); activity.Call("registerReceiver", receiver, filter);

4.9 问题9:AndroidJavaObject调用toString()返回null

现象:obj.Call<string>("toString")返回null,但Java端toString()明明返回字符串。

根本原因:

  • Java端toString()返回null,JNI层无法处理,转为C#null;
  • AndroidJavaObject的ToString()方法被重写,返回内部jobject信息。

根治方案:

  • Java端确保toString()不返回null;
  • Unity侧用Call<string>("toString")
返回列表