【Android -- 性能优化】内存优化

不断学习,做更好的自己!💪

视频号CSDN简书
欢迎打开微信,关注我的视频号:程序员朵朵点我点我

一、前言

内存优化可以说是性能优化中最重要的优化点之一,可以说,如果你没有掌握系统的内存优化方案,就不能说你对 Android 的性能优化有过多的研究与探索。

内存泄漏是一个缓慢积累的过程,一点一点的给你,温水煮青蛙一般,我们往往很难直观的看到,只能最后内存不够用了,程序奔溃了,才知道里面有大量的泄漏,但是到底是那些地方?估计是狼烟遍地,千疮百孔,都不知道如何下手。怎么办?最让人难受的是内存泄漏情况那么多,记不住,理解也不容易,关键是老会忘记。怎么办呢?老这么下去也不是事,总不能面试的时候突击,做项目的时候不知所措吧。所以一定要记住了解GC原理,这样才可以更准确的理解内存泄漏的场景和原因。不懂GC原理的可以先看一下这个JVM初探:内存分配、GC原理与垃圾收集器

本来GC的诞生是为了让java程序员更加轻松(这一点隔壁C++痛苦的一匹),java虚拟机会自动帮助我们回收那些不再需要的内存空间。通过引用计数法,可达性分析法等等方法,确认该对象是否没有引用,是否可以被回收。

有人会说真么强悍的功能看起来无懈可击啊,对,理论上可以达到消除内存泄漏,但是很多人不按常理出牌啊,往往很多时候,有的对象还保持着引用,但逻辑上已经不会再用到。就是这一类对象,游走于GC法律的边缘,我没用了,但是你又不知道我没用了,就是这么赖着不走,空耗内存。

因为有内存泄漏,所以内存被占用越来越多,那么GC会更容易被触发,GC会越来越频发,但是当GC的时候所有的线程都是暂停状态的,需要处理的对象数量越多耗时越长,所以这也会造成卡顿。
在这里插入图片描述
那么什么情况下会出现这样的对象呢? 基本可以分为以下四大类:
1. 集合类泄漏
集合类添加元素后,仍引用着集合元素对象,导致该集合中的元素对象无法被回收,从而导致内存泄露。

栗子:

static List<Object> mList = new ArrayList<>();
   for (int i = 0; i < 100; i++) {
       Object obj = new Object();
      mList.add(obj);
       obj = null;
    }

解决方案:
当mList没用的时候,我们如果不做处理的话,这就是典型的占着茅坑不拉屎,mList内部持有者众多集合元素的对象,不泄露天理难容啊。解决这个问题也超级简单。把mList清理掉,然后把它的引用也给释放掉。

mList.clear();
mList = null;

2. 单例/静态变量造成的内存泄漏
单例模式具有其 静态特性,它的生命周期 等于应用程序的生命周期,正是因为这一点,往往很容易造成内存泄漏。

栗子:

public class SingleInstance {

   private static SingleInstance mInstance;
   private Context mContext;

   private SingleInstance(Context context){
       this.mContext = context;
   }

   public static SingleInstance newInstance(Context context){
       if(mInstance == null){
           mInstance = new SingleInstance(context);
       }
       return sInstance;
   }
}

当我们在Activity里面使用这个的时候,把我们Acitivty的context传进去,那么,这个单例就持有这个Activity的引用,当这个Activity没有用了,需要销毁的时候,因为这个单例还持有Activity的引用,所以无法GC回收,所以就出现了内存泄漏,也就是生命周期长的持有了生命周期短的引用,造成了内存泄漏。

所以我们要做的就是生命周期长的和生命周期长的玩,短的和短的玩。就好比你去商场,本来就是传个话的,话说完就要走了,突然保安过来非要拉着你的手,说要和你天长地久。只要商场在一天,他就要陪你一天。天呢?太可怕了。叔叔我们不约,我有我的小伙伴,我还要上学呢,你赶紧找你的保洁阿姨去吧。你在商场的生命周期本来可能就是1分钟,而保安的生命周期那是要和商场开关门一致的,所以不同生命周期的最好别一起玩的好。

解决方案:

public class SingleInstance {

    private static SingleInstance mInstance;
    private Context mContext;

    private SingleInstance(Context context){
        this.mContext = context.getApplicationContext();
    }

    public static SingleInstance newInstance(Context context){
        if(mInstance == null){
            mInstance = new SingleInstance(context);
        }
        return sInstance;
    }
}

还有一个常用的地方就是 Toast。你应该知道和谁玩了吧。

3. 匿名内部类/非静态内部类
这里有一张宝图:
在这里插入图片描述
非静态内部类他会持有他外部类的引用,从图我们可以看到非静态内部类的生命周期可能比外部类更长,这就是二楼的情况一致了,如果非静态内部类的周明周期长于外部类,在加上自动持有外部类的强引用,我的乖乖,想不泄漏都难啊。

栗子:

