Android权限详解

5,264 阅读15分钟

权限的介绍

       权限是一种安全机制,在默认情况下任何应用都没有权限执行对其他应用、操作系统或用户有不利影响的任何操作。包括读取或写入用户的私有数据(例如联系人或电子邮件)、读取或写入其他应用程序的文件、执行网络访问、使设备保持唤醒状态等。

       由于每个 Android 应用都是在进程沙盒中运行,因此如果应用需要获取沙盒之外的资源和数据,则必须声明其所需的权限,然后 Android 系统提示用户是否允许提供该权限。

权限的分类

       Android中有很多权限,但并非所有的权限都是敏感权限,Android 6.0系统开始对所有权限进行了以下分类:

正常权限(Normal permissions)

       正常权限涵盖应用需要访问其沙盒外部数据或资源,但对用户隐私或其他应用操作风险很小的区域。如果应用声明其需要正常权限,系统会自动向应用授予该权限。

       在Android 8.1(API 级别 27)中,下列权限被分类为正常权限:

  • ACCESS_LOCATION_EXTRA_COMMANDS
  • ACCESS_NETWORK_STATE
  • ACCESS_NOTIFICATION_POLICY
  • ACCESS_WIFI_STATE
  • BLUETOOTH
  • BLUETOOTH_ADMIN
  • BROADCAST_STICKY
  • CHANGE_NETWORK_STATE
  • CHANGE_WIFI_MULTICAST_STATE
  • CHANGE_WIFI_STATE
  • DISABLE_KEYGUARD
  • EXPAND_STATUS_BAR
  • GET_PACKAGE_SIZE
  • INSTALL_SHORTCUT
  • INTERNET
  • KILL_BACKGROUND_PROCESSES
  • MANAGE_OWN_CALLS
  • MODIFY_AUDIO_SETTINGS
  • NFC
  • READ_SYNC_SETTINGS
  • READ_SYNC_STATS
  • RECEIVE_BOOT_COMPLETED
  • REORDER_TASKS
  • REQUEST_COMPANION_RUN_IN_BACKGROUND
  • REQUEST_COMPANION_USE_DATA_IN_BACKGROUND
  • REQUEST_DELETE_PACKAGES
  • REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
  • REQUEST_INSTALL_PACKAGES
  • SET_ALARM
  • SET_WALLPAPER
  • SET_WALLPAPER_HINTS
  • TRANSMIT_IR
  • USE_FINGERPRINT
  • VIBRATE
  • WAKE_LOCK
  • WRITE_SYNC_SETTINGS

危险权限(Dangerous permissions)

       危险权限涵盖应用需要涉及用户隐私信息的数据或资源,或者可能对用户存储的数据或其他应用的操作产生影响的区域。如果应用声明其需要危险权限,则用户必须明确向应用授予该权限。

权限组

       Android系统为了方便管理某一类特定功能的权限,把权限按照功能分成的不同的集合,这个集合就是权限组,权限组内包含一项或多项功能和特性相关的权限。所有的危险权限都属于各自的权限组,一共可以分为下列9组:

Permission Group Permission
CALENDAR READ_CALENDAR
WRITE_CALENDAR
CAMERA CAMERA
CONTACTS READ_CONTACTS
WRITE_CONTACTS
GET_ACCOUNTS
LOCATION ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
MICROPHONE RECORD_AUDIO
PHONE READ_PHONE_STATE
READ_PHONE_NUMBERS
CALL_PHONE
READ_CALL_LOG
WRITE_CALL_LOG
ADD_VOICEMAIL
USE_SIP
PROCESS_OUTGOING_CALLS
ANSWER_PHONE_CALLS
SENSORS BODY_SENSORS
SMS SEND_SMS
RECEIVE_SMS
READ_SMS
RECEIVE_WAP_PUSH
RECEIVE_MMS
STORAGE READ_EXTERNAL_STORAGE
WRITE_EXTERNAL_STORAGE

       所有危险的 Android 系统权限都属于权限组。如果设备运行的是 Android 6.0(API 级别 23),并且应用的 targetSdkVersion 是 23 或更高版本,则当用户请求危险权限时系统会发生以下行为:

  • 如果应用请求其AndroidManifest中列出的危险权限,并且应用目前在该权限所属权限组中没有任何权限的授权,则系统会向用户显示一个对话框,描述应用要访问的权限组。对话框不描述该组内的具体权限。例如,如果应用请求 READ_CONTACTS 权限,系统对话框只说明该应用需要访问设备的联系信息。如果用户批准,系统将向应用授予其请求的权限。
  • 如果应用请求其AndroidManifest中列出的危险权限,并且应用在同一权限组中已有另一项危险权限的授权,则系统会立即授予该权限,而无需与用户进行任何交互。例如,如果某应用已经请求并且被授予了 READ_CONTACTS 权限,然后它又请求 WRITE_CONTACTS,系统将立即授予该权限。

       如果设备运行的是 Android 5.1(API 级别 22)或更低版本,并且应用的 targetSdkVersion < 23,则系统会在安装时要求用户授予权限。再次强调,系统只告诉用户应用需要的权限组,而不告知具体权限。

       任何权限都可属于一个权限组,包括正常权限和应用定义的权限。但权限组仅当权限危险时才影响用户体验。可以忽略正常权限的权限组。

