android学习总结——内存优化

一、为什么要进行内存优化

Android应用程序开发过程中,内存的准确控制是判断一个程序好坏的重要标准之一
若我们的应用程序内存控制不够好,出现内存泄露或者内存溢出,会导致:

  1. 程序卡顿,响应速度缓慢
  2. 开启其他程序的时候,内存泄漏的程序放在后台没有进行关闭,但是也可能会莫名其妙的消失(内存越大它在,在后台越有可能死掉,如果内存小可能在后台停留的时间越长)
  3. 更严重点,有时会直接崩溃

    所以,为了开发一个质量高的应用程序,保证程序正常运行,需要对内存进行优化。

二、 怎样进行内存优化

我们的内存优化,主要是针对内存泄露和内存溢出进行优化。

  • 内存泄露:对象在内存Heap堆中分配的空间,当不再使用或者没有引用指向的情况下,仍不能被GC正常回收的情况。大量的内存泄露会造成OOM

  • 内存溢出:即OutOfMemoery,指APP向系统申请超过最大阈值的内存请求,系统不会再分配多余的空间,就会造成OOM

2.1 针对内存泄露进行的内存优化

  • 静态变量引起的内存泄露
    静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束。一旦静态变量初始化后,它所持有的引用只有等到进程结束才会释放。static的合理 使用一般用来修饰基本数据类型或者轻量级对象,尽量避免修复集合或者大对象,常用作修饰全局配置项、工具类方法、内部类。

  • 单例模式引起的内存泄露

    public class AppSettings {

        private static AppSettings sInstance;
        private Context mContext;

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

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

像上面代码中这样的单例,如果我们在调用getInstance(Context context)方法的时候传入的context参数是Activity、Service等上下文,就会导致内存泄露。此时可以将传入的context改为application

  • 非静态内部类导致内存泄露

非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄露。

非静态内部类导致的内存泄露在Android开发中有一种典型的场景就是使用Handler。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        start();
    }

    private void start() {
        Message msg = Message.obtain();
        msg.what = 1;
        mHandler.sendMessage(msg);
    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == 1) {
                // 做相应逻辑
            }
        }
    };
}

在以上代码中,mHandler会作为成员变量保存在发送的消息msg中,即msg持有mHandler的引用,而mHandler是Activity的非静态内部类实例,即mHandler持有Activity的引用,那么我们就可以理解为msg间接持有Activity的引用。msg被发送后先放到消息队列MessageQueue中,然后等待Looper的轮询处理(MessageQueue和Looper都是与线程相关联的,MessageQueue是Looper引用的成员变量,而Looper是保存在ThreadLocal中的)。那么当Activity退出后,msg可能仍然存在于消息对列MessageQueue中未处理或者正在处理,那么这样就会导致Activity无法被回收,以致发生Activity的内存泄露。

通常在Android开发中如果要使用内部类,但又要规避内存泄露,一般都会采用静态内部类+弱引用的方式

public class MainActivity extends AppCompatActivity {

    private Handler mHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mHandler = new MyHandler(this);
        start();
    }

    private void start() {
        Message msg = Message.obtain();
        msg.what = 1;
        mHandler.sendMessage(msg);
    }

    private static class MyHandler extends Handler {

        private WeakReference<MainActivity> activityWeakReference;

        public MyHandler(MainActivity activity) {
            activityWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = activityWeakReference.get();
            if (activity != null) {
                if (msg.what == 1) {
                    // 做相应逻辑
                }
            }
        }
    }
}

非静态内部类造成内存泄露还有一种情况就是使用Thread或者AsyncTask,跟handler类似。

补充:Java四种引用方式

  • 强引用:Java里最广泛使用的一种,也是对象默认的引用类型。如果一个对象具有强引用,那么垃圾回收器是不会对它进行回收操作的,当内存空间不足时,Java虚拟机将会抛出OutOfMemoryError错误,这时应用将会终止运行
  • 软引用:一个对象如果只有软引用,那么当内存空间充足时,垃圾回收器不会对它进行回收操作,只有当内存不足时,这个对象才会被回收。软引用可以用来实现内存敏感的告诉缓存。如果配合引用队列使用,当软引用指向的对象被垃圾回收器回收后,Java虚拟机将会把这个软引用加入到与之关联的引用队列中
  • 弱引用:弱引用时比软引用更弱的引用类型,只有弱引用指向的对象的生命周期更短,当垃圾回收器扫描到只具有弱引用的对象时,不论当前内存空间是否不足,都会对弱引用对象进行回收。

    • 未取消注册或回调导致的内存泄露
      比如注册广播、服务或者观察者模式时,在activity被销毁时,没有取消注册

    • 资源未关闭或释放导致的内存泄露
      常见的有进行文件存储、网络存储、数据库操作时没有及时关闭

    • Timer或TimerTask导致的内存泄露
      Timer和TimerTask在Android中通常会被用来做一些计时或循环任务,比如实现无限轮播的ViewPager:

public class MainActivity extends AppCompatActivity {

