盒子
盒子
文章目录
  1. 基础概念
    1. 何为性能问题
      1. (1). 响应时间
      2. (2). TPS(Transaction Per Second)
    2. 性能调优方式
  2. 项目优化细节
    1. 内存泄漏问题
      1. 静态单例类引用Activity的context
      2. 单例模式中通过内部类持有activity对象
      3. AsyncTask不正确使用造成的内存泄漏
      4. 内部Handler类引起内存泄露
      5. webview导致的内存泄漏
      6. Window Leaked
      7. 避免内存流失
      8. 监听器的注销
      9. Bitmap处理
      10. SharedPreference 存储value
      11. Cursor关闭
    2. 有效使用内存的建议
      1. 去掉bean里无用的字段
      2. 关闭页面,全局的list清空后置空
      3. 谨慎使用服务Service
      4. 使用优化后的数据容器
      5. 少用枚举enum结构
      6. 避免创建不必要的对象
      7. 使用异步处理数据较多的情况
      8. 使用 nano protobufs 序列化数据
      9. 使用ProGuard来剔除不需要的代码
      10. 降低整体尺寸APK
      11. 优化布局层次
  3. 检测工具
  4. 参考文章

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 渲染优化

检测工具

参考文章

支持一下
扫一扫,支持我
  • 微信扫一扫
  • 支付宝扫一扫