签名权限(Signature permissions)

       系统会在应用安装时自动授予应用签名权限,但是这有个前提,那就是申请使用权限的应用与定义许可的应用签名相同。

       一些签名权限不能用于第三方应用程序,在Android 8.1(API 级别 27)中,第三方应用程序可以使用以下签名权限:

  • BIND_ACCESSIBILITY_SERVICE
  • BIND_AUTOFILL_SERVICE
  • BIND_CARRIER_SERVICES
  • BIND_CHOOSER_TARGET_SERVICE
  • BIND_CONDITION_PROVIDER_SERVICE
  • BIND_DEVICE_ADMIN
  • BIND_DREAM_SERVICE
  • BIND_INCALL_SERVICE
  • BIND_INPUT_METHOD
  • BIND_MIDI_DEVICE_SERVICE
  • BIND_NFC_SERVICE
  • BIND_NOTIFICATION_LISTENER_SERVICE
  • BIND_PRINT_SERVICE
  • BIND_SCREENING_SERVICE
  • BIND_TELECOM_CONNECTION_SERVICE
  • BIND_TEXT_SERVICE
  • BIND_TV_INPUT
  • BIND_VISUAL_VOICEMAIL_SERVICE
  • BIND_VOICE_INTERACTION
  • BIND_VPN_SERVICE
  • BIND_VR_LISTENER_SERVICE
  • BIND_WALLPAPER
  • CLEAR_APP_CACHE
  • MANAGE_DOCUMENTS
  • READ_VOICEMAIL
  • REQUEST_INSTALL_PACKAGES
  • SYSTEM_ALERT_WINDOW
  • WRITE_SETTINGS
  • WRITE_VOICEMAIL

特殊权限(Special permissions)

       有许多权限其行为方式与正常权限及危险权限都不同。

  • YSTEM_ALERT_WINDOW
  • WRITE_SETTINGS

       这两个权限比较特殊,如果某应用需要其中一种权限,必须在清单中声明该权限,并且发送请求用户授权的 intent。系统将向用户显示详细管理屏幕,以响应该 intent。也就是说这两个权限不能通过代码申请方式获取,必须得用户打开软件设置页手动打开,才能授权。只靠AndroidManifest申请该权限是无效的。

Android权限的变化

       Android权限随着系统版本的升级有所改变,其中最大的变化发生在Android 6.0(API级别23)。

非运行时权限

       在Android 6.0之前的系统,所有权限有统一的处理方式,开发者在AndroidManifest文件中声明需要的权限,系统提示用户APP将获取的权限,需要用户同意授权才能继续安装,从此APP便永久的获得了授权,并且之后也无法撤销授权。

       这种情况下,应用安装时会弹出权限列表的对话框,此时用户只有两种选择:1. 允许所有权限,继续安装,即使有敏感权限。2. 因为对某些权限有疑惑,拒绝所有权限,放弃安装。也就是说用户无法只允许某些权限或者拒绝某些权限。

