[译]Handlers和内部类如何造成Context泄露

避免Android内存泄漏
本文探讨了Android应用程序中因不当使用Handler和内部类而导致的内存泄漏问题。解释了内存泄漏的原因,并提供了解决方案,包括使用静态内部类和弱引用。

How to Leak a Context: Handlers & Inner Classes
看一下下面代码:

public class SampleActivity extends Activity {

  private final Handler mLeakyHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      // ... 
    }
  }
}

虽然看起来没有很明显的问题,但是这段代码却存在着会导致严重的内存泄露的风险。Android Lint会给出以下提示:

In Android, Handler classes should be static or leaks might occur.

但是,究竟泄露会发生在哪里还有它是怎样可能会发生的?下面让我们一起来通过我们所知道的知识来确定问题的源头:
1. 当一个安卓应用第一次启动的时候,安卓框架会给APP的主线程创建一个Looper对象。Looper继承了一个简单的在一个圆环中一个接一个地处理消息对象的消息队列。所有的应用层框架事件(例如Activity的生命周期方法的调用,按钮点击事件等等)都是包含在消息对象的范围内,都会添加到Looper的消息队列中并且一个接一个地被处理掉。主线程的Looper通过应用的生命周期的存在而得以存在。
2. 当一个Handler在主线程被初始化的时候,它和Looper的消息队列关联着。那么被发送到消息队列的消息会对Handler持有一个引用以便当Looper最终会处理这个消息的时候,框架可以调用Handler的handleMessage(Message message)方法。
3. 在Java里面,非静态的匿名类会对他们的外部类持有一个隐式的引用。相反,静态的内部内则不会。
那么内存泄露究竟发生在哪里?这是非常微妙的(感觉翻译得很不微妙),但是请再看一下以下代码作为例子:

public class SampleActivity extends Activity {

  private final Handler mLeakyHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      // ...
    }
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Post a message and delay its execution for 10 minutes.
    mLeakyHandler.postDelayed(new Runnable() {
      @Override
      public void run() { /* ... */ }
    }, 1000 * 60 * 10);

    // Go back to the previous Activity.
    finish();
  }
}

当这个Activity被结束掉的时候,这个延迟的消息会在它被Looper处理掉之前继续在主线程的消息队列里面继续存活十分钟。而这个时候,消息对象持有着这个Activity的Handler一个引用,而这个Handler就对它的外部类(这里就是指SampleActivity)持有着一个隐式引用。这个引用会一直存在直到消息对象被处理掉,从而导致这个Activity的Context不能被GC进行垃圾回收并且泄露所有App的资源。记住,匿名Runnable类那一行也是如此。非静态的匿名内部类会对他们的外部类持有一个隐式引用,从而导致Contexxt会造成泄露。
要想解决这个问题,可以在一个新文件里面继承Handler或者使用一个静态的内部类进行替代。静态的内部类不会对它们的外部类持有一个隐式引用,所以Activity不会造成泄露。如果你需要在内部类里面调用外部类的方法,你可以让这个Handler对Activity持有一个弱引用,那么你就不会意外地泄露一个Context了。为了解决当我们初始化一个匿名Runnable类的时候发生的内存泄露,我们创建了这个类的静态成员变量(因为静态的内部类对象不会对他们的外部类持有引用):

public class SampleActivity extends Activity {

  /**
   * Instances of static inner classes do not hold an implicit
   * reference to their outer class.
   */
  private static class MyHandler extends Handler {
    private final WeakReference<SampleActivity> mActivity;

    public MyHandler(SampleActivity activity) {
      mActivity = new WeakReference<SampleActivity>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
      SampleActivity activity = mActivity.get();
      if (activity != null) {
        // ...
      }
    }
  }

  private final MyHandler mHandler = new MyHandler(this);

  /**
   * Instances of anonymous classes do not hold an implicit
   * reference to their outer class when they are "static".
   */
  private static final Runnable sRunnable = new Runnable() {
      @Override
      public void run() { /* ... */ }
  };

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Post a message and delay its execution for 10 minutes.
    mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

    // Go back to the previous Activity.
    finish();
  }
}

静态和非静态的内部类的区别是很微妙的(我还是感觉不出来哪里微妙了),但这是每一个安卓开发者都应该理解的一些东西。那么重点是什么呢?(还以为要翻译成底线哩) 那就是,在一个Activity里面如果内部类对象可以在Activity生命周期外存在,那么就应该避免使用非静态的内部类。相反,更加推荐使用一个静态内部类并且在其内部对Activity保持一个弱引用。

