【Android进阶笔记】内存优化(内存泄漏优化)

2,346 阅读5分钟

1. 内存抖动

短时间内创建大量对象,挤占 Eden 区,导致频繁 MinorGC,内存就会发生抖动。

1.1. 现象

MemoryProfile内存图为锯齿状,并伴随大量的白色垃圾桶。

常见引发的问题就是在最小一层循环里面创建大量对象

String s = "";
for (int i = 0; i < 10000; i++) {
    s += "," + i;  // 创建大量的字符串常量对象
}

1.2. 避免

通常会采取一些办法避免这类问题:

  • 尽量避免在循环体内创建对象,应该把对象创建移到循环体外。
  • 避免在自定义View的onMeasureonLayoutonDraw等方法中调用invalidate()方法(通知主线程当前画面已过期,再线程空闲时重绘),且不要创建大量对象。
  • 当需要大量使用 Bitmap 的时候,试着把它们缓存在数组中实现复用。
  • 对于能够复用的对象,同理可以使用对象池将它们缓存起来。
  • 字符串拼接,改用StringBufferStringBuilder来执行。

2. 内存泄漏

长生命周期对象持有短生命周期对象的引用,导致分配的内存空间没有及时回收,使得可使用内存越来越少。

2.1. 内存泄漏的检测与定位

Memory Profiler

使用Memory Profiler 捕获堆转储之后,筛选出我们自己的对象。

在退出 LeakActivity 回到 MainActivity 之后,并且手动出发了一次GC后,LeakActivity 还在内存当中,表是发生了内存泄漏。

LeakActivity$1LeakActivity$2 分别表示 LeakActivity 中第1个和第2个匿名内部类(编译的时候由系统自动起名为外部类名$1.class)对 LeakActivity 的引用。

右边四列分别表示

  • Allocations:Java 堆中的实例个数。
  • Native Size:native 层分配的内存大小。
  • Shallow Size:Java 堆中分配实际大小。
  • Retained Size:这个类的所有实例保留的内存总大小(并非实际大小)。

如果 Shallow Size 和 Retained Size 都非常小并且相等,都可以认为是已经被回收的对象(空的 Activity,大概是270)。但是 LeakActivity 明显不是这样,也说明了 LeakActivity 发生了内存泄漏。

左边选中 LeakActivity,右边 Instance 中一个一个去查看,现在只有一处,点击第一个,默认 Reference 以 Depth 排序,只看 Reference Depth 小于 Instance Depth 的行。右键 -> Jump to Source,则自动定位到代码。

如上图,展开发现是Handler引起的泄漏。

2.2. 常见的内存泄漏

2.2.1. 静态变量内存泄漏

stact 变量所指向的引用,虚拟机会一直保留该引用对象,除非把 static 变量设置为 null

public class ToastUtil {
    private static Toast toast;
    public static void show(Context context, String message) {
        if (toast == null) {
            // 静态变量toast会一直持有Toast对象的引用,Toast对象将不会被回收
            // 如果context为Activity Context,相应的Activity也会泄漏
            toast = Toast.makeText(context, message, Toast.LENGTH_SHORT);
        } else {
            toast.setText(message);
        }      
        toast.show();
    }
}

解决办法①:makeText() 中的参数 context 改为context.getApplicationContext(),长生命周期的 static Toast 持有的也是长生命周期的 Applaiction Context,就不会内存泄漏了。

private static Activity sActivity;
@Override
protected void onCreate(Bundle savedInstanceState) {
    // 静态变量sActivity会在APP生命周期内一直持有Activity的实例对象,内存泄漏
    sActivity = this;
}

解决办法②:在 Acticity 生命周期结束时,让静态变量断开与其引用关系。即在 onDestory() 回调方法中,执行 sActivity = null

2.2.2. 非静态内部类、匿名内部类内存泄漏

非静态内部类对象会持有外部类对象的引用,外部类的生命周期结束,但是仍可能被内部类所引用。

  • 编译器会自动为内部类添加一个成员变量, 用来指向外部类对象的引用。
  • 编译器会自动为内部类的构造方法添加一个参数,用来给上面的成员变量赋值。
  • 在调用内部类的构造方法时,会默认传入外部类的引用。

通过 Java在线反编译工具,并使用 Fernflower 反编译器,可以查看反编译内容:

