Android 12原生系統(tǒng)居然有內存泄露隱患?
大家好,我是皇叔,最近開了一個安卓進階漲薪訓練營,可以幫助大家突破技術&職場瓶頸,從而度過難關,進入心儀的公司。
詳情見文章:沒錯!皇叔開了個訓練營
作者:努比亞技術團隊
https://www.jianshu.com/p/b0de542204f8
一、引言
Android里面內存泄漏問題最突出的就是Activity的泄漏,而泄漏的根源大多在于因為生命周期較長的對象去引用生命周期較短的Activity實例,也就會造成在Activity生命周期結束后,還被引用導致無法被系統(tǒng)回收釋放。
Activity導致內存泄漏有兩種情況:
應用級:應用程序代碼實現(xiàn)的activity沒有很好的管理其生命周期,導致Activity退出后仍然被引用。
系統(tǒng)級:Android系統(tǒng)級實現(xiàn)的對activity管理不太友好,被應用調用導致內存泄漏。
本文主要講的是最近發(fā)現(xiàn)的系統(tǒng)級shouldShowRequestPermissionRationale方法使用導致的內存泄漏問題。
二、背景
Android 6.0 (API 23) 之前應用的權限在安裝時全部授予,運行時應用不再需要詢問用戶。在 Android 6.0 或更高版本對權限進行了分類,對某些涉及到用戶隱私的權限可在運行時根據(jù)用戶的需要動態(tài)獲取。主要流程如下:

權限申請流程
這個過程中會遇到這么幾個方法:
ContextCompat.checkSelfPermission,檢查應用是否有權限
ActivityCompat.requestPermissions,請求某個或某幾個權限
onRequestPermissionsResult,請求權限之后的授權結果回調
shouldShowRequestPermissionRationale
前三個方法的用途都非常清楚,使用也很簡單,這里不做過多解釋,今天主要看下shouldShowRequestPermissionRationale,看下它是干什么用的:
當APP調用一個需要權限的函數(shù)時,如果用戶拒絕某個授權,下一次彈框時將會有一個“禁止后不再詢問”的選項,來防止APP以后繼續(xù)請求授權。如果這個選項在拒絕授權前被用戶勾選了,下次為這個權限請求requestPermissions時,對話框就不彈出來了,結果就是app啥都不干。遇到這種情況需要在請求requestPermissions前,檢查是否需要展示請求權限的提示,這時候用的就是shouldShowRequestPermissionRationale方法。
shouldShowRequestPermissionRationale字面解釋是“應不應該解釋下請求這個權限的目的”,下面列舉了此方法使用時的4種情況及相應情況下的返回值:
都沒有請求過這個權限,用戶不一定會拒絕你,所以你不用解釋,故返回false;
請求了但是被用戶拒絕了,此時返回true,意思是你該向用戶好好解釋下了;
用戶選擇了拒絕并且不再提示,也不給你彈窗提醒了,所以你也不用解釋了,故返回fasle;
已經允許了,不需要申請也不需要提示,故返回false。
三、調用案例及內存泄漏隱患
3.1正常權限申請流程
通常我們申請權限時先調用checkSelfPermission方法檢驗應用是否有需要使用的權限,沒有相應權限時調用shouldShowRequestPermissionRationale方法檢查是否需要展示請求權限的提示。不需要展示提示時再調用requestPermissions方法進行權限請求。代碼如下:
3.2內存泄漏隱患發(fā)現(xiàn)
在Android S上,我們使用上述方式進行權限獲取時會發(fā)現(xiàn)只要你調用了shouldShowRequestPermissionRationale方法,當MainActivity生命周期結束后MainActivity都不會被回收。我們可以不給予所需權限多次進入退出此應用運行一段時間(保證每次都會調用到shouldShowRequestPermissionRationale方法)。使用adb命令dump meminfo查看內存情況??梢钥吹絘ctivity實例數(shù)為25,表明acticity雖然被銷毀但是因為被其他對象持有所以并沒有被GC。注意:此處測試是通過返回鍵退出activity的,我們的Demo在返回鍵的監(jiān)聽有調用finish方法確保結束activity。每次重新進入都會重新執(zhí)行onCreate方法。因為Android S上使用返回鍵退出應用并不會直接銷毀activity,而只有當應用主動調用finish或者非啟動類型的activity才會去銷毀。
Demo內存占用情況

使用Memory Profiler工具進行內存泄漏分析
可以看到Memory Profiler工具已經提示我們有com.nubia.application包下MainActivity有24個對象發(fā)生了內存泄露。
可以看到當前MainActivity總共實例有25個和adb命令查詢出來吻合。其他24個實例沒有被GC導致內存泄露。
References標簽頁可以看到其他MainActivity實例被AppOpsManager持有導致無法被GC。
我們可以看到Memory Profiler工具提示共有48個對象產生內存泄露那么其他24個是哪里產生的呢?點擊Leaks進行查看如下圖。

