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

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

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

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

shouldShowRequestPermissionRationale方法調(diào)用時序圖
看完調(diào)用流程圖后,我們再來一步一步分析shouldShowRequestPermissionRationale具體是怎么調(diào)用的,以及為什么會產(chǎn)生內(nèi)存泄漏?
先從Activity的shouldShowRequestPermissionRationale方法開始,Activity調(diào)用的是packageManager的shouldShowRequestPermissionRationale方法。
public boolean shouldShowRequestPermissionRationale( String permission) {//調(diào)用ContextImpl的getPackageManager()方法獲取PackageManager實例//然后調(diào)用PackageManager的shouldShowRequestPermissionRationale方法return getPackageManager().shouldShowRequestPermissionRationale(permission);}
我們知道Activity是從ContextWrapper繼承而來的,ContextWrapper中持有一個mBase實例,這個實例指向一個contextImpl對象,Activity的getPackageManager這個方法調(diào)用的就是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對象,然后調(diào)用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) {//調(diào)用PermissionManager的shouldShowRequestPermissionRationale方法return getPermissionManager().shouldShowRequestPermissionRationale(permName);}
ApplicationPackageManager中調(diào)用的是PermissionManager的shouldShowRequestPermissionRationale方法。獲取PermissionManager時會調(diào)用contextImpl的getSystemService方法。getSystemService方法調(diào)用由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對象獲取服務(wù)名return SystemServiceRegistry.getSystemServiceName(serviceClass);}public Object getSystemService(String name) {//通過服務(wù)名獲取服務(wù),傳入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;}//通過服務(wù)名獲取對應(yīng)服務(wù)final ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);...//返回對應(yīng)服務(wù)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的構(gòu)造方法中會創(chuàng)建PermissionUsageHelper對象并傳入context,這個context是SystemServiceRegistry中contextImpl對象持有的outerContext對象,就是一開始的Activity對象,所以PermissionUsageHelper的單實例持有了Activity的實例引用。
看到這里再結(jié)合上節(jié)的內(nèi)存泄漏隱患發(fā)現(xiàn)時hprof文件中的GC Root我們可以知道,內(nèi)存泄漏就是這個PermissionUsageHelper構(gòu)造時持有Activity對象最終在Acitvity生命周期結(jié)束時沒有被釋放導(dǎo)致的。
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ā)內(nèi)存泄漏問題mUsageHelper = new PermissionUsageHelper(context);}
我們再來看PermissionManager中的shouldShowRequestPermissionRationale方法,最終調(diào)用的是PermissionManagerService代理對象的shouldShowRequestPermissionRationale方法。
到這里我們基本走完了shouldShowRequestPermissionRationale方法的調(diào)用流程。也知道調(diào)用shouldShowRequestPermissionRationale方法產(chǎn)生的內(nèi)存泄漏是因為在獲取PermissionManager時創(chuàng)建PermissionUsageHelper導(dǎo)致的。這個問題僅在Android S上出現(xiàn),在Android_R上PermissionManager構(gòu)造方法并沒有去創(chuàng)建PermissionUsageHelper所以也不會有內(nèi)存泄露問題。下面我們再來看為什么創(chuàng)建PermissionUsageHelper時傳入Activity對象會導(dǎo)致內(nèi)存泄漏?Activity又是被誰一直持有的?
public boolean shouldShowRequestPermissionRationale( String permissionName) {try {final String packageName = mContext.getPackageName();//調(diào)用PermissionManagerService的shouldShowRequestPermissionRationale方法return mPermissionManager.shouldShowRequestPermissionRationale(packageName, permissionName, mContext.getUserId());} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}
在PermissionUsageHelper構(gòu)造方法中會把Activity對象賦值給mContext這個成員變量,然后獲取AppOpsManager對象,并調(diào)用AppOpsManager的startWatchingActive和startWatchingStarted方法傳入this作為回調(diào)用來監(jiān)聽?wèi)?yīng)用程序狀態(tài)和權(quán)限狀態(tài)監(jiān)聽。
這里的startWatchingActive和startWatchingStarted方法最終會調(diào)用AppOpsService的startWatchingActive和startWatchingStarted方法并把傳入的this也就是PermissionUsageHelper保存起來。而PermissionUsageHelper又持有Activivty實例,導(dǎo)致AppOpsService間接保存Acitivty實例。當(dāng)應(yīng)用程序主動調(diào)用destroy方法時,AppOpsService并沒有移除應(yīng)用監(jiān)聽和權(quán)限狀態(tài)監(jiān)聽,仍然保存著Acitivty實例導(dǎo)致Acitivty無法釋放產(chǎn)生內(nèi)存泄漏。
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)聽?wèi)?yīng)用程序狀態(tài),此處this實現(xiàn)了OnOpActiveChangedListener接口作為callbackck傳入mAppOpsManager.startWatchingActive(opStrs, context.getMainExecutor(), this);int[] ops = { OP_CAMERA, OP_RECORD_AUDIO };// 監(jiān)聽權(quán)限狀態(tài),此處this實現(xiàn)了OnOpStartedListener接口作為callbackck傳入mAppOpsManager.startWatchingStarted(ops, this);}
四、總結(jié)
Android S上增加權(quán)限指示器功能PermissionUsageHelper,這個類它獲取所有使用過麥克風(fēng)、相機(jī)和可能的位置許可的應(yīng)用程序,在特定的時間范圍內(nèi),以及可能的特殊屬性,監(jiān)聽?wèi)?yīng)用程序使用此類權(quán)限的狀態(tài)。調(diào)用shouldShowRequestPermissionRationale方法產(chǎn)生內(nèi)存泄露的根本原因是獲取PermissionManager時會創(chuàng)建PermissionUsageHelper對象并監(jiān)聽?wèi)?yīng)用程序狀態(tài)和權(quán)限狀態(tài)。
只有應(yīng)用進(jìn)程退出或者手機(jī)進(jìn)入IDLE狀態(tài),才釋放activity并且未使用camera、audio和location權(quán)限的應(yīng)用同樣會去做監(jiān)聽,在Android S上重構(gòu)權(quán)限模塊明顯導(dǎo)致了activity泄漏問題。不主動finish應(yīng)用的actiivity雖然不會導(dǎo)致這個問題,但是應(yīng)用場景使用情況很多,從框架原生代碼邏輯來說是不合理的。
這個問題目前僅在Android S上出現(xiàn),其他會使用PermissionManager的場景沒認(rèn)真研究,不確定有沒有這個問題。不過為了避免類似的情況發(fā)生,最好的解決辦法就是:
對PermissionUsageHelper進(jìn)行修改,判斷應(yīng)用是否有camera、location、audio權(quán)限,應(yīng)用程序activity退出時,主動調(diào)用stop方法,移除監(jiān)聽。
為了防止失聯(lián),歡迎關(guān)注我防備的小號
微信改了推送機(jī)制,真愛請星標(biāo)本公號??