    private ViewPager mViewPager;
    private PagerAdapter mAdapter;
    private Timer mTimer;
    private TimerTask mTimerTask;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
        mTimer.schedule(mTimerTask, 3000, 3000);
    }

    private void init() {
        mViewPager = (ViewPager) findViewById(R.id.view_pager);
        mAdapter = new ViewPagerAdapter();
        mViewPager.setAdapter(mAdapter);

        mTimer = new Timer();
        mTimerTask = new TimerTask() {
            @Override
            public void run() {
                MainActivity.this.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        loopViewpager();
                    }
                });
            }
        };
    }

    private void loopViewpager() {
        if (mAdapter.getCount() > 0) {
            int curPos = mViewPager.getCurrentItem();
            curPos = (++curPos) % mAdapter.getCount();
            mViewPager.setCurrentItem(curPos);
        }
    }

    private void stopLoopViewPager() {
        if (mTimer != null) {
            mTimer.cancel();
            mTimer.purge();
            mTimer = null;
        }
        if (mTimerTask != null) {
            mTimerTask.cancel();
            mTimerTask = null;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        stopLoopViewPager();
    }
}

当我们Activity销毁的时,有可能Timer还在继续等待执行TimerTask,它持有Activity的引用不能被回收,因此当我们Activity销毁的时候要立即cancel掉Timer和TimerTask,以避免发生内存泄漏。

  • 属性动画造成内存泄露
    动画同样是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity,这就造成Activity无法正常释放。因此同样要在Activity销毁的时候cancel掉属性动画,避免发生内存泄漏

总结

内存泄露在Android内存优化是一个比较重要的一个方面,很多时候程序中发生了内存泄露我们不一定就能注意到,所有在编码的过程要养成良好的习惯。总结下来只要做到以下这几点就能避免大多数情况的内存泄漏:

构造单例的时候尽量别用Activity的引用;
静态引用时注意应用对象的置空或者少用静态引用;
使用静态内部类+软引用代替非静态内部类;
及时取消广播或者观察者注册;
耗时任务、属性动画在Activity销毁时记得cancel;
文件流、Cursor等资源及时关闭;
Activity销毁时WebView的移除和销毁

2.2 针对内存溢出进行的内存优化

针对内存溢出的优化可以从四个方面入手:首先是减小对象的内存占用,其次是内存对象的重复利用,然后是避免对象的内存泄露,最后是内存使用策略优化。

(1) 减小对象的内存占用
避免OOM的第一步就是要尽量减少新分配出来的对象占用内存的大小,尽量使用更加轻量的对象。可优化的有:

  • 使用更加轻量的数据结构
    例如,我们可以考虑使用ArrayMap/SparseArray而不是HashMap等传统数据结构。HashMap相比起Android专门为移动操作系统编写的ArrayMap容器,在大多数情况下,都显示效率低下,更占内存,通常的HashMap的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外,SparseArray更加高效,在于他们避免了对key与value的自动装箱(autoboxing),并且避免了装箱后的解箱。
  • 避免在Android里面使用Enum
  • 减小Bitmap对象的内存占用
    可以通过缩放比例和解码格式进行优化。

    • inSampleSize:缩放比例,在把图片载入内存之前,我们需要先计算出一个合适的缩放比例,避免不必要的大图载入。
    • decode format:解码格式,选择ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差异。
  • 使用更小的图片
    在涉及给到资源图片时,我们需要特别留意这张图片是否存在可以压缩的空间,是否可以使用更小的图片。尽量使用更小的图片不仅可以减少内存的使用,还能避免出现大量的InflationException。假设有一张很大的图片被XML文件直接引用,很有可能在初始化视图时会因为内存不足而发生InflationException,这个问题的根本原因其实是发生了OOM。

(2)内存对象的重复利用

  • 在ListView/GridView等出现大量重复子组件的视图里对ConvertView的复用
  • Bitmap对象的复用。此处应该会用到缓存技术
  • 避免在onDraw方法里面执行对象的创建。类似onDraw等频繁调用的方法,一定需要注意避免在这里做创建对象的操作,因为他会迅速增加内存的使用,而且很容易引起频繁的gc,甚至是内存抖动。
  • StringBuilder。在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用StringBuilder来替代频繁的“+”。

(3)避免内存泄露

(4)内存使用策略优化

  • 优化布局层次,减少内存消耗
    越扁平化的视图布局,占用的内存就越少,效率越高。我们需要尽量保证布局足够扁平化,当使用系统提供的View无法实现足够扁平的时候考虑使用自定义View来达到目的。
    布局的优化有以下几种方式:

    • 使用include标签共享布局
    • 使用ViewStub标签实现延迟加载
    • 使用merge标签减少布局层次
    • 使用CompoundDrawable属性来替代布局中相邻的ImageView和TextView
  • 考虑不同的实现方式来优化内存占用
  • 谨慎使用第三方libraries
  • 资源文件需要选择合适的文件夹进行存放。
    我们知道hdpi/xhdpi/xxhdpi等等不同dpi的文件夹下的图片在不同的设备上会经过scale的处理。例如我们只在hdpi的目录下放置了一张100100的图片,那么根据换算关系,xxhdpi的手机去引用那张图片就会被拉伸到200200。需要注意到在这种情况下,内存占用是会显著提高的。对于不希望被拉伸的图片,需要放到assets或者nodpi的目录下。

三、内存优化相关辅助工具

  • 内存泄露检测函数库LeakCanary

该函数库可以检测我们应用中可能出现的内存泄露,帮助我们更好的修改代码

  • Android monitor
    使用Android studio自带的Android monitor工具,分析并检测应用使用过程中的内存使用情况
  • Hierarchy Viewer
    使用Android studio自带的Hierarchy Viewer查看界面布局的层级,来优化布局。 在AndroidStudio中的打开方式为Tools > android > Android Device Monitor> openPerspective> Hierarchy Viewer

相关参考链接:这里写链接内容

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值