使用Memory Profiler工具進行內存泄漏分析
可以看到除了MainActivity產生了內存泄露,ReportFragment也產生了內存泄露。查看ReportFragment的Instance Details標簽頁可以看到ReportFragment的實例被MainActivity持有而MainActivity被AppOpsManager持有所以產生了內存泄露。
至此Demo中所有內存泄露問題分析完成。
3.3內存泄漏隱患分析
在上一節(jié)中我們已經發(fā)現(xiàn)Demo在使用過程中存在內存泄漏問題。只要我們調用shouldShowRequestPermissionRationale方法,當Activity生命周期結束時就會發(fā)生Activity內存泄漏。那么這一節(jié)我們來具體分析下為什么調用shouldShowRequestPermissionRationale方法會發(fā)生內存泄漏。

shouldShowRequestPermissionRationale方法調用時序圖
看完調用流程圖后,我們再來一步一步分析shouldShowRequestPermissionRationale具體是怎么調用的,以及為什么會產生內存泄漏?
先從Activity的shouldShowRequestPermissionRationale方法開始,Activity調用的是packageManager的shouldShowRequestPermissionRationale方法。
public boolean shouldShowRequestPermissionRationale( 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() {if (mPackageManager != null) {return mPackageManager;}//獲取PackageManagerService代理對象final IPackageManager pm = ActivityThread.getPackageManager();if (pm != null) {// Doesn't matter if we make more than one instance.//創(chuàng)建ApplicationPackageManager實例,傳入contextImpl對象return (mPackageManager = new ApplicationPackageManager(this, pm));}return null;}
可以看到contextImpl的getPackageManager方法中會創(chuàng)建ApplicationPackageManager實例同時傳入contextImpl對象,然后調用ApplicationPackageManager的shouldShowRequestPermissionRationale方法。
private PermissionManager mPermissionManager;protected ApplicationPackageManager(ContextImpl context, IPackageManager pm) {//傳入的contextImpl和pm實例mContext = context;mPM = pm;}private PermissionManager getPermissionManager() {synchronized (mLock) {if (mPermissionManager == null) {//獲取PermissionManager對象//contextImpl.getSystemService實現(xiàn)mPermissionManager = mContext.getSystemService(PermissionManager.class);}return mPermissionManager;}}public boolean shouldShowRequestPermissionRationale(String permName) {//調用PermissionManager的shouldShowRequestPermissionRationale方法return getPermissionManager().shouldShowRequestPermissionRationale(permName);}
ApplicationPackageManager中調用的是PermissionManager的shouldShowRequestPermissionRationale方法。獲取PermissionManager時會調用contextImpl的getSystemService方法。getSystemService方法調用由SystemServiceRegistry來完成。
public final <T> T getSystemService( Class<T> serviceClass) {String serviceName = getSystemServiceName(serviceClass);return serviceName != null ? (T)getSystemService(serviceName) : null;}public String getSystemServiceName(Class<?> serviceClass) {//通過class對象獲取服務名return SystemServiceRegistry.getSystemServiceName(serviceClass);}public Object getSystemService(String name) {//通過服務名獲取服務,傳入contextImpl實例對象return SystemServiceRegistry.getSystemService(this, name);}
SystemServiceRegistry提供PermissionManager的實例。SystemServiceRegistry在靜態(tài)注冊PermissionManager會傳入contextImpl的outerContext對象,這個outerContext就是Activity對象。
public static Object getSystemService(ContextImpl ctx, String name) {if (name == null) {return null;}//通過服務名獲取對應服務final ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);...//返回對應服務final Object ret = fetcher.getService(ctx);...return ret;}static{//靜態(tài)注冊PermissionManagerregisterService(Context.PERMISSION_SERVICE, PermissionManager.class,new CachedServiceFetcher<PermissionManager>() {public PermissionManager createService(ContextImpl ctx)throws ServiceNotFoundException {//創(chuàng)建PermissionManager實例 傳入activity實例對象return new PermissionManager(ctx.getOuterContext());}});}
精彩的部分來了,在PermissionManager的構造方法中會創(chuàng)建PermissionUsageHelper對象并傳入context,這個context是SystemServiceRegistry中contextImpl對象持有的outerContext對象,就是一開始的Activity對象,所以PermissionUsageHelper的單實例持有了Activity的實例引用。
看到這里再結合上節(jié)的內存泄漏隱患發(fā)現(xiàn)時hprof文件中的GC Root我們可以知道,內存泄漏就是這個PermissionUsageHelper構造時持有Activity對象最終在Acitvity生命周期結束時沒有被釋放導致的。
public PermissionManager(@NonNull Context context) throws ServiceManager.ServiceNotFoundException {mContext = context;mPackageManager = AppGlobals.getPackageManager();//獲取PermissionManagerService代理對象mPermissionManager = IPermissionManager.Stub.asInterface(ServiceManager.getServiceOrThrow("permissionmgr"));mLegacyPermissionManager = context.getSystemService(LegacyPermissionManager.class);//TODO ntmyren: there should be a way to only enable the watcher when requested//Android S新增,初始化 PermissionUsageHelper 引發(fā)內存泄漏問題mUsageHelper = new PermissionUsageHelper(context);}
我們再來看PermissionManager中的shouldShowRequestPermissionRationale方法,最終調用的是PermissionManagerService代理對象的shouldShowRequestPermissionRationale方法。
到這里我們基本走完了shouldShowRequestPermissionRationale方法的調用流程。也知道調用shouldShowRequestPermissionRationale方法產生的內存泄漏是因為在獲取PermissionManager時創(chuàng)建PermissionUsageHelper導致的。這個問題僅在Android S上出現(xiàn),在Android_R上PermissionManager構造方法并沒有去創(chuàng)建PermissionUsageHelper所以也不會有內存泄露問題。下面我們再來看為什么創(chuàng)建PermissionUsageHelper時傳入Activity對象會導致內存泄漏?Activity又是被誰一直持有的?
public boolean shouldShowRequestPermissionRationale( String permissionName) {try {final String packageName = mContext.getPackageName();//調用PermissionManagerService的shouldShowRequestPermissionRationale方法return mPermissionManager.shouldShowRequestPermissionRationale(packageName, permissionName, mContext.getUserId());} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}
在PermissionUsageHelper構造方法中會把Activity對象賦值給mContext這個成員變量,然后獲取AppOpsManager對象,并調用AppOpsManager的startWatchingActive和startWatchingStarted方法傳入this作為回調用來監(jiān)聽應用程序狀態(tài)和權限狀態(tài)監(jiān)聽。
這里的startWatchingActive和startWatchingStarted方法最終會調用AppOpsService的startWatchingActive和startWatchingStarted方法并把傳入的this也就是PermissionUsageHelper保存起來。而PermissionUsageHelper又持有Activivty實例,導致AppOpsService間接保存Acitivty實例。當應用程序主動調用destroy方法時,AppOpsService并沒有移除應用監(jiān)聽和權限狀態(tài)監(jiān)聽,仍然保存著Acitivty實例導致Acitivty無法釋放產生內存泄漏。
public PermissionUsageHelper( Context context) {//Activity上下文對象mContext = context;mPkgManager = context.getPackageManager();//獲取AppOpsManagermAppOpsManager = context.getSystemService(AppOpsManager.class);mUserContexts = new ArrayMap<>();mUserContexts.put(Process.myUserHandle(), mContext);// TODO ntmyren: make this listen for flag enable/disable changesString[] opStrs = { OPSTR_CAMERA, OPSTR_RECORD_AUDIO };// 監(jiān)聽應用程序狀態(tài),此處this實現(xiàn)了OnOpActiveChangedListener接口作為callbackck傳入mAppOpsManager.startWatchingActive(opStrs, context.getMainExecutor(), this);int[] ops = { OP_CAMERA, OP_RECORD_AUDIO };// 監(jiān)聽權限狀態(tài),此處this實現(xiàn)了OnOpStartedListener接口作為callbackck傳入mAppOpsManager.startWatchingStarted(ops, this);}
四、總結
Android S上增加權限指示器功能PermissionUsageHelper,這個類它獲取所有使用過麥克風、相機和可能的位置許可的應用程序,在特定的時間范圍內,以及可能的特殊屬性,監(jiān)聽應用程序使用此類權限的狀態(tài)。調用shouldShowRequestPermissionRationale方法產生內存泄露的根本原因是獲取PermissionManager時會創(chuàng)建PermissionUsageHelper對象并監(jiān)聽應用程序狀態(tài)和權限狀態(tài)。
只有應用進程退出或者手機進入IDLE狀態(tài),才釋放activity并且未使用camera、audio和location權限的應用同樣會去做監(jiān)聽,在Android S上重構權限模塊明顯導致了activity泄漏問題。不主動finish應用的actiivity雖然不會導致這個問題,但是應用場景使用情況很多,從框架原生代碼邏輯來說是不合理的。
這個問題目前僅在Android S上出現(xiàn),其他會使用PermissionManager的場景沒認真研究,不確定有沒有這個問題。不過為了避免類似的情況發(fā)生,最好的解決辦法就是:
對PermissionUsageHelper進行修改,判斷應用是否有camera、location、audio權限,應用程序activity退出時,主動調用stop方法,移除監(jiān)聽。
為了防止失聯(lián),歡迎關注我防備的小號
微信改了推送機制,真愛請星標本公號??



