閱讀851 返回首頁    go 阿裏雲 go 技術社區[雲棲]


用了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。
android-wifimanager-leak-context

關於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已經被聲明為普通對象,而不是靜態的。

https://androidxref.com/7.0.0_r1/xref/frameworks/base/wifi/java/android/net/wifi/WifiManager.java#mAsyncChannel

發散

另外,查詢資料,發現不止WiFiManager,還有AudioManager等也可能存在內存泄漏問題。具體參考: https://android-review.googlesource.com/

因此,建議,除了和UI相關的係統service,其他一律使用ApplicationContext來獲取。

歡迎大家關注我的微信公眾號: sn0wdr1am
sn0wdr1am

參考

  1. WifiManager
  2. WifiManager use AsyncChannel leading to memory leak
  3. Memory leak in WifiManager/WifiService of Android 4.2
  4. Fix context leak with AudioManager
  5. @SystemService for WifiManager causes a memory leak #1628
  6. Memory leak in WiFiManager from Android SDK
  7. signed apk error [WifiManagerLeak]
  8. Android: 記一次Android內存泄露

聯係方式

最後更新:2017-11-15 00:34:35

  上一篇:go  MySQL 中存儲過程 中文亂碼問號???
  下一篇:go  一個優酷技術人的雙11