前言

权限是Android中一个非常重要的组成部分,许多操作都需要获取到权限才能进行。在Android6.0之后,权限机制发生了重大变化,加入了运行时权限这一概念。本文就详细讲解一下Android6.0前后的权限机制。

Android6.0之前的权限机制

Android6.0之前,权限机制是很简单的,应用只需要在AndroidManifest文件中将自己需要的权限声明即可。示例代码如下:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.permissiondemo">

    <!-- 请求网络权限 -->
    <uses-permission android:name="android.permission.INTERNET"></uses-permission>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        ......
    </application>

</manifest>

用户只要安装了应用,就代表授予应用其声明的所有权限。如果用户不认可应用声明的权限,就只能选择不安装该应用了。显然,在这种机制中用户处于弱势的地位。

Android6.0之后的权限机制

为了让用户拥有自主授予应用权限的权利,Android6.0加入了运行时权限这一概念。对于危险权限,应用必须在使用的时候进行申请,用户可以自主选择是否授予这些权限。如果拒绝授予权限,应用也不会崩溃,只是用户无法使用这一部分功能罢了。这样一来,用户的选择权就被大大加强了。

权限类别

Android大致将权限分为两类,即普通权限危险权限。对于普通权限,依旧使用Android6.0之前的权限机制,只需要在AndroidManifest中声明即可。而对于危险权限,则必须在应用运行时主动申请,由用户决定是否授予。危险权限列表如下:

  1. CALENDAR
  • READ_CALENDAR
  • WRITE_CALENDAR
  1. CAMERA
  • CAMERA
  1. CONTACTS
  • READ_CONTACTS
  • WRITE_CONTACTS
  • GET_CONTACTS
  1. LOCATION
  • ACCESS_FINE_LOCATION
  • ACCESS_COARSE_LOCATION
  1. MICROPHONE
  • RECORD_AUDIO
  1. PHONE
  • READ_PHONE_STATE
  • CALL_PHONE
  • READ_CALL_LOG
  • WRITE_CALL_LOG
  • ADD_VOICEMAIL
  • USE_SIP
  • PROCESS_OUTGOING_CALLS
  1. SENSORS
  • BODY_SENSORS
  1. SMS
  • SEND_SMS
  • RECEIVE_SMS
  • READ_SMS
  • RECEIVE_MMS
  • RECEIVE_WAP_PUSH
  1. STORAGE
  • READ_EXTERNAL_STORAGE
  • WRITE_EXTERNAL_STORAGE

可以看到,危险权限一共有9组24种权限。除此之外,其他权限都是普通权限。需要注意的是,对于每组权限,只要用户授予了其中任何一种权限,整个权限组都会被授予给应用。

注意:在实际测试中发现,如果应用在更新后使用了之前未经申请的运行时权限,即使此时该权限所在权限组中的其他权限已被申请,应用同样会报错Crash。(举例:假如在应用的1.0版本申请了SD卡读权限,在应用升级到1.1版本后,直接对SD卡执行写操作是会报错的)。当然,这也可能是因为国产手机的操作系统对运行时权限的粒度进行了调整造成的。因此,为了增加应用的稳定性,建议只要是使用到的运行时权限都进行显式申请。

运行时权限

对于危险权限,除了在AndroidManifest声明,还需要在代码中进行权限检查、权限申请操作。在涉及危险权限的地方,我们首先需要使用ContextCompat的checkSelfPermisson方法进行权限检查,其原型如下:

public static int checkSelfPermission(Context context,String permission)

这个方法有两个参数,第一个参数是Context对象,第二个参数则是权限名称。最后,该方法会返回一个int类型的数据,可能是PackageManager.PERMISSION_GRANTEDPackageManager.PERMISSION_DENIED,前者代表已授权、后者代表未授权。

当通过checkSelfPermisson方法得知权限未被授予,则要使用ActivityCompat的requestPermissions方法请求权限,其原型如下:

public static void requestPermissions(final Activity activity,
    final String[] permissions,final int requestCode)

这个方法有三个参数,第一个参数是Activity对象。第二个参数是字符串数组,代表我们想要申请的权限,由此也可以知道这个方法能够一次申请多个权限。第三个参数是请求码,用于在回调方法中区分申请权限的请求。这是一个异步方法,因此没有返回值。这个方法会弹出一个权限申请的对话框,在用户做出选择后,Activity中的onRequestPermissionsResult方法会被回调,该方法原型如下:

public void onRequestPermissionsResult(int requestCode,String[] permissions,int[] grantResults)

这个方法有三个参数,第一个参数是请求码,第二个参数是请求的权限字符串数组,第三个参数是权限请求的授权结果。通过对第三个参数进行判断,我们就可以知道申请的权限是否已被用户授予。

