JingBin's Home

Android 内存泄漏分析与优化

大范围借鉴及自己思考总结的内容,感谢各位博主的分享。

基础概念

何为性能问题

(1). 响应时间

指从用户操作开始到系统给用户以正确反馈的时间。一般包括逻辑处理时间 + 网络传输时间 + 展现时间。对于非网络类应用不包括网络传输时间。

展现时间即:网页或 App 界面渲染时间。
响应时间是:用户对性能最直接的感受。

(2). TPS(Transaction Per Second)

TPS为每秒处理的事务数,是系统吞吐量的指标,在搜索系统中也用QPS(Query Per Second)衡量。TPS一般与响应时间反相关。

通常所说的性能问题就是指响应时间过长、系统吞吐量过低。

对后台开发来说,也常将高并发下内存泄漏归为性能问题。
对移动开发来说,性能问题还包括电量、内存使用这两类较特殊情况。

性能调优方式

明白了何为性能问题之后,就能明白性能优化实际就是优化系统的响应时间,提高TPS。优化响应时间,提高TPS。方式不外乎这三大类:

  • (1) 降低执行时间,又包括几小类
    • a. 利用多线程并发或分布式提高 TPS
    • b. 缓存(包括对象缓存、IO 缓存、网络缓存等)
    • c. 数据结构和算法优化
    • d. 性能更优的底层接口调用,如 JNI 实现
    • e. 逻辑优化
    • f. 需求优化
  • (2) 同步改异步,利用多线程提高TPS
  • (3) 提前或延迟操作,错峰提高TPS

项目优化细节

内存泄漏问题

静态单例类引用Activity的context

