☰
Cordova权限插件实战:Android运行时权限检查与请求封装
2026/10/17 9:14:50 网站建设 项目流程

做 Cordova 开发最折磨人的一件事是什么?我觉得是权限。

我印象特别深——某次做一款带扫码功能的 App,真机调试天天崩,不是二维码识别的问题,而是 Android 6.0 之后引入的运行时权限机制,不再像以前那样允许用户装完 App 就放权,必须在运行时逐项申请。原生开发还好,直接调系统 API 就行;但 Cordova 项目里,权限请求得靠 JS 侧驱动,原生桥接层得自己写,一不留神就漏掉某个分支。

后来换了 cordova-plugin-android-permissions 这个插件,事情一下子顺了。它把“权限检查”“权限请求”“批量请求”三个高频动作全部封装成 JS 接口,不用碰一行 Java。我用它的时间算不上特别长,但确实是从当时那个扫码项目开始,一直用到现在。这篇就把我对这个插件的完整认知和实操记录下来,包括它的工作方式、核心 API、我在真实项目里的封装思路,以及排查权限问题的一些独家经验。

不管你是刚入坑 Cordova 的新手,还是已经写了几个混合应用的老手,只要项目里涉及相机、定位、相册、麦克风这类敏感权限,这篇文章都值得你花十分钟读完。

1. 为什么权限在 Cordova 项目里是个老大难

1.1 Android 权限模型的演进

在 Android 6.0(API 级别 23)之前,权限模型很简单:你在 AndroidManifest.xml 里声明什么权限,用户安装时就得一并同意。用户要么全盘接受,要么直接放弃安装。这个机制干净利落,但问题也明显——很多用户根本不看权限列表就一路点下一步,导致大量 App 常年占据着通讯录、短信、定位等敏感权限,隐私保护形同虚设。

Android 6.0 之后,情况变了。系统把权限分成了两类:安装时权限和运行时权限。像访问网络、读取设备状态这些属于安装时权限,和以前一样在安装时授予;但定位、相机、录音、读写存储这些敏感权限,必须在 App 运行过程中动态请求,用户点了允许才真正拿到。

这个设计是好事,但对开发者来说意味着更多工作:必须在代码里先检查权限状态,再决定是直接执行还是弹窗请求,还要处理用户拒绝之后的降级逻辑。

1.2 Cordova 项目的尴尬处境

原生 Android 开发的权限请求流程,核心是权限回调机制:通过系统弹窗请求,用户操作后由 onRequestPermissionsResult 回调把结果传回应用。而 Cordova 项目跑在 WebView 里,业务逻辑全部用 JavaScript 编写,几乎没有直接调用原生 API 的通道。

当时我面临的选择是:

  • 写自定义 Cordova 插件,用原生代码封装权限逻辑,JS 侧再写一层桥接。方案灵活但工作量大,而且一旦 Cordova 版本升级,原生工程的兼容性又得重新验证。
  • 找现成的权限插件直接用。当时社区里能搜到的有几个,其中 cordova-plugin-android-permissions 的接口设计最简洁,体积也很小。

我最后选择了后者。

2. 这个插件的工作原理与设计哲学

2.1 底层是个 JavaScript-Bridge

cordova-plugin-android-permissions 的本质是一个 Cordova 插件,通过桥接把 JavaScript 侧的调用转成 Android 原生权限 API 操作。

核心调用链大致是这样:

// JS 侧调用 cordova.plugins.permissions.checkPermission("CAMERA", successHandler, errorHandler);

插件原生层拿到这个调用后,根据传入的权限名映射到 Android 对应的权限字符串,然后调用checkSelfPermission做检查。最后把结果封装成一个包含hasPermission布尔值的对象,通过回调函数传回 JS 侧。

这个过程对你完全透明,你只需要关心 JS 侧的三个方法:

  • checkPermission:检查某个权限是否已被授予
  • requestPermission:请求单个权限
  • requestPermissions:批量请求多个权限

结构极其简单,没有花哨的 API,也没有过度设计。这正是我推荐它的理由——在 Cordova 这种业务场景里,做混合开发最怕的是插件抽象层次太深,出了问题不好排查。这个插件就三层逻辑:JS 调用、桥接、原生 API,出了问题一眼能定位。

2.2 权限常量的映射逻辑

