三星[4.4,5.0]使用EditText导致内存泄露

本文介绍了在三星4.4至5.0系统中,由于ClipboardUIManager.java导致的EditText内存泄露问题。问题源自ClipboardUIManager的单例持有Activity的Context,而非Application的Context。解决方案是确保在页面启动时,如onCreate或onResume,使用Application的Context初始化ClipboardUIManager。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 问题描述

  今天线上监控发现一个页面泄露的问题,GC ROOT如下:

* GC ROOT android.os.HandlerThread.mLooper
* references android.os.Looper.sMainLooper
* references android.os.Looper.mQueue
* references android.os.MessageQueue.mMessages
* references android.os.Message.sPool
* references android.os.Message.target
* references android.widget.Editor$Blink.this$0
* references android.widget.Editor.mTextView
* references android.widget.EditText.mClipboardExManager
* references android.sec.clipboard.ClipboardExManager.mClipboardUIManager
* references android.sec.clipboard.ClipboardUIManager.mContext
* leaks xxxxxxx.activity.XxxxInputActivity instance

  看包名都是系统的类,但是找了下系统源码,在4.0.4, 4.4.4, 5.1.1, 6.0.1里都没有出现过ClipboardUIManager.java这个类,所以应该是产商定制的类。到google里搜一下android.sec.clipboard.ClipboardUIManager.mContext,有不少相同的问题,都是出现在三星手机上的,具体问题讨论过程看这里

  对了一下出问题的手机型号和系统:SM-N9008S、SM-N9008、SM-A7000、SM-G9008V、SM-N9006,使用的官方ROM的确都是4.4.2、5.0版本,下面详细分析这个问题。

2. 问题分析:

  其实这种类型的堆栈是比较常见的,第一眼看上去就是Looper的队列里有未被消费的消息,这个消息持有了一个内部类对象(类似Editor$Blick.this$0这种类名),那这个消息对象是怎么产生的呢?

  看一下Editor.java的内部类Blink,它是设置Editor光标闪烁的消息类,每隔BINK=500ms的间隔就会向主线程消息队列post一次自己,切换下一次绘制光标对应的Drawable对象(其实就是取Drawable[] mCursorDrawable里的数组成员,这个数组长度是2),关键代码如下,比较短直接贴出来:android.widget.Editor$Blink.class

public class Editor {
    ...

    private class Blink extends Handler implements Runnable {
        private boolean mCancelled;

        public void run() {
            if (mCancelled) {
                return;
            }

            removeCallbacks(Blink.this);    // 先从队列删除未处理的消息

            if (shouldBlink()) {
                // 绘制当前这个周期光标对应的Drawable
                if (mTextView.getLayout() != null) {
                    mTextView.invalidateCursorPath();
                }

                // 发送下一次的消息,延时BLINK=500ms
                postAtTime(this, SystemClock.uptimeMillis() + BLINK);
            }
        }

        void cancel() {
            if (!mCancelled) {
                removeCallbacks(Blink.this);
                mCancelled = true;
            }
        }

        void uncancel() {
            mCancelled = false;
        }
    }
    ...
}

  那第一个消息是在哪里发的?退出时最后一个消息又在哪里删除的呢?看下面的代码:

public class Editor {
    ...

    void onAttachedToWindow() {
        ...
        resumeBlink();
    }

    private void resumeBlink() {
        if (mBlink != null) {
            mBlink.uncancel();
            makeBlink();
        }
    }

    void makeBlink() {
        if (shouldBlink()) {
            mShowCursor = SystemClock.uptimeMillis();
            if (mBlink == null) mBlink = new Blink();
            mBlink.removeCallbacks(mBlink);
            mBlink.postAtTime(mBlink, mShowCursor + BLINK);   // 第一个Blink消息
        } else {
            if (mBlink != null) mBlink.removeCallbacks(mBlink);
        }
    }
    ...

    void onDetachedFromWindow() {
        ...
        suspendBlink();
        ...
    }

    private void suspendBlink() {
        if (mBlink != null) {
            mBlink.cancel();
        }
    }

  可以清楚得看到,第一个闪烁事件是在View对象的onAttachedToWindow生命周期事件里发出的,最后一个消息是在对应的onDetachedFromWindow生命周期里删除的。

  分析到这里,事件的起因经过已经很清楚了:EditText在onAttachedToWindow的时候向主线程发送了一个要延迟500ms处理的消息,之后就连续按BLINK周期发送,除非onDetachedFromWindow被调用才会停止,但是因为内存泄露的原因,这个生命周期走不到,所以主线程还是会不停的接受BLINK消息对象,所以看起来是因为主线程消息队列引用造成的,而实际上却是由 android.sec.clipboard.ClipboardUIManager.java 这个单例类引起的!!

