性能优化(2.4)-内存泄漏的一些坑

主目录见:Android高级进阶知识(这是总目录索引)
[written by 无心追求]

Activity内部类泄漏

  • Activity如果存在内部类,无论是匿名内部类,或者是声明的内部类,都有可能造成Activity内存泄漏,因为内部类默认是直接持有这个activity的引用,如果内部类的生命周期比activity的生命周期要长,那么在activity销毁的时候内部类仍然存在并且持有activity的引用,那么activity自然无法被gc,造成内存泄漏

Activity内部Handler

class MyHandler extends Handler {
        
        MyHandler() {
            
        }

        @Override
        public void handleMessage(Message msg) {
            // to do your job
        }
    }
MyHandler myHandler = new MyHandler();

如上,在Activity内部如果声明一个这样的Handler,那么myHandler就默认持有Activity引用,假设Activity退出了,但是可能这时候才有myHandler的任务post,那么Activity是无法被回收的,可以采用以下方式解决:

static class MyHandler extends Handler {
        WeakReference<Activity> mActivityReference;

        MyHandler(Activity activity) {
            mActivityReference = new WeakReference<Activity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            final Activity activity = mActivityReference.get();
            if (activity != null) {
                if (msg.what == 1 && isJumpToHomePage) {
                    Intent intent = new Intent(activity, HomePageActivity.class);
//                    intent.putExtra("themeType", themeType);
//                    LogUtil.d("themeType == " + themeType);
                    activity.startActivity(intent);
                    activity.finish();
                }
            }
        }
    }

这里面是把MyHandler是一个内部静态类,静态类在java虚拟机加载的时候就是独立加载到内存中的,不会依赖于任何其他类,而且这里面是把activity以弱引用的方式传到MyHandler中,即便是静态MyHandler类对象一直存在,但是由于它持有的是activity弱引用,在gc回收的时候activity对象是可以被回收的,另外注意一点,对于Handler的使用如果有sendEmptyMessageDelayed()来延迟任务执行的话最好在Activity的onDestroy里面把Handler的任务都移除(removeCallbacks(null)),activity在退出后,就是应该在onDestroy方法里面把一些任务取消掉,做一些清理的操作

Activity内部线程

  • 在Activity里面有时候为了实现异步操作会单独开一个线程来执行任务,或者是异步的网络请求也是单独开线程来执行的,那么就会存在一个问题,如果内部线程的生命周期比Activity的生命周期要长,那么内部线程任然默认持有Activity的引用,导致Activity对象无法被回收,但是当这个线程执行完了之后,Activity对象就能被成功的回收了,这会造成一个崩溃风险,可能在线程里面有调用到一些Activity的内部对象,但是在Activity退出后这些对象有可能有些已经被回收了,就变成null了,这时候要是不进行null的判断就会报空指针异常,如果这个线程是一直跑的,那就会造成Activity对象一直不会被回收了,因此,在activity退出后一定要做相关的清理操作,中断线程,取消网络请求等等

Activity内部类回调监听

  • 在编码中常常会定义各种接口回调,类似有点击时间监听OnClickListener,这些回调监听有时候就定义在Activity内部,或者直接用Activity对象去实现这个接口,到时候设置监听的时候直接调用setListener(innerListener)或者setListener(this),innerListener是Activity内部定义的,this就是Activity对象,那么问题来了,回调监听并不一定马上返回,只有在触发条件满足的时候才会回调,这个时间是无法确定的,因此在Activity退出的时候应该显示的把回调监听都移除掉setListener(null),既释放了回调监听对象占用的内存,也避免回调监听继续持有activity引用;对与内部类还有一种解决方式,和内部Handler相似,定义成static内部类,然后把Activity对象的弱引用传递进去,这样也就万无一失,举个项目中遇到的实际场景:
private static class RecorderTimeListener implements TimeCallback {

        WeakReference<ChatActivity> target;

        RecorderTimeListener(ChatActivity activity) {
            target = new WeakReference<>(activity);
        }

        @Override
        public void onCountDown(final int time) {
            if (target == null || target.get() == null) {
                return;
            }
            final ChatActivity activity = target.get();
            activity.runOnUiThread(new Runnable() {

                @Override
                public void run() {
                    activity.volumeView.setResetTime(time);
                }
            });
        }

