Android 性能优化 - 内存 [进阶]

本文主要总结 Android 开发过程中对内存使用上的优化,通过及时有效的管理内存空间可以避免内存泄漏和 OOM 的发生。

前言

四种引用类型:

  • 强引用:默认引用类型,强引用的对象即使抛出 OOM 也不会被回收。
  • 软引用 SoftReference:当内存不足时会被回收。
  • 弱引用 WeakReference:在 GC 时,一旦发现了只具有弱引用的对象,都会进行回收。
  • 虚引用 PhantomReference:任何时候都可以被 GC 回收。

内存问题主要体现在:

  • 内存泄漏:指的是当一个对象不再被使用时,由于其他对象仍然持有该对象的强引用,导致该对象无法被释放,造成内存空间的浪费,大量占据内存空间可能引发 OOM;
  • 内存溢出:即 OOM,当向系统申请内存时,没有足够的内存空间时会引发 OOMOOM 发生的情况很多,而且最后 Crash 的地方并不一定是问题的根源,可能是其他的操作占用了大量内存。
  • 内存抖动:指的是频繁的有大量的对象创建和销毁,引发高频的系统 GC,当 GC 线程启动时其他线程都会暂停,会造成页面的卡顿等。

解决内存问题的原则:

  • 避免创建大对象,如不使用 inSampleSizeBitmap
  • 避免大量创建重复对象,如在循环中创建对象
  • 避免生命周期不可控的对象引用,如在子线程中引用上下文
  • 避免少的开辟新的内存空间,建议尽量复用可复用的内存空间,如后文介绍的 ByteArrayPool 以及 BitmapinBitmap 属性

内存分析和监控

  • 获取分配的内存和可用内存大小
1
2
long totalMemory = Runtime.getRuntime().totalMemory();
long freeMemory = Runtime.getRuntime().freeMemory();
  • Android Profiler

使用 AndroidStudio,通过 View -> ToolWindows -> Android Profiler,可以查看内存、网络、CPU 变化情况,还可以 Dump 内存记录,用于内存分析。

  • Memory View

使用 AndroidStudio,通过 View -> ToolWindows -> Memory View,可以结合断点调试 dump 指定断点处的内存使用情况,进行内存分析。

  • 内存监控

当应用内存不足时,会调用 ApplicationonTrimMemory() 方法,我们可以在这里做一些清理内存的操作,避免内存过大造成 OOM

1
2
3
4
5
6
7
public class MyApplication extends BaseApplication {
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
}
}

LeakCanary

LeakCanary-GitHub : A memory leak detection library for Android and Java.

LeakCanarysqure 开源的一个用于在 AndroidJava 平台下检测内存泄漏的工具,提供了两种依赖方式,在 release 版本下不会进行内存泄漏的检测,避免性能问题。

1
2
compile "com.squareup.leakcanary:leakcanary-android-no-op:1.5.1"
debugCompile "com.squareup.leakcanary:leakcanary-android:1.5.1"

Application 中初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MyApplication extends BaseApplication {
@Override
public void onCreate() {
super.onCreate();
initLeakCanary();
}
private RefWatcher mRefWatcher;
public void initLeakCanary() {
if (LeakCanary.isInAnalyzerProcess(this)) {
return;
}
mRefWatcher = LeakCanary.install(this);
}
public static RefWatcher getRefWatcher(Context context) {
MyApplication application = (MyApplication) context.getApplicationContext();
return application.mRefWatcher;
}
}

初始化之后就可以自动检测 Activity 的内存泄露问题,如果需要检测 Fragment 等其他对象的内存问题,需要在希望对象被回收的时候注册检测监听

1
2
3
4
5
6
7
public class MyFragment extends BaseFragment {
@Override
public void onDestroy() {
super.onDestroy();
MyApplication.getRefWatcher(getContext()).watch(this,"fragment");
}
}

内部类导致内存泄漏