  public static ClipboardUIManager getInstance(Context context) {
    if (sInstance == null)
      sInstance = new ClipboardUIManager(context);
    return sInstance;
  }

  我们看一下ClipboardUIManager这个单例类的使用,入参context被 ClipboardUIManager.mContext 成员持有,所以泄露是必然,而这个 mContext 存在的目的只是为了调用getSystemService(Context.CLIPBOARD_SERVICE)方法,在这种需求下应该把Activity的Context转换为Application的Context,显然这是SAMSUNG系统的Bug。

  为了验证这个这个想法,找了一台三星5.0系统的手机,把ClipboardUIManager对象打印出来,果然里面的mContext成员持有了我的Activity对象,所以真相大白了。

  最后再啰嗦一句,因为Blink不是静态内部类,所以android.widget.Editor$Blink.this$0是Javac编译器为这个内部类自动生成的成员变量,通过构造方法的第一个参数传进来,用于引用外部类对象。

3. 解决方案

  问题梳理清楚,剩下的就是解决方案了。唯一可行的办法是创建这个单例的时候从外面传入的Context参数就是Application的Context,可以用Activity.getApplicationContext()获取,最关键的是getInstance这个操作必须在系统调用之前由我们来触发一次,不然就没机会补救了。

  View的onAttachedToWindow发生在Activity的onResume生命周期之后,所以一个页面如果包含EditText的话,ClipboardUIManage.getInstance(Context)的调用必须发生这个页面的onCreate、onStart或者onResume里。站在全局的角度看,最好放在Application的onCreate里处理掉,这样应用内所有页面就会都很安全,下面上最终的代码:

    try {
        if ("samsung".equalsIgnoreCase(Build.MANUFACTURER) &&
                Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT &&
                Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP) {
            Class cls = Class.forName("android.sec.clipboard.ClipboardUIManager");
            Method m = cls.getDeclaredMethod("getInstance", Context.class);
            m.setAccessible(true);
            Object o = m.invoke(null, getApplicationContext());
            Log.i(TAG, o.toString());
        }
    } catch (Throwable e) {
        e.printStackTrace();
    }

  这里增加了准确的系统和版本判断,一方面避免其他正常的手机抛无谓的异常,另一方面解决问题就应该是针对性地处理,有错再继续改,不能怕出错就不清不楚地全部覆盖上,后面的人不知道怎么回事又不敢删。

  在第一部分的问题描述里说明了,出问题手机使用的官方ROM对应的Android系统版本是4.4.2、5.0,同时 Square 的 leakcanary 也收集了一些已知系统的泄露bug,在AndroidExcludedRefs.java这个文件里,其中就包含SAMSUNG的这个系统泄露,SDK_INT >= KITKAT(19) && SDK_INT <= LOLLIPOP(21),对应的系统版本正好是4.4、4.4.4、5.0

  CLIPBOARD_UI_MANAGER__SINSTANCE(
      SAMSUNG.equals(MANUFACTURER) && SDK_INT >= KITKAT && SDK_INT <= LOLLIPOP) {
    @Override void add(ExcludedRefs.Builder excluded) {
      excluded.instanceField("android.sec.clipboard.ClipboardUIManager", "mContext")
          .reason("ClipboardUIManager is a static singleton that leaks an activity context."
              + " Fix: trigger a call to ClipboardUIManager.getInstance() in Application.onCreate()"
              + " , so that the ClipboardUIManager instance gets cached with a reference to the"
              + " application context. Example: https://gist.github.com/cypressious/"
              + "91c4fb1455470d803a602838dfcd5774");
    }
  },

  最后附上 Android版本、源码和API Level对应关系表

Code nameVersionAPI level
Marshmallow6.0API level 23
Lollipop MR15.1API level 22
Lollipop5.0API level 21
KitKat4.4 - 4.4.4API level 19
Jelly Bean4.3.xAPI level 18
Jelly Bean4.2.xAPI level 17
Jelly Bean4.1.xAPI level 16
Ice Cream Sandwich4.0.3 - 4.0.4API level 15, NDK 8
Ice Cream Sandwich4.0.1 - 4.0.2API level 14, NDK 7
Honeycomb3.2.xAPI level 13
Honeycomb3.1API level 12, NDK 6
Honeycomb3.0API level 11
Gingerbread2.3.3 - 2.3.7API level 10
Gingerbread2.3 - 2.3.2API level 9, NDK 5
Froyo2.2.xAPI level 8, NDK 4
Eclair2.1API level 7, NDK 3
Eclair2.0.1API level 6
Eclair2.0API level 5
Donut1.6API level 4, NDK 2
Cupcake1.5API level 3, NDK 1
(no code name)1.1API level 2
(no code name)1.0API level 1

END

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值