运行时权限

       由于Android 6.0之前的权限管理相对不太安全,所以从Android 6.0开始采用新的权限模型,用户开始在应用运行时向其授予权限,而不是在应用安装时授予。此方法可以简化应用安装过程,因为用户在安装或更新应用时不需要授予权限。它还让用户可以对应用的功能进行更多控制;例如,用户可以选择为相机应用提供相机访问权限,而不提供设备位置的访问权限。用户可以随时进入应用的“Settings”屏幕调用权限。

       特别注意:权限对话框不是开发者调用某个权限的功能时由系统自动弹出,而是需要开发者手动调用,如果直接调用而并未申请权限的话,将会导致APP的奔溃。

       在所有版本的 Android 中,都需要在AndroidManifest中同时声明正常权限和危险权限,但这个声明的影响受应用的targetSDKVersion和手机系统版本共同控制,于是会出现下列4种情况:

  1. targetSDKVersion < 23 & API(手机系统) < 6.0:如果应用在在AndroidManifest中同时声明正常权限和危险权限,用户必须在应用安装时同时授予这些权限,否则无法安装应用,并且用户无法在应用安装之后取消授权。
  2. targetSDKVersion >= 23 & API(手机系统) < 6.0:这种情况跟第一种情况处理相同。
  3. targetSDKVersion < 23 & API(手机系统) >= 6.0:如果应用在在AndroidManifest中同时声明正常权限和危险权限,用户必须在应用安装时同时授予这些权限,否则无法安装应用,跟上两种情况不同的是,这种情况下用户可以在应用安装完成后取消授权(取消授权时,系统会弹出提示,让用户谨慎操作)。
  4. targetSDKVersion >= 23 & API(手机系统) >= 6.0:这种情况下正常权限会在应用安装时自动被授予,危险权限不会在安装时被授予,必须在运行时向用户请求,用户可以授予或拒绝每项权限,且即使用户拒绝权限请求,应用仍可以继续运行有限的功能。而且用户授权以后仍然可以在设置界面中取消授权。

       总结一下,AndroidMnifest中声明的正常权限都会在应用安装时被授予,危险权限的授予会根据应用的targetSDKVersion和手机系统版本不同而有所差异。

Android O的运行时权限策略变化

       在 Android O 之前,如果应用在运行时请求权限并且被授予该权限,系统会错误地将属于同一权限组并且在清单中注册的其他权限也一起授予应用。对于针对Android O的应用,此行为已被纠正。系统只会授予应用明确请求的权限。然而一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准。

       例如:应用在AndroidManifest中声明READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE,应用请求READ_EXTERNAL_STORAGE,并且用户授予了改权限。在Android O之前,系统会自动授予WRITE_EXTERNAL_STORAGE权限,因为该权限也属于STORAGE权限组并且也在清单中注册过。但是在Android O及以后,系统只会授予READ_EXTERNAL_STORAGE权限,不过该应用之后申请WRITE_EXTERNAL_STORAGE时,系统会立即授予该权限,不会提示用户。

获取权限的正确方式

       一款应用跟用户的首次交互,在树立用户对应用的整体印象方面扮演重要的角色。如何正确的获取权限,对用户的存留有着很大的作用。比如用户新打开一款新应用,最不想看到的就是一大串请求获取权限的弹框,这种操作在用户体验上会造成极其负面的影响,往往会导致用户的流失。应用要在索要权限之前与用户沟通,这样才能使用户保持投入。

       发送权限请求时,我们当然希望所有用户都接受。要达成这个目的,就应当建立一套权限策略。权限策略取决于你所请求的权限类型的明确与重要程度。非常重要的权限应当预先请求,次要权限可以在情景中再请求。

获取权限的时机:

       确定用户是否接受请求的最关键因素,是它们何时需要用到。
       简单的原则:除非需要,否则不要请求获取权限。

1. 重要的权限预先请求

       对许多应用而言,获取不到数据权限会改变整个用户体验。例如,如果应用需要依赖短信服务,拒绝这项权限就导致这款应用无法使用。所幸,用户会希望消息类应用获取短信权限,所以把它前置是有意义的。

       总结:确保用户清晰理解应用是做什么的(基于应用的描述或之前的熟悉经历),只预先请求用户希望应用获取的权限。

2. 次要的权限在情景中请求

       通常情况下,如果新用户一上来就体验到一连串权限请求,你就错失了一个吸引用户的重要机会。应用要在情景中请求权限,并且告知用户这项权限能提供什么。因为只要用户被吸引,他们就更容易接受请求。

       总结:在进行相关任务时请求获取权限,用户更容易接受。

获取权限的方式:

       应用应该清晰阐明为何需要每项权限,要提供功能的名称或详细解释。而且,如果想要用户同意,就要礼貌地请求。
       简单的原则:清晰无疑的说明用户将会获得什么,以此换取他们的允许。

1. 解释权限的用途及益处

       对于不太明确的权限,需要教导用户这项权限包含什么。例如:预先请求的权限,可以通过引导页来解释应用的功能,还有为什么会获取意料之外的权限;在情景中请求的权限,可以通过引导图向用户解释允许这项权限会带来的好处。

2. 在真正的请求之前“预先”请求

       iOS的默认请求,每个功能只能触发一次。最坏的情况就是用户拒绝了系统权限,因为在iOS中要找回那个权限非常复杂。多数情况下,最好是“预先”请求用户允许,然后在放出真实的iOS权限获取提示。

3. 在操作情景中请求权限

       用户触发的对话框甚至比情景创建界面更有效,因为用户盼着请求出现,更愿意允许权限获取,他们想要使用这个功能。等到需要某个功能时再请求权限。