<think>好的,我现在要回答用户关于Ansible的handlers是否能监控文件内部变化的问题。首先,我需要回忆一下Ansible的基本知识,特别是handlers的工作原理。 用户之前问过关于Apache的MPM配置,现在转向Ansible,可能是在自动化部署或配置管理方面遇到了问题。用户想知道handlers是否能够检测到文件内容的改变,而不仅仅是文件的存在与否或元数据的变化。 首先,我得明确Ansible中的handlers是什么。Handlers是特殊的任务,只在被其他任务通知(notify)时触发,通常用于在配置更改后重启服务。例如,修改了配置文件后,重启服务。 接下来,关键问题是,当使用Ansible的copy或template模块更新文件时,如果文件内容发生了变化,handler会被触发。但如果文件内容没有变化,即使文件被触摸(touch)或权限改变,默认情况下handler不会触发。这里需要确认,是否handlers监控的是内容变化还是其他属性。 比如,假设有一个任务使用template模块生成配置文件,如果模板渲染后的内容与目标文件不同,任务会标记为changed,从而触发handler。如果内容相同,则任务状态是ok,不会触发。 但是,用户问的是“监控到文件内部是否改变”,也就是是否handlers能感知到文件内容的变化。根据我的理解,Ansible的模块(如copy、template)本身负责检测文件内容是否有变化,并据此决定任务的状态是否为changed。然后,如果任务状态是changed,且该任务notify了handler,那么handler才会执行。因此,handler是否被触发,取决于任务是否报告了changed状态,而任务是否changed则由模块判断,通常基于内容差异。 但可能存在特殊情况,比如使用command模块直接修改文件内容,这时候Ansible无法自动检测内容变化,除非显式注册变量并检查文件内容的变化,然后手动触发handler。这种情况下,handlers不会自动监控文件内部变化,需要用户自己处理。 所以,总结来说,当使用Ansible内置的文件处理模块(如copy、template)时,handlers可以间接监控到文件内容的改变,因为这些模块会自动检测内容变化并标记任务状态。但对于其他方式修改文件的情况,handlers无法自动检测,需要额外步骤。 可能需要进一步验证这个结论,查看Ansible文档中关于handlers模块如何处理changed状态的部分。例如,template模块的文档会说明它是基于内容的校验来决定是否changed。而如果用户使用shell模块直接修改文件内容,Ansible无法知道内容是否改变,因此任务状态不会自动标记为changed,也就不会触发handler。 另外,用户可能需要知道如何强制触发handler,即使内容没有变化。例如,通过使用meta: flush_handlers或者修改任务的参数,如force_handlers,但这些属于高级用法。 最后,应该给出一个明确的答案,并结合实例说明,确保用户理解不同情况下的行为差异,以及如何正确使用handlers来响应文件内容的变化。</think>### **Ansible Handlers 监控文件内部变化的机制** 在 Ansible 中,**Handlers** 是一种特殊任务,用于在任务状态为 `changed` 时触发后续操作(如重启服务)。**Handlers 本身不直接监控文件内容变化**,而是依赖 Ansible 模块(如 `copy`、`template`)检测文件变化,再通过 `notify` 通知 Handler 执行。 --- ### **核心原理** 1. **模块的职责**: - 文件操作模块(如 `copy`、`template`)会**自动检测目标文件的内容是否变化**。 - 若内容变化 → 任务状态标记为 `changed` → 触发 `notify` 关联的 Handler。 - 若内容未变化 → 任务状态为 `ok` → **不触发 Handler**。 2. **Handler 的触发逻辑**: Handler 的执行取决于任务是否报告 `changed` 状态,而非文件属性(如权限、时间戳)或外部修改。 --- ### **示例场景** #### **情况1:使用 `template` 模块生成配置文件** ```yaml - name: 生成 Nginx 配置文件 ansible.builtin.template: src: nginx.conf.j2 dest: /etc/nginx/nginx.conf notify: 重启 Nginx # 当且仅当文件内容变化时触发 ``` - **结果**: - 若模板渲染后的内容与目标文件不同 → 触发 `重启 Nginx` Handler。 - 若内容相同 → 不触发 Handler。 #### **情况2:使用 `command` 直接修改文件** ```yaml - name: 手动修改配置文件 ansible.builtin.command: "sed -i 's/old/new/' /etc/nginx/nginx.conf" notify: 重启 Nginx ``` - **结果**: - `command` 模块**无法检测文件内容变化**,任务状态始终为 `changed` → **每次都会触发 Handler**(即使内容未实际改变)。 - 需配合 `register` + `changed_when` 手动判断内容变化。 --- ### **如何强制监控文件内容变化?** #### **方法1:通过 `stat` 模块 + 条件判断** ```yaml - name: 检查文件内容变化 ansible.builtin.stat: path: /etc/nginx/nginx.conf register: file_stat - name: 修改文件内容 ansible.builtin.shell: "echo 'new content' > /etc/nginx/nginx.conf" when: file_stat.content | b64decode != "new content" notify: 重启 Nginx ``` - **逻辑**: 通过 `stat` 模块获取文件内容哈希或明文,手动判断是否变化,再决定是否触发 Handler。 #### **方法2:使用 `lineinfile` 或 `blockinfile` 模块** ```yaml - name: 确保某行存在 ansible.builtin.lineinfile: path: /etc/nginx/nginx.conf line: "key=value" notify: 重启 Nginx ``` - **优势**: 这些模块能精确检测文件内容变化(如行级修改),仅在实际变化时标记 `changed`。 --- ### **注意事项** 1. **Handler 的幂等性**: Handler 应设计为可重复执行且安全(如 `systemctl restart nginx` 是幂等的)。 2. **Handler 的执行时机**: Handler 默认在所有任务执行完毕后触发。若需立即触发,可使用 `meta: flush_handlers`。 3. **外部修改的盲区**: 若文件被外部进程(非 Ansible)修改,Ansible **无法感知**,需结合其他工具(如 `checksum` 校验)同步状态。 --- ### **总结** | 操作方式 | 是否触发 Handler(基于内容变化) | 原因 | |-------------------------|----------------------------------|--------------------------------------| | `template`/`copy` 模块 | ✔️ | 模块自动检测内容差异 | | `command`/`shell` 模块 | ❌(需手动判断) | 无法直接检测内容变化 | | `lineinfile`/`blockinfile` | ✔️ | 模块支持行级内容检测 | **结论**: Ansible 的 Handlers 可以**间接监控文件内容变化**,但需依赖模块的自动检测能力。对于非文件操作模块,需手动实现内容变化的判断逻辑。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值