单例模式不正确的获取context:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LoginManager {
private Context context;
private static LoginManager manager;
public static LoginManager getInstance(Context context) {
if (manager == null)
manager = new LoginManager(context);
return manager;
}
private LoginManager(Context context) {
this.context = context;
}

在LoginActivity中:

1
2
3
4
5
6
7
8
9
public class LoginActivity extends Activity {
private LoginManager loginManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
loginManager = LoginManager.getInstance(this);
}

在LoginManager的单例中context持有了LoginActivity的this对象,即使登录成功后我们跳转到了其他Activity页面,LoginActivity的对象仍然得不到回收因为他被单例所持有,而单例的生命周期是同Application保持一致的。

正确的获取context的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LoginManager {
private Context context;
private static LoginManager manager;
public static LoginManager getInstance(Context context) {
if (manager == null)
manager = new LoginManager(context);
return manager;
}
private LoginManager(Context context) {
this.context = context.getApplicationContext();
}

我们单例中context不再持有Activity的context而是持有Application的context即可,因为Application本来就是单例,所以这样就不会存在内存泄漏的的现象了。

单例模式中通过内部类持有activity对象

下面是一个单例的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class TestManager {
public static final TestManager INSTANCE = new TestManager();
private List<MyListener> mListenerList;
private TestManager() {
mListenerList = new ArrayList<MyListener>();
}
public static TestManager getInstance() {
return INSTANCE;
}
public void registerListener(MyListener listener) {
if (!mListenerList.contains(listener)) {
mListenerList.add(listener);
}
}
public void unregisterListener(MyListener listener) {
mListenerList.remove(listener);
}
}
interface MyListener {
public void onSomeThingHappen();
}

然后是activity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestActivity extends AppCompatActivity {
private MyListener mMyListener=new MyListener() {
@Override
public void onSomeThingHappen() {
}
};
private TestManager testManager=TestManager.getInstance();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
testManager.registerListener(mMyListener);
}
}

我们知道在java中,非静态的内部类的对象都是会持有指向外部类对象的引用的,因此我们将内部类对象mMyListener让单例所持有时,由于mMyListener引用了我们的activity对象,因此造成activity对象也不能被回收了,从而出现内存泄漏现象。

修改以上代码,避免内存泄漏,在activity中添加以下代码:

1
2
3
4
5
@Override
protected void onDestroy() {
testManager.unregisterListener(mMyListener);
super.onDestroy();
}

退出界面时,取消相关注册监听!

AsyncTask不正确使用造成的内存泄漏

我们在来看一种更加容易被忽略的内存泄漏现象,对于AsyncTask不正确使用造成内存泄漏的问题:

1
2
3
4
5
6
7
8
mTask=new AsyncTask<String,Void,Void>()
{
@Override
protected Void doInBackground(String... params) {
//doSamething..
return null;
}
}.execute("a task");

我们在使用AsyncTask的时候不宜在其中执行太耗时的操作,假设activity已经退出了,然而AsyncTask里任务还没有执行完成或者是还在排队等待执行,就会造成我们的activity对象被回收的时间延后,一段时间内内存占有率变大。

解决方法在activity退出的时候应该调用cancel()函数:

1
2
3
4
5
6
@Override
protected void onDestroy() {
//mTask.cancel(false);
mTask.cancel(true);
super.onDestroy();
}

退出界面时,结束当前页面的线程。

内部Handler类引起内存泄露

原因:Handler在Android中用于消息的发送与异步处理,常常在Activity中作为一个匿名内部类来定义,此时Handler会隐式地持有一个外部类对象(通常是一个Activity)的引用。当Activity已经被用户关闭时,由于Handler持有Activity的引用造成Activity无法被GC回收,这样容易造成内存泄露。 正确的做法是将其定义成一个静态内部类(此时不会持有外部类对象的引用),在构造方法中传入Activity并对Activity对象增加一个弱引用,这样Activity被用户关闭之后,即便异步消息还未处理完毕,Activity也能够被GC回收,从而避免了内存泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static class MyHandler extends Handler {
private WeakReference<Activity> reference;
public MyHandler(Activity activity) {
reference = new WeakReference<Activity>(activity);
}
@Override
public void handleMessage(Message msg) {
if (reference.get() != null) {
switch (msg.what) {
case 0:
// do something...
break;
default:
// do something...
break;
}
}
}
}
webview导致的内存泄漏

用代码New一个WebView而不是在XML中静态写入(不过貌似不能设置进度条了,不需要进度条的可以忽略):

在XML文件中用layout占位:

1
2
3
4
<RelativeLayout
android:id="@+id/base_web_view_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />

接下来只需要在Activity中New一个WebView并且添加到我们的容器中就ok了:

1
2
3
4
5
6
RelativeLayout webview_container = (RelativeLayout) findViewById(R.id.base_web_view_container);
web_view_ = new WebView(yourApplicationContext);
web_view_.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
web_view_.setOnWebCallback(yourWebCallback);
webview_container.addView(web_view_);

关于WebView的context应该用Activity还是Application的context,这里网上较为一致的观点都是采用Application的,理由是这样不会造成Activity的context的内存泄漏。

销毁时的动作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void clearWebViewResource() {
if (web_view_ != null) {
LogUtils.d(TAG,"Clear webview's resources");
web_view_.removeAllViews();
// in android 5.1(sdk:21) we should invoke this to avoid memory leak
// see (https://coolpers.github.io/webview/memory/leak/2015/07/16/
// android-5.1-webview-memory-leak.html)
((ViewGroup) web_view_.getParent()).removeView(web_view_);
web_view_.setTag(null);
web_view_.clearHistory();
web_view_.destroy();
web_view_ = null;
}
}

尽量不要将WebView作为listview的头部使用,这样的话WebView会被一次性加载到内存中。

Window Leaked

按字面了解,Window Leaked大概就是说一个窗体泄漏了,也就是我们常说的内存泄漏,为什么窗体会泄漏呢?

  • 产生原因:
    我们知道Android的每一个Activity都有个WindowManager窗体管理器,同样,构建在某个Activity之上的对话框、PopupWindow也有相应的WindowManager窗体管理器。因为对话框、PopupWindown不能脱离Activity而单独存在着,所以当某个Dialog或者某个PopupWindow正在显示的时候我们去finish()了承载该Dialog(或PopupWindow)的Activity时,就会抛Window Leaked异常了,因为这个Dialog(或PopupWindow)的WindowManager已经没有谁可以附属了,所以它的窗体管理器已经泄漏了。

  • 解决方法:
    关闭(finish)某个Activity前,要确保附属在上面的Dialog或PopupWindow已经关闭(dismiss)了。

避免内存流失

内存流失可能会导致出现大量的 GC 事件,如自定义组件的 onDraw() ,避免大量创建临时对象,比如 String ,以免频繁触发 GC。GC 事件通常不影响您的 APP 的性能,然而在很短的时间段,发生许多垃圾收集事件可以快速地吃了您的帧时间,系统上时间的都花费在 GC ,就有很少时间做其他的东西像渲染或音频流。

监听器的注销
  • 对于观察者, 广播, Listener等, 注册和注销没有成对出现而导致的内存泄露.
  • 使用CountDownTimer倒计时时,退出activity要取消:timer.cancel()
  • 使用LocationManager获取地理位置,及时取消注册:locationManager.removeUpdates(mListener);
  • 使用dialog或BottomSheetDialog,消失时移除监听,对象置空
  • 使用RxBus,退出activity取消注册
  • 使用一些三方的库,仔细查看是否需要取消注册的情况
Bitmap处理

fresco为例:

  • (最好是加载图片宽高大小的图片,多余的尺寸会导致内存浪费,不过webp后缀的图片无法设置宽高,这是个问题?)加载特别特别大的图片时最容易导致这种情况。如果你加载的图片比承载的View明显大出太多,那你应该考虑将它Resize一下。
  • Android 无法绘制长或宽大于2048像素的图片。这是由OpenGL渲染系统限制的,如果它超过了这个界限,Fresco会对它进行Resize。
  • decode format:解码格式,选择ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差异。在不需要特别清晰的图片情况下,使用RBG_565为好。
SharedPreference 存储value

sp在创建的时候会把整个文件全部加载进内存,如果你的sp文件比较大,那么会带来两个严重问题:

  • 第一次从sp中获取值的时候,有可能阻塞主线程,使界面卡顿、掉帧。
  • 解析sp的时候会产生大量的临时对象,导致频繁GC,引起界面卡顿。
  • 这些key和value会永远存在于内存之中,占用大量内存。

储存数据量过大后,取值小屏手机vivoY23L,v4.4.4会取值失败。

Cursor关闭

如查询数据库的操作,使用到Cursor,也要对Cursor对象及时关闭。

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
if (cursor != null) {
cursor.moveToFirst();
// do something.
}
}catch (Exception e) {
e.printStackTrace();
}finally {
if (cursor != null) {
cursor.close();
cursor = null;
}
}

