其实现在的手机越做越好了,手机的内存也是越来越大,6G已经是标配了,8G ,12G也是很常见了。但是,作为一款优秀的安卓开发,内存优化的问题是永恒的话题。
下面就来看一下,主要从哪些方面来优化内存:
一、防止内存泄露
1、单例模式导致的内存泄露
单例模式在安卓开发中还是会经常用到的,但是如果使用不当,还是会造成内存泄漏。因为是单例,所以使得该对象的生命周期跟整个app一样长。但是若有一个对象已经没用了用处,但是单例还持有该对象的引用。那么就会造成内存泄漏。
public class AppSettings {
private static AppSettings sInstance;
private Context context;
private AppSettings(Context context){
this.context = context;
}
public static AppSettings getInstance(Context context){
if(sInstance == null){
sInstance = new AppSettings(context);
}
return sInstance;
}
}
如上代码。如果调用getInstance 方法传入的Context 是activity 或者是 Service ,就会造成内存泄漏。
可以修改如下:
private AppSettings(Context context){
this.context = context.getApplicationContext();
}
ApplicationContext 的生命周期跟整个程序生命周期一样,就不会造成内存泄漏。
2、静态变量导致的内存泄漏
public class TestScrollActivity extends AppCompatActivity {
private static Info info;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test_scroll);
if(info == null){
info = new Info(this);
}
}
}
class Info{
Activity activity;
public Info(Activity activity){
this.activity = activity;
}
}
如上代码。静态变量的生命周期必然是比activity 长,所以也会造成无法释放activity。
3、非静态内部类导致内存泄漏
在Java中非静态内部类或者匿名内部类都是默认持有外部类的引用的。比如handle。
public class TestScrollActivity extends AppCompatActivity {
private Handler mHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test_scroll);
}
}
如上代码。mHandler会一直持有activity 的引用,导致activity无法被释放。
解决方法,使用静态的内部类。
4、未取消的注册或者回调造成内存泄漏
比如广播的注册,如果不取消注册则会造成内存泄漏。
5、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 ,以避免发生内存
泄漏。
6、集合中的对象未清理造成内存泄露
这个比较好理解,如果一个对象放入到 ArrayList 、 HashMap 等集合中,这个集合就会持有该对象
的引用。当我们不再需要这个对象时,也并没有将它从集合中移除,这样只要集合还在使用(而
此对象已经无用了),这个对象就造成了内存泄露。并且如果集合被静态引用的话,集合里面那
些没有用的对象更会造成内存泄露了。所以在使用集合时要及时将不用的对象从集合 remove ,或
者 clear 集合,以避免内存泄漏。
7、资源未关闭或释放导致内存泄露
在使用 IO 、 File 流或者 Sqlite 、 Cursor 等资源时要及时关闭。这些资源在进行读写操作时通常都
使用了缓冲,如果及时不关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄露。
因此我们在不需要使用它们的时候就及时关闭,以便缓冲能及时得到释放,从而避免内存泄露。
8、属性动画造成内存泄露
动画同样是一个耗时任务,比如在 Activity 中启动了属性动画( ObjectAnimator ),但是在销毁
的时候,没有调用 cancle 方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,
动画引用所在的控件,所在的控件引用 Activity ,这就造成 Activity 无法正常释放。因此同样要
在 Activity 销毁的时候 cancel 掉属性动画,避免发生内存泄漏。
@Override
protected void onDestroy() {
super.onDestroy();
mAnimator.cancel();
}
9、WebView 造成内存泄露
关于 WebView 的内存泄露,因为 WebView 在加载网页后会长期占用内存而不能被释放,因此我
们在 Activity 销毁后要调用它的 destory() 方法来销毁它以释放内存。
另外在查阅 WebView 内存泄露相关资料时看到这种情况:
Webview 下面的 Callback 持有 Activity 引用,造成 Webview 内存无法释放,即使是调用了
Webview.destory() 等方法都无法解决问题(Android5.1 之后)。
最终的解决方案是:在销毁 WebView 之前需要先将 WebView 从 父容器中移除,然后在销毁 WebView 。
详细分析过程请参考这篇文章:WebView 内存泄漏解决方法
@Override
protected void onDestroy() {
super.onDestroy();
// 先从父控件中移除 WebView
mWebViewContainer.removeView(mWebView);
mWebView.stopLoading();
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.removeAllViews();
mWebView.destroy();
}
总结
内存泄露在 Android 内存优化是一个比较重要的一个方面,很多时候程序中发生了内存泄露我们
不一定就能注意到,所有在编码的过程要养成良好的习惯。总结下来只要做到以下这几点就能避
免大多数情况的内存泄漏:
构造单例的时候尽量别用 Activity 的引用;
静态引用时注意应用对象的置空或者少用静态引用;
使用静态内部类+软引用代替非静态内部类;
及时取消广播或者观察者注册;
耗时任务、属性动画在 Activity 销毁时记得 cancel ;
文件流、 Cursor 等资源及时关闭;
Activity 销毁时 WebView 的移除和销毁。
二、资源的复用
1、bitmap的复用
比如一个图片,点击之后要更换,那么此时最好就用bitmap的复用。避免每次新建bitmap ,以此减少内存的开销。
当然使用Glide框架来加载图片也是可以的,Glide默认是实现了bitmap的复用。
2、数据过长时,不要使用NestedScrollView嵌套RecyclerView
NestedScrollView嵌套RecyclerView时,默认是一次性加载recyclerview的所有项。所以也不存在ViewHolder的复用了。如果数据量过多,就会造成OOM。
解决办法:给RecyclerView添加 HeadView 以及 FootView。
RecyclerView添加 HeadView 以及 FootView
三、减少内存开销
1、使用轻量的数据结构
如使用ArrayMap/SparseArray替代HashMap,HashMap更耗内存,因为它需要额外的实例对象来记录Mapping操作,SparseArray更加高效,因为它避免了Key Value的自动装箱,和装箱后的解箱操作。
2、尽量不使用枚举类型
可以用静态常量或者注解@IntDef替代
3、Bitmap优化
a.尺寸压缩:通过InSampleSize设置合适的缩放
b.颜色质量:设置合适的format,ARGB_6666/RBG_545/ARGB_4444/ALPHA_6,存在很大差异
c.inBitmap:使用inBitmap属性可以告知Bitmap解码器去尝试使用已经存在的内存区域,新解码的Bitmap会尝试去使用之前那张Bitmap在Heap中所占据的pixel data内存区域,而不是去问内存重新申请一块区域来存放Bitmap。利用这种特性,即使是上千张的图片,也只会仅仅只需要占用屏幕所能够显示的图片数量的内存大小,但复用存在一些限制,具体体现在:在Android 4.4之前只能重用相同大小的Bitmap的内存,而Android 4.4及以后版本则只要后来的Bitmap比之前的小即可。使用inBitmap参数前,每创建一个Bitmap对象都会分配一块内存供其使用,而使用了inBitmap参数后,多个Bitmap可以复用一块内存,这样可以提高性能。
4、StringBuilder替代String
在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用StringBuilder来替代频繁的“+”。
5、避免在类似onDraw这样的方法中创建对象
因为它会迅速占用大量内存,引起频繁的GC甚至内存抖动
6、可以使用样式文件来代替图片
比如一些按钮的边框之类的,那么尽量用xml文件来代替图片。