本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每个工作日都有文章更新。

各位小伙伴们早上好,不知道你们有没有惊讶于我的速度,因为不久之前我才新发布的开源库PermissionX今天又更新了。

是的,在不到一个月的时间里,PermissionX又迎来了一次重大的版本更新。如果你觉得一个月还不算快的话,可别忘了,两周之前我还发布了LitePal的新版本。对于我来说,这个速度已经是相当极限了。

不过,可能还有不少朋友不知道PermissionX是什么,这里我给出上一篇文章的链接,还没看过的小伙伴先去补补课 Android运行时权限终极方案,用PermissionX吧 。

本来按照迭代计划,下一个版本中,我是准备给PermissionX增加自定义权限提示对话框样式的功能。然而随着第一个版本的发布,根据大家的反馈,我意识到了另一个更加紧急的需求,就是对Java语言的支持。

真的很遗憾看到,即使在今天,Kotlin在国内仍然还只是少部分开发者群体使用的语言,然而这就是现实。因此,如果PermissionX只支持Kotlin语言的话,势必将大部分的开发者都拒之了门外。

其实最初我让PermissionX只支持Kotlin语言,是因为我实在不想同时维护两个版本,这样修改任何功能都需要在两个地方各改一遍,维护成本过高。

然而后面我又做了一些更全面的思考,发现只需要稍微付出一点点语法方面的代价,就可以让一份代码同时支持Java和Kotlin两种语言,那么本篇文章我们就来学习一下是如何实现的。


兼容Java和Kotlin

首先我们来回顾一下PermissionX的基本用法,这段代码在上一篇文章中我们是见过的:

PermissionX.init(this)    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)    .onExplainRequestReason { deniedList ->        showRequestReasonDialog(deniedList, "即将申请的权限是程序必须依赖的权限", "我已明白")    }    .onForwardToSettings { deniedList ->        showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白")    }    .request { allGranted, grantedList, deniedList ->        if (allGranted) {            Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()        } else {            Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()        }    }

是的,在Android程序的权限申请变得如此简单。

首先调用init()方法进行初始化,调用permissions()方法来指定要申请哪些权限,在onExplainRequestReason()方法中针对那些被拒绝的权限向用户解释申请的原因并重新申请,在onForwardToSettings()方法中针对那些被永久拒绝的权限向用户解释为什么它们是必须的,并自动跳转到应用设置当中提醒用户手动开启权限。最后调用request()方法开始请求权限,并接收申请的结果。

整段用法简洁明了,而且PermissionX帮助开发者解决了权限申请过程中最痛苦的一些逻辑处理,比如权限被拒绝了怎么办?权限被永久拒绝了怎么办?

那么之所以能将PermissionX的用法设计得这么简单明了,主要得感谢Kotlin的高阶函数功能。上述代码示例当中的onExplainRequestReason()方法、onForwardToSettings()方法、request()方法,实际上都是高阶函数。对于高阶函数中接收的函数类型参数,我们可以直接传入一个Lambda表达式,然后在Lambda表达式当中处理回调逻辑即可。

然而问题也就出现在了这里,由于Java是没有高阶函数这个概念的,因此这种便捷性的语法在Java语言当中并不适用,所以也就导致了PermissionX不支持Java的情况。

不过,这个问题是可以解决的!

事实上,在Kotlin语言当中,我们除了可以向高阶函数传递Lambda表达式,还可以向另一种SAM函数传递Lambda表达式。SAM的全称是Single Abstract Method,又叫做单抽象方法。具体来讲,如果Java中定义的某个接口,里面只有一个待实现方法(也就是所谓的单抽象方法),那么此时我们也可以向其传递Lambda表达式。

举一个具体的例子,所有Android开发者一定都调用过setOnClickListener()方法,这个方法可以用于给一个控件注册点击事件。

在Java当中我们会这样写:

button.setOnClickListener(new View.OnClickListener() {    @Override    public void onClick(View view) {            }});

这段代码因为所有人都实在是太熟悉了,因此没什么解释的必要。

但是可以看到,在setOnClickListener()方法中,我们创建了一个View.OnClickListener的匿名类,那么View.OnClickListener的代码是什么样的呢?点击查看其源码,如下所示:

public class View implements Callback, android.view.KeyEvent.Callback, AccessibilityEventSource {    public interface OnClickListener {        void onClick(View view);    }

可以看到,OnClickListener是一个接口,并且这个接口当中只有一个onClick()方法,因此这就是一个单抽象方法接口。

那么根据上面的规则,Kotlin允许我们向一个接收单抽象方法接口的函数传递Lambda表达式。因此,在Kotlin当中,我们给一个按钮注册点击事件通常都是这么写的:

button.setOnClickListener {}

看到这里,有没有受到点启发呢?反正我是受到了。也就是说,如果PermissionX想要同时兼容Java和Kotlin语言的话,可以很好地利用单抽象方法接口这个特性。将原本的高阶函数都改成这种SAM函数,那么不就自然可以兼容两种语言了吗?

没错,我也确实是这样做的,不过具体在实现的过程中还是遇到了一点问题。

因为高阶函数的功能是十分强大的,我们除了可以定义一个函数类型的参数列表以及它的返回值,还可以定义它的所属类。来看PermissionX中的一段示例代码:

fun onExplainRequestReason(callback: ExplainScope.(deniedList: MutableList<String>) -> Unit): PermissionBuilder {explainReasonCallback = callbackreturn this}

以上代码对于没接触过Kotlin的朋友来说,可能会像天书一样难以理解,然而如果你学过Kotlin的话,就知道这只是定义了一个简单的函数类型参数。是的,这里我又要推荐我写的新书《第一行代码 第3版》了,还没有阅读过的朋友可以认真考虑一下,能在很大程序上帮助你轻松上手Kotlin语言。

那么上述代码中,我们将函数类型的所属类定义在了ExplainScope当中,这意味着什么?意味着,在Lambda表达式当中,我们就自动拥有了ExplainScope的上下文,因此可以直接调用ExplainScope类中的任何方法。所以,你也已经猜到了,本篇文章第一段示例代码中调用的showRequestReasonDialog()方法就是定义在ExplainScope类当中的。

然而Kotlin中这个非常棒的特性,很遗憾,在Java当中也没有,而且即使通过SAM函数也无法实现。

所以,这里我不得不付出一点语法特性的代价,将Kotlin这种定义所属类上下文的特性改成了传递参数的方式。也因为这个原因,新版PermissionX的语法无法做到和上一个版本百分百兼容,而是要稍微做出一点点修改。

那么新版的PermissionX中实现和刚才同样的功能需要这样写:

PermissionX.init(this)    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)    .onExplainRequestReason { scope, deniedList ->        scope.showRequestReasonDialog(deniedList, "即将申请的权限是程序必须依赖的权限", "我已明白")    }    .onForwardToSettings { scope, deniedList ->        scope.showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白")    }    .request { allGranted, grantedList, deniedList ->        if (allGranted) {            Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()        } else {            Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()        }    }

可以看到,只是在onExplainRequestReason()和onForwardToSettings()方法的Lambda表达式参数列表中增加了一个scope参数,然后调用解释权限申请原因对话框的时候,前面也要加上scope对象,仅此一点点变化,其他用法部分和之前是完全一模一样的。

而Kotlin在用法层面做出这一点点的牺牲,带来的却是Java语言的全面支持,使用Java实现同样的功能只需要这样写:

PermissionX.init(this).permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE).onExplainRequestReason(new ExplainReasonCallbackWithBeforeParam() {@Overridepublic void onExplainReason(ExplainScope scope, List<String> deniedList, boolean beforeRequest) {scope.showRequestReasonDialog(deniedList, "即将申请的权限是程序必须依赖的权限", "我已明白");}}).onForwardToSettings(new ForwardToSettingsCallback() {@Overridepublic void onForwardToSettings(ForwardScope scope, List<String> deniedList) {scope.showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白");}}).request(new RequestCallback() {@Overridepublic void onResult(boolean allGranted, List<String> grantedList, List<String> deniedList) {if (allGranted) {Toast.makeText(MainActivity.this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show();} else {Toast.makeText(MainActivity.this, "您拒绝了如下权限:" + deniedList, Toast.LENGTH_SHORT).show();}}});

单纯从两种语言上来对比,Kotlin版的代码肯定是要远比Java版的更简洁,但是很多朋友或许就是更加习惯Java的这种语法结构吧,看起来可能也更加亲切一些。


支持Android 11

目前Android 11的Beta版本已在上周四正式发布了,我这次也算是走在了时代的前沿,第一时间研究了Android 11中的各种新特性。

其中,权限相关的部分有了较大的变化,不过大家也不用担心,需要我们开发者进行适配的地方并不多,只是你应该了解这些变化。

首先,那个让无数开发者极其讨厌的“拒绝并不再询问”选项没有了。但是别高兴的太早,Android 11只是将它换成了另外一种展现形式。假如应用程序申请的某个权限被用户拒绝了两次,那么Android系统会自动将其视为“拒绝并不再询问”来处理。

另外权限申请对话框现在允许取消了,如果用户取消了权限对话框,将会视为一次拒绝。

Android 11中还引入了权限过期的机制,本来用户授予了应用程序某个权限,该权限会一直有效,现在如果某应用程序很长时间没有启动,Android系统会自动收回用户授予的权限,下次启动需要重新请求授权。

另外,Android 11针对摄像机、麦克风、地理定位这3种权限提供了单次授权的选项。因为这3种权限都是属于隐私敏感的权限,如果像过去一样用户同意一次就代表永久授权,可能某些恶意应用会无节制地采集用户信息。在Android 11中请求摄像机权限,界面如下图所示。

可以看到,图中多了一个“仅限这一次”的选项。如果用户选择了这个选项,那么在整个应用程序的生命周期内,我们都是可以获取到摄像机数据的。但是当下次启动程序时,则需要再次请求权限。

以上部分就是Android 11中权限相关的主要变化,你会发现,这些变化其实并没有影响到我们的代码编写,也不用做什么额外的适配,所以只需要了解一下就行了。

不过接下来的部分,就是我们需要进行适配的地方了。

Android 10系统首次引入了android:foregroundServiceType属性,如果你想要在前台Service中获取用户的位置信息,那么必须在AndroidManifest.xml中进行以下配置声明:

<manifest>    ...    <service ... android:foregroundServiceType="location" />manifest>

而在Android 11系统中,这个要求扩展到了摄像机和麦克风权限。也就是说,如果你想要在前台Service中获取设备的摄像机和麦克风数据,那么也需要在AndroidManifest.xml中进行声明:

<manifest>    ...    <service ...android:foregroundServiceType="location|camera|microphone" />manifest>

接下来再来看另外一个需要适配的地方。

Android 10系统中引入了一个新的权限:ACCESS_BACKGROUND_LOCATION,用于允许应用程序在后台请求设备的位置信息。不过这个权限是不可以单独申请的,而是要和ACCESS_FINE_LOCATION或ACCESS_COARSE_LOCATION一起申请才行。这个也很好理解,怎么可能连前台请求位置信息都没同意呢,就允许在后台请求位置信息了。

在Android 10系统中,如果我们同时申请前台和后台定位权限,那么将会出现如下界面:

可以看到,界面上的选项有些不同,“始终允许”表示同时允许了前台和后台定位权限,“仅在使用此应用时允许”表示只允许前台定位权限,“拒绝”表示都不允许。

但是如果我们在Android 11系统中同时申请前台和后台定位权限会怎么样呢?很遗憾地告诉你,会崩溃。

因为Android 11系统要求,ACCESS_BACKGROUND_LOCATION权限必须单独申请,并且在那之前,应用程序还必须已经获得了ACCESS_FINE_LOCATION或ACCESS_COARSE_LOCATION权限才行。

这个规则其实PermissionX是可以不用考虑的,如果开发者在Android 11中同时申请前台和后台定位权限 ,那么就让系统直接抛出异常也是合理的,因为这种请求方式违反了Android 11的规则。

然而为了让开发者更方便地使用PermissionX,减少这种差异化编程的的场景,我还是决定对Android 11的这个新规则进行适配。

具体思路也是比较简单的,如果应用程序同时申请了前台和后台定位权限,那么就先忽略后台定位权限,只申请前台定位以及其他权限,等所有权限都申请完毕后再单独去申请后台定位权限。

看上去很简单是不是?可是当我具体去实现的时候差点没把我累死,同时也暴露出了PermissionX的扩展性设计得非常糟糕的问题。

其实本来我一直觉得PermissionX的代码写得非常出色,还鼓励大家去阅读源码,然而这次为了兼容Android 11我才发现,有多个地方的耦合性太高,牵一发而动全身,导致难以扩展功能。

PermissionX中有很多可以注册回调监听的地方,权限被拒绝时有回调,权限被永久拒绝时有回调,权限申请结束时有回调。而在代码逻辑中去通知这些回调的地方就更多了,传入一个空权限列表是不会进行权限请求的,直接回调结束。传入的权限列表如果全部都已经授权了,也会直接回调结束。还有点击解释权限申请原因对话框上的取消按钮,也要终止后续的权限请求。

以上还只是处理了一些边界情况,都不是正式的权限请求流程,正式请求之后的回调逻辑就更多了。

那么如此复杂的回调逻辑带来了一个什么问题?我很难找到一个切入点去判断除了后台定位权限之外的其他权限都处理完了(那么多的回调点都需要处理),然后再单独去申请后台定位权限。另外,后台定位权限还要复用之前的逻辑,这样每个回调的地方我都要知道当前是在请求非后台定位权限,还是后台定位权限(否则将无法知道接下来应该是去请求后台定位权限,还是结束请求回调给开发者)。

我大概尝试了两种不同的if else设计思路来实现兼容Android 11系统的功能,最终都失败了。写到后面逻辑越来越复杂,改了这个bug出现那个bug,实在无法继续。

最终我决定将PermissionX的整体架构全部推翻重来。这是一个不容易的决定,但是既然已经知道PermissionX的扩展性设计得非常糟糕,早晚我都是要解决这个问题的。

新版PermissionX的整体架构改成了链式任务的执行模式,根据不同的权限类型将请求分成两种任务,权限的请求以及结果的回调都是封装在任务当中的。当一个任务执行结束之后会判断是否还有下一个任务要执行,如果有的话就执行下一个任务,没有的话就回调结束。示意图如下所示:

部分链式任务的实现代码如下:

/** * Maintain the task chain of permission request process. * @author guolin * @since 2020/6/10 */public class RequestChain {    /**     * Holds the first task of request process. Permissions request begins here.     */    private BaseTask headTask;    /**     * Holds the last task of request process. Permissions request ends here.     */    private BaseTask tailTask;    /**     * Add a task into task chain.     * @param task  task to add.     */    public void addTaskToChain(BaseTask task) {        if (headTask == null) {            headTask = task;        }        // add task to the tail        if (tailTask != null) {            tailTask.next = task;        }        tailTask = task;    }    /**     * Run this task chain from the first task.     */    public void runTask() {        headTask.request();    }}

这里我使用了链表这种数据结构来实现,每当新增一个任务的时候,就将它添加到链表的尾部。执行任务的时候则从第一个任务开始执行,然后依次向后,直到所有任务执行结束才回调给开发者。

然后在请求权限的request()方法中,我构建了这样一条任务链:

/** * Request permissions at once, and handle request result in the callback. * * @param callback Callback with 3 params. allGranted, grantedList, deniedList. */public void request(RequestCallback callback) {    requestCallback = callback;    // Build the request chain.    // RequestNormalPermissions runs first.    // Then RequestBackgroundLocationPermission runs.    RequestChain requestChain = new RequestChain();    requestChain.addTaskToChain(new RequestNormalPermissions(this));    requestChain.addTaskToChain(new RequestBackgroundLocationPermission(this));    requestChain.runTask();}

可以看到,这里先是创建了RequestChain的实例,然后向链表中添加一个RequestNormalPermissions任务用于请求普通的权限,又添加了一个RequestBackgroundLocationPermission任务用于请求后台定位权限,接着调用runTask()方法就可以从链表头部依次向后执行任务了。

现在,当你使用PermissionX来进行权限处理,可以完全不用理会Android 11上的权限机制差异,所有判断逻辑PermissionX都会在内部帮你处理好。假如你同时请求了前台和后台定位权限,在Android 10系统中会将它们一起申请,在Android 11系统中会将它们分开申请,在Android 9或以下系统,则不会去申请后台定位权限,因为那个时候还没有这个权限。

另外,使用这种链式任务的执行模式之后,PermissionX未来的扩展性会变得非常好。因为除了上述我们讨论的权限之外,Android系统还有一些更加特殊的权限,比如悬浮窗权限。这种权限是不可以调用代码来进行申请的,而是要跳转到一个专门的设置界面,提醒用户手动开启。而现在的PermissionX,想要支持这种权限,其实只需要再添加一个新的任务就行了。当然,这个功能是相对比较靠后的版本计划,下一个版本的重点还是自定义权限提示对话框样式的功能。


如何升级

关于PermissionX新版本的内容变化就介绍到这里,升级的方式非常简单,改一下dependencies当中的版本号即可:

dependencies {    ...    implementation 'com.permissionx.guolindev:permissionx:1.2.2'}

尤其是还在使用Java语言的开发者们,这次的版本更新是非常值得一试的。

另外,如果你的项目还没有升级到AndroidX,那么可以使用Permission-Support这个版本,用法都是一模一样的,只是dependencies中的依赖声明需要改成:

dependencies {    ...    implementation 'com.permissionx.guolindev:permission-support:1.2.2'}

最后附上PermissionX的开源库地址,欢迎大家star和fork。

https://github.com/guolindev/PermissionX

本篇文章的内容还是比较充实的,既讲了PermissionX的新版用法,又讲了一些Kotlin的知识,还讲了Android 11的权限变更,当然最后还有新版PermissionX的架构设计思路,希望大家都有学到一些知识吧。


关注我的技术公众号,每天都有优质技术文章推送。

微信扫一扫下方二维码即可关注:

更多相关文章

  1. Android(安卓)悬浮窗权限各机型各系统适配大全
  2. Android系列之Android系列之如何成为Android编程高手第三篇
  3. 【译】Android中的安全数据— Android中的加密(第2部分)
  4. android点滴2
  5. 百度地图开发Marker|Polyline隐藏或显示
  6. 解决requestFeature() must be called before adding content错
  7. Mtk Android(安卓)app 默认激活设备管理器
  8. Android(安卓)权限中文说明
  9. Android(安卓)图像处理(类型转换,比例缩放,倒影,圆角)

随机推荐

  1. Android模拟器无法保存数据
  2. Activity被系统重启
  3. 【Android】android:padding属性设置对Im
  4. 关于android中的EditView,TextView的图片
  5. android 背景图片设置
  6. Android(安卓)Studio生成javadoc出错的解
  7. Android(安卓)在从全屏切换到非全屏的时
  8. Android(安卓)Studio 配置 OpenCV for An
  9. Android的framework层音量控制原理分析--
  10. 【android】与pc机进行UDP通信