        @Override
        public void onMaxTime() {
            if (target == null || target.get() == null) {
                return;
            }
            final ChatActivity activity = target.get();
            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    activity.isMaxTime = true;
                    activity.stopRecord();
                }
            });
        }
    }

private class StartRecorderListener implements StartCallback {


        @Override
        public boolean onWait() {
            cancelRecord();
            return true;
        }

        @Override
        public void onStarted() {
            if (playerManager.isPlaying()) {
                playerManager.stop();
            }
            recordWaveView.setVisibility(View.VISIBLE);
            animation = (AnimationDrawable) recordWaveView.getBackground();
            animation.start();

            volumeView.showMoveCancelView();
            volumeDialog.show();

            viewHandler.postDelayed(volumeRunnable, 100);
        }

        @Override
        public void onFailed(int errorCode) {
            if (errorCode == RecorderManager.ERROR_START_FAIL) {
                showHintDialog(R.string.chat_permission_dialog_title, R.string.chat_permission_dialog_message);
            }
        }
    }

private void startRecord() {
        SystemDateUtil.init(this);
        LogUtil.i(ChatKey.TAG_INFO, "--------------------------录音开始--------------------------");
        final long startSendTime = SystemDateUtil.getCurrentDate().getTime();
        sliceSender = dialogMsgService.createSliceSender(
                AccountUtil.getCurrentFamilyChatDialogId(),
                AccountUtil.getCurrentImAccountId(), new DialogMsgService.OnSendVoiceMsgListener() {
                    @Override
                    public void onSuccess() {
                        LogUtil.d(TAG, "录音上传成功");
                        sendBigData(sliceSender.getGroupId(), ChatMsgBeh.MsgType.EMOJI,
                                SystemDateUtil.getCurrentDate().getTime() - startSendTime, SendMsgEvent.CODE_SEND_SUCCESS);
                    }

                    @Override
                    public void onFailure() {
                        sendBigData(sliceSender.getGroupId(), ChatMsgBeh.MsgType.EMOJI,
                                SystemDateUtil.getCurrentDate().getTime() - startSendTime, SendMsgEvent.CODE_SEND_FAILURE);
                        LogUtil.d(TAG, "录音上传失败");
                    }
                });
        RecorderManager.getInstance(this).startRecorder(sliceSender, new StartRecorderListener(), new RecorderTimeListener(this));
        LogUtil.i(ChatKey.TAG_INFO, "groupId:" + sliceSender.getGroupId());
    }

如上StartRecorderListener是内部类,RecorderTimeListener是静态内部类并传入Activity弱引用,如果把StartRecorderListener的实现改成RecorderTimeListener的实现,那么Activity内存泄漏就不存在了

动画导致内存泄漏

  • 进入Activity界面后如果有一些和控件绑定在一起的属性动画在运行,退出的时候要记得cancel掉这些动画
自定义控件ImageButton中:
public void start(float startAngle, float endAngle) {
        setStop(false);

        final AnimatorSet as = new AnimatorSet();
        final ObjectAnimator oa = ObjectAnimator.ofFloat(this, "progress",
                startAngle, endAngle);
        oa.setDuration(duration);
        oa.setInterpolator(new DecelerateInterpolator(1.1f));
        oa.setRepeatCount(count);
//      oa.setRepeatMode(ObjectAnimator.INFINITE);
        oa.addUpdateListener(new AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animator) {
                if (stop && as.isRunning()) {
                    as.cancel();
//                    oa.removeAllListeners();
                } else {
                    float p = (float) animator.getAnimatedValue();
                    setProgress(p);
                }
            }
        });
        as.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
            }
        });
        as.play(oa);
        as.start();
    }
    
    public void cancel() {
        setStop(true);
    }

    public void setStop(boolean stop) {
        this.stop = stop;
        if (stop) {
            setProgress(0.0f);
        }
    }

