简介:在增强现实(AR)应用开发中,二维码识别是实现虚实交互的关键技术之一。其原理是通过摄像头捕捉图像,利用图像处理算法提取二维码的定位图案和数据区域,再通过解码算法还原信息。这项技术的核心价值在于为AR应用提供了低成本、高可靠性的物理世界锚点,是实现商品识别、空间定位、信息叠加等场景的基础。在移动端和AR眼镜等资源受限设备上,需要平衡识别精度与性能开销。本文聚焦于Unity3D与安卓原生混合开发架构,通过集成ZXing库和Camera2 API,详细解析了在Rokid AR眼镜上实现低延迟、高稳定扫码功能的全流程,涵盖权限管理、图像流处理、跨平台通信等工程实践要点,并提供了针对性能优化和常见问题的解决方案。
1. 项目概述:当Unity3d遇上RokidAR,扫码识别如何落地?
最近在做一个AR眼镜上的应用,核心需求之一就是扫码识别。设备用的是Rokid AR眼镜,开发引擎是Unity3d,语言自然是C#。这个组合听起来挺酷,但实际做起来,从调用原生摄像头到处理图像流,再到精准识别二维码,每一步都可能遇到意想不到的坑。网上关于Rokid AR眼镜在Unity3d下的扫码功能源码分享并不多,大多是零散的片段,不成体系。我把自己从零搭建、调试到最终稳定运行的完整过程,包括核心源码和踩过的那些“坑”,在这里系统地梳理一遍。无论你是刚开始接触AR眼镜开发,还是正在为扫码功能的性能或兼容性头疼,这篇内容应该都能给你提供一条清晰的路径和可直接复用的解决方案。
这个项目的核心目标很明确:在Rokid AR眼镜上,通过Unity3d应用,实现一个稳定、高效、低延迟的二维码(或条形码)扫码识别功能。它不仅仅是调用一个API那么简单,涉及到Unity与安卓原生(Rokid眼镜基于安卓系统)的交互、摄像头权限管理、图像数据的高效传递、识别算法的选择与集成,以及最终将识别结果无缝反馈回Unity场景。整个过程需要兼顾性能(毕竟AR眼镜算力有限)和用户体验(识别要快、准、稳)。接下来,我会从设计思路开始,一步步拆解如何实现它。
2. 整体架构设计与技术选型考量
2.1 为什么选择“Unity + 原生插件”的混合架构?
在Rokid AR眼镜上做扫码,首先面临架构选择。纯Unity方案,比如使用UnityEngine.WebCamTexture,虽然简单,但存在明显短板:一是对摄像头参数(如对焦模式、分辨率、帧率)的控制力很弱,难以针对扫码场景优化;二是图像数据需要从GPU内存读回CPU内存进行识别,在移动设备上开销较大,容易引起卡顿和发热。而纯原生开发(用Android Studio写个扫码Activity)虽然控制力强,但又失去了Unity在3D内容渲染和交互逻辑上的高效与便捷。
因此,混合架构成了最务实的选择。核心思路是:在Unity中触发扫码,通过C#调用由Java(或Kotlin)编写的安卓原生插件,由原生插件负责打开摄像头、预览并获取图像帧,然后调用高效的本地识别库(如ZXing、ML Kit)进行识别,最后将识别结果(字符串)回调给Unity的C#脚本。这样,我们既利用了原生层对硬件和系统API的精细控制,保证了识别性能和稳定性,又保留了Unity作为主逻辑和渲染引擎的灵活性。
2.2 核心组件与数据流拆解
基于混合架构,整个扫码模块可以分解为以下几个核心组件,数据在它们之间流动:
Unity C# 入口脚本 (QRScannerManager):这是整个功能的“大脑”。它负责初始化、提供开始/停止扫描的接口、处理权限申请(在Unity 2019.3及以上版本,需要使用
UnityEngine.Android.Permission),并接收来自原生层的识别结果回调。安卓原生插件 (Android Plugin):这是一个包含Java/Kotlin代码和必要资源(如
AndroidManifest.xml中声明摄像头权限和Activity)的.aar或.jar文件,或者直接放置在Assets/Plugins/Android目录下的Java源码。它的核心是一个Activity或Fragment,用于:- 管理摄像头生命周期(打开、关闭)。
- 配置摄像头参数(设置为后置摄像头、连续对焦模式、合适的分辨率)。
- 通过
Camera2 API或CameraX库获取预览图像流。 - 将预览图像传递给识别引擎。
识别引擎 (Barcode Library):负责从图像中解析出二维码/条形码信息。我们选用ZXing(“Zebra Crossing”)。它是一个久经考验、开源且轻量级的库,对二维码识别有很好的支持,并且可以很容易地集成到安卓项目中。相比一些更庞大的AI视觉库,ZXing在AR眼镜这种资源受限的设备上优势明显。
通信桥梁 (Unity ↔ Android):这是混合开发的关键。我们使用两种主要方式:
- UnitySendMessage: 一种简单的、由原生代码向Unity发送消息的机制。适合传递简单的字符串结果。
- AndroidJavaClass / AndroidJavaObject: 在Unity的C#脚本中直接实例化和调用Java对象的方法,实现更复杂的双向交互。我们主要用这个方式来启动原生的扫码Activity。
整个数据流是这样的:用户点击Unity UI按钮 → C#脚本通过AndroidJavaClass调用原生插件方法启动扫码Activity → 原生Activity打开摄像头并开始预览 → 每一帧预览图像被送入ZXing库解码 → 解码成功 → 原生代码通过UnitySendMessage将结果字符串发送回指定的Unity GameObject和方法 → Unity C#脚本收到结果,进行后续处理(如加载内容、显示信息等)。
注意:在Rokid AR眼镜这类定制安卓设备上,务必确认其系统API级别和摄像头兼容性。有些厂商可能会对标准Android API进行修改或封装。最稳妥的方式是查阅Rokid官方的开发者文档(如果有),或者准备一个简单的原生测试程序来验证摄像头基础功能是否正常。
3. 安卓原生插件开发与核心源码解析
这一部分是整个项目的基石。我们将创建一个原生的扫码Activity,并集成ZXing库。
3.1 创建Android Studio项目与依赖配置
首先,创建一个新的Android Studio项目,选择“Empty Activity”,最低API级别需要与你的Unity项目设置以及Rokid眼镜的系统版本匹配(通常至少API 24+)。然后,在app/build.gradle文件中添加ZXing核心库的依赖:
dependencies { implementation 'com.google.zxing:core:3.5.2' // 如果需要处理复杂的图像(如旋转、裁剪),可以添加`javase`客户端 // implementation 'com.google.zxing:javase:3.5.2' }ZXing的core库包含了所有编码和解码的核心算法,对于扫码功能来说已经足够。
3.2 扫码Activity的核心实现
我们创建一个名为QRScanActivity的Activity。以下是其核心代码片段的解析:
1. 权限检查与申请在onCreate或onResume中,需要动态申请摄像头权限,这是Android 6.0 (API 23) 以上的强制要求。
private static final int REQUEST_CAMERA_PERMISSION = 1; private void checkCameraPermission() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION); } else { setupCamera(); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_CAMERA_PERMISSION) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { setupCamera(); } else { // 权限被拒绝,可以提示用户或直接关闭Activity finish(); } } }2. 使用Camera2 API初始化摄像头Camera2 API比旧的Camera API更强大和灵活,能更好地控制摄像头。关键步骤包括:获取CameraManager、选择摄像头ID(通常是后置摄像头)、创建CameraDevice.StateCallback、创建CaptureRequest.Builder用于预览。
private CameraDevice cameraDevice; private CameraCaptureSession cameraCaptureSession; private CaptureRequest.Builder previewRequestBuilder; private void setupCamera() { CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); try { String cameraId = manager.getCameraIdList()[0]; // 通常0是后置摄像头 CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); // 选择一个适合预览的尺寸,例如1280x720,平衡性能与识别精度 Size previewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class), 1280, 720); manager.openCamera(cameraId, new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice camera) { cameraDevice = camera; createCameraPreviewSession(); } // ... 其他回调方法 onDisconnected, onError }, null); } catch (CameraAccessException e) { e.printStackTrace(); } }3. 创建预览会话并设置图像分析器这是将摄像头数据与识别逻辑连接起来的关键。我们使用ImageReader来获取预览帧,并将其送入一个后台线程进行分析。
private ImageReader imageReader; private HandlerThread backgroundThread; private Handler backgroundHandler; private void createCameraPreviewSession() { try { SurfaceTexture texture = textureView.getSurfaceTexture(); texture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); Surface surface = new Surface(texture); // 创建ImageReader,用于获取YUV_420_888格式的图像数据,这是摄像头最原始的输出格式之一,ZXing可以处理。 imageReader = ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), ImageFormat.YUV_420_888, 2); imageReader.setOnImageAvailableListener(onImageAvailableListener, backgroundHandler); previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); previewRequestBuilder.addTarget(surface); previewRequestBuilder.addTarget(imageReader.getSurface()); // 关键:将ImageReader的Surface也作为目标 cameraDevice.createCaptureSession(Arrays.asList(surface, imageReader.getSurface()), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { cameraCaptureSession = session; try { // 设置连续自动对焦,这对扫码至关重要 previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); session.setRepeatingRequest(previewRequestBuilder.build(), null, backgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } // ... 其他回调 }, null); } catch (CameraAccessException e) { e.printStackTrace(); } } // 图像可用监听器 private final ImageReader.OnImageAvailableListener onImageAvailableListener = new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { Image image = reader.acquireLatestImage(); if (image != null) { // 将Image对象传递给后台线程进行识别 processImage(image); image.close(); // 非常重要!必须关闭Image释放资源 } } };4. 图像处理与ZXing识别在后台线程中,我们将Image对象转换为ZXing可以处理的BinaryBitmap。
private void processImage(Image image) { // 1. 从Image中获取YUV数据 Image.Plane[] planes = image.getPlanes(); ByteBuffer yBuffer = planes[0].getBuffer(); ByteBuffer uBuffer = planes[1].getBuffer(); ByteBuffer vBuffer = planes[2].getBuffer(); int ySize = yBuffer.remaining(); int uSize = uBuffer.remaining(); int vSize = vBuffer.remaining(); byte[] nv21 = new byte[ySize + uSize + vSize]; yBuffer.get(nv21, 0, ySize); vBuffer.get(nv21, ySize, vSize); // 注意:YUV_420_888格式,V平面可能在U前面 uBuffer.get(nv21, ySize + vSize, uSize); // 2. 创建ZXing的LuminanceSource和BinaryBitmap PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource( nv21, image.getWidth(), image.getHeight(), 0, 0, // 子区域,这里用全图 image.getWidth(), image.getHeight(), false // 非反转 ); BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); // 3. 尝试解码 try { Result rawResult = new MultiFormatReader().decode(bitmap); if (rawResult != null) { final String resultText = rawResult.getText(); runOnUiThread(new Runnable() { @Override public void run() { // 识别成功,处理结果(例如震动、播放声音,并准备将结果传回Unity) handleScanSuccess(resultText); } }); } } catch (NotFoundException e) { // 未识别到二维码,正常情况,继续下一帧 } catch (FormatException | ChecksumException e) { // 格式或校验错误,可忽略或记录 } } private void handleScanSuccess(String result) { // 震动反馈 Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); if (vibrator != null && vibrator.hasVibrator()) { vibrator.vibrate(200); } // 将结果发送回Unity UnityPlayer.UnitySendMessage("QRScannerManager", "OnQRCodeScanned", result); // 关闭Activity,返回Unity finish(); }这里有几个关键点:UnityPlayer.UnitySendMessage是Unity提供的用于从Java回调到C#的静态方法。第一个参数是Unity场景中某个GameObject的名字,第二个参数是该GameObject上某个脚本的公共方法名,第三个参数是要传递的字符串消息。“QRScannerManager”和“OnQRCodeScanned”需要与Unity侧的设置对应。
3.3 插件打包与Unity集成
将写好的Java代码、AndroidManifest.xml(需声明CAMERA权限和QRScanActivity)以及资源文件,按照Unity安卓插件的标准目录结构放置:
YourUnityProject/ └── Assets/ └── Plugins/ └── Android/ ├── AndroidManifest.xml (合并到最终Manifest) ├── res/ (可选,图标等资源) ├── libs/ (存放.jar或.aar依赖,如zxing-core-3.5.2.jar) └── src/ (你的Java源码,例如com/yourcompany/qrscan/QRScanActivity.java)也可以使用Android Studio将整个模块打包成.aar文件,放入libs目录,并在AndroidManifest.xml中通过<activity android:name="com.yourcompany.qrscan.QRScanActivity" />声明。
4. Unity C# 端整合与调用
原生插件准备好后,我们需要在Unity中创建调用它的桥梁。
4.1 创建扫码管理器脚本
在Unity中创建一个C#脚本,命名为QRScannerManager.cs,并将其挂载到一个空的GameObject上,该GameObject的名字必须与Java代码中UnitySendMessage的第一个参数一致(这里是“QRScannerManager”)。
using UnityEngine; using System.Runtime.InteropServices; // 用于DllImport(如果使用JNI直接调用) using UnityEngine.Android; // 用于Unity 2019.3+的权限系统 public class QRScannerManager : MonoBehaviour { // 用于接收识别结果的回调方法,必须是public的 public void OnQRCodeScanned(string resultText) { Debug.Log($"QR Code Scanned: {resultText}"); // 在这里处理识别结果,例如更新UI、加载场景、发送网络请求等 // 可以触发一个事件,让其他脚本订阅 OnScanSuccess?.Invoke(resultText); } // 定义一个事件,方便其他模块订阅扫描结果 public event System.Action<string> OnScanSuccess; void Start() { // 确保GameObject不被销毁 DontDestroyOnLoad(this.gameObject); RequestCameraPermission(); } void RequestCameraPermission() { #if UNITY_ANDROID && !UNITY_EDITOR if (!Permission.HasUserAuthorizedPermission(Permission.Camera)) { Permission.RequestUserPermission(Permission.Camera); } #endif } // 提供给UI按钮调用的开始扫描方法 public void StartQRScan() { #if UNITY_ANDROID && !UNITY_EDITOR // 使用AndroidJavaClass调用原生Activity using (AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) using (AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity")) { // 假设你的扫码Activity全路径是 com.yourcompany.qrscan.QRScanActivity AndroidJavaObject intent = new AndroidJavaObject("android.content.Intent", currentActivity, new AndroidJavaClass("com.yourcompany.qrscan.QRScanActivity")); currentActivity.Call("startActivity", intent); } #else Debug.LogWarning("QR Scanning is only supported on Android devices."); #endif } }4.2 Unity项目设置与打包关键点
Player Settings:
- Other Settings:
- Package Name: 设置一个唯一的包名(如
com.YourCompany.YourApp)。 - Minimum API Level: 设置为与你的原生插件兼容的级别(例如 Android 8.0 ‘Oreo’ / API 26)。
- Target API Level: 建议设置为最新的稳定版或与设备匹配的版本。
- Package Name: 设置一个唯一的包名(如
- Publishing Settings:
- 勾选Custom Main Gradle Template和Custom Main Manifest。这允许我们自定义构建配置,确保插件正确集成。
- Other Settings:
自定义
mainTemplate.gradle: 在Assets/Plugins/Android目录下找到或创建mainTemplate.gradle,确保添加了对ZXing库的依赖,因为Unity的默认Gradle模板可能不包含它。dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) // 添加ZXing依赖 implementation 'com.google.zxing:core:3.5.2' // ... Unity和其他依赖 }自定义
AndroidManifest.xml: 在Assets/Plugins/Android目录下创建或修改AndroidManifest.xml,合并插件的权限和Activity声明。<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.unity3d.player"> <uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera" android:required="true" /> <!-- 如果使用震动反馈,还需要此权限 --> <uses-permission android:name="android.permission.VIBRATE" /> <application ...> <activity android:name="com.unity3d.player.UnityPlayerActivity" ...> ... </activity> <!-- 声明你的扫码Activity --> <activity android:name="com.yourcompany.qrscan.QRScanActivity" android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen" android:screenOrientation="landscape" <!-- 根据Rokid眼镜的显示方向设置 --> android:exported="false"/> <!-- 通常不导出给其他应用调用 --> </application> </manifest>注意
android:screenOrientation,Rokid AR眼镜通常是横屏显示,可能需要设置为landscape或sensorLandscape。
5. 性能优化与实战调试技巧
在Rokid AR眼镜上运行,性能优化至关重要。以下是几个关键的优化点和调试技巧:
5.1 图像处理与识别性能优化
降低识别频率:不需要对每一帧预览图像都进行识别。可以在
ImageReader的回调中设置一个简单的帧率控制,例如每5帧或每100毫秒处理一帧。这能显著降低CPU使用率。private long lastProcessTime = 0; private static final long PROCESS_INTERVAL_MS = 100; // 100毫秒处理一次 public void onImageAvailable(ImageReader reader) { long currentTime = System.currentTimeMillis(); if (currentTime - lastProcessTime > PROCESS_INTERVAL_MS) { lastProcessTime = currentTime; Image image = reader.acquireLatestImage(); if (image != null) { processImage(image); image.close(); } } else { // 丢弃这一帧,直接acquireLatestImage然后close Image image = reader.acquireLatestImage(); if (image != null) image.close(); } }缩小识别区域:如果UI设计允许,可以只对预览画面的中心区域(比如50%的面积)进行识别,而不是全图。这可以通过在创建
PlanarYUVLuminanceSource时指定子矩形区域来实现,能大幅减少需要处理的数据量。选择合适的预览分辨率:过高的分辨率(如4K)会给图像传输和解码带来巨大压力。对于二维码识别,1280x720 (720p)或1920x1080 (1080p)通常已经足够清晰,且性能友好。在
chooseOptimalSize方法中优先选择这些分辨率。
5.2 内存与资源管理
及时关闭Image:这是最容易导致内存泄漏的地方。
ImageReader.acquireLatestImage()获取的Image对象,必须在使用完毕后调用image.close()。否则,摄像头缓冲区会被迅速耗尽,导致预览卡顿甚至崩溃。管理后台线程:在Activity的
onPause或onDestroy中,务必停止后台线程并关闭摄像头。@Override protected void onPause() { super.onPause(); closeCamera(); stopBackgroundThread(); } private void stopBackgroundThread() { if (backgroundThread != null) { backgroundThread.quitSafely(); try { backgroundThread.join(); backgroundThread = null; backgroundHandler = null; } catch (InterruptedException e) { e.printStackTrace(); } } } private void closeCamera() { if (cameraCaptureSession != null) { cameraCaptureSession.close(); cameraCaptureSession = null; } if (cameraDevice != null) { cameraDevice.close(); cameraDevice = null; } if (imageReader != null) { imageReader.close(); imageReader = null; } }
5.3 Rokid AR眼镜特定适配与调试
摄像头ID与方向:不同型号的Rokid眼镜摄像头数量和位置可能不同。
manager.getCameraIdList()[0]可能不是预期的后置摄像头。需要通过CameraCharacteristics检查LENS_FACING属性来筛选出LENS_FACING_BACK的摄像头。此外,摄像头传感器的自然方向可能与屏幕方向不一致,需要根据CameraCharacteristics.SENSOR_ORIENTATION对预览和识别图像进行旋转校正。使用ADB Logcat进行真机调试:这是定位问题最有效的方法。将Rokid眼镜通过USB连接至电脑,在命令行使用
adb logcat -s Unity来过滤Unity的日志,或者使用adb logcat | grep -E "(你的包名|Camera|QRScan)"来查看相关日志。在代码关键位置添加Log.d(“QRScan”, “message”)输出,能帮你清晰地跟踪执行流程和变量状态。处理Unity与原生Activity的切换:启动原生扫码Activity后,Unity的渲染会暂停。当扫码完成返回Unity时,确保Unity场景能正确恢复。可以在Unity脚本的
OnApplicationPause方法中处理一些状态保存和恢复的逻辑。
6. 常见问题排查与解决方案实录
在实际开发中,我遇到了不少问题,这里把典型的几个和解决方法列出来,希望能帮你节省时间。
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| Unity调用后黑屏或闪退 | 1. 摄像头权限未获取。 2. AndroidManifest.xml中未正确声明Activity或权限。3. 使用的摄像头ID无效或摄像头被占用。 4. 原生代码(如Camera2 API)使用不当导致崩溃。 | 1. 检查logcat崩溃日志,定位错误行。2. 确保动态权限申请流程正确,并在真机上确认权限已开启。 3. 检查 AndroidManifest.xml,确保<activity>标签的name属性路径完全正确。4. 编写一个极简的、只打开摄像头预览的原生Demo,先在Rokid眼镜上单独测试,排除Unity环境干扰。 |
| 能打开摄像头但识别不到码 | 1. 图像数据格式转换错误(YUV转码错误)。 2. 识别区域或图像尺寸问题。 3. 环境光线太暗或二维码对比度太低。 4. ZXing库未正确集成。 | 1. 在processImage方法中,将nv21字节数组保存为文件(或通过Socket发送到电脑),用图片查看工具检查图像是否正确。2. 尝试识别一个非常简单、高对比度的二维码,排除算法问题。 3. 在原生代码中,尝试使用 BitmapFactory.decodeResource加载一张本地包含二维码的图片进行识别,测试ZXing库本身是否工作。 |
| 识别速度慢,眼镜发热严重 | 1. 识别频率过高(每帧都识别)。 2. 预览分辨率设置过高。 3. 图像处理(如YUV转换)在UI线程进行。 | 1. 实现帧率控制,如上一节所述。 2. 将预览分辨率从1080p降至720p试试。 3. 确保 processImage方法在backgroundHandler所在的线程执行,绝对不在主线程。 |
| 返回Unity后,识别结果未收到 | 1.UnitySendMessage参数错误。2. Unity中接收消息的GameObject未激活或已被销毁。 3. 原生Activity在发送消息前已 finish()。 | 1. 检查UnitySendMessage的第一个参数(GameObject名称)和第二个参数(方法名)是否与Unity场景中的对象和脚本方法完全一致(大小写敏感)。2. 确保挂载 QRScannerManager脚本的GameObject在场景中是激活且持久的(用了DontDestroyOnLoad)。3. 在 handleScanSuccess中,在UnitySendMessage之后、finish()之前加一个短暂延时,或者确保消息发送成功后再结束Activity。 |
| 打包APK后功能失效,编辑器正常 | 1. Proguard混淆移除了必要的类或方法。 2. .aar或.jar依赖未正确打入APK。3. mainTemplate.gradle配置未生效。 | 1. 在proguard-user.txt中添加规则,保持ZXing和你的插件类不被混淆:-keep class com.google.zxing.** { *; }和-keep class com.yourcompany.qrscan.** { *; }。2. 检查构建日志,确认依赖是否正确添加。可以解压生成的APK,查看 libs目录下是否有你的插件和ZXing的jar包。3. 确认 Assets/Plugins/Android下的mainTemplate.gradle和AndroidManifest.xml文件确实被使用(有时需要删除Library缓存强制重新生成)。 |
最后,分享一个调试时的小技巧:在Unity编辑器中,虽然无法运行原生插件,但你可以通过模拟回调来测试整个逻辑流。在QRScannerManager的StartQRScan方法里,用#if UNITY_EDITOR预编译指令包裹一段模拟代码,随机生成一个字符串并直接调用OnQRCodeScanned,这样可以在不连接真机的情况下,验证UI和业务逻辑是否正确响应扫描结果。
本文还有配套的精品资源,点击获取