
做 uni-app 项目的朋友应该都有过这种体验JS 层逻辑写得好好的一碰到蓝牙、NFC、身份证读卡器这类硬件能力或者要接入某个只有原生 SDK 的厂商服务瞬间就抓瞎了。我自己第一次在 uni-app 项目里对接一脸谱人脸识别 SDK 时就卡了将近两周后来老老实实把安卓原生插件这套东西啃下来才算是把路走通。这篇博文就是把我摸索过程中踩过的坑、验证过的方案以及最终沉淀下来的标准操作流程一次性整理出来给正准备入坑或者已经被坑得焦头烂额的同学一个完整参考。原生插件这词听起来很玄乎但本质上就是把安卓的 Java/Kotlin 代码包成一个 uni-app 能调用的模块。你平时在 HBuilderX 里写着 vue 文件突然要调一个安卓独有的系统接口这时候 JS 桥接层够不着原生插件就派上用场了。它能帮你做的事远超想象调用系统级 API、集成第三方商用 SDK、处理大数据量计算、甚至自己画原生 UI 组件再嵌进页面。只要你的 uni-app 项目跑在安卓端这就是帮你突破 JS 能力边界的最直接武器。我写这套内容前专门翻了翻最近大家在群里问得最多的问题发现大多数人不是不想学而是卡在“不知道从哪儿下手”。网上资料要么只讲了某一个环节要么版本偏老跟着做一遍根本跑不起来。所以这篇文章我会从开发环境搭建一直写到插件打包上架全程按我正在用的这套稳定方案来尽量做到让一个没写过原生代码的 uni-app 开发者也能照着把插件跑通。1. 原生插件到底是什么以及你什么时候需要它1.1 为什么 uni-app 应用会需要原生插件uni-app 能一套代码跑三端靠的是对 JS 层的封装但封装是有边界的。你写uni.scanCode这类内置 API 时很爽因为框架把原生实现帮你藏起来了可一旦你想调用某个传感器、某个系统服务或者集成第三方厂商的 SDK框架就会告诉你“此 API 仅支持 App 端”。说得直白点uni-app 的 JS 运行环境被关在一个盒子里盒子的围墙能开多少窗户取决于官方帮你封了多少原生能力。以我实际项目为例当时客户要求 App 内嵌入一个证件识别功能供应商给的是带 so 库和 aar 的安卓 SDKiOS 端也只有对应的 framework。这种商业 SDK 又不可能有人专门给你做 uni-app 适配你没有别的选择只能通过原生插件把这层桥搭起来。类似场景还包括对接打印机、读取身份证、使用高精定位算法、播放特殊格式的音视频流这些都是官方 API 覆盖不到的地方。值得注意的是不要一遇到问题就想着上原生插件。插件开发有维护成本而且每次 HBuilderX 升级、安卓 API 变化都有可能影响插件运行。能用uni-app内置 API 或现有市场插件解决的优先用现成的只有当需求已经明确超出 JS 层能力范围或者性能表现完全无法接受时才是你打开 Android Studio 写原生代码的最佳时机。1.2 插件的三种形态Module、Component 与 JS 插件uni-app 安卓原生插件从承载方式上看分三类。第一种是Module 插件最常见也最实用它只封装逻辑、不涉及 UI你的 App 里某个页面某个按钮点击后去调用一个原生方法这个方法内部做了什么事页面感知不到。蓝牙连接、数据加解密、通讯协议解析这类都适合做成 Module 类型实现就是写一个继承自UniModule的 Java 类暴露方法给 JS 层调用。第二种是Component 插件这种是用来提供原生控件的比如一个自定义的扫码取景框、一个视频播放器内核它直接作为页面里的一个组件来使用写法上类似module-file-system。Component 插件开发难度比 Module 高一些因为要处理组件生命周期、属性同步、事件回调非必要不建议新手先碰。第三种是JS 插件这个严格来说不是原生插件而是纯 JS 封装的公用代码模块适合封装网络请求、工具函数等跨端统一逻辑不需要原生参与。我在实际项目中通常把公共的 JS 工具、请求封装做成这类插件放到市场里方便多个项目复用。如果你是第一次接触 uni-app 原生插件我的建议很直接从 Module 类型开始把环境、打包、调试这条路跑通再考虑 Component。Module 插件的开发链路最短能最快验证你的工具链是不是通的也能帮你建立对 uni-app 原生通信机制的整体理解。1.3 本地插件和云端插件的运行机制差异插件开发分两套使用方式一个是本地直接导入 HBuilderX 工程里用一个是上传到 uni-app 插件市场再云打包时拉取。本地插件适合你个人或者公司内部项目自用直接把 android 目录放到项目nativeplugins文件夹下就能识别云端插件适合发布给其他开发者使用你需要在插件市场完成上架别人集成时在 manifest.json 里点选一下就能打包。很多人第一次用本地插件时改完原生代码重新运行自定义基座发现怎么调都是旧逻辑这就是没搞清楚运行机制。本地插件模式下的生效过程是HBuilderX 在打自定义调试基座自定义基座时会把nativeplugins下的插件代码一并编译进去基座就是你的调试环境。所以只要你改了原生代码必须重新制作自定义基座等编译完成后基座下载安装到手机然后再运行 uni-app 项目新逻辑才会真正生效。这个流程搞不清楚后面你连 debug 都会一头雾水。云端插件则依赖插件市场的在线打包服务你在 manifest.json 里勾选了某个付费或免费插件云打包服务器会自动下载对应的 aar 或源码合并进 APK。好处是不用本地配环境但相对的调试时你不可能每次都走云打包开发阶段建议先把插件做成本地形式跑通了再上传到市场做云端发布。2. 开发环境准备与工程搭建2.1 开发工具和版本选择做安卓原生插件开发两样工具跑不掉Android Studio和HBuilderX。Android Studio 负责编写和调试原生代码HBuilderX 负责跑 uni-app 项目、制作自定义基座、最终打正式包。版本方面我个人的推荐配置是Android Studio 用较新稳定版我目前用的是 Electric Eel 之后的版本Gradle 版本不用刻意去追最新用模板默认的就行。另外需要注意uni-app 开发原生插件有专门的Android 离线 SDK里面包含了 uni-app 的运行时库和一些示例工程。这里有个容易踩的坑很多人直接去官网下载最新版离线 SDK然后发现跟自己的 HBuilderX 版本不匹配编译报一堆错。我的做法是在 HBuilderX 的帮助菜单里点“查看版本”记下完整的版本号比如 3.99.xx再去找对应版本的离线 SDK 下载。版本匹配这件事比用什么 Android Studio 更重要因为 uni-app 的运行时 API 是跟着主版本走的不一致就是无解。还有个小细节Java 环境。新版 Android Studio 自带 JBR 没问题但如果你在命令行里跑 Gradle 打包确保JAVA_HOME指向 JDK 17 或以上。模板工程如果默认配了 JDK 8你一定得手动改成 17否则 Gradle 同步那一步就会卡住。2.2 下载官方 uniplugin 模板工程uni-app 官方提供了一个叫uniplugin_android的 GitHub 模板工程这是目前最省事的起点。你不需要从零去创建安卓工程直接 clone 下来改一改就能用。这个模板里已经写好了 Module 和 Component 的示例代码还配好了打包脚本和 manifest 配置你只需要在此基础上添加自己的类和方法。模板的核心目录结构是这样的app目录示例 app可以单独运行起来调试插件uniplugin_moduleModule 插件示例代码所在 moduleuniplugin_componentComponent 插件示例代码libs目录放置你自己要集成的 aar、jar、so 文件克隆下来之后先用 Android Studio 打开等待 Gradle 同步完成跑一遍示例 app确认环境没问题再做修改。如果同步失败优先检查 Gradle 版本和网络另外把compileSdk和targetSdk版本调到你手机上能支持的范围。2.3 创建独立的插件 Module我第一次做插件时犯过一个大错直接在主 app 里写插件代码结果打自定义基座时怎么都不识别。正确的做法是在 Android Studio 里创建一个独立的Android Library Module让它承载插件代码编译产物是 aar这样 HBuilderX 才能按约定的格式识别和集成。在模板工程里uniplugin_module就是一个现成的 Library Module它的build.gradle开头是com.android.library并且没有任何 applicationId。如果你要新建自己的 Module照着它的样子复制一个再改名就行。就算你只有一个插件类也建议拆成独立 Module以后插件多了、相互依赖了这个结构能帮你省很多事。配置好之后在 Module 的src/main/assets下创建dcloud_uniplugins.json文件这个文件就是插件注册表uni-app 运行时就是靠读它来把 JS 调用映射到原生类上的。格式大致是{ nativePlugins: [ { plugins: [ { type: module, name: MyPlugin, class: com.example.myplugin.MyPluginModule } ] } ] }这里的name就是你在 JS 层uni.requireNativePlugin(MyPlugin)时传入的名字一定要保持一致。class是完整的类路径包名写错一个字符运行时就报找不到插件。2.4 把 Module 集成到 HBuilderX 工程原生代码这边准备好之后回到 HBuilderX在你项目的根目录下创建nativeplugins文件夹里面再建一个以插件名命名的子目录比如MyPlugin然后把你在 Android Studio 里写好的源码、libs、以及dcloud_uniplugins.json按照官方要求的目录结构放进去。结构大概长这样nativeplugins/ └── MyPlugin/ ├── android/ │ ├── libs/ │ ├── src/ │ └── build.gradle └── package.jsonpackage.json里填插件的基本信息包括插件标识、名称、版本号、插件类型、平台标识等。注意这个标识在 HBuilderX 里显示用不能跟其他插件重名。我遇到过一次因为 package.json 格式写错HBuilderX 直接拒绝识别插件报错信息还特别隐晦后来才排查到是 JSON 里多个逗号。做完这些在 HBuilderX 的 manifest.json 里切到“App 原生插件配置”点“本地插件”就能看到你刚放进来的插件了。勾选启用然后重新制作自定义基座集成就算完成。3. 核心开发流程与实操实现3.1 编写 UniModule 子类与核心注解插件的核心就是你写的那个继承UniModule的 Java 类。你在这个类里写的方法就是 JS 层能直接调用的原生接口。模板的UniModule类自带了一个onCreate生命周期这里可以做一些插件初始化的工作比如获取上下文、初始化 SDK 实例。方法定义有几个关键约束第一方法必须是 public 的第二必须用UniJSMethod注解标注运行时才能识别第三方法必须接收一个UniJSCallback参数用来把结果异步回传给 JS。下面是一个简洁的示例public class MyPluginModule extends UniModule { private static final String TAG MyPluginModule; UniJSMethod(uiThread false) public void doSomething(String param, UniJSCallback callback) { JSONObject result new JSONObject(); try { result.put(code, 0); result.put(data, Hello from native, param param); callback.invoke(result); } catch (Exception e) { result.put(code, -1); result.put(error, e.getMessage()); callback.invoke(result); } } }也许你注意到了uiThread false这个设置它表示这个方法会跑在非 UI 线程上。如果你的原生逻辑是耗时操作比如网络请求、文件读取建议设成 false 避免阻塞主线程如果只是简单的数据查询、页面跳转、Toast 提示设成 true 更省事。需要注意的是如果指定了uiThread false你在方法里操作 UI 就必须要切回主线程否则会崩溃。3.2 JS 层调用插件方法与参数传递细节代码写完之后在 uni-app 的 vue 页面里调用。第一步是用uni.requireNativePlugin(MyPlugin)拿到插件实例注意这一步要在插件准备好之后做也就是页面onLoad或之后别在全局声明时直接调用否则可能拿到空值。const plugin uni.requireNativePlugin(MyPlugin) plugin.doSomething(hello, (res) { console.log(result JSON.stringify(res)) })参数传递这块有几个容易踩的坑。原生方法如果接收 String 参数直接传就行接收 int、float 也没问题uniapp 会帮你做类型转换但如果要传复杂对象不能直接传一个 JS 对象JS 那边的对象到原生这边会变成一个 JSONObject原生这边方法签名最好写JSONObject或JSONArray用optString、optInt这类安全取值方法不要用getString直接拿因为 key 不存在时会抛异常而不是返回空值。回调方面UniJSCallback的invoke方法可以多次调用。每次调用 JS 这边的回调函数都会收到一次消息。利用这个特性你可以实现进度上报、原生日志输出到页面等场景。但要注意多次回调是有性能开销的别拿它当高频数据通道如果每秒要推几十次数据给 JS建议走事件通道而不是回调。3.3 事件发送让原生主动通知 JS 层有一种场景是原生在后台收到了系统广播、长连接消息这时候需要主动把数据推给 JS 层而不是等 JS 来调用。为此uni-app 给插件模块准备了事件机制。在 Module 里可以用mUniSDKInstance.fireGlobalEventCallback向上层广播事件。mUniSDKInstance.fireGlobalEventCallback(onMessageReceived, jsonObject);JS 层用uni.$on(onMessageReceived, callback)监听即可。这里要注意事件名的全局唯一性因为你不知道项目里还有没有别的插件用了同样的名字最好加上插件前缀比如myplugin_onMessage。另外记得在页面onUnload或组件销毁时调用uni.$off取消监听避免内存泄漏和重复回调。事件机制还有一个妙用用它把大型数据从原生分片传给 JS。比如一个几兆的日志文件一次 callback 传过大的 JSON 会导致页面卡顿甚至内存溢出切成小块用事件循环推页面就不卡了。3.4 自定义调试基座与真机运行流程写完代码不是直接跑 uni-app 就能生效的前面说过必须重新制作自定义基座。流程很简单HBuilderX 菜单栏点“运行”-“运行到手机或模拟器”-“制作自定义调试基座”等编译结束后就变成了一个带原生插件的 App。基座制作完成后在手机和电脑用数据线连好确保手机开启 USB 调试然后选择“运行到手机或模拟器”里刚才制作的那个自定义基座。这时候启动的是打包好原生代码的调试壳子才能真正调用到你的插件。调试时有个很有用的技巧在原生代码的各个关键节点打上日志然后用 Android Studio 的 Logcat 查看。HBuilderX 控制台只能看到前端 JS 的报错原生层的异常很多时候是静默崩溃。记得先通过adb把设备连上 Android Studio再运行基座这样 Logcat 里能实时看到插件打出来的所有日志。3.5 开发环境在 Android Studio 里的调试方法原生插件有个比较麻烦的地方uni-app 项目的调试实际上分两层。前端层调试可以用 HBuilderX 的调试工具原生层的断点调试需要一点额外手段。我常用的场景是单独把 Android Studio 工程跑起来应用初始化时加载一个测试页面直接在这个测试页面里调用插件方法这样能在原生代码里打断点、看变量、查调用栈效率比在黑盒里猜高得多。具体做法是模板工程里本身带了一个示例 app你可以在这个 app 的启动 Activity 里写几行代码模拟 uni-app 的调用逻辑比如直接创建MyPluginModule实例并调用doSomething方法然后正常 debug。这个方法不涉及真机上的 uni-app 框架主要在原生上下文里验证 SD 卡路径、SDK 初始化、so 库加载等容易出问题的环节。如果你非要边跑 uni-app 边断点也可以稍微绕一下在代码里用android.os.Debug.waitForDebugger()然后以调试模式启动基座。但这种方式经常不稳定不如我前面说的“独立调试”来得顺手。经验之谈原生逻辑复杂时先脱离 uni-app 环境跑通一遍再回到集成环境验证。4. 插件打包、签名与发布上架4.1 本地插件打包成 aar 的两种方式插件开发完成后你最终要面对两件事一是自己项目里要打正式包二是上传到插件市场让别人用。如果你只是自己项目用最简单的方式就是把 Module 的源码直接放在 HBuilderX 的nativeplugins目录云打包时服务器会自动编译集成你不用手动出 aar。但如果你要上传到插件市场或者公司内网有一个人维护插件、多个项目使用这时候就需要把插件 Module 导出成 aar 文件交给使用者。导出方式有两种一种是在 Android Studio 里选中你的 Module执行 Gradle 面板里的assembleRelease任务然后在build/outputs/aar目录下找到产出的 aar另一种是用命令行在gradlew所在目录执行./gradlew :uniplugin_module:assembleRelease生成 aar 之后把它连同依赖的第三方 aar、jar、so 一起按照官方要求的目录放到插件的android/libs下。这里有个血泪教训如果 Module 依赖了另外一个本地 Library Module你光导出当前 Module 的 aar 是不够的还要把依赖的 Library 也一起导出并且手动关联到插件工程里否则别人集成时会发现一堆类找不到。4.2 插件市场发布流程与审核要点上传插件市场之前你需要在插件市场后台用开发者账号登录创建一个新插件填写插件标识、名称、简介、版本号等信息然后上传一个 zip 包。这个 zip 包的内容就是你本地nativeplugins里那个插件目录的完整复制确保压缩包的根目录直接就是android、package.json等。审核阶段最容易被驳回的几个问题我帮你提前避掉。第一包名不能和 uni-app 官方内置模块冲突第二插件内不能存在明显的调试日志输出尤其是不能把密钥、token 打出来第三如果插件涉及隐私权限定位、相机、读取设备信息必须要在简介和隐私协议里说明用途第四不能包含任何诱导用户前往第三方下载链接的代码。关于公共插件和私有插件上传市场时可以设置。公共插件所有人可见可下载一般配合收费或免费使用私有插件只有你指定的小组或账号可见适合公司内部多项目共享。我的建议是公司内部的项目插件走私有除非你要对外输出技术能力否则没必要开源出来给自己找维护负担。4.3 云端打包与 DCloud 证书签名配置在 HBuilderX 里点“发行”-“原生 App 云打包”会要求你配置安卓证书。证书的生成方式网上很多但我要提醒的是公私钥库之间的关系。云端打包时你只需要把 keystore 文件、别名、两个密码配置好DCloud 的服务器会用你的证书对 APK 进行正式签名。这里有个很关键的坑如果你的 App 之前已经用某个证书签名上架过后续的所有版本必须用同一把证书签名否则 Android 系统会认为这是两个不同的 App用户无法覆盖安装应用商店也无法更新。我见过不止一个项目因为证书丢失被迫换包名重新上架的惨案。所以拿到证书后至少备份三份一份本地磁盘、一份公司服务器、一份加密网盘。如果你用的插件包含原生代码云打包时务必选择“使用云端证书”并配置好证书信息不要用默认测试证书。测试证书打出来的包也能装但一上架就会被市场拒绝而且没法和正式证书互相覆盖升级。4.4 离线打包场景下如何集成原生插件有些企业客户要求必须在本地出包不能把代码交给云端这时就要走离线打包路线。离线打包的意思是你在本地用 Android Studio 打开官方离线 SDK把自己的 uni-app 前端资源包wgt 或 apk 资源塞进去配合原生插件代码一起编译出 APK。离线打包时的原生插件集成方式和 HBuilderX 云打包略有区别。你需要手动在 App 工程的assets目录下配置dcloud_uniplugins.json同时把插件的 aar 或源码作为依赖引入 App 工程并在 App 的build.gradle里声明依赖关系。别少看这一步漏了 json 配置插件就像没装一样JS 那端调了直接报 “plugin not found”。离线打包对开发者的安卓工程能力要求更高你要自己管理 Gradle 依赖、so 库适配、混淆规则但换来的是打包流程完全可控适合对包体大小、签名、渠道包数量有严格要求的场景。5. 常见问题、踩坑记录与效率提升技巧5.1 高频报错信息与处理对照插件的报错信息往往很简短但背后原因千差万别。我把这两年碰到的高频错误汇总成了表格方便你按图索骥排查。报错信息可能原因处理方式plugin not founddcloud_uniplugins.json 配置错误或未生效检查 json 格式、类路径、插件名称是否一致ModuleNotFoundError插件的 Module 没有编译进基座重新制作自定义基座确认勾选了插件ClassNotFoundExceptionaar 未正确引入或依赖 Library 缺失检查 libs 目录、build.gradle 依赖UnsatisfiedLinkErrorso 库缺失或架构不匹配补齐 armeabi-v7a、arm64-v8a 对应的 soThe callback is not exist回调创建时机不对或已销毁避免在页面关闭后再调用 callbackjava.lang.SecurityException缺少动态权限或权限未申请在原生代码中用 requestPermissions 动态申请JSONException参数 key 不存在或类型转换失败原生用 optXxx 方法JS 端传 JSON 对象时要 stringify这张表不是万能的但覆盖了八成新手接插件时的报错情况。遇到任何问题先别着急改代码打开 Logcat 定位到具体异常堆栈再对照表里的方向排查效率会高很多。5.2 架构、so 库与混淆相关的经典大坑安卓设备 CPU 架构主要分 armeabi-v7a 和 arm64-v8a现在的 64 位手机如果只放了 32 位 so运行时会报Unable to load library。解决方式是在插件 Module 的build.gradle里用ndk { abiFilters armeabi-v7a, arm64-v8a }指定需要的架构同时确保 so 文件都放在了src/main/jniLibs/armeabi-v7a和src/main/jniLibs/arm64-v8a对应的目录下。另外一个隐蔽问题是混淆。如果你在打正式 release 包时开了混淆而插件类混淆后被改了名字uni-app 运行时按原类名找就找不到了。所以插件 Module 的混淆规则必须加上一到两条 keep-keep class com.example.myplugin.** { *; }同时还要 keep 住所有实现UniModule、UniComponent的子类以及dcloud_uniplugins.json里声明的类。这不是细节这是 release 包能不能跑的生死线。5.3 内存、线程与回调安全的实操建议原生插件和 JS 进行大量数据交互时最常见的问题就是 UI 卡顿和内存飙升。我写过一次很蠢的代码在uiThread false的方法里循环几千次调用callback.invoke结果页面直接卡死。后来改成用事件通道分片推送并在每批之间做 10 毫秒延时问题才缓解。另一个安全问题是在异步线程里回调 JS 后组件可能已经被销毁再操作就会崩溃。处理方式是在原生代码里判断mUniSDKInstance是否仍然有效或者捕获IllegalStateException别让异常向上抛。前端那边也尽量做到组件销毁时uni.$off清理监听。多线程改写还有个要点如果你用了 Executor 或子线程一定要保证callback.invoke是在同一个线程里按顺序调用。市面上一些网上找的代码直接用多个线程并发调同一个 callback结果前端拿到数据的顺序是乱的逻辑立刻出错。5.4 我的开发效率提速心得最后分享几个我这两年用下来特别提效的小习惯。第一单独维护一个“插件调试专用 uni-app 项目”项目里只有测试页面用来集中验证所有插件的边界情况不用把插件塞进正式的大项目里反复打包节省大量时间。第二原生代码尽量做日志开关用一个静态变量控制日志输出级别开发时全量打发正式版时一键关掉。这样既不影响调试又不会把冗余日志带到线上包。第三插件里的业务逻辑要尽量薄尽量把数据处理放在前端原生只做“能力提供”。这看起来多了一层通信开销但维护和排错会舒服很多。实践下来插件越薄bug 越少。第四每次更新插件版本的时候同步维护一个 CHANGELOG哪怕只是两三行。因为插件的使用方往往不止你一个人别人更新后出了问题看变更记录能少吵很多架。结语一点实际的建议原生插件这条路说穿了就是给 uni-app 装上第三只手的活儿。刚开始你会觉得配置繁琐、坑很多但把模板工程和打包流程吃透之后再开发第二个插件速度会快得超乎你想象。我个人比较推荐的成长路径是先拿一个 Module 类型的小功能练手比如让原生返回一个设备唯一标识从环境搭建到打出正式包完整走一遍然后尝试接入一个真实的第三方 SDK感受一下 aar 依赖和混淆的威力最后再碰 Component 类型去做一个自定义的原生 UI 控件。每一步都走扎实了你在 uni-app 项目里的“能力天花板”就算彻底打开了。