public class TestActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        new MyAscnyTask().execute();
    }

    class MyAscnyTask extends AsyncTask<Void, Integer, String>{
        @Override
        protected String doInBackground(Void... params) {
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "";
        }
    }
}

我们经常会用这个方法去异步加载,然后更新数据。貌似很平常,我们开始学这个的时候就是这么写的,没发现有问题啊,但是你这么想一想,MyAscnyTask 是一个非静态内部类,如果他处理数据的时间很长,极端点我们用 sleep 100 秒,在这期间 Activity 可能早就关闭了,本来 Activity 的内存应该被回收的,但是我们知道非静态内部类会持有外部类的引用,所以 Activity 也需要陪着非静态内部类MyAscnyTask 一起天荒地老。好了,内存泄漏就形成了。

解决方案:

既然 MyAscnyTask 的生命周期可能比较长,那就把它变成静态,和 Application 玩去吧,这样 MyAscnyTask 就不会再持有外部类的引用了。两者也相互独立了。

public class TestActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        new MyAscnyTask().execute();
    }
//改了这里 注意一下 static
   static  class MyAscnyTask extends AsyncTask<Void, Integer, String>{
        @Override
        protected String doInBackground(Void... params) {
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "";
        }
    }
}

说完非静态内部类,我再来看看匿名内部类,这个问题很常见,匿名内部类和非静态内部类有一个共同的地方,就是会只有外部类的强引用,所以这哥俩本质是一样的。但是处理方法有些不一样。但是思路绝对一样。换汤不换药。

栗子:

public class TestActivity extends Activity {
private TextView mText;
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
//do something
mText.setText(" do someThing");
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
mText = findVIewById(R.id.mText);
        //  匿名线程持有 Activity 的引用,进行耗时操作
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

    
        mHandler. sendEmptyMessageDelayed(0, 100000);
    }


想必这两个方法是我们经常用的吧,很熟悉,也是这么学的,没感觉不对啊,老师就是这么教的,通过我们上面的分析,还这么想吗?关键是 耗时时间过长,造成内部类的生命周期大于外部类,对弈非静态内部类,我们可以静态化,至于匿名内部类怎么办呢?一样把它变成静态内部类,也就是说尽量不要用匿名内部类。完事了吗?很多人不注意这么一件事,如果我们在 handleMessage 方法里进行UI的更新,这个 Handler 静态化了和 Activity 没啥关系了,但是比如这个mText,怎么说?全写是 activity.mText,看到了吧,持有了 Activity 的引用,也就是说Handler费劲心思变成静态类,自认为不持有 Activity 的引用了,准确的说是不自动持有 Activity 的引用了,但是我们要做UI更新的时候势必会持有 Activity 的引用,静态类持有非静态类的引用,我们发现怎么又开始内存泄漏了呢?处处是坑啊,怎么办呢?我们这里就要引出弱引用的概念了。

引用分为强引用,软引用,弱引用,虚引用,强度一次递减。

  • 强引用
    我们平时不做特殊处理的一般都是强引用,如果一个对象具有强引用,GC 宁可 OOM 也绝不会回收它。看出多强硬了吧。
  • 软引用(SoftReference)
    如果内存空间足够,GC 就不会回收它,如果内存空间不足了,就会回收这些对象的内存。
  • 弱引用(WeakReference)
    弱引用要比软引用,更弱一个级别,内存不够要回收他,GC 的时候不管内存够不够也要回收他,简直是弱的一匹。不过 GC 是一个优先级很低的线程,也不是太频繁进行,所以弱引用的生活还过得去,没那么提心吊胆。
  • 虚引用
    用的甚少,我没有用过,如果想了解的朋友,可以自行谷歌百度。

所以我们用弱引用来修饰 Activity,这样 GC 的时候,该回收的也就回收了,不会再有内存泄漏了。很完美。

public class TestActivity extends Activity {
    private TextView mText;
    private MyHandler myHandler = new MyHandler(TestActivity.this);
    private MyThread myThread = new MyThread();

    private static class MyHandler extends Handler {

        WeakReference<TestActivity> weakReference;

        MyHandler(TestActivity testActivity) {
            this.weakReference = new WeakReference<TestActivity>(testActivity);

        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            weakReference.get().mText.setText("do someThing");

        }
    }

    private static class MyThread extends Thread {

        @Override
        public void run() {
            super.run();

            try {
                sleep(100000);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        mText = findViewById(R.id.mText);
        myHandler.sendEmptyMessageDelayed(0, 100000);
        myThread.start();
    }
//最后清空这些回调 
    @Override
    protected void onDestroy() {
        super.onDestroy();
        myHandler.removeCallbacksAndMessages(null);
    }

4. 资源未关闭造成的内存泄漏

  • 网络、文件等流忘记关闭
  • 手动注册广播时,退出时忘记 unregisterReceiver()
  • Service 执行完后忘记 stopSelf()
  • EventBus 等观察者模式的框架忘记手动解除注册
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kevin-Dev

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值