在内存问题上我们简单的把内部类分为静态内部类和非静态内部类:

  • 静态内部类:与外部类独立,内部类的创建不需要依赖外部类实例,而是依赖于 Class 本身,他只能访问外部类的静态变量和方法,可以看作一个完全独立的类,与外部类完全隔离,不会存在内存泄漏的问题。

  • 非静态内部类:也就是其他类型的内部类,主要包括局部内部类、匿名内部类等,他们的创建依赖于外部类的实例,非静态内部类可以访问外部的非静态成员,甚至是私有成员,这是因为内部类中隐式的持有了外部类的引用,在编译后,会形成 OuterClass$InnerClass 类,而这个类中就会持有外部类的引用,因此当外部类被销毁后,由于内部类仍旧强引用持有了外部类,因此外部类不能被及时回收,造成内存泄漏问题。

不是说所有的内部类都会造成内存泄漏,外部类销毁时通常也会销毁内部类,内存泄漏往往发生在由于一些原因导致内部类无法被销毁的情况,如生命周期不统一。

生命周期不统一导致内存泄漏

生命周期不统一指的是 ObjB 引用了 ObjA,但是 ObjBObjA 生命更长,当 ObjA 自己销毁时,由于 ObjB 还在活跃,导致 ObjA 无法被回收。

因为内部类持有外部类的引用,如果内部类存在于一个新的线程里面,那么内部类的生命周期就依赖于新的线程的生命周期,两者不一致,当外部类被销毁时,就会造成内存问题,比较常见的体现在 HandlerAsyncTaskThread 等涉及线程的操作。

  • Handler
1
2
3
4
5
6
Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};

Activity 中使用 Handler 发送的 Message 排队在 Looper 线程的 MessageQueue 中,同时 Message 中持有 Handler 的引用,而由于Handler 匿名内部类隐式的持有了外部类的引用,就相当于 Activity 被强关联在了这条消息上面。即 Activity -> Handler -> Message -> MessageQueue -> Looper

Activity 被销毁后还是会按照指定的时间发送该消息,造成内存泄漏。因此在 Activity 非静态的声明一个 Handler 会警告:匿名的 Handler 可能会引发内存泄漏

1
This Handler class should be static or leaks might occur (anonymous android.os.Handler)

这个问题尤其体现在使用 Handler 发送了一个延时消息,当 Activity 被销毁后,这个消息才被发送出来,开始执行。

除了匿名声明 Handler 之外,当使用 Handler 发送一个 Runnable 时也会存在一样的问题,这里 Handler 不是内部类了,但是发送的 Runnale 里面也会隐式持有外部类的应用,即 Activity -> Runnale -> Message -> MessageQueue -> Looper

1
2
3
4
5
6
7
Handler mHandler = new Handler();
mHandler.post(new Runnable() {
@Override
public void run() {
}
});
  • Thread 和 AsyncTask

通常我们在子线程执行耗时任务,执行完成后再回到主线程操作,如下面的例子中,ThreadAsyncTask 结束的时机是没办法控制的,而他们都会持有外部 Activity 的引用,导致无法回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
new Thread(new Runnable() {
@Override
public void run() {
}
}).start();
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
return null;
}
}.execute();

针对生命周期不一致问题的优化方案