如上如果不cancel掉属性动画就会一直运行并且一直去执行控件的onDraw方法,那么ImageButton持有了Activity对象,而属性动画ObjectAnimator持有了ImageButton,ObjectAnimator一直在运行,那么Activity对象也就不能被释放了

  • 属性动画的对象尽量不要用static修饰,static修饰和,这个对象一旦被创建那么就一直存在了,属性动画一旦start之后,那么就一直运行,这时候就算退出activity的时候cancel掉动画也仍然会持有activity引用,就像下面这个例子:
private static ValueAnimator valueAnimator;

private void startValueAnimator() {
        int displayTime2Show = displayTime - 1;
        if (displayTime2Show > 1) {
            valueAnimator = ValueAnimator.ofInt(displayTime2Show, 1);
            valueAnimator.setDuration(displayTime2Show * 1000);
            valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    tvStartPageTime.setText(animation.getAnimatedValue().toString());
                }
            });
            valueAnimator.start();
        }

    }
protected void onPause() {
        if (valueAnimator != null && valueAnimator.isRunning()) {
            valueAnimator.cancel();
            valueAnimator = null;
        }
        super.onPause();
    }

即便是在activity退出后cancel掉动画,activity依然无法被释放,为什么?因为valueAnimator是静态的,而且添加了动画属性改变的监听addUpdateListener,在监听回调里面有tvStartPageTime(TextView)控件,默认持有Activity对象,因此即便Activity退出,动画cancel掉也无法释放持有的引用,修改方法有两种,一种是把valueAnimator的static修饰去掉,另一中国是:

protected void onPause() {
valueAnimator.removeAllUpdateListeners();
        if (valueAnimator != null && valueAnimator.isRunning()) {
            valueAnimator.cancel();
            valueAnimator = null;
        }
        super.onPause();
    }

加一句监听器的移除代码removeAllUpdateListeners()

传Context参数的时候使用Activity对象造成内存泄漏

  • 在android中常常会用到Context环境变量,Activity继承了Context,所以在传入Context的时候常常直接在Activity中传入this即Activity本对象,这是比较不好的习惯,在没有规定一定要传Activity对象的时候尽量采用全局的Context对象,即ApplicationContext来作为参数传递进去,因为ApplicationContext只要app在运行那么它就一直存在,因此即便有一个对象长期引用它,生命周期也不会比ApplicationContext长,所以不会造成ApplicationContext的内存泄漏,因为ApplicationContext只要App在运行就不允许被回收
  • 在Android程序中要慎用单例,如果单例需要传Context对象,那么就需要谨慎了因为在单例中如果把Context保存起来,那么这个单例一旦被创建,就一直存在了,如果传入的是Activity对象,那将一直持有Activity对象引用导致内存泄漏,解决版本是传入ApplicationContext对象,或者在Activity退出的时候销毁这个单例对象,单例在什么时候时候使用,如果一个对象并不会被频繁的调用,那就没必要用单例,对于可能会被频繁调用的对象方法可以采用单例,这样做可以避免反复创建对象和gc对象造成的内存抖动;对于需要保存的全局变量也可以用单例封装起来;单例只要创建了就一直有存在引用,所以是不会被gc的
  • 使用静态变量来保存Activity对象,这是一个非常不好的编码习惯,static修饰的代码片段,变量或者类是在app加载的时候就已经加载到内存中了,所以和单例有点相似,static变量也会一直持有Activity对象直到APP被杀死或者显示的把static变量置空

在Android5.0以上的WebView泄漏

  • 如果Activity引用了WebView控件来加载一个网页或者加载一个本地的网页,在退出activity之后即便你调用了webView.destroy()方法,也无法释放webview对于activity持有的引用,原因和解决方案可参考Android5.1的WebView内存泄漏,如这篇文章所分析的解决方案确实有效,亲测可用!

子线程中不当的使用Looper.prepare()和Looper.loop()方法造成内存泄漏

  • Looper.loop()是一个无限循环的方法,它是反复的去MessageQueue里面去取出Message并分发给对应的Handler去执行,如果在子线程中调用了Looper.prepare()和Looper.loop()方法,Looper.loop()会导致这个线程一直不死,一直堵在这里,因此线程就无法结束运行,在Looper.prepare()和Looper.loop()之间的所有对象都没办法被释放,解决方案就是在不用的时候及时的把Looper给quit掉

