用了WifiManager這麼多年,今天才知道徹底用錯了
作者:snowdream
Email:yanghui1986527#gmail.com
Github: https://github.com/snowdream
原文地址:https://snowdream.github.io/blog/2017/11/13/android-wifimanager-leak-context/
問題
之前在處理內存泄漏相關問題時,碰到一個奇怪的問題。有一個閃屏界麵,由於包含大圖片,屢次內存泄漏,屢次修改。屢次修改,屢次還內存泄漏。
直到有一天,通過MAT工具分析一個相關hprof文件時,發現一個新的case: 內存泄漏矛頭直指WifiManager。
關於WifiManager內存泄漏問題,在Android官方網站得到確認:
1. Memory leak in WifiManager/WifiService of Android 4.2
1. WifiManager use AsyncChannel leading to memory leak
解決
對於WifiManager,我一直都是這麼用的:
WifiManager wifiManager = ((WifiManager) this.getSystemService(Context.WIFI_SERVICE));
但是當我查閱WifiManager相關文檔後,我終於改變了看法。
在WifiManager官方文檔 https://developer.android.com/reference/android/net/wifi/WifiManager.html 中,提到一句話:
"On releases before N, this object should only be obtained from an application context, and not from any other derived context to avoid memory leaks within the calling process."
大概意思便是:
在Android N以前,你應該隻通過ApplicationContext來獲取WifiManager,否則可能麵臨內存泄漏問題。
WifiManager wifiManager = ((WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE));
分析
為什麼WifiManager可能發生內存泄漏?
下麵我們具體分析一下:
以Android 5.1.1_r6為例進行分析。
1.打開在線源碼網站: https://androidxref.com/ 。找到ContextImpl.java類源碼。
2.從ContextImpl.java源碼中,我們可以看到:一個進程可能創建多個WifiManager。同時,我們把Activity(也就是ctx.getOuterContext()),傳給了WifiManager。
class ContextImpl extends Context {
@Override
public Object getSystemService(String name) {
ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
return fetcher == null ? null : fetcher.getService(this);
}
static {
registerService(WIFI_SERVICE, new ServiceFetcher() {
public Object createService(ContextImpl ctx) {
IBinder b = ServiceManager.getService(WIFI_SERVICE);
IWifiManager service = IWifiManager.Stub.asInterface(b);
return new WifiManager(ctx.getOuterContext(), service);
}});
}
}
3.我們再接著瀏覽 WifiManager源碼。這裏把Context傳給了sAsyncChannel,而這個sAsyncChannel竟然是一個靜態變量。
public class WifiManager {
private static AsyncChannel sAsyncChannel;
public WifiManager(Context context, IWifiManager service) {
mContext = context;
mService = service;
init();
}
private void init() {
synchronized (sThreadRefLock) {
if (++sThreadRefCount == 1) {
Messenger messenger = getWifiServiceMessenger();
if (messenger == null) {
sAsyncChannel = null;
return;
}
sHandlerThread = new HandlerThread("WifiManager");
sAsyncChannel = new AsyncChannel();
sConnected = new CountDownLatch(1);
sHandlerThread.start();
Handler handler = new ServiceHandler(sHandlerThread.getLooper());
sAsyncChannel.connect(mContext, handler, messenger);
try {
sConnected.await();
} catch (InterruptedException e) {
Log.e(TAG, "interrupted wait at init");
}
}
}
}
}
4.再接著瀏覽AsyncChannel的源碼。這個context被保存在了AsyncChannel內部。
換一句話來說:你傳進來的Activity/Fragment,被一個靜態對象給持有了。一旦這個靜態對象沒有正確釋放,就會造成內存泄漏。
public class AsyncChannel {
/* Context for source /
private Context mSrcContext;
/**
* Connect handler and messenger.
*
* Sends a CMD_CHANNEL_HALF_CONNECTED message to srcHandler when complete.
* msg.arg1 = status
* msg.obj = the AsyncChannel
*
* @param srcContext
* @param srcHandler
* @param dstMessenger
*/
public void connect(Context srcContext, Handler srcHandler, Messenger dstMessenger) {
if (DBG) log("connect srcHandler to the dstMessenger E");
// We are connected
connected(srcContext, srcHandler, dstMessenger);
// Tell source we are half connected
replyHalfConnected(STATUS_SUCCESSFUL);
if (DBG) log("connect srcHandler to the dstMessenger X");
}
/**
* Connect handler to messenger. This method is typically called
* when a server receives a CMD_CHANNEL_FULL_CONNECTION request
* and initializes the internal instance variables to allow communication
* with the dstMessenger.
*
* @param srcContext
* @param srcHandler
* @param dstMessenger
*/
public void connected(Context srcContext, Handler srcHandler, Messenger dstMessenger) {
if (DBG) log("connected srcHandler to the dstMessenger E");
// Initialize source fields
mSrcContext = srcContext;
mSrcHandler = srcHandler;
mSrcMessenger = new Messenger(mSrcHandler);
// Initialize destination fields
mDstMessenger = dstMessenger;
if (DBG) log("connected srcHandler to the dstMessenger X");
}
}
5.最後。既然google聲稱Android 7.0已經改了這個問題。那我們就來圍觀一下這個改動:WiFiManager中的AsyncChannel已經被聲明為普通對象,而不是靜態的。
發散
另外,查詢資料,發現不止WiFiManager,還有AudioManager等也可能存在內存泄漏問題。具體參考: https://android-review.googlesource.com/
因此,建議,除了和UI相關的係統service,其他一律使用ApplicationContext來獲取。
參考
- WifiManager
- WifiManager use AsyncChannel leading to memory leak
- Memory leak in WifiManager/WifiService of Android 4.2
- Fix context leak with AudioManager
- @SystemService for WifiManager causes a memory leak #1628
- Memory leak in WiFiManager from Android SDK
- signed apk error [WifiManagerLeak]
- Android: 記一次Android內存泄露
聯係方式
- Email:yanghui1986527#gmail.com
- Github: https://github.com/snowdream
- Blog: https://snowdream.github.io/blog/
- 簡書:https://www.jianshu.com/u/748f0f7e6432
- 雲棲博客:https://yq.aliyun.com/u/snowdream86
最後更新:2017-11-15 00:34:35
上一篇:
MySQL 中存儲過程 中文亂碼問號???
下一篇:
一個優酷技術人的雙11
java中的this關鍵字
LC3 初日見聞 + 阿裏巴巴望京綠地中心一遊
Java JUC之Atomic係列12大類實例講解和原理分解
金融安全資訊精選 2017年第八期:Equifax數據泄露事件本周五個進展,企業用戶如何使用SOC 2 報告來評估CSP安全性,Alert Logic發布雲安全報告:雲上發生安全事件數更少
技術人員談管理之項目風險規避
.NET Core的日誌[5]:利用TraceSource寫日誌
linux c語言 select函數用法
java.util.concurrent包(1)——lock和synchronized對比
myEclipe的一個bug 缺少引用的包時出現不能生成編譯後的class文件的解決辦法
助力企業發展的大數據利器