尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Unity3D与Rokid AR眼镜混合开发:高性能二维码识别方案全解析

Unity3D与Rokid AR眼镜混合开发:高性能二维码识别方案全解析 简介在增强现实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:namecom.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.Actionstring 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.GetStaticAndroidJavaObject(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: 建议设置为最新的稳定版或与设备匹配的版本。Publishing Settings:勾选Custom Main Gradle Template和Custom Main Manifest。这允许我们自定义构建配置确保插件正确集成。自定义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 version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.unity3d.player uses-permission android:nameandroid.permission.CAMERA / uses-feature android:nameandroid.hardware.camera android:requiredtrue / !-- 如果使用震动反馈还需要此权限 -- uses-permission android:nameandroid.permission.VIBRATE / application ... activity android:namecom.unity3d.player.UnityPlayerActivity ... ... /activity !-- 声明你的扫码Activity -- activity android:namecom.yourcompany.qrscan.QRScanActivity android:themeandroid:style/Theme.Translucent.NoTitleBar.Fullscreen android:screenOrientationlandscape !-- 根据Rokid眼镜的显示方向设置 -- android:exportedfalse/ !-- 通常不导出给其他应用调用 -- /application /manifest注意android:screenOrientationRokid 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和业务逻辑是否正确响应扫描结果。本文还有配套的精品资源点击获取
返回列表