一、为什么要进行内存优化
Android应用程序开发过程中,内存的准确控制是判断一个程序好坏的重要标准之一。
若我们的应用程序内存控制不够好,出现内存泄露或者内存溢出,会导致:
- 程序卡顿,响应速度缓慢
- 开启其他程序的时候,内存泄漏的程序放在后台没有进行关闭,但是也可能会莫名其妙的消失(内存越大它在,在后台越有可能死掉,如果内存小可能在后台停留的时间越长)
更严重点,有时会直接崩溃
所以,为了开发一个质量高的应用程序,保证程序正常运行,需要对内存进行优化。
二、 怎样进行内存优化
我们的内存优化,主要是针对内存泄露和内存溢出进行优化。
内存泄露:对象在内存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
相关参考链接:这里写链接内容