插件使用一个字符串常量来标识权限,比如"CAMERA"、"READ_PHONE_STATE"。它在原生层会把这些常量映射为 Android 实际的权限名。

以 CAMERA 为例,Android 原生里的权限名是android.permission.CAMERA,插件只传一个"CAMERA"就行。这大大降低了记忆成本,毕竟 Android 原生权限名是以完整包名命名的,非常长。

我在一次面试别人时习惯随手画这个映射:

插件常量Android 原生权限
CAMERAandroid.permission.CAMERA
RECORD_AUDIOandroid.permission.RECORD_AUDIO
READ_PHONE_STATEandroid.permission.READ_PHONE_STATE
ACCESS_FINE_LOCATIONandroid.permission.ACCESS_FINE_LOCATION
READ_EXTERNAL_STORAGEandroid.permission.READ_EXTERNAL_STORAGE
WRITE_EXTERNAL_STORAGEandroid.permission.WRITE_EXTERNAL_STORAGE

注意:插件对权限名的匹配是区分大小写的,用"Camera"是无效的。我第一次用的时候踩过这个坑,建议统一使用文档里的全大写常量。

3. 核心 API 逐层拆解:不做表面功夫

3.1 checkPermission:先查状态再决定下一步

这个方法是所有权限操作的地基。它做的事情非常简单:请求系统返回当前 App 对某个权限的授予状态。

