用了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
  2. 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.打开在线源码网站: http://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已经被声明为普通对象,而不是静态的。

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

发散

另外,查询资料,发现不止WiFiManager,还有AudioManager等也可能存在内存泄漏问题。具体参考: https://android-review.googlesource.com/#/c/platform/frameworks/base/+/140481/

因此,建议,除了和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
    ](https://issuetracker.google.com/issues/36964970)
  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]](https://stackoverflow.com/a/42639000)
  8. Android: 记一次Android内存泄露

联系方式

相关文章
|
消息中间件 架构师 Java
华为资深架构师十年总结:进阶成为架构师需要掌握哪些技能?
当前你感到迷茫吗? 有很多人对现在的工作感到很没意思,因为当前的工作,完全是实现业务,技术上没有任何提高。在这种状态下就会感到迷茫,特别是一想到将来这种迷茫感就更强烈了。
175 0
|
消息中间件 运维 Kubernetes
【架构师之路 六】两年期计划
【架构师之路 六】两年期计划
124 0
|
设计模式 运维 架构师
学习笔记 | 技术创业之路: 从架构师走向创业者
学习笔记 | 技术创业之路: 从架构师走向创业者
332 0
|
JavaScript 前端开发 API
原来我误会了 changeOrigin 那么多年
前端开发中经常要配置接口代理服务,用来解决开发阶段的跨域问题。有一个常用的 changeOrigin 配置项,它究竟配置了个啥?本文做了一个小小的探究。
559 0
|
运维 监控 安全
一线技术人的成长思考总结
作为长期奋战在一线的技术人,我深刻体会到几个思维能力对技术人成长的重要性,熟练运用这几种思维可以帮助我们快速的进入到新的领域,在分析、定位和解决问题上有很大帮助。作为长期奋战在一线的技术人,我深刻体会到几个思维能力对技术人成长的重要性,熟练运用这几种思维可以帮助我们快速的进入到新的领域,在分析、定位和解决问题上有很大帮助。
一线技术人的成长思考总结
|
搜索推荐
码上公益|7天解决多年档案管理难题
爱心极客用数字技术,助力公益组织档案管理高效化、精细化,守护每一份爱心。
344 0
码上公益|7天解决多年档案管理难题
|
数据采集 jstorm 运维
阿里 10 年:一个普通技术人的成长之路
不管是什么角色,成长是我们每个人都必须经历的过程。作为一个技术人,成长不仅是技术上的不断精进,也包括日常工作中的方方面面。本文主要讲述了阿里巴巴高级技术专家在阿里 10 年的成长之路,分享他从一个普通技术人开始,在阿里的三个阶段,以及在晋升、转岗、带团队、做事等方面的心得感悟。
阿里 10 年:一个普通技术人的成长之路
|
数据采集 jstorm 运维
阿里10年:一个普通技术人的成长之路
不管是什么角色,成长是我们每个人都必须经历的过程。作为一个技术人,成长不仅是技术上的不断精进,也包括日常工作中的方方面面。本文分享阿里巴巴高级技术专家在阿里10年的成长之路,分享他从一个普通技术人开始,在阿里的三个阶段,以及在晋升、转岗、带团队、做事等方面的心得感悟。
阿里10年:一个普通技术人的成长之路
|
移动开发 人工智能 前端开发
素人程序员:如何在技术浪潮更迭中保持较高成长速度?
  技术更迭是有加速度的,一个又一个风口,一波又一波浪潮,昨天火了人工智能,今天已经大谈区块链。普通程序员们在这个进程里,应该怎样找准自己的位置,获得持续的个人成长呢?   技术更迭是有加速度的   从 2010 年开始,被定义为移动互联网的元年,移动开发也是从这一年开始逐渐开始火爆的。笔者也是从毕业之后加入这个浪潮的。据说移动开发火爆之时,理发师通过几个月培训以后也可以拿到月薪 1,2W 的薪水,可见那个时候对移动人才的饥渴程度。但是到了 2014 年底开始,移动开发的入职要求回归理性,要求逐渐提高,到现在基本大公司社招也不再招高级以下的移动开发了。
186 0
|
SQL 缓存 架构师
五年成为阿里技术专家,架构师需要懂哪些技术?
  很早很早之前,我对于架构的概念一点都不理解,依稀记得,架构( architecture)这个词,来自于建筑领域。   这对于我这个没写过几行代码的人来说,瞬间就有了一种“不明觉厉”的崇拜感。   架构,感觉好厉害的样子,从名称上来说,好像是设计根骨,设计底层,设计最核心的东西的人。   架构师,一定很NB,我什么时候能成为架构师呢?   后来懂了一点点代码,去写增删改查,更是体会不出来架构的概念,不就是Sql语句吗?明明DBA更厉害啊,做各种的慢Sql优化,所有的Sql都要让DBA审核,DBA对于Mysql,或者是Oracle的各种性能调忧很厉害,而熟悉业务的开发人员又常常能写出几
230 0