☰
Cocos Creator调用Android震动:从JSB到JNI的原生交互实践
2026/10/14 5:44:36 网站建设 项目流程

搞过Cocos Creator接原生功能的朋友,应该都遇到过这种场景:游戏里想调用一下手机的震动反馈,找了一圈发现JS层根本没有现成API,要么就得自己写一堆jsb绑定代码,要么就得在原生工程里折腾半天。这篇文章想分享的就是这个看似很小、但特别有代表性的功能——震动,怎么通过Java原生交互在Cocos Creator里跑通。整个链路不复杂,但里面牵扯到的JNI、jsb反射、权限配置、异步回调这些问题,一通百通。如果你正好在做原生功能扩展,或者刚接触Cocos Creator与Android原生的互通,这篇文章应该能帮你少踩不少坑。

我先把这段话开头里最关键的一个点挑明:标题里说的“获取手机震动”,真正要做的事情其实是用JavaScript调用Android侧的Java代码,触发系统震动服务。不是“拿到震动传感器的数据”,也不是“读取震动状态”,而是“让手机按你的节奏震起来”。这个语义搞清楚了,后面写代码才不会跑偏。

1. 为什么拿“震动”当原生交互的入门案例

1.1 震动功能背后的跨语言链路

Cocos Creator开发的游戏跑在Android上,JS引擎和Java虚拟机是两套完全隔离的运行时。JS代码操作不了Android系统服务,Java代码也拿不到JS里的游戏对象,唯一能连通两者的是一座桥。这座桥在Cocos Creator里叫JSB(JavaScript Binding),底层就是JNI(Java Native Interface)。

震动这个功能,从JS发起请求到手机真正震动,完整链路是这样的:

上面这行是占位,实际写作时不能输出mermaid。改为文字表述。

JS层点击按钮 -> 调用jsb.reflection.callStaticMethod或通过JNI封装后的全局函数 -> 进入C++层 -> 通过JNI调用Java静态方法 -> Java调用系统Vibrator服务 -> 系统驱动马达震动 -> 结果再按原路返回给JS回调。

这条链路每一步都有文章可做。震动功能本身逻辑极短(System服务一行代码的事),但它把整条链路完整地串起来了,而且功能可验证、可感知,测试的时候手机一颤就知道成功没成功。这是我认为它特别适合当入门案例的核心原因——小功能,大链路。

1.2 两条调用路线的取舍

实际开发中,JS调Java有两条主流路线。一条是纯反射路线,直接在JS层调用Cocos提供的jsb.reflection.callStaticMethod,把类名、方法名、签名、参数一股脑传进去,让引擎在运行时帮你反射调用Java静态方法。另一条是JNI路线,在C++层手写extern "C"接口,再通过JNIEnv找到Java类、调用Java方法,最后把C++函数暴露给JS。

两条路线怎么选?我的建议是分场景:

  • 只想快速实现功能、不想动引擎源码的,用反射路线,Cocos Creator 2.x到3.x都保留了这套接口,改动成本最低。
  • 要做高性能、高频次、参数复杂的通信(比如每帧同步位置、批量传输数据),必须走JNI路线,反射的开销在那种场景下撑不住。
  • 如果涉及Java回调JS(比如原生侧主动通知游戏“震动结束了”),反射路线只能自己搞轮询或定时器,而JNI路线可以在C++层直接把回调结果分发到JS线程。

震动这个场景,震动时长、震动节奏这些参数都是低频一次性传参,反射路线完全够用。但为了让这篇文章更有深度,我把两条路线都讲一遍,你根据自己的项目阶段选择。

2. 准备阶段:先把原生工程跑起来

2.1 准备一个自带原生工程的版本

做原生交互的前提是,你已经有了一个构建出来的Android原生工程可以改。Cocos Creator的项目工程在没构建之前就是一堆资源、脚本、场景文件,没有Java代码可以改。所以要先把工程构建成Android项目,默认会生成一个带原生壳的工程目录。

不同版本构建界面有差异,但套路是一致的:在构建发布面板选择Android平台,填写包名、应用名,选好构建模板目录,点击构建。构建完成之后,你会得到一个完整的Android Studio工程,里面有app模块、原生代码目录,后面所有改动都落在这个工程里。