有效使用内存的建议

去掉bean里无用的字段

有时候我们通过GsonFormat直接生成返回的json的Bean,而有一些我们并未使用的字段也一并生成了,建议删除这些无用字段,不然将无可避免的占用一定的内存空间。

关闭页面,全局的list清空后置空

用完就清空,并设置为null,不要到处引用不然会导致不能及时释放。

谨慎使用服务Service

离开了 APP 还在运行服务是最糟糕的内存管理错误之一,当 APP 处在后台,我们应该停止服务,除非它需要运行的任务。我们可以使用JobScheduler替代实现,JobScheduler把一些不是特别紧急的任务放到更合适的时机批量处理。如果必须使用一个服务,最佳方法是使用IntentService,限制服务寿命,所有请求处理完成后,IntentService 会自动停止。

使用优化后的数据容器

考虑使用优化过数据的容器 SparseArray / SparseBooleanArray / LongSparseArray 代替 HashMap 等传统数据结构,通用 HashMap 的实现可以说是相当低效的内存,因为它需要为每个映射一个单独的条目对象

关于HashMap,ArrayMap,SparseArray, 这篇文章有个比较直观的比较, 可以看下

少用枚举enum结构

枚举一般是用来列举一系列相同类型的常量,它是一种特殊的数据类型,使用枚举能够确保参数的安全性。但是Android开发文档上指出,使用枚举会比使用静态变量多消耗两倍的内存,应该尽量避免在Android中使用枚举。

那么枚举为什么会更消耗内存呢? - 分析链接

避免创建不必要的对象

诸如一些临时对象, 特别是循环中的.

使用异步处理数据较多的情况

如果一些数据需要处理再显示在UI上,对于数据量比较大的情况强烈建议异步处理后再在主线程处理。

使用 nano protobufs 序列化数据

Protocol buffers 是一个语言中立,平台中立的,可扩展的机制,由谷歌进行序列化结构化数据,类似于 XML 设计的,但是更小,更快,更简单。如果需要为您的数据序列化与协议化,建议使用 nano protobufs。

使用ProGuard来剔除不需要的代码

使用 ProGuard 来剔除不需要的代码,移除任何冗余的,不必要的,或臃肿的组件,资源或库完善 APP 的内存消耗。

降低整体尺寸APK

您可以通过减少 APP 的整体规模显著减少 APP 的内存使用情况。文章:Android APK瘦身实践

优化布局层次

通过优化视图层次结构,以减少重叠的 UI 对象的数量来提高性能。文章:Android 渲染优化

检测工具

参考文章