前言
最近在整理回顾零碎知识点,今天整理下Android内存优化方案分享给大家。
在Android开发中,一些不好的编程习惯会导致我们的开发的app存在内存泄露的情况。下面简单介绍一些在Android开发中常见的内存泄露场景及优化方案。
1.单例导致内存泄露
单例模式在Android开发中会经常用到,但是如果使用不当就会导致内存泄露。因为单例的静态特性使得它的生命周期同应用的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有它的引用,那么在整个应用程序的生命周期它都不能正常被回收,从而导致内存泄露。
public class Singleton {private static Singleton singleton = null;private Context mContext;public Singleton(Context mContext) {this.mContext = mContext;}public static Singleton getSingleton(Context context){if (null == singleton){singleton = new Singleton(context);}return singleton;}}
像上面代码中这样的单例,如果我们在调用getInstance(Context context)方法的时候传入的context参数是Activity、Service等上下文,就会导致内存泄露。
当我们退出Activity时,该Activity就没有用了,但是因为singleton作为静态单例(在应用程序的整个生命周期中存在)会继续持有这个Activity的引用,导致这个Activity对象无法被回收释放,这就造成了内存泄露。
为了避免这样单例导致内存泄露,我们可以将context参数改为全局的上下文:
public Singleton(Context mContext) {this.mContext = mContext.getApplicationContext();}
全局的上下文Application Context就是应用程序的上下文,和单例的生命周期一样长,这样就避免了内存泄漏。单例模式对应应用程序的生命周期,所以我们在构造单例的时候尽量避免使用Activity的上下文,而是使用Application的上下文。
2.非静态内部类导致内存泄露
非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄露。
非静态内部类导致的内存泄露在Android开发中有一种典型的场景就是使用Handler,很多开发者在使用Handler是这样写的:
public class MainActivity extends AppCompatActivity {@Overrideprotected 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() {@Overridepublic void handleMessage(Message msg) {if (msg.what == 1) {// 做相应逻辑}}};}
当Activity退出后,msg可能仍然存在于消息对列MessageQueue中未处理或者正在处理,那么这样就会导致Activity无法被回收,以致发生Activity的内存泄露。
通常在Android开发中如果要使用内部类,但又要规避内存泄露,一般都会采用静态内部类+弱引用的方式。
public class MainActivity extends AppCompatActivity {private Handler mHandler;@Overrideprotected 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);}@Overridepublic void handleMessage(Message msg) {MainActivity activity = activityWeakReference.get();if (activity != null) {if (msg.what == 1) {// 做相应逻辑}}}}}
mHandler通过弱引用的方式持有Activity,当GC执行垃圾回收时,遇到Activity就会回收并释放所占据的内存单元。这样就不会发生内存泄露了。
protected void onDestroy() {super.onDestroy();mHandler.removeCallbacksAndMessages(null);}
内部类造成内存泄露还有一种情况就是使用Thread或者AsyncTask。
比如在Activity中直接new一个子线程Thread:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);new Thread(new Runnable() {@Overridepublic void run() {// 模拟相应耗时逻辑try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}}).start();}}
或者直接新建AsyncTask异步任务:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);new AsyncTask<Void, Void, Void>() {@Overrideprotected Void doInBackground(Void... params) {// 模拟相应耗时逻辑try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}return null;}}.execute();}}
这种方式新建的子线程Thread和AsyncTask都是匿名内部类对象,默认就隐式的持有外部Activity的引用,导致Activity内存泄露。要避免内存泄露的话还是需要像上面Handler一样使用静态内部类+弱应用的方式(代码就不列了,参考上面Hanlder的正确写法)。
3.未取消注册或回调导致内存泄露
比如我们在Activity中注册广播,如果在Activity销毁后不取消注册,那么这个刚播会一直存在系统中,同上面所说的非静态内部类一样持有Activity引用,导致内存泄露。因此注册广播后在Activity销毁后一定要取消注册。
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);this.registerReceiver(mReceiver, new IntentFilter());}private BroadcastReceiver mReceiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {// 接收到广播需要做的逻辑}};@Overrideprotected void onDestroy() {super.onDestroy();this.unregisterReceiver(mReceiver);}}
在注册观察则模式的时候,如果不及时取消也会造成内存泄露。比如使用Retrofit+RxJava注册网络请求的观察者回调,同样作为匿名内部类持有外部引用,所以需要记得在不用或者销毁的时候取消注册。
4.Timer和TimerTask导致内存泄露
Timer和TimerTask在Android中通常会被用来做一些计时或循环任务,比如实现无限轮播的ViewPager:
public class MainActivity extends AppCompatActivity {private ViewPager mViewPager;private PagerAdapter mAdapter;private Timer mTimer;private TimerTask mTimerTask;@Overrideprotected 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() {@Overridepublic void run() {MainActivity.this.runOnUiThread(new Runnable() {@Overridepublic 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;}}@Overrideprotected void onDestroy() {super.onDestroy();stopLoopViewPager();}}
当我们Activity销毁的时,有可能Timer还在继续等待执行TimerTask,它持有Activity的引用不能被回收,因此当我们Activity销毁的时候要立即cancel掉Timer和TimerTask,以避免发生内存泄漏。
5.集合中的对象未清理造成内存泄露
这个比较好理解,如果一个对象放入到ArrayList、HashMap等集合中,这个集合就会持有该对象的引用。当我们不再需要这个对象时,也并没有将它从集合中移除,这样只要集合还在使用(而此对象已经无用了),这个对象就造成了内存泄露。并且如果集合被静态引用的话,集合里面那些没有用的对象更会造成内存泄露了。所以在使用集合时要及时将不用的对象从集合remove,或者clear集合,以避免内存泄漏。
6.资源未关闭或释放导致内存泄露
在使用IO、File流或者Sqlite、Cursor等资源时要及时关闭。这些资源在进行读写操作时通常都使用了缓冲,如果及时不关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄露。因此我们在不需要使用它们的时候就及时关闭,以便缓冲能及时得到释放,从而避免内存泄露。
7.属性动画造成内存泄露
动画同样是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity,这就造成Activity无法正常释放。因此同样要在Activity销毁的时候cancel掉属性动画,避免发生内存泄漏。
@Overrideprotected void onDestroy() {super.onDestroy();mAnimator.cancel();}
8.WebView造成内存泄露
关于WebView的内存泄露,因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存。
最终的解决方案是:在销毁WebView之前需要先将WebView从父容器中移除,然后在销毁WebView。
@Overrideprotected void onDestroy() {super.onDestroy();// 先从父控件中移除WebViewmWebViewContainer.removeView(mWebView);mWebView.stopLoading();mWebView.getSettings().setJavaScriptEnabled(false);mWebView.clearHistory();mWebView.removeAllViews();mWebView.destroy();}
内存优化
1.检测内存泄漏的工具
1.LeakCanary
2.Android Studio profile
3.MAT
2.图片压缩
1. bitmap 压缩
大家都知道 bitmap 占用内存很大,用完之后要 recycle 一下。不知道大家有没有用过,图片加载出来内存就爆掉了(OOM)情况。首先一张图片从网络获下来,从 InputStream 转成 Bitmap,这个 bitmap 占了多少内存怎么计算?献上代码:
Bitmap.getAllocationByteCount();
其实就是 ByteCount = 长* 宽 * 4(假设这里每一个像素点是是RGB888) 那就是 4 个字节。也有一个像素点 RGB565 占 3 个字节,当然占更多字节的 RGB888 更加高清无码。起初版本 Glide 使用 RGB565,目前 Glide4.XX 的默认都是 RGB888,当然自己可以配置一下。
为了解决这个问题一般都是通过下面代码:
BitmapFactory.Options options = new BitmapFactory.Options();options.inJustDecodeBounds = true;// 通过这个bitmap获取图片的宽和高Bitmap bitmap = BitmapFactory.decodeFile(\"/sdcard/MTXX/3.jpg\", options);float realWidth = options.outWidth;float realHeight = options.outHeight;//计算出scaleoptions.inSampleSize = scale;options.inJustDecodeBounds = false;// 注意这次要把options.inJustDecodeBounds 设为 false,这次图片是要读取出来的。bitmap = BitmapFactory.decodeFile(\"/sdcard/MTXX/3.jpg\", options);
先获取他的图片大小,根据自己需要的大小计算出缩放比例。(图片大小都是放在图片的头部,这时候不会去加载整张图片)进行缩放,得出符合自己的控件尺寸的大小。(当然还有些非法的图片头部是获取不出 长* 宽。这时候记得搞个默认的缩放率,防止 OOM)有时候为了优化内存,还不如压缩一张图片 所节约的内存来的更快。譬如 一张 1080 * 1920 图片再乘以 4 等于 7.9 M。我压缩到 一张缩略图 200*200 等于 156KB。瞬间节约了7M 空间。
总结
内存泄露在Android内存优化是一个比较重要的一个方面,很多时候程序中发生了内存泄露我们不一定就能注意到,所有在编码的过程要养成良好的习惯。总结下来只要做到以下这几点就能避免大多数情况的内存泄漏:
1.构造单例的时候尽量别用Activity的引用;
2.静态引用时注意应用对象的置空或者少用静态引用;
3.使用静态内部类+软引用代替非静态内部类;
4.及时取消广播或者观察者注册;
5.耗时任务、属性动画在Activity销毁时记得cancel;
6.文件流、Cursor等资源及时关闭;
7.Activity销毁时WebView的移除和销毁。