如果你用的是Cocos Creator 3.x,尤其注意一个设置项:构建时选择的应用ID就是Android的应用ID,后面Java类的包名和它直接相关,别随手填个乱码,后面写JNI找类时会很痛苦。我之前接过一个项目,构建的时候应用ID填成了类似com.a.b这种占位符,后来改包名牵一发动全身,教训深刻。

2.2 在Android Studio里确认包名

构建完成后,先不要急着写代码,用Android Studio打开生成的工程,做三件事:

  1. 找到AppActivity这个Java文件,它的包名就是你的应用包名,后面写的桥接类最好放在同一个包下,不用额外配import。
  2. 确认AndroidManifest.xml里已经声明了震动权限。
  3. 确认构建变体选的是debug还是release,两个变体的混淆规则不一样,release包要做防混淆处理。

包名这个东西,普通功能可能无所谓,震动这种直接通过类名字符串反射调用的功能极其敏感。你JS层写的是带包名的完整路径,Java类路径对不上就静默失败,而且日志还不太明显,排查起来非常头大。

2.3 写出震动服务的Java封装类

Android系统提供的震动服务叫Vibrator,获取方式是getSystemService(Context.VIBRATOR_SERVICE)。新版Android(8.0及以上)推荐用VibrationEffect来构建设备支持的震动效果,旧版则直接调vibrate(long milliseconds)。

为了兼容旧机型,我一般会在Java封装类里做个版本判断:

package com.example.game; import android.content.Context; import android.os.Build; import android.os.VibrationEffect; import android.os.Vibrator; public class VibrationBridge { private static Vibrator vibrator; public static void init(Context context) { vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); } public static void vibrate(long milliseconds) { if (vibrator == null) { return; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { VibrationEffect effect = VibrationEffect.createOneShot(milliseconds, VibrationEffect.DEFAULT_AMPLITUDE); vibrator.vibrate(effect); } else { vibrator.vibrate(milliseconds); } } }

这个类我在实际项目里一直这么写,主要是为了统一入口。init在哪里调用?建议在AppActivity的onCreate里或者Application的onCreate里调一次,把ApplicationContext传进去,避免Activity全局持有导致的泄漏。震动服务本身是系统级单例服务,用ApplicationContext拿不会有什么问题。

3. 从JavaScript到Java的桥接实现

3.1 路线一:JS层直接反射调用

先讲最简单的。在Cocos Creator的JS/TS脚本里,直接通过全局对象jsb访问反射方法。

TS代码:

export function vibrate(ms: number) { if (jsb && jsb.reflection) { jsb.reflection.callStaticMethod( "com/example/game/VibrationBridge", "vibrate", "(J)V", ms ); } }

这段代码看起来简单,但里面全是细节。callStaticMethod的四个参数分别是:类路径、方法名、方法签名、实际参数。方法签名是JNI规范里的参数描述符号,(J)V表示“接收一个long参数,返回void”。这里的J是大写,对应Java里的long类型,如果写成小写j就找不到方法,签名不匹配会导致调用静默失败,这条经验我吃了不少亏。

还需要注意,ms这个参数在TypeScript里是number,但Java实参是long。JS数字默认是双精度浮点,直接传过去可能会精度丢失或类型不匹配。Cocos的jsb.reflection内部会做一次转换尝试,但保险起见,对大数值还是优先在JS侧做整数检查,或者改用(int)参数、签名写成(I)V更稳妥。

反射路线的优势是改动小,不需要动C++工程,适合快速验证。缺点也明显:每调用一次都要经历一次字符串解析、类查找、方法查找的反射过程,高频场景性能不佳。另外在release包里如果Java类被混淆了,类名和方法名会变,反射就会找不到,必须在proguard规则里把桥接类keep住。

3.2 路线二:JNI手动封装

如果你对性能有要求,或者需要双向通信,那就得学第二种方式:自己写JNI函数。

在Cocos Creator的native引擎工程里,找到对应的C++源码目录,新增一个C++文件,比如命名成VibrationBridge.cpp。

#include "cocos/cocos.h" #include <jni.h> #include "platform/android/jni/JniHelper.h" using namespace cocos2d; static void vibrateJNI(long milliseconds) { JniMethodInfo methodInfo; bool isFound = JniHelper::getStaticMethodInfo( methodInfo, "com/example/game/VibrationBridge", "vibrate", "(J)V" ); if (!isFound) { CCLOG("vibrate method not found"); return; } methodInfo.env->CallStaticVoidMethod(methodInfo.classID, methodInfo.methodID, (jlong)milliseconds); methodInfo.env->DeleteLocalRef(methodInfo.classID); } extern "C" void JSB_VibrationBridge_vibrate(long milliseconds) { vibrateJNI(milliseconds); }