注意:在Fragment中不要通过ActivityCompat请求权限,应该直接使用Fragment的requestPermissions方法申请权限。

除了以上三个方法,还有一个优化应用使用体验的方法,即shouldShowRequestPermissionRationale。当通过requestPermissions方法申请权限,且用户选择了拒绝授予后,这个方法会返回true。这时,我们就应该对请求的权限进行详细说明,这有助于用户授予权限。该方法原型如下:

public static boolean shouldShowRequestPermissionRationale(Activity activity,String permission)

这个方法有两个参数,第一个参数是Activity对象,第二个参数是字符串数组,代表我们想要申请的权限。

一个简单的例子

上面简单讲解了运行时权限的使用步骤,这里提供一个简单的例子,通过在运行时请求权限实现拨打电话的功能。

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.call_phone).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(ContextCompat.checkSelfPermission(MainActivity.this,
                        Manifest.permission.CALL_PHONE)!= PackageManager.PERMISSION_GRANTED){
                    //如果用户已经拒绝过一次权限申请,该方法返回true
                    if(ActivityCompat.shouldShowRequestPermissionRationale(
                            MainActivity.this,Manifest.permission.CALL_PHONE)){
                        //提示用户这一权限的重要性
                        Toast.makeText(MainActivity.this,"拨号功能是本应用的核心功能,如果不授予权限,程序是无法正常工作的~",
                                Toast.LENGTH_SHORT).show();
                    }
                    //请求权限
                    ActivityCompat.requestPermissions(MainActivity.this,
                            new String[]{Manifest.permission.CALL_PHONE},1);
                }else{//权限已被授予
                    callPhone();
                }
            }
        });
    }

    //拨打电话
    private void callPhone(){
        try {//可能出现异常,需要使用try-catch语句块
            Intent intent=new Intent(Intent.ACTION_CALL);
            intent.setData(Uri.parse("tel:10086"));
            startActivity(intent);
        } catch (SecurityException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[]
            permissions, @NonNull int[] grantResults) {
        switch (requestCode){
            case 1:
                if(grantResults.length>0&&grantResults[0]==
                        PackageManager.PERMISSION_GRANTED){
                    //权限已授予
                    callPhone();
                }else{
                    //权限未授予
                    Toast.makeText(this,"在未授予权限的情况下,程序无法正常工作",
                            Toast.LENGTH_SHORT).show();
                }
                break;
            default:
                break;
        }
    }
}

可以看到,为了保持兼容性,我们使用了ContextCompat以及ActivityCompat,也建议这样使用。除此之外,我们还需要在AndroidManifest文件中声明需要用到的权限。如下:

<!-- 请求拨号权限 -->
<uses-permission android:name="android.permission.CALL_PHONE"></uses-permission>

需要注意的是,在部分国产手机中,即使采用以上方式,可能也无法触发未授予权限那部分的逻辑。因此,建议使用开源库的方式进行运行时权限申请。

demo下载地址:PermissionDemo

开源库

  1. AndPermission
  2. easypermissions
  3. PermissionsDispatcher
  4. PermissionHelper

自定义权限

除了使用系统提供的权限,我们还可以自定义权限。思考这样一个场景,我们的应用有一个Activity可以被其他应用启动,但是需要其他应用具备相应的权限。这时,我们就可以在自己的应用中定义一个权限,其他应用只有声明这一权限,才可以成功启动我们的Activity

首先,在被启动应用的AndroidManifest文件中自定义权限,示例代码如下:

<!-- 自定义权限 -->
<permission
    android:name="com.example.selfpermissondemo.permisson.OPEN_ACTIVITY"
    android:protectionLevel="normal"
    android:label="自定义权限"/>

name是自定义权限的名称,protectionLevel是权限的安全级别,可以使用normal、dangerous、signature,、signatureOrSystem中的一种,label则是权限的简短描述。

对于需要权限才能启动的Activity,我们需要在<activity>标签中声明permission属性,示例代码如下:

<activity
    android:name=".PermissionActivity"
    android:permission="com.example.selfpermissondemo.permisson.OPEN_ACTIVITY">
    <intent-filter>
        <action android:name="com.example.selfpermissondemo.permission.activity"></action>
        <category android:name="android.intent.category.DEFAULT"></category>
    </intent-filter>
</activity>

在其他应用中,如果想启动该Activity,就必须在AndroidManifest文件中声明该自定义权限,否则将会抛出SecurityException异常。

<!-- 声明自定义权限 -->
<uses-permission android:name="com.example.selfpermissondemo.permisson.OPEN_ACTIVITY"></uses-permission>