1. 项目概述为什么Unity与Android交互是个“技术雷区”如果你是一名Unity开发者并且你的项目需要调用Android原生功能比如获取设备信息、集成第三方SDK、处理推送通知或者仅仅是做一个简单的“退出游戏”确认框那么你大概率已经或即将踏入这个“技术雷区”。Unity与Android的交互表面上看只是简单的函数调用但实际操作起来却是一个充满细节陷阱、版本兼容性问题和性能隐患的复杂过程。我见过太多项目因为一个不起眼的交互问题导致应用在特定机型上崩溃、ANR应用无响应或者仅仅是启动就慢了好几秒。这个“雷区”的核心在于Unity本质上是一个运行在Android Dalvik/ART虚拟机上的一个“应用”而它又要通过JNIJava Native Interface去调用另一个“世界”Android Java/Kotlin层的代码。这中间涉及了Unity Player的Activity生命周期、Android的UI线程与Unity渲染线程的通信、数据类型的转换、内存管理以及不同Android版本和厂商ROM的“个性化”行为。任何一个环节处理不当轻则功能异常重则直接导致应用崩溃。因此理解这些陷阱并掌握优化策略不是“加分项”而是保证项目稳定上线的“必修课”。2. 交互架构的基石JNI与AndroidJavaClass/AndroidJavaObject在深入陷阱之前我们必须先夯实基础。Unity提供了两种主要方式与Android交互底层的C# JNI调用和封装好的AndroidJavaClass/AndroidJavaObject。理解它们的区别是避坑的第一步。2.1 两种方式的原理与选型考量AndroidJavaClass/AndroidJavaObject是Unity封装的高级API使用起来非常直观就像在C#里调用Java对象一样。它的优点是简单易用无需手动处理JNIEnv、jobject等复杂指针。// 示例调用Android的Toast显示消息 AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer); AndroidJavaObject currentActivity unityPlayer.GetStaticAndroidJavaObject(currentActivity); AndroidJavaClass toastClass new AndroidJavaClass(android.widget.Toast); AndroidJavaObject toast toastClass.CallStaticAndroidJavaObject(makeText, currentActivity, Hello from Unity!, toastClass.GetStaticint(LENGTH_SHORT)); toast.Call(show);然而这种便利性背后隐藏着性能开销。每一次new AndroidJavaClass、Call或GetStatic操作Unity引擎底层都会执行一次完整的JNI调用涉及查找类、方法ID、参数编组Marshaling等步骤。在频繁调用的场景如一帧内多次获取传感器数据这会成为性能瓶颈。纯C# JNI调用则更为底层和高效。你需要直接使用IntPtr、JNIEnv等接口代码更复杂但控制粒度更细性能更好。通常用于对性能要求极高的核心交互或者封装成稳定的插件供项目内反复使用。// 简化示例通过JNI获取当前Activity private static IntPtr _unityActivity IntPtr.Zero; public static IntPtr GetUnityActivity() { if (_unityActivity IntPtr.Zero) { using (AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) using (AndroidJavaObject activity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) { _unityActivity activity.GetRawObject(); } } return _unityActivity; } // 后续可以使用 _unityActivity 和 JNIEnv 方法进行高效调用选型策略对于一次性或低频调用如初始化SDK、显示一个对话框优先使用AndroidJavaClass/AndroidJavaObject开发效率高。对于高频调用如每帧获取陀螺仪数据、音频输入必须使用纯JNI进行封装甚至考虑将数据缓存到本地通过共享内存等方式减少JNI调用次数。对于需要封装给团队使用的插件建议内部采用高效JNI实现对外提供简洁的C# API。2.2 版本适配与架构选择背后的“坑”你以为选好了调用方式就万事大吉了Android的碎片化会让你知道什么叫“防不胜防”。这里最大的陷阱是Target API Level目标API级别和Android Gradle插件版本的兼容性问题。陷阱1JNI方法签名变更从Android 8.0API 26开始一些原生方法Native Method的签名或行为发生了改变。如果你的插件使用了JNI并且在AndroidManifest.xml中声明了targetSdkVersion 26但插件编译时依赖的却是旧版本的Android SDK那么在调用这些方法时可能会遇到UnsatisfiedLinkError或静默失败。规避策略始终使用与你项目targetSdkVersion匹配或更新的Android SDK来编译你的原生插件.so文件或AAR。在Unity中可以通过Player Settings - Android - Publishing Settings - Minify下的ProGuard规则来保留必要的JNI方法。陷阱2Unity版本与Gradle构建系统的兼容性Unity 2019.3及以后版本默认使用Gradle来构建APK并逐渐淘汰了旧的内部构建系统。许多老旧的Android插件特别是直接提供AndroidManifest.xml和res文件夹的在Gradle构建下可能会因为资源合并冲突、依赖冲突而失败。规避策略升级插件寻找插件作者提供的Gradle兼容版本或使用.aar格式的插件包它比原始的.jar资源文件更规范。手动处理Gradle文件在Unity项目的Assets/Plugins/Android目录下可以放置mainTemplate.gradle文件来自定义依赖和构建规则。这是解决依赖冲突的终极手段。// mainTemplate.gradle 示例排除冲突的依赖包 dependencies { implementation(com.some.plugin:plugin-aar:1.0.0) { exclude group: com.android.support, module: support-v4 } }注意JetpackAndroidX迁移如果插件使用了Android支持库android.support.v7而你的项目或其他插件使用了AndroidX会发生严重的冲突。Unity 2020.1 提供了Player Settings - Android - Publishing Settings - Jetifier选项可以自动迁移支持库到AndroidX但这并非百分百可靠手动检查和调整依赖仍是必要的。3. 线程安全的“隐形杀手”主线程与子线程通信这是导致ANR和诡异崩溃的最常见原因之一。一个黄金法则所有涉及Android UI的操作必须在主线程UI线程上执行。3.1 为什么不能在Unity线程直接调用UIUnity的脚本如MonoBehaviour.Update运行在Unity自有的渲染线程或脚本线程上这不是Android的UI线程。如果你在这个线程里直接调用一个显示Toast、弹出Dialog或者更新TextView的方法在低版本Android上可能侥幸运行但在高版本或某些厂商ROM上会立即抛出CalledFromWrongThreadException导致应用崩溃。3.2 安全的线程间通信方案方案一使用UnityPlayer.currentActivity.runOnUiThread这是最直接、最常用的方法。它将一个Runnable任务投递到Android UI线程队列中执行。public void ShowToastOnUIThread(string message) { AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer); AndroidJavaObject activity unityPlayer.GetStaticAndroidJavaObject(currentActivity); // 注意这里传入的Runnable是一个AndroidJavaProxy它允许C#代码实现Java接口 activity.Call(runOnUiThread, new AndroidJavaRunnable(() { AndroidJavaClass toastClass new AndroidJavaClass(android.widget.Toast); AndroidJavaObject toast toastClass.CallStaticAndroidJavaObject(makeText, activity, message, toastClass.GetStaticint(LENGTH_SHORT)); toast.Call(show); })); }关键细节AndroidJavaRunnable是Unity提供的一个AndroidJavaProxy用于实现Java的Runnable接口。确保回调内的代码简洁避免嵌套复杂的逻辑。方案二通过Android Handler/Looper机制对于更复杂的、需要从子线程频繁向主线程发送消息的场景可以在Android侧创建一个Handler关联到主线程的Looper。Unity侧通过JNI调用这个Handler的post或sendMessage方法。// Android插件代码 (Java) public class UnityBridge { private static Handler mainHandler; static { mainHandler new Handler(Looper.getMainLooper()); } public static void postToMainThread(Runnable task) { mainHandler.post(task); } }// Unity C# 代码 AndroidJavaClass bridgeClass new AndroidJavaClass(com.yourcompany.UnityBridge); bridgeClass.CallStatic(postToMainThread, new AndroidJavaRunnable(() { // 在主线程执行UI操作 }));方案三谨慎使用AsyncTask已废弃虽然AsyncTask曾是标准方案但它在Android 11API 30中已被正式废弃。在新项目中应避免使用老项目迁移时也需注意。实操心得不要在runOnUiThread的回调里执行耗时操作这会让UI线程卡住直接导致ANR。UI线程只应该用于快速更新界面。任何可能耗时的操作如网络请求、文件读写、复杂计算都应该在子线程中发起待完成后再通过上述方法回到主线程更新UI。4. 内存泄漏与对象管理的“慢性病”JNI交互中的对象管理不当不会立刻导致崩溃但会像“慢性病”一样逐渐消耗内存最终引发OOMOut Of Memory和程序卡顿。4.1 本地引用Local Reference的泄漏每次通过JNI包括AndroidJavaObject创建的Java对象在本地都会有一个对应的“本地引用”。JNI规范要求开发者需要管理这些引用尤其是在本地方法中。虽然Unity的封装层在一定程度上帮我们管理了AndroidJavaObject的生命周期通过C#的Dispose模式或垃圾回收但在高频或复杂调用中仍需注意。陷阱在循环中大量创建AndroidJavaObject而不及时释放。// 错误示例每一帧都创建新的Java对象 void Update() { AndroidJavaObject jo new AndroidJavaObject(java.util.Date); string time jo.Callstring(toString); // jo 的本地引用在本帧结束后才会被GC回收如果Update频率高会快速积累 }优化策略缓存复用对于频繁使用的Java类静态实例或Activity对象在Awake或Start中初始化并缓存。private AndroidJavaObject _cachedActivity; void Start() { AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer); _cachedActivity unityPlayer.GetStaticAndroidJavaObject(currentActivity); } void SomeMethod() { // 使用 _cachedActivity而不是每次都new _cachedActivity.Call(runOnUiThread, ...); } void OnDestroy() { if (_cachedActivity ! null) { _cachedActivity.Dispose(); // 显式释放 _cachedActivity null; } }使用using语句对于确定生命周期的临时对象使用using确保其Dispose被及时调用。using (AndroidJavaClass clazz new AndroidJavaClass(some.class)) { // 使用clazz } // 离开作用域时自动Dispose对于纯JNI调用务必记得使用JNIEnv.DeleteLocalRef来释放手动创建的本地引用。4.2 全局引用Global Reference与弱全局引用Weak Global Reference如果你需要将一个Java对象如一个回调监听器长期保存在C#侧以便后续调用你不能仅仅保存它的本地引用因为本地引用在方法返回后可能失效。这时需要创建全局引用。陷阱创建了全局引用但在C#对象销毁时忘记删除它导致Java对象永远无法被垃圾回收。private IntPtr _globalListenerRef IntPtr.Zero; void SetupListener() { // 假设创建了一个Java监听器对象 jobject listener ... _globalListenerRef JNIEnv.NewGlobalRef(listener); JNIEnv.DeleteLocalRef(listener); // 删除本地引用 } // 如果忘记在OnDestroy中删除全局引用就会内存泄漏正确做法void OnDestroy() { if (_globalListenerRef ! IntPtr.Zero) { JNIEnv.DeleteGlobalRef(_globalListenerRef); _globalListenerRef IntPtr.Zero; } }更优选择考虑使用弱全局引用JNIEnv.NewWeakGlobalRef。它允许Java对象在只有弱引用指向时被GC回收。当你需要使用时先通过JNIEnv.IsSameObject检查对象是否还存在。这适用于缓存那些生命周期可能比C#侧对象更短的Java对象如Activity的Context。5. 数据传递的“性能黑洞”从基础类型到复杂对象在UnityC#和AndroidJava之间传递数据每一次跨越边界都是一次性能开销。数据类型越复杂开销越大。5.1 基础类型与字符串基础类型int,float,bool等的传递开销相对较小。但字符串是个需要特别注意的类型。C#的string和Java的String在内存中的编码格式都是UTF-16但JNI传递时仍然涉及一次内存拷贝和编码确认。优化策略避免高频传递长字符串例如不要每帧都传递一个完整的JSON字符串。可以考虑在Android侧解析JSON只将必要的数值结果传递回Unity。使用StringBuilder谨慎对于需要在两端多次拼接的字符串在Java侧使用StringBuilder最后一次性传递结果。但这通常只在字符串操作本身是瓶颈时才有效JNI调用的开销往往更大。5.2 复杂对象与序列化传递自定义类对象、列表或字典是开发中的常见需求。直接传递是不可能的必须进行序列化。常见陷阱与方案对比方案实现方式优点缺点适用场景JSON序列化使用JsonUtility(Unity)、Gson/Jackson(Android)将对象转为字符串传递。实现简单跨语言兼容性好可读性强。性能开销大序列化/反序列化 字符串JNI传递频繁调用时是性能杀手。配置信息初始化、低频的复杂数据交换。ProtoBuf / FlatBuffers使用二进制序列化协议。性能极高数据体积小序列化/反序列化速度快。需要预先定义Schema.proto文件增加复杂度二进制数据不可读。高频数据交换如游戏实时状态同步、对性能要求苛刻的场景。直接传递基础类型数组对于纯数值数组如float[]位置数据、int[]状态数组可以通过JNI直接访问数组指针。性能极致几乎零拷贝。仅适用于基础类型数组需要手动管理内存和同步。传感器数据流陀螺仪、加速度计、顶点数据传递。共享内存/内存映射文件在Native层C/C开辟一块内存Unity和Android原生代码均可直接访问。性能极高真正的零拷贝。实现最复杂需要编写JNI和Native代码同步和生命周期管理困难。极高性能要求的场景如摄像头YUV帧实时处理、音频PCM数据流。实操建议对于大多数游戏交互JSON用于低频配置ProtoBuf用于高频网络或跨平台数据基础类型数组用于传感器数据是一个平衡了开发效率和运行性能的务实选择。5.3 使用JNI直接操作数组高级优化这是一个可以极大提升传感器等数据传递效率的技巧。以传递陀螺仪数据为例// Unity C# 侧 [DllImport(your_native_lib)] private static extern IntPtr GetGyroDataNative(); // 获取Native层数组指针 [DllImport(your_native_lib)] private static extern int GetGyroDataLengthNative(); // 获取数组长度 void Update() { IntPtr arrayPtr GetGyroDataNative(); int length GetGyroDataLengthNative(); if (arrayPtr ! IntPtr.Zero length 0) { // 将原生内存数据直接拷贝到C#数组避免JNI多层调用 float[] gyroData new float[length]; Marshal.Copy(arrayPtr, gyroData, 0, length); // 使用 gyroData... } }对应的NativeC/C代码需要管理一个全局的float[]数组并由Android的传感器服务直接填充。Unity通过P/Invoke拿到数组指针直接读取实现了近乎零开销的数据传递。6. 生命周期同步的“幽灵问题”Activity与Unity PlayerUnity游戏运行在一个特殊的Activity通常是UnityPlayerActivity中。这个Activity的生命周期onCreate, onPause, onResume, onDestroy必须与Unity引擎的生命周期Awake, OnApplicationPause, OnApplicationFocus, OnDestroy妥善同步否则会出现诸如“切回游戏时黑屏”、“声音播放异常”、“传感器失效”等问题。6.1 常见生命周期不同步陷阱陷阱1在Unity的OnApplicationPause(false)即恢复时Android Activity的onResume尚未完成如果你在OnApplicationPause(false)中立即调用一个需要Activity上下文如显示对话框的Android方法可能会因为上下文未就绪而失败。解决方案Unity提供了AndroidLifecycleEventListener接口位于UnityEngine.Android命名空间。你可以实现它来更精确地监听Android原生的生命周期回调确保调用时机正确。public class AndroidLifecycleHandler : AndroidLifecycleEventListener { public void OnActivityCreated(AndroidJavaObject activity, AndroidJavaObject savedInstanceState) {} public void OnActivityDestroyed(AndroidJavaObject activity) {} public void OnActivityPaused(AndroidJavaObject activity) { // Android Activity进入后台 } public void OnActivityResumed(AndroidJavaObject activity) { // Android Activity回到前台此时可以安全调用UI相关方法 Debug.Log(Android Activity Resumed, safe to show UI now.); } // ... 其他方法 } // 在Awake中注册监听 void Awake() { if (Application.platform RuntimePlatform.Android) { new AndroidLifecycleHandler(); } }陷阱2Unity场景切换时Android Native UI未正确隐藏假设你在Android侧弹出了一个原生对话框当Unity加载新场景时这个对话框可能不会自动消失会悬浮在画面上。解决方案在Unity场景切换或游戏暂停时主动通知Android侧关闭所有弹出的UI。在Android插件中维护一个当前打开对话框的引用。在Unity的OnApplicationPause(true)或特定场景卸载事件中调用一个Android方法如dismissAllDialogs()来关闭所有对话框。陷阱3Android配置变更如屏幕旋转导致Activity重建默认情况下屏幕旋转会销毁并重建Activity。如果Unity侧没有正确处理可能会导致游戏状态丢失或交互上下文失效。解决方案固定屏幕方向在Player Settings - Resolution and Presentation - Default Orientation中设置为固定方向如Landscape Left。这是游戏最常用的方法一劳永逸。处理配置变更在AndroidManifest.xml中为Unity的Activity添加android:configChanges属性声明由自己处理某些配置变更避免Activity重建。activity android:namecom.unity3d.player.UnityPlayerActivity android:configChangesfontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen同时需要在Unity中监听OnConfigurationChanged事件通过AndroidLifecycleEventListener或JNI来手动调整游戏布局对于UI适配要求高的应用。6.2 初始化与反初始化的最佳实践初始化将Android插件的初始化如获取Context、初始化SDK放在Unity的Awake或Start方法中并确保只初始化一次使用单例模式或静态标志位。反初始化在Unity的OnDestroy或OnApplicationQuit中务必调用Android插件的清理方法释放资源、注销监听器、关闭连接等。许多SDK如推送、语音、广告要求显式调用destroy或release方法否则可能引起内存泄漏或后台服务异常。void OnDestroy() { if (Application.platform RuntimePlatform.Android) { // 调用Android侧清理方法 using (AndroidJavaClass pluginClass new AndroidJavaClass(com.your.plugin.UnityBridge)) { pluginClass.CallStatic(release); } // 释放缓存的全局引用等 } }7. 调试与问题排查的“实战手册”当交互出现问题时清晰的排查路径能节省大量时间。7.1 日志输出你的第一道防线Unity侧使用Debug.Log。确保在Player Settings - Android - Other Settings - Debugging中勾选了Enable ADB和Logging。Android侧使用Log.d(String tag, String msg)。这是最重要的调试手段。你需要通过adb logcat命令来查看。查看所有日志adb logcat -s Unity DEBUG(过滤Unity和DEBUG级别)查看特定Tag的日志adb logcat -s “YourPluginTag”查看崩溃堆栈adb logcat *:E(查看所有ERROR级别日志)一个关键技巧是在JNI调用前后、回调函数入口处都打上日志可以清晰看到执行流。7.2 常见错误与崩溃分析错误现象可能原因排查步骤应用启动即崩溃1. AndroidManifest.xml配置错误权限、Activity声明。2. 插件依赖的.so库架构不匹配armeabi-v7a, arm64-v8a。3. 插件初始化代码在Awake中抛出异常。1. 检查adb logcat输出的崩溃堆栈重点关注AndroidRuntime和Unity的报错。2. 检查APK包中lib/目录下是否有对应架构的.so文件。3. 将初始化代码用try-catch包裹输出异常信息。调用某个方法时崩溃1. 方法名或签名错误。2. 调用线程不正确非UI线程调用UI。3. 传递的参数类型或数量不对。4. 对应的Java对象为null。1. 使用javap -s命令检查Java类的准确方法签名。2. 确保UI操作在runOnUiThread中。3. 检查C#调用代码与Java方法签名的严格匹配注意bool对应Java的Zint对应I等。4. 在调用前检查AndroidJavaObject是否为null。ANR应用无响应1. 在主线程UI线程执行了耗时操作如同步网络请求、大量文件IO。2. 死锁如Unity与Android互相等待回调。1. 检查所有通过runOnUiThread或直接在主线程回调中执行的代码。2. 使用adb logcat查看ANR发生时的堆栈信息(adb logcat -b events)。3. 梳理回调逻辑避免循环等待。功能间歇性失效1. 生命周期不同步在Activity未就绪时调用。2. 对象被垃圾回收如回调监听器使用了弱引用或未保持引用。3. 多线程竞争条件。1. 添加生命周期日志确认调用时机。2. 检查关键对象的引用是否被意外释放。3. 检查是否有多个线程同时访问和修改同一资源考虑加锁。性能低下卡顿1. 高频JNI调用如每帧多次。2. 传递的数据量过大如图片字节数组。3. 序列化/反序列化开销大。1. 使用Profiler查看Update或FixedUpdate中JNI调用的耗时。2. 优化数据传递策略见第5节。3. 考虑将高频调用移至Native插件或使用数据缓冲池。7.3 使用Android Profiler与Unity Profiler进行性能剖析Android Profiler (Android Studio)连接设备后可以监控应用的CPU、内存、网络、电量使用情况。特别关注在Unity与Android交互时Java堆内存是否有异常增长可能内存泄漏以及CPU使用率是否出现尖峰可能由低效的JNI调用或序列化引起。Unity Profiler在Development Build下运行游戏通过Profiler查看。重点关注CPU Usage查看Mono和GfxDevice线程的耗时。如果Mono线程出现不明的耗时峰值很可能来自低效的C#-Java交互。Memory查看GC Alloc垃圾回收分配。频繁的、大量的GC Alloc往往意味着在交互中产生了大量临时对象如字符串、数组需要优化。7.4 一个实用的调试工具类我通常会编写一个简单的调试工具类它封装了日志和错误处理便于在开发阶段快速定位问题。public static class AndroidInteropDebugger { private const string TAG Unity2Android; public static bool IsDebugBuild true; public static void Log(string message) { if (!IsDebugBuild) return; Debug.Log($[{TAG}] {message}); // 同时输出到Android Logcat (可选) try { using (AndroidJavaClass logClass new AndroidJavaClass(android.util.Log)) { logClass.CallStaticint(d, TAG, message); } } catch {} } public static T SafeCallT(AndroidJavaObject jo, string methodName, params object[] args) { try { return jo.CallT(methodName, args); } catch (System.Exception e) { LogError($Call {methodName} failed: {e.Message}); return default(T); } } // ... 类似的安全封装方法 }8. 高级优化与未来考量8.1 使用Unity新的Android交互APIUnity 2022.2从Unity 2022.2开始Unity引入了一套新的、更现代的Android交互API位于UnityEngine.Android命名空间旨在替代部分AndroidJavaClass的使用。例如AndroidActivity类提供了更安全、更方便的方式来获取当前Activity。// 新的API方式 var currentActivity AndroidActivity.CurrentActivity; if (currentActivity ! null) { // 使用 currentActivity 进行调用 }这套API内部做了更多优化和错误处理建议在新项目中优先考虑使用。但需要注意其版本兼容性。8.2 将复杂功能封装为Android Archive (AAR)对于功能复杂的插件强烈建议将其打包成.aar文件而不是零散的.jar、资源文件和AndroidManifest.xml。AAR是一个标准的Android库格式包含了编译好的代码、资源、清单文件和预编译的Native库由Gradle自动处理依赖和合并能极大减少配置冲突。打包AAR的简单步骤在Android Studio中创建一个新的Android Library模块。实现你的Java/Kotlin代码并处理好与Unity的接口通常是一个继承自UnityPlayerActivity的类或提供静态方法。在模块的build.gradle中配置好依赖。执行Build - Make Module在build/outputs/aar/目录下找到生成的.aar文件。将其放入Unity项目的Assets/Plugins/Android目录中。8.3 拥抱Kotlin与Coroutines如果你的Android插件需要处理异步操作如网络请求、数据库查询强烈建议使用Kotlin协程Coroutines来代替传统的AsyncTask或ThreadHandler。协程的代码更简洁能更好地处理生命周期避免回调地狱。在Unity中调用一个挂起函数suspend function的Kotlin协程可以通过回调或者使用CompletableFuture进行桥接。这要求你的插件有更良好的架构设计但带来的可维护性和性能提升是值得的。8.4 关注Unity版本升级与Android API演进Unity每年都会发布多个主要版本Android系统也在不断更新。在升级Unity或提高项目的targetSdkVersion时务必重新测试所有的Android交互功能。关注官方文档的更新日志特别是关于Android构建支持和JNI相关的改动。例如Unity对Android App Bundle (AAB)格式的支持、对Play Asset Delivery的集成等都可能影响插件的工作方式。我个人在多个项目的踩坑经验是建立一个简单的“交互测试场景”里面包含了所有关键的Android功能调用UI、传感器、文件、网络等。在每次引擎升级或插件更新后跑一遍这个测试场景是预防线上事故最有效的手段。