然后在引擎的JS绑定注册表里把这个C++函数暴露给JS层。每个版本的绑定方式不一样,有的用JsbRegister,有的用se::Object,具体查你当前引擎版本的扩展机制。

这条路线的核心优势在于:Java侧可以通过JNI反向回调C++函数,C++再把数据同步到JS线程。震动结束、传感器数据变化、异步任务完成通知,这些场景都需要这个能力。反射路线做不了这件事,或者做起来非常别扭。

3.3 权限与配置的坑

不管走哪条路线,AndroidManifest.xml里必须声明震动权限:

<uses-permission android:name="android.permission.VIBRATE" />

这个权限属于普通级别,不需要运行时动态申请,Android 6.0之后也不会弹窗问用户,只需要静态声明即可。但真有人会漏掉这一行——我自己都漏过。漏掉的结果是方法调用不报错、不闪退,但手机就是纹丝不动,排查方向很容易跑偏到JS层,浪费一两个小时。

另外提一个Cocos Creator 3.x特有的配置点:如果你用的构建模板开启了主包或分包,某些包的AndroidManifest可能是单独维护的,要确认权限加在了正确的那份manifest里,别只在构建出来的临时目录里加了权限,重新构建后又丢了。

注意:震动权限对很多第三方SDK来说都是默认会带上的,如果你工程里接了很多SDK,权限很可能早就存在了。但不要依赖这一点,自己项目显式声明才是可控行为。

4. 构建运行与真机调试全流程

4.1 构建工程并入Android Studio

前面说过,构建之后拿到的是一个Android工程。实际项目里,开发流程一般是这样的:Cocos Creator负责写游戏逻辑和JS/TS层,Android Studio负责Java层和C++层原生代码,两者交替修改。

写完Java和C++代码之后,需要重新构建。此时选择可能有两种:一种是直接回到Cocos Creator里重新构建整个游戏工程,耗时较长但最干净;另一种是只在Android Studio里做增量编译,把修改的Java/C++代码打包进APK,速度快很多。

我推荐开发阶段用第二种:Android Studio增量编译。每次Cocos Creator构建完成一次之后,后续的原生代码改动全部在Android Studio里处理。注意重新构建原生工程时,Cocos会用新的资源覆盖原生的assets目录,但不会清理你手动添加的Java文件和自定义C++源文件,所以你的改动会保留。这个机制偶尔也会出问题:某些版本构建时会把原生目录整个清空重来,务必养成改完原生代码立刻备份或纳入版本管理的习惯。

4.2 JS层异步回调的必要性

震动本身是瞬时动作,调用完就结束了,不需要回调。但如果你做的是一次带时长的震动(比如300ms),产品想要“震动结束后再播放音效”,那就要在Java侧做监听(VibrationEffect有个setOnVibrationEndListener之类的能力),然后再把事件通知回JS层。

这个过程中最容易踩的坑是线程问题。Android的震动结束回调发生在主线程,但Cocos的JS引擎跑在独立的游戏线程上,直接跨线程调用JS函数不安全。正确做法是通过Cocos提供的调度机制,把回调投递到游戏线程执行。

落地时一般这么操作:Java侧记一个回调ID,震动结束后调用C++层的通知函数,C++函数里用Director::getInstance()->getScheduler()调度一个block或lambda,在游戏主线程里执行JS回调。

简化描述的话,逻辑是这样的:

  1. Java检测到震动结束。
  2. Java调用C++的notify函数。
  3. C++把任务切到游戏线程。
  4. 游戏线程执行JS回调函数。

这套机制不仅震动要用,任何原生事件要通知JS(比如SDK登录成功、支付结果、定位返回)都是同一个套路。把这一步吃透了,等于是把原生互联的双向通信彻底打通了。

4.3 震动API的扩展玩法

基础的单次震动只要能跑通,后面扩展就顺手了。我在实际项目中还做过这些变体:

  • 按固定间隔重复震动,比如设备收到游戏内警报时连续震三下,间隔200ms。Android的VibrationEffect支持Waveform模式,传一个节奏数组和重复索引即可。
  • 自定义震动强度,不同场景用不同振幅,配合DEFAULT_AMPLITUDE之外的数值,做手柄感反馈。
  • 多端同步震动:游戏里一个炸弹爆炸,手机震动的同时通过WebSocket通知其他设备也震动,这种玩法就是震动API加上网络层的活了,原理还是那一套。

