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层做了三件事:
- FindClass:通过
env->FindClass("com/example/MyClass")查找类定义,返回一个jclass(Local Reference); - GetMethodID:通过
env->GetMethodID(cls, "<init>", "()V")获取构造函数方法ID; - NewObject:通过
env->NewObject(cls, methodID)创建Java对象实例,返回一个jobject(Local Reference); - 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类型 | 转换说明 | 边界情况 |
|---|---|---|---|
string | jstring | UTF-16 → Modified UTF-8 | 空字符串""转为null jstring,Java端需判空 |
int | jint | 直接映射 | 无 |
float | jfloat | 直接映射 | 无 |
bool | jboolean | true→JNI_TRUE(1),false→JNI_FALSE(0) | Java端接收为boolean,非Boolean对象 |
byte[] | jbyteArray | 数组拷贝 | 大数组性能损耗,建议用ByteBuffer替代 |
AndroidJavaObject | jobject | 传递Global Ref | Java端需用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")