单纯的内部类不会造成内存泄漏,不用过分保护,对于生命周期不可控的内部类,采用静态声明,结合虚引用来将内部类和外部类隔离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static class NoLeakHandler extends Handler {
private WeakReference<Activity> mActivityWeakRef;
public NoLeakHandler(Activity activity) {
mActivityWeakRef = new WeakReference<>(activit
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Activity activity = mActivityWeakRef.get();
if (activity != null) {
activity.finish();
}
}
}
Handler mHandler = new NoLeakHandler(this);

使用线程进行操作时,也需要使用虚引用与 Context 交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class NoLeakAsyncTask extends AsyncTask<Void, Void, Void> {
private WeakReference<Activity> mActivityWeakRef;
public NoLeakAsyncTask(Activity activity) {
mActivityWeakRef = new WeakReference<>(activity);
}
@Override
protected Void doInBackground(Void... voids) {
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
Activity activity = mActivityWeakRef.get();
if (activity != null) {
activity.finish();
}
}
}

Activity 销毁时,将 Handler 的消息队列清空

1
mHandler.removeCallbacksAndMessages(null);

静态引用导致内存泄漏

静态引用的变量不依赖于某个类实例,所以他不会随着某个实例的销毁而随之销毁,因此一旦声明静态引用了某个对象实例,即使抛出异常也不会被回收。

使用静态变量要慎重,避免为了简单的共享数据静态的声明占用大量内存的数据对象,尤其是集合类对象。

类比上面的说法,也可以看作生命周期不统一造成的问题,因为静态引用的变量具有和 Application 相同的生命周期,而 Activity 的生命周期通常较短。

静态引用 Context 导致内存泄漏

这边单独拿出来说是因为在 Android 中静态引用 Context 也是内存泄漏重灾区,如下列举常见的几种可能静态引用 Context 的场景如:

  • 某个对象没有静态引用 Context,但是这个对象在其他位置被静态引用了,导致 Context 间接的静态引用。
  • 单例,单例其实也是静态的引用,不能在单例中引用 Context
  • Toast,为了管理 Toast,比如避免大量 Toast 排队通常会写一个 ToastUtils,里面就会静态持有 Toast 对象,而 Toast 中是引用了 Context 的。
  • View,由于 View 中引用了 Context,静态的 View 就很危险。
  • Animator,属性动画通常绑定到一个 View 上面,静态引用 Animator 相当于静态引用了 View
  • Animation,补间动画中并没有显式的引用 View 或者 Context,但是他有一个 mListenerHandler,这个监听在 View.draw() 方法中如果当前 ViewAnimation 不为空,会给他一个 mAttachInfo.mHandler,而这里面引用了 ViewRootImpl.mContext
  • 特别注意静态引用的集合数据类型,如 ListMap,里面通常会存储大量的对象,如果这些对象中有某些对象引用了 Context,同样会造成内存泄漏。

当不可避免的需要静态引用 Context 时,使用虚引用代替

1
2
WeakReference<Context> mContextWeakRef;
mContextWeakRef = new WeakReference<>(getContext());

尽可能使用 ApplicationContext,而不是 Activity 的。

1
2
Context appContext = context.getApplicationContext();
Application application = (Application) context.getApplicationContext();

资源回收不及时

指的是一些对象我们使用完后要及时关闭、回收或者解除注册,来保证内存可以被及时回收,如:

  • Cursor
  • IO 流
  • Bitmap
  • Animation
  • BroadcastReceiver

尽可能及时的回收资源和内存空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Cursor
Cursor cursor = ...;
cursor.close();
// Stream
FileOutputStream outputStream = ...
outputStream.close();
// Bitmap
Bitmap bitmap = ...;
bitmap.recycle();
// Animation
Animation animation = ...;
animation.cancel();
animation.setAnimationListener(null);
// BroadcastReceiver
BroadcastReceiver broadcastReceiver = ...
context.unregisterReceiver(broadcastReceiver);
// byte
byte[] bytes = new byte[1024 * 8];
bytes = null;

Bitmap 占用大量内存

图片是应用运行过程中内存占用的大户,大多数的 OOM,都是因为 Bitmap 处理不当导致的,因此把 Bitmap 单独拿出来说一下。

  • 图片质量要求不是那么高的时候,使用 RBG_565
Config 描述
ALPHA_8 8位Alpha位图
RGB_565 16位RGB位图
ARGB_4444 16位ARGB位图
ARGB_8888 32位ARGB位图
1
2
3
4
5
6
7
Bitmap bitmap = null;
bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
bitmap = BitmapFactory.decodeFile(filePath, options);
  • 针对显示的大小计算 inSampleSize 对图片进行采样显示
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
26
public Bitmap decodeFile(String filePath) {
// 想显示的图片大小
int reqWidth = 100;
int reqHeight = 100;
// 先获取图片宽高
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filePath, options);
int imgHeight = options.outHeight;
int imgWidth = options.outWidth;
// 计算 sampleSize
int inSampleSize = 1;
if (imgHeight > reqHeight || imgWidth > reqWidth) {
final int halfHeight = imgHeight / 2;
final int halfWidth = imgWidth / 2;
// 在保证解析出的 bitmap 宽高分别大于目标尺寸宽高的前提下
// 取可能的 inSampleSize 的最大值
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
options.inSampleSize = inSampleSize;
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(filePath, options);
}
  • 使用 inBitmap 优化 Bitmap 内存使用

参考1: Android Developer Manage Memory

参考2: 关于 inBitmap 的知乎回答

参数 inBitmap 主要是用来复用已经存在 Bitmap 内存空间,他要求你将一个已经存在的 Bitmap 放入 options,这样新创建的 Bitmap 将会重新放在这块内存空间上,减少了内存空间的重复开辟和回收。

这个参数结合 LruCache 将会有更好的使用效果,在 LruCache 中被移除的 Bitmap 不用立刻进行回收,而是存储起来为下一个将要创建 Bitmap 提供内存空间,从而避免开辟过多的内存,造成浪费和 OOM

重复创建对象

在代码中常常有一些循环操作和发生频率比较高的操作,在这类操作中应该尽量避免创建对象,虽然不会导致内存泄漏但是频繁创建和销毁对象会占用大量内存和引起显著的内存抖动,例如:

  • 高频操作,接入 Weex 时用到自定义请求支持 OkHttpClientAdapter,每次发送请求都会走这个 Adapter,同时创建 OkHttpClient 然后发送请求,在实际应用中请求发送频率很高,因此创建了大量的 OkHttpClient,后来将创建对象的代码提取到构造方法中,这个问题得到了改善。
  • 循环,开发过程中,循环次数往往不可控,应该避免在循环中创建新的对象。
  • 字符串拼接操作,每次字符串拼接都会产生一个新的字符串,因此如果有频繁的拼接操作,请使用 StringBuilder
  • 方法名的定义导致使用的不当,在 getXXX() 方法中不应该进行创建对象的操作,如果有要加入仅创建一次的判断,因为当别人和自己在使用该方法可能会直接调用,因为对使用者来说,这只是一个获取操作,就会频繁的使用它,并不知道内部返回了一个全新的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private ViewModel mViewModel;
// 不应该在 get 方法中不加判断的创建对象
public ViewModel getViewModel() {
return new ViewModel();
}
// 如果仅执行创建新对象的操作应该命名为 newXXX()
public ViewModel newViewModel() {
return new ViewModel();
}
// 如果一定要使用 get() 方法,需要增加创建一次的判读
public ViewModel getViewModel() {
if(mViewModel == null){
mViewModel = new ViewModel();
}
return mViewModel;
}

大量使用 byte 内存问题

通常我们不会大量使用 byte[] ,一般用他来处理 IO 流,但是当我们有需求频繁进行 IO 时,比如文件读取、网络请求等需求,每次创建新的 byte[] 会占据大量的内存空间,原则上我们应该尽量减少内存空间的开辟,针对这种场景我们可以使用 ByteArrayPool 来管理和复用已经存在的内存空间,避免内存占用过多和内存抖动的发生。

下面是参考 Glide 源码中的一个设计,ByteArrayPool 是一个单例,里面维护一个 Queue,每次使用 byte[] 时,从 Queue 中取出并删除,使用完了再放回去,以便其他人可以继续使用已经开辟的内存空间。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public final class ByteArrayPool {
private static final String TAG = "ByteArrayPool";
private static final int TEMP_BYTES_SIZE = 64 * 1024; // 64 KB.
private static final int MAX_SIZE = 2 * 1048 * 1024; // 512 KB.
private static final int MAX_BYTE_ARRAY_COUNT = MAX_SIZE / TEMP_BYTES_SIZE;
private final Queue<byte[]> tempQueue = Util.createQueue(0);
private static final ByteArrayPool BYTE_ARRAY_POOL = new ByteArrayPool();
public static ByteArrayPool get() {
return BYTE_ARRAY_POOL;
}
private ByteArrayPool() { }
public void clear() {
synchronized (tempQueue) {
tempQueue.clear();
}
}
// 从队列中取出一个 byte[],没有就创建新的返回
public byte[] getBytes() {
byte[] result;
synchronized (tempQueue) {
result = tempQueue.poll();
}
if (result == null) {
result = new byte[TEMP_BYTES_SIZE];
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Created temp bytes");
}
}
return result;
}
// 使用完了放回队列,但是要维持总内存不超过 max
public boolean releaseBytes(byte[] bytes) {
if (bytes.length != TEMP_BYTES_SIZE) {
return false;
}
boolean accepted = false;
synchronized (tempQueue) {
if (tempQueue.size() < MAX_BYTE_ARRAY_COUNT) {
accepted = true;
tempQueue.offer(bytes);
}
}
return accepted;
}
}
------ 本文结束 🎉🎉 谢谢观看  ------