EditText使用setTransformationMethod导致的内存泄漏

  • 这个问题只有在4.0的android系统上才会存在,在5.0以上的系统已经不存在了,应该是属于Android的一个缺陷
    [图片上传失败...(image-73f0a7-1511154115320)]
    问题的根源应该就是这:
loginPasswdEt.setTransformationMethod(PasswordTransformationMethod.getInstance());
loginPasswdEt.setTransformationMethod(HideReturnsTransformationMethod.getInstance());

而PasswordTransformationMethod和HideReturnsTransformationMethod分别都是一个单例:

private static PasswordTransformationMethod sInstance;

private static HideReturnsTransformationMethod sInstance;
PasswordTransformationMethod

public CharSequence getTransformation(CharSequence source, View view) {
        if (source instanceof Spannable) {
            Spannable sp = (Spannable) source;

            /*
             * Remove any references to other views that may still be
             * attached.  This will happen when you flip the screen
             * while a password field is showing; there will still
             * be references to the old EditText in the text.
             */
            ViewReference[] vr = sp.getSpans(0, sp.length(),
                                             ViewReference.class);
            for (int i = 0; i < vr.length; i++) {
                sp.removeSpan(vr[i]);
            }

            removeVisibleSpans(sp);

            sp.setSpan(new ViewReference(view), 0, 0,
                       Spannable.SPAN_POINT_POINT);
        }

        return new PasswordCharSequence(source);
    }
    
private static class ViewReference extends WeakReference<View>
            implements NoCopySpan {
        public ViewReference(View v) {
            super(v);
        }
    }

上面是5.0系统的源码,里面已经用ViewReference来包装view设置到Spannable中了,所以是把view的弱引用传进去了,因此可以被gc回收,而在4.0android系统上,很可能就不是这么做的,所以4.0系统上面就是View对象被PasswordTransformationMethod和HideReturnsTransformationMethod单例长期持有,而View又持有Activity对象,所以针对4.0系统我们只需要释放这两个单例对象即可:

private void releaseMemoryLeak() {
        int sdk = Build.VERSION.SDK_INT;
        if (sdk >= Build.VERSION_CODES.LOLLIPOP) {
            return;
        }
        try {
            Field field1 = PasswordTransformationMethod.class.getDeclaredField("sInstance");
            if (field1 != null) {
                field1.setAccessible(true);
                field1.set(PasswordTransformationMethod.class, null);
            }
            Field field2 = HideReturnsTransformationMethod.class.getDeclaredField("sInstance");
            if (field2 != null) {
                field2.setAccessible(true);
                field2.set(HideReturnsTransformationMethod.class, null);
            }
        } catch (NoSuchFieldException e) {
            SyncLogUtil.e(e);
        } catch (IllegalAccessException e) {
            SyncLogUtil.e(e);
        }
    }

加上上述代码后验证发现内存不再泄漏,搞定。

控件的BackGround导致的内存泄漏(4.0android系统已经解决)

  • 有时候为了避免图片反复的加载,就把第一次加载后的Bitmap或者Drawable用静态变量保存起来,但是要是把这种静态修饰的图片对象设置成控件的背景,那就呵呵了
private static Drawable sBackground;
@Override
protected void onCreate(Bundle state) {
  super.onCreate(state);

  TextView label = new TextView(this);
  label.setText("Leaks are bad");

  if (sBackground == null) {
    sBackground = getDrawable(R.drawable.large_bitmap);
  }
  label.setBackgroundDrawable(sBackground);

  setContentView(label);
}

因为在View的setBackgroundDrawable方法里面有一句:

public void setBackgroundDrawable(Drawable background) {
......省略很多代码
background.setCallback(this);
mBackground = background;
}

Drawable对象把View对象作为回调保存起来了,不过在4.0系统以后引入回调来保存View对象了,所以已经不会造成内存泄漏问题了:

public final void setCallback(Callback cb) {
        mCallback = new WeakReference<Callback>(cb);
    }

这里依然要举例子出来是想说明不恰当的使用static来修饰变量很有可能导致对象无法被回收

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容