作者:作者:努比亚技术团队
Android里面内存泄漏问题最突出的就是Activity的泄漏,而泄漏的根源大多在于因为生命周期较长的对象去引用生命周期较短的Activity实例,也就会造成在Activity生命周期结束后,还被引用导致无法被系统回收释放。
Activity导致内存泄漏有两种情况:
- 应用级:应用程序代码实现的activity没有很好的管理其生命周期,导致Activity退出后仍然被引用。
- 系统级:Android系统级实现的对activity管理不太友好,被应用调用导致内存泄漏。
本文主要讲的是最近发现的系统级shouldShowRequestPermissionRationale方法使用导致的内存泄漏问题。
一、背景
Android 6.0 (API 23) 之前应用的权限在安装时全部授予,运行时应用不再需要询问用户。在 Android 6.0 或更高版本对权限进行了分类,对某些涉及到用户隐私的权限可在运行时根据用户的需要动态获取。主要流程如下:
这个过程中会遇到这么几个方法:
- ContextCompat.checkSelfPermission,检查应用是否有权限
- ActivityCompat.requestPermissions,请求某个或某几个权限
- onRequestPermissionsResult,请求权限之后的授权结果回调
- shouldShowRequestPermissionRationale
前三个方法的用途都非常清楚,使用也很简单,这里不做过多解释,今天主要看下shouldShowRequestPermissionRationale,看下它是干什么用的:
当APP调用一个需要权限的函数时,如果用户拒绝某个授权,下一次弹框时将会有一个“禁止后不再询问”的选项,来防止APP以后继续请求授权。如果这个选项在拒绝授权前被用户勾选了,下次为这个权限请求requestPermissions时,对话框就不弹出来了,结果就是app啥都不干。遇到这种情况需要在请求requestPermissions前,检查是否需要展示请求权限的提示,这时候用的就是shouldShowRequestPermissionRationale方法。
shouldShowRequestPermissionRationale字面解释是“应不应该解释下请求这个权限的目的”,下面列举了此方法使用时的4种情况及相应情况下的返回值:
- 都没有请求过这个权限,用户不一定会拒绝你,所以你不用解释,故返回false;
- 请求了但是被用户拒绝了,此时返回true,意思是你该向用户好好解释下了;
- 用户选择了拒绝并且不再提示,也不给你弹窗提醒了,所以你也不用解释了,故返回fasle;
- 已经允许了,不需要申请也不需要提示,故返回false。
二、调用案例及内存泄漏隐患
2.1正常权限申请流程
通常我们申请权限时先调用checkSelfPermission方法检验应用是否有需要使用的权限,没有相应权限时调用shouldShowRequestPermissionRationale方法检查是否需要展示请求权限的提示。不需要展示提示时再调用requestPermissions方法进行权限请求。代码如下:
2.2内存泄漏隐患发现
在Android S上,我们使用上述方式进行权限获取时会发现只要你调用了shouldShowRequestPermissionRationale方法,当MainActivity生命周期结束后MainActivity都不会被回收。我们可以不给予所需权限多次进入退出此应用运行一段时间(保证每次都会调用到shouldShowRequestPermissionRationale方法)。使用adb命令dump meminfo查看内存情况。可以看到activity实例数为25,表明acticity虽然被销毁但是因为被其他对象持有所以并没有被GC。 注意:此处测试是通过返回键退出activity的,我们的Demo在返回键的监听有调用finish方法确保结束activity。每次重新进入都会重新执行onCreate方法。因为Android S上使用返回键退出应用并不会直接销毁activity,而只有当应用主动调用finish或者非启动类型的activity才会去销毁。
此时我们已经发现此Demo存在明显的内存泄露问题。下面我们使用Memory Profiler工具抓取hprof文件进行内存分析,看看是哪里发生了内存泄漏。
-
可以看到Memory Profiler工具已经提示我们有com.nubia.application包下MainActivity有24个对象发生了内存泄露。
-
可以看到当前MainActivity总共实例有25个和adb命令查询出来吻合。其他24个实例没有被GC导致内存泄露。
-
References标签页可以看到其他MainActivity实例被AppOpsManager持有导致无法被GC。
我们可以看到Memory Profiler工具提示共有48个对象产生内存泄露那么其他24个是哪里产生的呢?点击Leaks进行查看如下图。
-
可以看到除了MainActivity产生了内存泄露,ReportFragment也产生了内存泄露。查看ReportFragment的Instance Details标签页可以看到ReportFragment的实例被MainActivity持有而MainActivity被AppOpsManager持有所以产生了内存泄露。
-
至此Demo中所有内存泄露问题分析完成。
2.3内存泄漏隐患分析
在上一节中我们已经发现Demo在使用过程中存在内存泄漏问题。只要我们调用shouldShowRequestPermissionRationale方法,当Activity生命周期结束时就会发生Activity内存泄漏。那么这一节我们来具体分析下为什么调用shouldShowRequestPermissionRationale方法会发生内存泄漏。
我们先来看一下shouldShowRequestPermissionRationale方法调用整个过程的时序图:
看完调用流程图后,我们再来一步一步分析shouldShowRequestPermissionRationale具体是怎么调用的,以及为什么会产生内存泄漏?
先从Activity的shouldShowRequestPermissionRationale方法开始,Activity调用的是packageManager的shouldShowRequestPermissionRationale方法。
public boolean shouldShowRequestPermissionRationale(@NonNull String permission) {
//调用ContextImpl的getPackageManager()方法获取PackageManager实例
//然后调用PackageManager的shouldShowRequestPermissionRationale方法
return getPackageManager().shouldShowRequestPermissionRationale(permission);
}
我们知道Activity是从ContextWrapper继承而来的,ContextWrapper中持有一个mBase实例,这个实例指向一个contextImpl对象,Activity的getPackageManager这个方法调用的就是contextImpl的getPackageManager方法。
private PackageManager mPackageManager;
...
public PackageManager getPackageManager()