public class Test {

    // ① 匿名内部类实现接口并重写接口方法
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
        }
    };

    // ② 匿名内部类重写类方法
    Object object = new Object() {
    };

    // ③ 静态常量 + 匿名内部类实现接口并重写接口方法
    static final Runnable runnable2 = new Runnable() {
        @Override
        public void run() {
        }
    };

    // ④ 非静态内部类
    class InnerClass {
        void test() {
            runnable.run();
        }
    }

    // ⑤ 静态内部类
    static class InnerClass2 {
    }
}
// ⑥ 外部类
class OuterClass {
}

Test$1.classnew Runnable() {} 的内容:

import com.company.Test;
class Test$1 implements Runnable {
    // 持有外部类的引用
    final Test this$0;
    // 外部类的引用通过构造方法传入
    Test$1(Test this$0) {
        this.this$0 = this$0;
    }

    public void run() {}
}

Test$2.classnew Object() {} 的内容:

import com.company.Test;
class Test$2 {
    // 持有外部类的引用
    final Test this$0;
    // 外部类的引用通过构造方法传入
    Test$1(Test this$0) {
        this.this$0 = this$0;
    }
}

Test$3.classstatic final Runnable runnable2 = new Runnable() {} 的内容:

class Test$3 implements Runnable {
    public void run() {}
}

Test$InnerClass.classclass InnerClass {} 的内容:

import com.company.Test;
class Test$InnerClass {
	// 持有外部类的引用
    final Test this$0;
	// 外部类的引用通过构造方法传入
    Test$InnerClass(Test this$0) {
        this.this$0 = this$0;
    }
	// 通过外部类的引用,直接访问外部类的成员
    void test() {
        this.this$0.runnable.run();
    }
}

Test$InnerClass2.classstatic class InnerClass2 {} 的内容:

class Test$InnerClass2 {
    // 不会持有外部类的引用
    Test$InnerClass2() {
    }
}

OuterClass.classclass OuterClass {} 的内容:

class OuterClass {
	// 不会持有外部类的引用
    OuterClass() {
    }
}

【非静态内部类】

class Outer {
    // 非静态内部类,默认持有外部类的引用,方法体内可直接调用外部类的成员和方法
    class Inner1 { /* ... */ }
    // 静态内部类,不会持有外部类的引用,方法体内不可直接调用外部类的成员和方法
    static class Inner2 { /* ... */ }
}

