资讯动态

uniapp原生插件开发实战:从后台定位到息屏播报的跨端解决方案

发布时间:2026/10/9 15:48:00 来源:尧图企业网站定制
1. 原生插件开发这件事到底卡在哪先说个现象。我见过太多团队在uniapp里折腾了几个月最后栽在原生能力上。明明JS层写得飞起一到调用系统摄像头、后台定位、息屏播报这些场景就束手无策。社区里翻来覆去就那几个回答“去写原生插件吧”“这个只能做原生”。然后呢没有然后了。其实uniapp的架构逻辑很简单它本身是个跨端框架JS引擎跑业务逻辑遇到系统能力就通过桥层调原生。问题在于框架封装的API覆盖不了所有场景。uni.scanCode能扫码但你要自定义扫码界面加个闪光灯切换就得碰原生uni.startLocation能定位但你要App退到后台还在持续上报位置就得写原生插件iOS上你要息屏播报锁屏状态下继续朗读内容这更是纯原生的事。所以原生插件开发是绕不开的坎尤其做App端。如果你只做小程序那原生插件的概念基本可以忽略但做App、做iOS/Android双端原生插件就是基本功。这篇文章就是把我自己踩过的坑、趟出来的路梳理一遍。从设计思路到实际操作从Android到iOS从后台定位到自动截屏结合我实际开发过的场景尽量说人话给能直接用的方案。2. 先搞清楚架构插件不是你想的那样2.1 原生插件的本质是一层桥很多人一听“原生插件”就发怵觉得得精通Java/Kotlin、Objective-C/Swift才能碰。说实话门槛没你想得那么高。uniapp的原生插件本质上就是在原生层写一个类注册给JS层调用。JS调uni.requireNativePlugin(ModuleName)拿到插件实例然后调方法、传参数、收回调。以Android为例uniapp的插件机制基于它自己的SDK。你需要继承UniModule或者实现UniModule接口然后在方法上标UniJSMethod注解。就这么简单。方法里面的实现逻辑才是你Java/Kotlin功底发挥作用的地方。public class MyLocationModule extends UniModule { UniJSMethod(uiThread false) public void startMonitor(JSONObject options, UniJSCallback callback) { // 这里写原生逻辑 } }iOS端稍微不一样uniapp在iOS上用的是UniPluginProtocol协议你需要创建一个继承NSObject的类遵循这个协议然后用WXModuleMethod之类的方式暴露方法。整体思路一致只是语言和框架细节不同。2.2 你真正需要的“原生能力”长什么样我给原生插件开发按需求场景分了个类方便你对号入座框架没封装但有现成系统API的场景比如后台定位、前台服务、息屏播报、自动截屏、相册授权、消息推送集成框架封装了但不够灵活的场景比如自定义扫码、自定义分享、自定义视频播放器纯原生性能要求的场景比如大数据量计算、图像处理、音视频编解码这里要特别提醒一点不要所有功能都往原生插件里塞。uniapp的优势在于跨端和快速迭代你业务逻辑写原生等于自废武功。原生插件只放那些“不得不原生”的部分能JS解决的就JS解决。2.3 社区里常见的误区我发现很多人对uniapp原生插件有一个误解以为插件市场里买一个、下载一个就能解决所有问题。插件市场确实有不少成熟方案但坑也很多。比如插件只适配了特定基座版本你的自定义基座一升级就崩比如插件作者只适配了Android没适配iOS比如插件内部写死了业务逻辑你要定制就得改源码但很多付费插件不给源码。所以我的建议是能用官方插件用官方插件用不了就自己写。自己写虽然要啃原生代码但主动权在自己手里出了问题能排查不会两眼一抹黑。3. Android端原生插件开发实操3.1 环境准备本地Android工程才是正道网上很多教程说用Android Studio直接打开uniapp官方提供的插件模板工程。但我实际用下来的感受是这个模板工程版本更新频繁跟你的本地SDK版本容易对不上。更稳的做法是自己创建一个标准Android工程然后引入uniapp的SDK依赖。这里有个核心知识点uniapp的离线打包SDK。你去DCloud官网下载最新的离线打包SDK里面包含uniapp-release.aar和uniapp-v8-release.aar这类文件。把这些aar放进你的Android工程的libs目录然后在build.gradle里配置依赖。dependencies { implementation fileTree(dir: libs, include: [*.aar, *.jar]) implementation androidx.appcompat:appcompat:1.3.1 implementation com.alibaba:fastjson:1.2.83 // 其他依赖 }注意uniapp的SDK对Java版本有要求我用的Android Studio版本是2023.1.1JDK配置的17Gradle版本8.0以上这些问题网上都有对应文档照着配就行。3.2 从零写一个后台定位插件后台定位是我做过的原生插件里最典型的场景。plus.geolocation.watchPosition在App退到后台以后经常被系统挂起定位频率越来越低甚至完全停止。这时候就需要原生层启动一个前台服务来保活定位。思路是在原生层创建一个Service里面注册LocationManager监听通过Notification让服务变成前台服务。这样App退到后台定位服务依然能跑。public class LocationService extends Service { private LocationManager locationManager; private String provider LocationManager.GPS_PROVIDER; Override public int onStartCommand(Intent intent, int flags, int startId) { startForeground(1, createNotification()); startLocation(); return START_STICKY; } private void startLocation() { locationManager (LocationManager) getSystemService(Context.LOCATION_SERVICE); if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) PackageManager.PERMISSION_GRANTED) { locationManager.requestLocationUpdates(provider, 5000, 0, locationListener); } } }然后在你注册的UniModule里通过startService拉起这个Service。每次定位结果通过UniJSCallback回调给JS层。这里有个关键点UniJSMethod(uiThread false)注解一定不能漏。因为定位是耗时操作如果在UI线程跑可能会ANR。但回调JS的时候要注意UniJSCallback只能在UI线程调用所以你需要用runOnUiThread切回主线程再回调。3.3 自动截屏插件权限和兼容性问题自动截屏这个需求看起来简单实际上全是坑。Android上截屏有两种方式一种是通过MediaProjection需要用户授权弹窗一种是root之后直接/system/bin/screencap。做商业App基本选了第一种。MediaProjection的完整流程很繁琐大致是申请MediaProjectionManager然后通过createScreenCaptureIntent拉起授权弹窗授权以后拿到MediaProjection再创建VirtualDisplay配合ImageReader取帧。MediaProjectionManager mpm (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); Intent captureIntent mpm.createScreenCaptureIntent(); startActivityForResult(captureIntent, REQUEST_CODE);拿到结果以后在onActivityResult里读取MediaProjection实例然后创建虚拟屏幕。这里要提醒几个容易踩的坑Android 10以上对MediaProjection的权限管控更严格需要用户在前台的时候授权授权以后服务必须保持前台运行否则系统会回收ImageReader的格式要用PixelFormat.RGBA_8888拿到的是原始像素数据转成Bitmap还得自己处理部分国产ROM会对后台截屏做限制华为、小米的系统需要额外引导用户开启“允许后台弹出界面”之类的权限。3.4 热更新和插件的关系再聊一个热门话题热更新。很多人以为热更新能解决所有版本迭代问题。实际上uniapp的热更新只能更新JS层代码和打包在App里的静态资源。如果你改了原生插件那就必须重新打包App。这一点在项目规划时要特别留意。如果你们的原生插件只是偶尔调个系统能力那热更新够用但如果你频繁改原生功能热更新就名存实亡了。我见过最惨的情况是团队把业务逻辑全写在原生插件里然后想靠热更新躲过应用商店审核结果每次改需求都得重新发版审核周期卡得死死的。所以原生插件开发的基本原则是插件方法越原子越好。比如一个定位插件就暴露startMonitor(interval, callback)和stopMonitor(callback)两个方法业务判断全在JS层。这样原生代码稳定了JS层才可能通过热更新灵活调整。3.5 上架安卓应用市场的适配问题写完插件只是第一步上架安卓应用市场才是噩梦的开始。国内安卓市场对隐私合规要求越来越严尤其是定位权限。如果你的App用了后台定位但没在隐私政策里说明酷安、小米、华为这些渠道都可能驳回。这个问题必须在插件层面就考虑好。比如定位插件要支持“仅前台定位”和“后台定位”两种模式由JS层根据业务需求去选择。如果你的应用根本不需要后台定位就别申请ACCESS_BACKGROUND_LOCATION权限免得审核被卡。另外部分应用市场要求App必须支持64位架构。你在做原生插件的时候build.gradle里的abiFilters要配置armeabi-v7a和arm64-v8a两种。尤其如果你引用了第三方so库一定要确认它提供了64位版本否则上架审核直接不通过。4. iOS端原生插件开发要点4.1 iOS插件的基础写法iOS端的原生插件开发思路跟Android一致但语言换成Objective-C或Swift。uniapp在iOS上对Swift的支持没那么顺畅我试过用Swift写插件某些方法回调会有类型转换问题后来干脆改用Objective-C。一个最简单的iOS插件大致长这样#import UniPlugin.h interface MyModule : NSObject UniPlugin end implementation MyModule - (void)startMonitor:(NSDictionary *)options callback:(UniJSCallback *)callback { // 原生逻辑 if (callback) { [callback invokeWithDict:{status: success}]; } } end在UniPlugin的init方法里你需要用WXModuleManager注册模块然后JS层才能通过uni.requireNativePlugin找到它。4.2 息屏播报这个硬骨头iOS息屏播报的核心难点在于App退到后台、屏幕锁定以后系统会休眠音频播放会被打断。你需要做两件事一是配置UIBackgroundModes里的audio二是用AVAudioSession设置播放模式。AVAudioSession的配置是关键。你必须设置为AVAudioSessionCategoryPlayback并且调用setActive:error:。这样系统才会允许你的App在后台继续播放音频。另一个问题是锁屏状态下App的执行时间有限。如果你只是播放固定音频系统会允许你播完但如果你要在锁屏状态下做语音合成比如用AVSpeechSynthesizer播报文字内容就得用AVAudioSession配合beginBackgroundTask延长后台执行时间。这段逻辑在uniapp的JS层完全做不了必须原生实现。我当时的做法是写了一个SpeechModule暴露speak(text)和stop()两个方法内部用AVSpeechSynthesizer做合成和播放。这样业务层只需要在JS里调用插件不用关心后台会话怎么管理。4.3 iOS的App Store审核注意事项iOS端最烦的不是写代码而是审核。你的原生插件如果涉及后台定位、后台音频都要在App Store的审核备注里写清楚用途。苹果审核团队很较真你说后台定位是为了“安全追踪”他就可能会继续追问具体场景。我踩过一次坑插件里写了后台音频能力但App内没有任何显眼的音频功能入口被审核拒了一次。后来在Info.plist的用途说明里写了详细解释并在App里加了一个“语音播报”的开关入口才算过审。所以iOS原生插件的设计要遵循“显性使用”原则插件有什么能力App里就得有对应的可见功能。别写了后台定位却不放定位按钮别写了息屏播报却不放播报开关这是被审核毙掉最常见的理由。5. 从插件到包体常见问题与排查技巧5.1 “source size 2612kb exceed max limit 2mb”的解法这个报错是微信小程序打包时的经典问题。小程序主包大小限制2MB子包限制各自有额度。你搜一下会发现无数人问无数人答但很多答不到点上。先说结论这个问题跟原生插件开发的关系不大但跟uniapp的工程配置强相关。主要原因通常是静态资源没压缩、图片直接扔在static目录、第三方库体积过大。实操上我一般用这几招解决把图片资源放到云端本地只保留占位图用optimization配置开启treeShaking去掉用不到的API用分包加载pages.json里配置subPackages把非首屏页面拆进子包检查manifest.json里是否有冗余模块配置另外有个容易忽略的点如果你在uniapp项目里引入了uni_modules插件即使你没用到某些插件也可能被打进包里。这时候去uni_modules目录检查一下不用的插件删掉。5.2 uniapp不打印日志信息怎么排查这是个很让人抓狂的问题。代码跑了效果也对但控制台就是干干净净。没有报错没有输出你根本不知道哪里有问题。绝大多数情况是因为manifest.json里的vue编译配置开了production模式或者HBuilderX控制台的日志级别被过滤了。还有一个很低级但常见的原因代码里写的是console.log但运行环境是App真机App端的日志得在HBuilderX的“真机运行”面板里看不是浏览器控制台。还有一个小概率原因是原生插件里面打印的日志。Java层用Log.e打的日志在Android Studio的Logcat里能看见但不会出现在HBuilderX里。如果你在JS层调用插件后没有任何输出先怀疑插件方法有没有被正确调用再怀疑回调有没有触发。5.3 自定义基座绕不开的环节写完原生插件你不可能每次都在HBuilderX里用标准基座调试因为标准基座不带你的插件。这时候就要打自定义基座。流程是Android Studio里把你写好的插件工程编译成aar然后放进uniapp离线打包SDK的工程里再打一个apk作为自定义基座。HBuilderX里配置好自定义基座路径运行到手机时选择自定义基座。这里有个容易出错的地方插件aar里的AndroidManifest.xml和uniapp主工程的AndroidManifest.xml可能有权限冲突。比如你的插件声明了ACCESS_BACKGROUND_LOCATION主工程也要声明否则编译报错。用tools:replace属性可以解决冲突但需要你在Android Studio里手动配置。5.4 uniapp和uni-appx的区别到底是什么最近很多人问uniappx。简单说uni-appx是DCloud推的下一个跨端框架用uts语言TS超集写逻辑可以编译到Android、iOS、Web、小程序等端。但它跟uniapp原生插件开发是两条技术路线。uniappx的编译器能力更强性能更好但它目前生态还没那么成熟第三方插件不如uniapp丰富。如果你现在的项目已经用了uniapp原生插件方案短期内还是要以uniapp为主。想迁移到uniappx得等它把原生插件生态补齐了再说。我个人建议新项目可以关注uniappx但现有uniapp项目别轻易迁移。迁移成本远高于收益除非你遇到了uniapp解决不了的性能瓶颈。6. 一个完整案例自定义分享插件的全流程最后用一个完整的自定义分享插件把前面的知识点串起来。这个需求很常见uniapp的uni.share只支持官方渠道但你要分享到钉钉、企业微信这些非官方渠道就得写原生插件。6.1 需求拆解需求是JS层传入分享文案和链接原生层拉起钉钉分享面板。Android端用钉钉SDKiOS端用钉钉SDK两端逻辑不同但接口要统一。我做了一个ShareModule暴露了三个方法shareToDingTalk(options, callback)分享到钉钉shareToWeCom(options, callback)分享到企业微信isAppInstalled(channel, callback)判断目标App是否安装JS层只需要关心channel是dingtalk还是wecom逻辑全在原生层判断。6.2 Android端实现Android端引入钉钉SDK后在Module里拿到JS传进来的参数然后调用钉钉SDK的分享接口。分享成功后通过回调通知JS层。UniJSMethod(uiThread true) public void shareToDingTalk(JSONObject options, UniJSCallback callback) { String text options.optString(text); String url options.optString(url); DDShareMessage message new DDShareMessage(); message.text text; message.url url; // 调用钉钉SDK DingTalkShare.shareMessage(context, message, new DDShareListener() { Override public void onSuccess() { callback.invoke(makeResult(0, success)); } Override public void onError(int code, String msg) { callback.invoke(makeResult(code, msg)); } }); }注意调用钉钉SDK的部分必须在UI线程执行所以uiThread true必须设置。6.3 iOS端实现iOS端使用钉钉的DTApi流程跟Android类似。需要先在AppDelegate的application:didFinishLaunchingWithOptions里注册DTApi注册的AppKey在钉钉开放平台申请。- (void)shareToDingTalk:(NSDictionary *)options callback:(UniJSCallback *)callback { DTShareTextObject *textObj [[DTShareTextObject alloc] init]; textObj.text options[text]; DTMediaMessage *message [[DTMediaMessage alloc] init]; message.mediaObject textObj; DTApi *api [DTApi sharedApi]; [api sendReq:message completion:^(DTBaseResp *resp) { if (resp.errCode 0) { [callback invokeWithDict:{status: success}]; } else { [callback invokeWithDict:{status: fail, msg: resp.errStr}]; } }]; }这套代码写下来JS层调用就很统一了const shareModule uni.requireNativePlugin(ShareModule) shareModule.shareToDingTalk({ text: 这是一条分享文案, url: https://example.com }, (res) { if (res.status success) { uni.showToast({ title: 分享成功 }) } })7. 避坑总结我最后想说的几句话原生插件开发这件事技术上不复杂但细节多、坑多、版本兼容性多。我把几个心得体会放在这里第一别贪多。原生插件只写不得不原生的部分其他一切放JS层。这样你的插件体积小、逻辑稳定、热更新才能发挥价值。第二版本管理要做好。uniapp的SDK、Android的AGP版本、Gradle版本、iOS的Xcode版本都是牵一发动全身的东西。升级版本之前先在本地用自定义基座回归一遍所有插件功能。第三要想清楚插件和基座的关系。自定义基座能帮你调试但打包上线用的还是正式包。发布前一定用正式包流程打一遍别在基座环境里没问题、一发正式包就崩。第四社区资料要会看。官方文档写得比较简略很多细节是在论坛的“踩坑记录”里沉淀的。谷歌搜英文资料、看stackoverflow上的Android原生问题往往比搜中文问题得到更有效的答案。原生插件开发说到底考验的不是你Java或OC写得有多花哨而是你对跨端架构边界的理解。把JS和原生的边界划清楚把需要的系统能力准确地暴露出来这个项目就成功了一半。至于另一半就是耐心排查和持续踩坑。希望这篇文章能让想碰原生插件的人少走点弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