Android 运行时权限的使用

       使用运行时权限主要分为以下三个步骤:

1.检查是否拥有权限

       当我们需要使用某一个权限时,我们要先检查一下,系统是否已经授予了我们这个权限。使用ContextCompat.checkSelfPermission(Context context, String permission)方法来检查是否具有某项权限。

       例如:当想调用系统相机时,需要去检查一下,是否有打开相机的权限。

int permissionCheck = ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.WRITE_CALENDAR);
if(permissionCheck == PackageManager.PERMISSION_GRANTED){
    // 有此权限
}

返回值有两种:

  • PackageManager.PERMISSION_GRANTED 表示用户具有此权限,可以继续打开相机
  • PackageManager.PERMISSION_DENIED时 表示未授权、必须向用户请求权限

2.申请权限

       通过ActivityCompat.requestPermissions(Activity activity, String[] permissions, int requestCode)方法请求你所需要的权限。

此方法的三个参数:

  • activity 当前Activity实例
  • permissions 权限组(可以同时请求多个权限)
  • requestCode 请求码(回调的时候需要用到)

       例如:申请相机权限

// 先检查权限
if (ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.READ_CONTACTS) 
        != PackageManager.PERMISSION_GRANTED) {

    // 解释一下为什么需要此权限,稍后会讲到这个方法
    if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity, Manifest.permission.CAMETA)) {
        // 可以做一个弹出框,解释一下为什么需要此权限
    } else {
        // 请求权限
        ActivityCompat.requestPermissions(thisActivity,
                new String[]{Manifest.permission.CAMETA},
                MY_PERMISSIONS_REQUEST_READ_CONTACTS);
    }
}else{
    //已有权限、直接进行操作
}

       当用户第一次拒绝请求权限之后和永远拒绝权限之前,ActivityCompat.shouldShowRequestPermissionRationale()方法会返回true。我们就可以给用户再次做一个解释,为什么需要这个权限。当用户点击不在提醒此权限授权后,返回值是false。不再提醒之后,请求权限是直接返回授权失败的。

3.处理权限回调

       用户处理权限之后,会回调用onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults)方法。

@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
    switch (requestCode) {
        case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
            // 如果请求被取消、grantResults 是空的,这里需要做一个判断是否大于0
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // 这里我们只请求了Camera权限,所以只要取grantResults[0]来判断是否授权
                // 打开相机
            } else {
                // 用户拒绝授权
            }
            return;
        }

    }
}

Android 使用运行时权限的特殊处理

1.Fragment中运行时权限的特殊处理

  • 在Fragment中申请权限,不要使用ActivityCompat.requestPermissions,直接使用Fragment的requestPermissions方法,否则会回调到Activity的onRequestPermissionsResult
  • 如果在Fragment中嵌套Fragment,在子Fragment中使用requestPermissions方法,onRequestPermissionsResult不会回调回来,建议使用getParentFragment().requestPermissions方法,这个方法会回调到父Fragment中的onRequestPermissionsResult,加入以下代码可以把回调透传到子Fragment
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    List fragments = getChildFragmentManager().getFragments();
    if (fragments != null) {
        for (Fragment fragment : fragments) {
            if (fragment != null) {
                fragment.onRequestPermissionsResult(requestCode,permissions,grantResults);
            }
        }
    }
}

2.连续多次申请权限的问题

public final void requestPermissions(@NonNull String[] permissions, int requestCode) {
      if (mHasCurrentPermissionsRequest) {
          onRequestPermissionsResult(requestCode, new String[0], new int[0]);
          return;
      }
      Intent intent = getPackageManager().buildRequestPermissionsIntent(permissions);
      startActivityForResult(REQUEST_PERMISSIONS_WHO_PREFIX, intent, requestCode, null);
      mHasCurrentPermissionsRequest = true;
  }

       上面是android7.0的源码里的,mHasCurrentPermissionsRequest这个是一个布尔值的flag,标记当前是否有正在请求的权限。因为方法是异步执行的,所以如果你在申请权限的时候连续两次执行此方法,你会在方法第二次请求的时候他会直接执行onRequestPermissionsResult方法,返回的permissions和grantResults都是长度为0的空数组,这是如果你在onRequestPermissionsResult不判断长度直接取值会奔溃报数组越界,所以在返回结果处理的时候最好做一下长度不为0的判断,而且最好不要在上次请求结果没有返回的时候就再次执行新的权限请求,这样毫无意义!

相关开源项目

       使用标注的方式,动态生成类处理运行时权限,目前还不支持嵌套Fragment

       基于RxJava的运行时权限检测框架

       简化运行时权限的处理,比较灵活

       Google官方的例子