【匿名内部类】

  • 直接 new interface、直接 new abstract class 、直接 new class 后并重写(或只写大括号,但是不重写)其中的成员方法,而不是手动新建一个类继承或实现它们再重写方法。因为没有显式的指名新的类名,编译器会自动为他们分别生成一个匿名内部类。
  • 编译器会自动将匿名内部类创建成非静态内部类,并取名为 外部类名$1.class、``外部类名$2.class`等以此类推。
// 直接new一个接口,并重写成员方法
new Runnable() {
    @Override
    public void run() { /* ... */ }
};
// 直接new一个抽象类,并重写成员方法
new AbstractSet<Object>(){
    @NonNull
    @Override
    public Iterator<Object> iterator() { /* ... */ }
    @Override
    public int size() { /* ... */ }
};
// 直接new一个类,并重写成员方法
new Handler(){
    @Override
    public void handleMessage(@NonNull Message msg) { /* ... */ }
};
// 直接new一个类,只写大括号,但不重写任何成员方法。抽象类、接口也同理
new Handler() {};

【常见的 Handler 内存泄漏】

public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) { /* ... */ }
// new Handler()并重写方法,编译器会自动生成MainActivity$1.class的非静态内部类
// 因此new出的这个对象会默认持有MainActivity对象的引用,从而导致内存泄漏
// Activity退出后,msg可能还在MessageQueue中没被处理完毕,则Activity对象不能回收
    };
}

解决办法:改为静态内部类+弱引用,因为静态内部类不需依赖外部类的成员和方法。

类似的还有直接 new Thread,new Runnable,new AsyncTask,new Listiner 等也可能造成内存泄漏。

public class MainActivity extends AppCompatActivity {
    private Handler mHandler;
    // 静态常量不会持有外部类的引用,不会内存泄漏,但是会生成匿名内部类
    private static final Runnable sRunnable = new Runnable() {
        @Override
        public void run() { /* ... */ }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        mHandler = new MyHandler(this);
    }
    @Override
    protected void onDestroy() {
        // 虽然不会导致Activity泄漏,但msg可能还在MessageQueue中,最好移除掉
        mHandler.removeCallbacksAndMessages(null);
    }
    // 自己写一个静态内部类,不让编译器自动生成非静态内部类
    private static class MyHandler extends Handler {
        // 使用弱引用持有外部类的引用
        private WeakReference<MainActivity> activityWeakReference;
        public MyHandler(MainActivity activity) {
            activityWeakReference = new WeakReference<>(activity);
        }
        @Override
        public void handleMessage(Message msg) { /* ... */ }
    }
}

2.2.3. 单例模式内存泄漏

单例的静态特性使得它的生命周期同App的生命周期一样长,如果单例一直持有一个短生命周期的引用,就容易导致内存泄漏。

public class Singleton {
    // 静态变量一直持有Singleton对象 
    private static Singleton singleton = null;
    // Singleton对象又持有Context对象
    private Context mContext;
    private Singleton(Context mContext) {
        this.mContext = mContext;
    }
    public static Singleton getSingleton(Context context){
        // 如果context为Activity Context,则Activity就始终无法回收,导致内存泄漏
        if (null == singleton) singleton = new Singleton(context);
        return singleton;
    }
}

public class Singleton {
    // 静态变量一直持有Singleton对象 
    private volatile static Singleton singleton = null;
    // Singleton对象又持有Context对象
    private Context mContext;
    private Singleton(Context mContext) {
        this.mContext = mContext;
    }
    public static Singleton getSingleton(Context context){
        if (singleton == null) {
            synchronized (Singleton.class){
                if (singleton == null) {
                    // 如果context为Activity,则Activity内存泄漏
                    singleton = new Singleton(context);
                }
            }
        }
        return singleton;
    }
}

解决办法①: 调用的时候传入 ApplicationContext 或改为 this.mContext = mContext.getApplicationContext();

解决办法②: 将该属性的引用方式改为弱引用。

public class Singleton {
    private volatile static Singleton singleton = null;
    // 使用弱引用,使Singleton对象持有Context对象
    private WeakReference<Context> mContextWeakRef;
    private Singleton(Context mContext) {
        this.mContextWeakRef = new WeakReference<Context>(mContext);
    }
    public static Singleton getSingleton(Context context){
        if (singleton == null) {
            synchronized (Singleton.class){
                if (singleton == null) {
                    // 如果context为Activity,则Activity内存泄漏
                    singleton = new Singleton(context);
                }
            }
        }
        return singleton;
    }
}

2.2.4. 系统服务内存泄漏

使用系统服务时,会调用 getSystemService() 方法,还有可能注册系统服务的监听器,这两处都可能引起内存泄漏。类似的还有 BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap 等,也需要在 onDestry 中及时的关闭、注销或者释放内存。

// 直接调用getSystemService方法,往往是Activity,所以系统服务会一直持有它的引用
SensorManager sm = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
Sensor sensor = sm.getDefaultSensor(Sensor.TYPE_ALL);
// 设置监听器传入了Activity,同样会被系统服务一直持有它的引用
sm.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);

解决办法:

  • 使用 getApplicationContext().getSystemService() 获取系统服务。
  • onDestory 里面注销监听器,断开系统服务与 Activity 的引用关系。

2.3. 防止内存泄漏

  • 需要 Context 时优先考虑 ApplicationContext
动作ApplicationServiceActivity
启动Activity⭕️ New Task⭕️ New Task⭕️
弹出Dialog⭕️
实例化布局⭕️⭕️⭕️
启动Service⭕️⭕️⭕️
绑定Service⭕️⭕️⭕️
发送广播⭕️⭕️⭕️
注册广播接收器⭕️⭕️⭕️
加载res资源⭕️⭕️⭕️
  • 生命周期比 Activity 长的内部类对象,且内部类使用了外部类的成员变量,则

    1. 手动将内部类改写成静态内部类;
    2. 在静态内部类中使用弱引用来引用外部类的成员变量;
  • 不再使用的对象,显式地赋值为 null,例如 Bitmap,先调用 recycle() 后,在赋值为 null

2.4. LeakCanary 工具

在 dependencies 中加入 debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6' ,无需再添加任何调用代码。

当有内存泄漏发生时,LogCat 就会打印相关信息:

====================================
    HEAP ANALYSIS RESULT
    ====================================
    1 APPLICATION LEAKS
    
    References underlined with "~~~" are likely causes.
    Learn more at https://squ.re/leaks.
    
    333202 bytes retained by leaking objects
    Displaying only 1 leak trace out of 2 with the same signature
    Signature: d01f393e64386a95c368a4c4209c91c136f7720
    ┬───
    │ GC Root: Local variable in native code
    │
    ├─ android.os.HandlerThread instance
    │    Leaking: NO (PathClassLoader↓ is not leaking)
    │    Thread name: 'LeakCanary-Heap-Dump'
    │    ↓ Thread.contextClassLoader
    ├─ dalvik.system.PathClassLoader instance
    │    Leaking: NO (ToastUtil↓ is not leaking and A ClassLoader is never leaking)
    │    ↓ ClassLoader.runtimeInternalObjects
    ├─ java.lang.Object[] array
    │    Leaking: NO (ToastUtil↓ is not leaking)
    │    ↓ Object[].[188]
    ├─ com.example.androidtest.ToastUtil class
    │    Leaking: NO (a class is never leaking)
    │    ↓ static ToastUtil.toast
    │                       ~~~~~
    ├─ android.widget.Toast instance
    │    Leaking: YES (This toast is done showing (Toast.mTN.mWM != null && Toast.mTN.mView == null))
    │    Retaining 166.6 kB in 1384 objects
    │    mContext instance of com.example.androidtest.LeakActivity with mDestroyed = true
    │    ↓ Toast.mContext
    ╰→ com.example.androidtest.LeakActivity instance
          Leaking: YES (ObjectWatcher was watching this because com.example.androidtest.LeakActivity received
          Activity#onDestroy() callback and Activity#mDestroyed is true)
          Retaining 69.9 kB in 1256 objects
          key = 1f27bf38-b690-4983-b3f1-64bd991e7f9f
          watchDurationMillis = 7553
          retainedDurationMillis = 2549
          mApplication instance of android.app.Application
          mBase instance of android.app.ContextImpl
    ====================================
    0 LIBRARY LEAKS
    
    A Library Leak is a leak caused by a known bug in 3rd party code that you do not have control over.
    See https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#4-categorizing-leaks
    ====================================
    0 UNREACHABLE OBJECTS
    
    An unreachable object is still in memory but LeakCanary could not find a strong reference path
    from GC roots.
    ====================================
    METADATA
    
    Please include this in bug reports and Stack Overflow questions.
    
    Build.VERSION.SDK_INT: 29
    Build.MANUFACTURER: samsung
    LeakCanary version: 2.6
    App process name: com.example.androidtest
    Stats: LruCache[maxSize=3000,hits=2715,misses=45083,hitRate=5%]
    RandomAccess[bytes=2304319,reads=45083,travel=16121653814,range=16075212,size=20801120]
    Heap dump reason: 5 retained objects, app is visible
    Analysis duration: 3857 ms
    Heap dump file path: /data/user/0/com.example.androidtest/cache/leakcanary/2021-03-02_13-54-08_891.hprof
    Heap dump timestamp: 1614664454960
    Heap dump duration: 1642 ms
    ====================================

原理

  • RefWatcher.watch()创建了一个KeyedWeakReference用于去观察对象。
  • 然后,在后台线程中,它会检测引用是否被清除了,并且是否没有触发GC。
  • 如果引用仍然没有被清除,那么它将会把堆栈信息保存在文件系统中的.hprof文件里。
  • HeapAnalyzerService被开启在一个独立的进程中,并且HeapAnalyzer使用了HAHA开源库解析了指定时刻的堆栈快照文件heap dump。
  • heap dump中,HeapAnalyzer根据一个独特的引用key找到了KeyedWeakReference,并且定位了泄露的引用。
  • HeapAnalyzer为了确定是否有泄露,计算了到GC Roots的最短强引用路径,然后建立了导致泄露的链式引用。
  • 这个结果被传回到app进程中的DisplayLeakService,然后一个泄露通知便展现出来了。