用了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.打开在线源码网站: 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

参考

  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内存泄露

联系方式

时间: 2024-11-02 19:33:57

用了WifiManager这么多年,今天才知道彻底用错了的相关文章

刷牙刷了这么多年,我们居然都搞错了!

大家都知道每天刷牙有益健康,可普遍存在的刷牙错误会让你事倍功半. 错误一:刷牙太使劲 刷牙的时候力量太大,可能伤害牙齿,也会伤害到牙龈,引起口腔溃疡.其实,并非用的力气越大才越能把牙齿刷干净.如果刷牙方法不对,用的力气太大反而会伤害牙齿. 正确方法:刷牙用力大小因人而异,并没有特别量化的标准.刷牙力气应不轻不重,以刷干净牙面为宜.大约相当于手指拿起一支冰棒的力量就足够了,使用手腕的力量刷牙而不是手臂的力量. 错误二:刷牙时间太短 很多人以为刷牙的目的是清除食物残渣.但其实刷牙的目的是消灭牙菌斑.

深度:中美SaaS,差异究竟在哪里?

编者按 :程远先生曾经历了Box 从2009年转型企业级SaaS一直到2013年成为行业领军的完整崛起之路,他对国内外SaaS领域发展情况的深入研究对行业内的创业者而言具有极大帮助.中美SaaS创业起步阶段有何异同?中美SaaS产品形态.策略.竞争格局有什么共性与差异?国外有哪些成功的行业经验同样适用于国内SaaS领域?这些问题的答案,都将在本期<崔牛八点半>为您一一揭晓. 主持嘉宾: 亿方云创始人 程远 浙江大学软件工程学士,卡耐基梅隆大学(CMU)计算机科学硕士: 美国最大企业云存储服务商

大安防时代下 店铺安防怎么“防”?

安徽电视台经济生活近日报道,1月8号凌晨3点多,位于安徽合肥包河区水阳江路上,瞿大哥和鲁大姐开的一家社区烟酒茶超市遭盗窃,损失两万元左右. 令店主闹心的是,该超市装有防盗报警系统,每年都上交千元服务费给安防公司,而事发时防盗报警系统反应迟钝,本应五到十分钟内赶到现场的安防公司人员,用了三四十分钟才到,致使店内损失严重,而提及赔偿,安防公司又以提供不了被盗数量的有力证据迟迟不肯赔付,令店主更加闹心.安防成"空防"店主怎能不闹心,笔者也从该事件中看到这家公司的几个不足之处,在这里与大家谈一

使用 “~.” 退出无响应的 SSH 连接

大家有时候会发现 ssh 挂在那里没有响应了,可能是客户端的问题,也可能是服务器端的问题,也可能是客户端和服务器之间的网络问题:可能是客户端电脑休眠后连接断了.可能是网络断了.可能是 WiFi 信号不好.可能是网络延迟大了.可能是服务器挂了.也可能是服务器上的 sshd 进程挂了,-,可能是技术问题,也可能是非技术问题,可以找出无数可能. 我常遇到或者说每天都遇到的情形是,离开办公桌前忘了退出 ssh 会话.工作的时候长时间 ssh 到服务器上,工作完盖上 Mac 走人,回家后发现那些没退出的

互联网金融江湖革命:银行IT公司辟蹊径推创新

中介交易 SEO诊断 淘宝客 云主机 技术大厅 本报记者 刘飞 北京报道 这是个内外变局的时代. 内部,中国银行业正面临一场去杠杆化的调整.外部,一场声势浩大的互联网金融浪潮正冲击着中国银行业. 未来何去何从?拥抱技术,成为不容置疑的最佳选项,中国银行业已经开始行动,从银行系电商,到手机银行.微信银行,再到手机支付,各家电子银行部推出的特色招牌"菜",尽管仍仅停留在模块创新上,但已经在金融走向互联网的道路上架起了撬动的战略支点. 那么,撬动传统金融界的杠杆又在哪里?需要建立什么样的统一

谁扼住了泰康的增长势头 引入高盛投资招猜测

15年,有的公司成为传奇,有的公司成为往事; 15年,中国诞生了全球市值最大的公司,也打造了最赚钱的大公司; 但是,在这一轮中国大公司崛起的浪潮中,却找不到一家可以称得上伟大的世界级公司. 显然,在泰康人寿保险股份有限公司(以下简称泰康人寿)董事长兼首席执行官陈东升看来,"伟大"是一个值得玩味的词儿,他甚至对于自己在泰康人寿15周年庆典上所做的演讲题目中是否用到"伟大",而反复思量. "我们现在只是打了一个基础,伟大,可能要三代人的努力才能成就."

判断文本中不存在指定字符串(忽略大小写)的正则表达式的问题

问题描述 判断文本中不存在指定字符串(忽略大小写)的正则表达式的问题 最近遇到个正则表达式的问题,判断不存在指定字符串的正则表达式是这样:((?!MySQL).)*,但忽略大小写判断(?!)加上去后,死活不成功,((?!(?!)MySQL).)*,这个是加上忽略大小后的写法,谁能帮我看看到底是怎么回事 解决方案 ?i 这个应该是忽略大小写的 解决方案二: 谢谢,才发现我写错了,?i写成?!

DreamweaverMX Ultradev探索(4-1)

dreamweaver Dreamweaver MX Ultradev探索(4)修改数据库中的记录 在上一章里我们讲了如何向数据库里添加新的内容.但不知大家注意到这样一种情况没有,如果我一条数据库记录在提交后才发现在出错了呢?怎么办?当然就要修改.这次修改我们可是通过WEB来修改数据库里的内容.听起来好像也不是太容易.没关系,通过前面几章的介绍,大家应当发现,在DW MX里调用数据库非常的简单,只要你点几下鼠标就行了.呵...这次更新数据库的内容也一样,非常之简单,let's go!~~! 好,

野心:教你如何利用群发软件来做SEO外链

做网站的人都知道,做SEO最忌讳的就是群发做外链,因为很多人都视群发为SEO的作弊方法,因此很多人对群发外链退而避之.今天我也来谈下个人对群发外链的看法.我觉得,群发做外链是不会影响SEO的,也许你会认为你那是瞎扯淡,但是请看我的分析.为什么群发外链也是SEO的一个重要环节?今天就以从优化一个新网站的角度来说分析吧. 做SEO的人都知道,新站初期是不怎么需要做外链的,新站的前20天左右只要做好内部建设就行了,也就是不断地充实网站的文章,做好网站的内部优化,包括网站的内部锚文本等等.因此,新站的头