cordova.plugins.permissions.checkPermission("CAMERA", function(result) { if (result.hasPermission) { // 已有相机权限,直接打开相机 } else { // 没有权限,需要请求 } }, function(error) { console.error("权限检查失败", error); } );

回调函数里接收的result对象,核心字段就是hasPermission,它的值是一个布尔类型:true代表已授予,false代表未授予。基于这个结果,你再决定后续流程。

这个“先检查再请求”的流程看起来简单,但很多新手项目里并没有严格遵守。我在别人的项目里经常见到这样的写法:一进某个页面直接调requestPermission请求权限,完全没做状态检查。这意味着每一次页面加载,用户都会看到系统权限弹窗,体验极其糟糕。

正确的流程一定是:先checkPermission,如果已有权限直接走正常流程,如果确实没有,再调requestPermission,这才是合理的权限交互。

3.2 requestPermission:发起权限请求的核心方法

当checkPermission返回hasPermission: false时,下一步就是调用requestPermission:

cordova.plugins.permissions.requestPermission("CAMERA", function(result) { if (result.hasPermission) { // 用户授予了权限 } else { // 用户拒绝了权限 } }, function(error) { console.error("权限请求失败", error); } );

和checkPermission一样,requestPermission的成功回调里的result.hasPermission可以判断用户是否接受了请求。

有些开发者会问:为什么requestPermission成功回调里还要再判断一次?直接假设成功不就行了吗?

不行。这里有一个关键细节:requestPermission的“成功回调”不代表“用户授权成功”,它只代表“请求流程已经走完”。用户可能同意,也可能拒绝。所以必须再读一次hasPermission的值。

我见过太多代码这样写:

cordova.plugins.permissions.requestPermission("CAMERA", function() { openCamera(); // 当用户拒绝后依然打开相机,直接崩溃 });

这是典型的错误示范。用户点了拒绝之后依然去打开相机,未授权状态下调用相机 API 会让应用直接闪退。务必在回调里判断hasPermission后再决定是否继续。

3.3 requestPermissions:批量处理多个权限

实际项目里几乎不会只有一个权限需要处理,尤其是像拍照上传这类功能,经常相机、存储、定位一次性全要。逐个请求会弹三次弹窗,用户大概率直接觉得 App 有毛病。

这时用requestPermissions来批量处理:

var permissions = [ "CAMERA", "READ_EXTERNAL_STORAGE", "ACCESS_FINE_LOCATION" ]; cordova.plugins.permissions.requestPermissions(permissions, function(result) { // result 对象里包含所有请求权限的状态 if (result.hasPermission) { // 这里要注意:这个 hasPermission 指所有权限都授予了吗? } }, function(error) { console.error("权限请求失败", error); } );

这里的回调逻辑值得多说一句。requestPermissions的successCallback里result.hasPermission为true时代表所有权限都拿到了。但如果有部分权限被拒绝,hasPermission就是false,此时它不会明确告诉你具体是哪一个被拒了——你得再依次调用checkPermission去查。

所以我从来不在批量请求的回调里直接写业务逻辑,而是封装成一个“检查所有权限是否就绪”的工具函数,拿到结果之后逐项核对。

3.4 权限持久状态与低版本兼容

Android 6.0 以下的设备没有运行时权限的概念,所有权限在安装时就全部授予了。插件内部会对这种情况做兼容处理:在低版本上checkPermission直接返回hasPermission: true。

但这里有个潜在问题:如果你在项目里使用了某个权限,却忘了在 AndroidManifest.xml 里声明,那么在高版本设备上checkPermission会一直返回false,在低版本上却可能返回true。为什么?因为插件在检查权限时,会先判断本应用有没有声明这个权限。没有声明,系统直接返回拒绝状态;而在低版本上,很多国产 ROM 会默认宽泛处理,导致同一个权限在不同设备上表现不一致。

所以,插件能帮你封装权限请求的机械操作,但Manifest 里的权限声明必须你自己确保无遗漏。

4. 从安装到封装:一次完整的权限请求实战

4.1 安装与基础配置

先安装插件:

cordova plugin add cordova-plugin-android-permissions

这个插件没有额外的原生依赖,也不需要修改 build.gradle,装完即用,这也是我一直用它的原因之一——在很多项目里,时间紧任务重,能少改一个原生配置就少一个风险点。

4.2 完整示例:一个拍照上传功能的权限流程

下面是一个我从实际项目里提炼出来的完整流程。场景是:用户点击“拍照上传”,应用先检查相机和存储权限,没有就弹窗请求,用户拒绝则引导去设置页,全部授权通过后再打开相机。

function ensurePermissions(permissions) { return new Promise(function(resolve, reject) { var permissionAPI = cordova.plugins.permissions; permissionAPI.checkPermission(permissions, function(checkResult) { if (checkResult.hasPermission) { resolve(); } else { permissionAPI.requestPermissions(permissions, function(requestResult) { if (requestResult.hasPermission) { resolve(); } else { reject(new Error("部分权限被拒绝")); } }, function(error) { reject(error); }); } }, function(error) { reject(error); }); }); } // 调用示例 function takePhoto() { var requiredPermissions = ["CAMERA", "READ_EXTERNAL_STORAGE"]; ensurePermissions(requiredPermissions) .then(function() { // 权限就绪,打开相机 navigator.camera.getPicture(onSuccess, onError, { sourceType: Camera.PictureSourceType.CAMERA, destinationType: Camera.DestinationType.FILE_URI }); }) .catch(function(error) { // 权限不足,提示用户或跳转设置 console.error("权限流程被阻断", error); showPermissionGuide(); }); }

我把权限检查、请求、结果返回这三个步骤全部封装在ensurePermissions函数里,业务层不用关心权限细节,只负责“通过后干什么”和“失败后怎么处理”。

细心的读者会注意到,我在这里把 CAMERA 和 READ_EXTERNAL_STORAGE 同时传给了checkPermission。这个插件允许这样传吗?允许。checkPermission的参数既可以是一个字符串,也可以是一个字符串数组,数组模式会返回一个聚合结果。这在我最初读文档的时候没有注意到,后来看插件源码才发现这个隐藏能力,直接减少了一次循环调用。

4.3 封装一个通用的权限检查工具模块

在正式项目里,我不会在每个页面都写一遍上面那段逻辑,而是把它抽成一个独立的工具模块,全局复用。推荐做法是封装成下面这样的结构:

// modules/permission-helper.js var PermissionHelper = (function() { var api = cordova.plugins.permissions; function check(permission) { return new Promise(function(resolve, reject) { api.checkPermission(permission, resolve, reject); }); } function request(permission) { return new Promise(function(resolve, reject) { api.requestPermission(permission, resolve, reject); }); } function requestAll(permissions) { return new Promise(function(resolve, reject) { api.requestPermissions(permissions, resolve, reject); }); } return { check: check, request: request, requestAll: requestAll }; })();

这个工具模块的优点是:

  • 统一入口:所有权限相关调用都走这个模块,不会散落在各个页面。
  • Promise 化:把回调风格的 API 包装成 Promise,让业务代码可以用 async/await 清晰表达流程。
  • 容易加策略:比如在request失败后统一跳转设置页,只需在模块内加一个方法。

实际项目里,我还给这个模块加了一个ensureGranted方法,专门处理“先查后请再失败兜底”的完整流程。这样业务层调用起来就一句话:

await PermissionHelper.ensureGranted(["CAMERA", "READ_EXTERNAL_STORAGE"]);

不过要注意一点,这个插件在 iOS 上并不生效,它只面向 Android。iOS 的权限逻辑需要配合另一套机制(比如文档里提到的另一些插件)来处理。如果你的项目是跨双端的,这个模块最好包一层平台判断,在 iOS 上直接放行或在外部实现 iOS 的逻辑。

5. 常见问题与排查技巧实录

5.1 用户拒绝两次后,系统不再弹权限窗口

这是运行时权限机制里最容易踩的坑。Android 系统中,如果用户在一定时间内连续拒绝某个权限两次,后续再请求该权限时,系统不会再弹出权限窗口,而是直接回调onRequestPermissionsResult并返回拒绝状态。此时插件能做的实际上是有限的——它无法再次强制弹出系统弹窗。

我在项目里遇到这个情况时,解决办法是:检测到requestPermission返回拒绝,同时判断checkPermission确认权限确实没有授予,就引导用户进入应用设置页手动开启权限。Android 里打开设置页的方式很多,Cordova 项目里也可以借助cordova.plugins.settings这类插件,或者直接用自定义 URL Scheme 跳转,但不建议在一个权限插件里强行塞太多能力。

function showPermissionGuide() { // 提示文字示例 // "权限已被拒绝,请到系统设置中手动开启。" // 然后引导用户跳转到应用设置页 }

这个逻辑看似简单,但体现了权限设计中的一个重要观念:权限请求不仅仅是“弹个窗”,它是一整套用户引导策略。如果用户在权限弹窗上选了拒绝,你得有下一步预案。

5.2 Android 13 之后存储权限被拆分

在 Android 13(API 级别 33)及以上,系统对存储权限做了调整,不再要求 App 申请读写外部存储权限才能访问媒体文件。取而代之的是新的媒体权限,比如访问图片可以用READ_MEDIA_IMAGES,访问视频可以用READ_MEDIA_VIDEO,访问音频可以用READ_MEDIA_AUDIO。旧版的READ_EXTERNAL_STORAGE在这一版本被弱化。

这里有一个比较现实的兼容问题:如果你的 App 最低支持版本低于 Android 13,你在 Manifest 里声明了 READ_EXTERNAL_STORAGE,那么在高版本设备上这个权限实际上是“虚设”的——App 运行时不一定会把它当作敏感权限去请求,真正访问相册时又需要新的媒体权限。

处理方案有两个:

  • 在 Manifest 里针对不同 SDK 版本做条件声明,低版本声明旧权限,高版本声明新权限。
  • 代码层面,先判断运行系统的 SDK 版本,再决定往插件里传哪个权限常量。

我在项目里比较推荐第二种,用代码判版本。因为用 Manifest 的条件声明虽然能区分权限声明,但插件侧的权限请求也得同步区分,两头都要维护,容易漏。

function getAlbumPermission() { if (window.device && parseFloat(window.device.version) >= 13) { return "READ_MEDIA_IMAGES"; } return "READ_EXTERNAL_STORAGE"; }

注意:不同版本 Android 的权限字符串不一样,判断 SDK 时要小心浮点数比较,最好用整数比较。我在某次项目中直接拿版本字符串转浮点比较,结果在 Android 9 上表现正常,在 Android 13 上因为版本号带小数点格式不同而踩了坑。

5.3 权限回调不触发,日志看到 "Permission request callback is not registered"

我在某个项目实施中遇到过一个问题:点击按钮请求权限后,什么都没发生,控制台却出现 "Permission request callback is not registered" 的告警。

深入研究后发现了背后的原因:Cordova 插件的回调机制是通过一个全局的 Callback ID 来关联的。如果请求权限的 JS 调用发出后,应用在原生层没有正确注册这个回调,那么当系统权限弹窗操作完成时,原生代码不知道要把结果发给谁,于是这个回调就丢了。

排查思路是:

  1. 确认 Cordova 平台的版本和插件版本是否兼容。老版本的 Cordova 对 callback 机制的实现会和新版本插件有细微差别。
  2. 检查是否在deviceready事件触发之前就调用了插件。所有 Cordova 原生插件都必须在deviceready之后才能调用,权限插件也不例外。

我习惯在入口处统一做桥接就绪检查:

document.addEventListener("deviceready", function() { window.cordovaReady = true; });

业务代码里加上if (!window.cordovaReady) return;的守卫,从根源上防止这类时序问题。

5.4 使用 hasPermission 与 checkPermission 的混淆

我刚用这个插件时,经常把 catch 回调里的 error 和 success 回调里的 result 搞混。后来仔细读了插件源码才发现,这两个方法是完全不同的语义:

  • checkPermission是纯查询,只返回状态。
  • hasPermission是插件内部对查询结果的一层封装,并不存在一个单独的hasPermission方法可以在任何时刻调用。

网上有些旧博客会把它们混着写,导致部分读者以为有两个独立 API。所以,与其查二手资料,我建议直接去仓库读plugin.xml和src/android/下的源码,总共不过几百行,十几分钟就能读完,之后再用这个插件就有一种“尽在掌握”的感觉。

6. 权限请求策略:用户体验比实现更重要

6.1 按需请求,不要贪多

曾经有一版方案里,我把未来可能用到的权限全部在启动页面就请求了一遍。理由是“反正迟早要用,不如早点拿”。上线后数据反馈很差,应用商店对启动时即请求权限的行为也比较敏感,用户还会觉得 App 有点“不正常”。

后来我调整策略:只在真正触发相关功能时才请求对应权限。比如用户点击“拍照”按钮的那一刻请求相机权限,点击“保存图片”时请求存储权限。这样用户对权限请求有明确的上下文感知,授权率反而提升了。

插件本身是支持这种按需请求策略的,因为它所有请求方法都是命令式的,随时可以调用,完全由你的业务逻辑控制时机。

6.2 “拒绝后引导”的专业处理

在权限被拒绝后,最常见的错误做法是:弹一个提示框,告诉用户“权限被拒绝,无法使用”,然后就没有然后了。

稍微好一点的做法是:提供明确的“去设置”按钮,帮用户跳转到系统设置中的应用权限页面。

实际项目中,我建议把这段引导做到用户体验的闭环里。比如这样:

function handlePermissionDenied(featureName) { // 弹出提示,说明需要该权限的原因 // 如果用户选择去设置,调用打开设置页的方法 // 如果用户选择取消,则停留在当前页面,并提供重试入口 }

这里有个细节值得分享:用户从设置页开启权限后,回到 App 并不会自动触发页面刷新,很多开发者忽略这一点,导致用户手动授权成功回到 App 后,界面还是显示权限不足的状态。

我的处理方法是:在resume事件里重新执行一次权限检查,并且把页面状态同步刷新。

document.addEventListener("resume", function() { if (needToRecheckPermission) { recheckAndUpdateUI(); } });

这个小改动看似不起眼,但体验提升非常明显,尤其是对权限敏感的功能页面。

6.3 测试时要覆盖的权限场景

权限相关的 Bug 在测试阶段往往不那么显眼,但是上线后投诉率极高。我总结了至少这三个必须覆盖的场景:

  1. 首次安装:权限未授予时触发功能,应该弹出权限请求。
  2. 拒绝一次后再次触发:系统仍会弹出权限请求,但这次是第二次请求,部分系统会带“不再询问”选项。
  3. 永久拒绝:此时不应再次弹窗,而是直接走引导流程。

这三种状态对应的插件返回结果不同,代码走向也必须不同。我建议在测试清单里把这三个场景列为一类必测项,每次发版前都跑一遍。

写在最后

回顾我自己的使用经历,cordova-plugin-android-permissions 算得上 Cordova 生态里比较省心的插件之一。它本身非常简单,没有过度封装,几乎是把原生权限 API 直接翻译成 JavaScript 接口。但越是这种简单的基础组件,越需要我们把它用扎实:先检查再请求、回调里二次确认、拒绝后引导去设置、按需申请不乱要权限,这些都是权限设计里绕不开的基本功。

如果你正被混合开发的权限问题折腾,我建议试试这个插件,并且按照上面的步骤把它封装成你自己的工具模块。只要把“检查——请求——处理拒绝”这条链路理顺,权限这个大坑就算被你填掉一半了。

另外一个小小的建议:在做完权限功能之后,一定用真机重新测一遍,模拟器上好多权限状态和真机差距很大,尤其是相机、录音这类需要真实硬件支撑的权限。等你能熟练处理这些边界情况之后,再看到系统权限弹窗,就不会心慌了。

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

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

立即咨询