给自己做的桥接类留好扩展位,别写完一个vibrate(long)就收工,起码把createWaveform、(如果硬件支持)振幅参数都封装上,后面产品提需求时直接改参数就行。

5. 常见问题排查实录

5.1 震动没反应?从三层排查

震动调用无响应的原因,按概率排序是这几类,我按排查顺序整理成一张表:

现象排查方向处理办法
权限缺失检查AndroidManifest.xml里是否有VIBRATE权限补上权限,重新构建
真机系统震动总开关关闭部分手机在设置里有“触摸震动”总开关,关闭后代码调震动也无效到系统设置里打开,属于系统级行为,非代码问题
类路径写错JS或JNI里写的类路径和Java实际包名不一致核对com/example/game这种路径格式,注意是斜杠不是点
方法签名不匹配(J)V、(I)V等参数描述符与Java方法实际参数不一致逐个参数核对签名,long要写J,int写I,String写Ljava/lang/String;
混淆导致类名变了release包中Java类被ProGuard或R8混淆在proguard-rules.pro里keep桥接类和它所有方法
Vibrator对象为空没调用init,或者init时拿到的context不对确保getSystemService传入的是ApplicationContext

在实际项目中,我最常遇到的其实是“方法签名写错”和“类路径写错”这两个,因为它们没有报错,全靠眼力排查。建议在Java桥接类每个方法入口先打一条Log,JS调用后看日志有没有进来,就能快速判断是JS没调到还是Java执行出问题。

5.2 findClass失败与ClassNotFoundException

这种情况多数发生在release包、或启动时就初始化失败的场景。如果你用的是反射路线,ClassNotFoundException大概率是混淆导致。如果是JNI路线,先检查你的Java类是不是静态类、方法是不是static,JNI找类和找方法对这两点要求很严格,非静态方法会直接找不到MethodID。

另外,如果桥接类被打到某个动态加载的模块(比如插件化框架、动态下发代码),那么类加载器都不一样,JNI找类时可能根本看不到这个类。做SDK接入时遇到这类问题要格外注意类加载器上下文。

5.3 iOS端怎么办

标题写的是与Java原生交互,但Cocos Creator是跨平台引擎,震动需求通常两端都要做。iOS侧简单得多,AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)一行代码搞定,不需要像Android这样配置权限。

我的做法是:在C++封装层做一个平台判断,Android走JNI调用Java实现,iOS走Objective-C/Swift的系统API,JS层只暴露同一个接口,不感知平台差异。这样游戏逻辑代码写一次,两边复用。如果你只做Android发布,可以跳过iOS适配,但接口设计上建议先抽象一层,免得后期改起来动筋动骨。

5.4 真机测试的必要性

模拟器上震动几乎都测不了真效果,很多模拟器连VIBRATOR_SERVICE都没实现,调用后无反馈。所以这个功能的测试必须在真机上做。测试时还要覆盖几种型号和Android版本,至少在API 26以下和API 26以上各测一台,因为VibrationEffect分支只在Android 8及以上走,老机型走的是旧接口。

有一个小技巧:如果你手头没有老系统真机,直接在代码里把新老接口都写上,然后用版本判断分流。别偷懒只写新接口,现在仍然有相当一批低版本Android设备在跑,崩不崩另说,接口压根不生效就很尴尬。

实操总结与个人体验

震动这个功能做下来,最大的感受是:原生交互的难点从来不在“调方法”这一下,而在于链路长、环节多、每层都有自己的一套规则。类路径格式、签名规范、线程切换、混淆处理,随便哪个细节没到位,功能就静默失败,而且报错日志往往还看不出来。

我个人的习惯是:任何原生桥接功能,都会先在Java侧打日志验证入口,再在JS侧打日志验证出口,前后夹逼定位问题。震动只是第一步,等你把这个链路跑通,后面接支付SDK、接广告SDK、调系统相册,全是同一个套路,代码结构都能复用大半。

最后再分享一个小技巧:震动桥接类的Java包名和C++构造里的类路径不要用硬编码字符串到处贴,定义一个常量统一管理。真到了要改包名那天,你会感谢这个决定——不然满项目搜字符串找类路径,很容易漏改一处,那画面我经历过,不太想经历第二次。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询