Android性能优化—内存抖动和内存泄漏

LMK机制;
内存抖动、内存泄漏与内存溢出
优化的结果:使得app流畅不卡

内存抖动

短时间内有大量对象创建与销毁,它伴随着频繁的GC。
在这里插入图片描述
比较典型的就是字符串的拼接造成内存抖动。

比如:

String str = "";
for(int i=0; i< 10; i++) {
	str += i;
}

+=操作会编译成StringBuilder对象,然后调用StringBuilder的append方法进行拼接。
所以上述代码会创建10个StringBuilder对象,每执行一次+操作都会新创建一个StringBuilder对象。

优化方法:避免使用+或者+=操作,使用StringBuilder来实现字符串的拼接

StringBuilder str = new StringBuilder();
for (int i = 0; i < 10; i++) {
    str.append(i);
}

内存抖动实战

一个不断旋转的试图,优化前的代码:

public class IOSStyleLoadingView1 extends View {

    private final Context context;
    private double radius;
    private double insideRadius;
    private float northwestXStart;
    private float northwestYStart;
    private float northXStart;
    private float northYStart;
    private float notheastXStart;
    private float notheastYStart;
    private float eastXStart;
    private float eastYStart;
    private float southeastXStart;
    private float southeastYStart;
    private float southXStart;
    private float southYStart;
    private float southwestXStart;
    private float southwestYStart;
    private float westXStart;
    private float westYStart;

    private float northwestXEnd;
    private float northwestYEnd;
    private float northXEnd;
    private float northYEnd;
    private float notheastXEnd;
    private float notheastYEnd;
    private float eastXEnd;
    private float eastYEnd;
    private float southeastXEnd;
    private float southeastYEnd;
    private float southXEnd;
    private float southYEnd;
    private float southwestXEnd;
    private float southwestYEnd;
    private float westXEnd;
    private float westYEnd;

    private int currentColor = 7;

    String color[] = new String[]{
            "#a5a5a5",
            "#b7b7b7",
            "#c0c0c0",
            "#c9c9c9",
            "#d2d2d2",
            "#dbdbdb",
            "#e4e4e4",
            "#e4e4e4"
    };

    public IOSStyleLoadingView1(Context context) {
        this(context,null,0);
    }

    public IOSStyleLoadingView1(Context context, AttributeSet attrs) {
        this(context,null,0);
    }

    public IOSStyleLoadingView1(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        radius = UIKits.dip2Px(context, 9);
        insideRadius = UIKits.dip2Px(context, 5);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStrokeWidth(UIKits.dip2Px(context, 2));
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeCap(Paint.Cap.ROUND);

        Path p0 = new Path();
        paint.setColor(Color.parseColor(color[0]));//Color.parseColor方法会调用String的substring()方法,substring()方法会产生新的String对象
        p0.moveTo(northwestXStart, northwestYStart);
        p0.lineTo(northwestXEnd, northwestYEnd);
        canvas.drawPath(p0, paint);

        Path p1 = new Path();
        paint.setColor(Color.parseColor(color[1]));
        p1.moveTo(northXStart, northYStart);
        p1.lineTo(northXEnd, northYEnd);
        canvas.drawPath(p1, paint);

        Path p2 = new Path();
        paint.setColor(Color.parseColor(color[2]));
        p2.moveTo(notheastXStart, notheastYStart);
        p2.lineTo(notheastXEnd, notheastYEnd);
        canvas.drawPath(p2, paint);

        Path p3 = new Path();
        paint.setColor(Color.parseColor(color[3]));
        p3.moveTo(eastXStart, eastYStart);
        p3.lineTo(eastXEnd, eastYEnd);
        canvas.drawPath(p3, paint);

        Path p4 = new Path();
        paint.setColor(Color.parseColor(color[4]));
        p4.moveTo(southeastXStart, southeastYStart);
        p4.lineTo(southeastXEnd, southeastYEnd);
        canvas.drawPath(p4, paint);

        Path p5 = new Path();
        paint.setColor(Color.parseColor(color[5]));
        p5.moveTo(southXStart, southYStart);
        p5.lineTo(southXEnd, southYEnd);
        canvas.drawPath(p5, paint);

        Path p6 = new Path();
        paint.setColor(Color.parseColor(color[6]));
        p6.moveTo(southwestXStart, southwestYStart);
        p6.lineTo(southwestXEnd, southwestYEnd);
        canvas.drawPath(p6, paint);

        Path p7 = new Path();
        paint.setColor(Color.parseColor(color[7]));
        p7.moveTo(westXStart, westYStart);
        p7.lineTo(westXEnd, westYEnd);
        canvas.drawPath(p7, paint);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        double centerX = getMeasuredWidth() / 2;
        double centerY = getMeasuredHeight() / 2;
        double leg = radius * Math.cos(Math.PI / 4);
        double insideLeg = insideRadius * Math.cos(Math.PI / 4);


        northwestXStart = (float) (centerX - leg);
        northwestYStart = (float) (centerY - leg);
        northXStart = (float) centerX;
        northYStart = (float) (centerY - radius);
        notheastXStart = (float) (centerX + leg);
        notheastYStart = (float) (centerY - leg);
        eastXStart = (float) (centerX + radius);
        eastYStart = (float) centerY;
        southeastXStart = (float) (centerX + leg);
        southeastYStart = (float) (centerY + leg);
        southXStart = (float) centerX;
        southYStart = (float) (centerY + radius);
        southwestXStart = (float) (centerX - leg);
        southwestYStart = (float) (centerY + leg);
        westXStart = (float) (centerX - radius);
        westYStart = (float) centerY;

        northwestXEnd = (float) (centerX - insideLeg);
        northwestYEnd = (float) (centerY - insideLeg);
        northXEnd = (float) centerX;
        northYEnd = (float) (centerY - insideRadius);
        notheastXEnd = (float) (centerX + insideLeg);
        notheastYEnd = (float) (centerY - insideLeg);
        eastXEnd = (float) (centerX + insideRadius);
        eastYEnd = (float) centerY;
        southeastXEnd = (float) (centerX + insideLeg);
        southeastYEnd = (float) (centerY + insideLeg);
        southXEnd = (float) centerX;
        southYEnd = (float) (centerY + insideRadius);
        southwestXEnd = (float) (centerX - insideLeg);
        southwestYEnd = (float) (centerY + insideLeg);
        westXEnd = (float) (centerX - insideRadius);
        westYEnd = (float) centerY;
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        startAnimation();
    }

    private ValueAnimator valueAnimator;

    public void startAnimation() {
        valueAnimator = ValueAnimator.ofInt(7, 0);
        valueAnimator.setDuration(400);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                if ((int) animation.getAnimatedValue() != currentColor) {
                    String b[] = new String[color.length];//移动后的数组
                    for (int c = 0, size = color.length - 1; c < size; c++) {
                        b[c + 1] = color[c];
                    }
                    b[0] = color[color.length - 1];
                    color = b;
                    invalidate();
                    currentColor = (int) animation.getAnimatedValue();
                }
            }
        });
        valueAnimator.start();
    }

}

利用Android Studio内置的Profiler工具查看内存使用情况:
在这里插入图片描述
可以看到内存产生抖动,并且随着程序的运行,内存一直在增加,选择一段时间分析内存中的对象:
在这里插入图片描述
可以看到内存中的有大量的Path对象和String对象,一般来说是有异常的,分析代码看看是否能避免这种情况。

可以在onDraw方法里进行优化,优化后的代码:

/**
 * 优化后的IOSStyleLoadingView
 * onDraw方法里不创建Path和Paint对象,不调用Color.parseColor创建String对象
 *
 */
public class IOSStyleLoadingView extends View {

    private final Context context;
    private double radius;
    private double insideRadius;
    private float northwestXStart;
    private float northwestYStart;
    private float northXStart;
    private float northYStart;
    private float notheastXStart;
    private float notheastYStart;
    private float eastXStart;
    private float eastYStart;
    private float southeastXStart;
    private float southeastYStart;
    private float southXStart;
    private float southYStart;
    private float southwestXStart;
    private float southwestYStart;
    private float westXStart;
    private float westYStart;

    private float northwestXEnd;
    private float northwestYEnd;
    private float northXEnd;
    private float northYEnd;
    private float notheastXEnd;
    private float notheastYEnd;
    private float eastXEnd;
    private float eastYEnd;
    private float southeastXEnd;
    private float southeastYEnd;
    private float southXEnd;
    private float southYEnd;
    private float southwestXEnd;
    private float southwestYEnd;
    private float westXEnd;
    private float westYEnd;

    private int currentColor = 7;

    String color[] = new String[]{
            "#a5a5a5",
            "#b7b7b7",
            "#c0c0c0",
            "#c9c9c9",
            "#d2d2d2",
            "#dbdbdb",
            "#e4e4e4",
            "#e4e4e4"
    };

    int[] colors = new int[8];


    public IOSStyleLoadingView(Context context) {
        this(context, null, 0);
    }

    public IOSStyleLoadingView(Context context, AttributeSet attrs) {
        this(context, null, 0);
    }

    /**
     * 提前在构造方法里调用Color.parseColor解析好数据,而不是在onDraw方法里频繁调用
     *
     *
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    public IOSStyleLoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        radius = UIKits.dip2Px(context, 9);
        insideRadius = UIKits.dip2Px(context, 5);

        for (int i = 0; i < color.length; i++) {
            colors[i] = Color.parseColor(color[i]);
        }

        paint.setAntiAlias(true);
        paint.setStrokeWidth(UIKits.dip2Px(context, 2));
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeCap(Paint.Cap.ROUND);
    }

    Path path = new Path();
    Paint paint = new Paint();

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);


        path.reset();
        paint.setColor(colors[(currentColor++) % 8]);
        path.moveTo(northwestXStart, northwestYStart);
        path.lineTo(northwestXEnd, northwestYEnd);
        canvas.drawPath(path, paint);

        path.reset();
        paint.setColor(colors[(currentColor++) % 8]);
        path.moveTo(northXStart, northYStart);
        path.lineTo(northXEnd, northYEnd);
        canvas.drawPath(path, paint);

        path.reset();
        paint.setColor(colors[(currentColor++) % 8]);
        path.moveTo(notheastXStart, notheastYStart);
        path.lineTo(notheastXEnd, notheastYEnd);
        canvas.drawPath(path, paint);

        path.reset();
        paint.setColor(colors[(currentColor++) % 8]);
        path.moveTo(eastXStart, eastYStart);
        path.lineTo(eastXEnd, eastYEnd);
        canvas.drawPath(path, paint);

        path.reset();
        paint.setColor(colors[(currentColor++) % 8]);
        path.moveTo(southeastXStart, southeastYStart);
        path.lineTo(southeastXEnd, southeastYEnd);
        canvas.drawPath(path, paint);

        path.reset();
        paint.setColor(colors[(currentColor++) % 8]);
        path.moveTo(southXStart, southYStart);
        path.lineTo(southXEnd, southYEnd);
        canvas.drawPath(path, paint);

        path.reset();
        paint.setColor(colors[(currentColor++) % 8]);
        path.moveTo(southwestXStart, southwestYStart);
        path.lineTo(southwestXEnd, southwestYEnd);
        canvas.drawPath(path, paint);

        path.reset();
        paint.setColor(colors[(currentColor++) % 8]);
        path.moveTo(westXStart, westYStart);
        path.lineTo(westXEnd, westYEnd);
        canvas.drawPath(path, paint);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        double centerX = getMeasuredWidth() / 2;
        double centerY = getMeasuredHeight() / 2;
        double leg = radius * Math.cos(Math.PI / 4);
        double insideLeg = insideRadius * Math.cos(Math.PI / 4);


        northwestXStart = (float) (centerX - leg);
        northwestYStart = (float) (centerY - leg);
        northXStart = (float) centerX;
        northYStart = (float) (centerY - radius);
        notheastXStart = (float) (centerX + leg);
        notheastYStart = (float) (centerY - leg);
        eastXStart = (float) (centerX + radius);
        eastYStart = (float) centerY;
        southeastXStart = (float) (centerX + leg);
        southeastYStart = (float) (centerY + leg);
        southXStart = (float) centerX;
        southYStart = (float) (centerY + radius);
        southwestXStart = (float) (centerX - leg);
        southwestYStart = (float) (centerY + leg);
        westXStart = (float) (centerX - radius);
        westYStart = (float) centerY;

        northwestXEnd = (float) (centerX - insideLeg);
        northwestYEnd = (float) (centerY - insideLeg);
        northXEnd = (float) centerX;
        northYEnd = (float) (centerY - insideRadius);
        notheastXEnd = (float) (centerX + insideLeg);
        notheastYEnd = (float) (centerY - insideLeg);
        eastXEnd = (float) (centerX + insideRadius);
        eastYEnd = (float) centerY;
        southeastXEnd = (float) (centerX + insideLeg);
        southeastYEnd = (float) (centerY + insideLeg);
        southXEnd = (float) centerX;
        southYEnd = (float) (centerY + insideRadius);
        southwestXEnd = (float) (centerX - insideLeg);
        southwestYEnd = (float) (centerY + insideLeg);
        westXEnd = (float) (centerX - insideRadius);
        westYEnd = (float) centerY;
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        startAnimation();
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (valueAnimator != null) {
            valueAnimator.removeAllUpdateListeners();//移除监听器,否则会造成内存泄漏
            valueAnimator.cancel();//取消动画
        }
    }

    private ValueAnimator valueAnimator;

    public void startAnimation() {
        valueAnimator = ValueAnimator.ofInt(7, 0);
        valueAnimator.setDuration(400);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                if ((int) animation.getAnimatedValue() != currentColor) {
//                    String b[] = new String[color.length];//移动后的数组
//                    for (int c = 0, size = color.length - 1; c < size; c++) {
//                        b[c + 1] = color[c];
//                    }
//                    b[0] = color[color.length - 1];
//                    color = b;
                    invalidate();
                    currentColor = (int) animation.getAnimatedValue();
                }
            }
        });
        valueAnimator.start();
    }

}

再次查看内存使用情况:

在这里插入图片描述
可以看到内存平稳,选择一段时间分析内存中的对象:在这里插入图片描述
可以看到内存中不会再有大量的对象。

预防内存抖动

1.避免在循环中创建对象;
2.避免在频繁调用的方法中创建对象,如View的onDraw方法;
3.允许复用的情况下,使用对象池进行缓存,如:Handler的Message单链表(obtain);

内存抖动会造成的问题:卡顿和OOM

卡顿

在这里插入图片描述
内存抖动会引起频繁的gc,gc是会STW的,会暂停用户线程。

OOM

在这里插入图片描述
CMS垃圾回收器老年代是标记-清除算法:不会移动存活的对象,会产生内存碎片。

像上述图中的内存,虽然有很多内存可用,但却是不连续的,如果申请连续的10个字节(假设图中一个空格代表一个字节)的内存就会产生OOM,因为没有连续的10个字节的可用内存。

比如申请bitmap时就很可能产生OOM。

内存泄漏

程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费。
长生命周期对象持有短生命周期对象强引用,从而导致短生命周期对象无法被回收!

注意这里是持有对象的强引用,如果是持有对象的软引用或者是弱引用或者是虚引用则不会造成内存泄漏。

可达性分析法

通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所有的引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。
在这里插入图片描述

GC Roots

在Java语言中可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。
  • JVM的内部引用(class对象、异常对象NullPointException、OutofMemoryError,系统类加载器)。
  • 所有被同步锁(synchronized关键)持有的对象。
  • JVM内部的JMXBean、JVMTI中注册的回调、本地代码缓存等
  • JVM实现中的“临时性”对象,跨代引用的对象(在使用分代模型回收只回收部分代时)

也可以参考:
Garbage Collection Roots

Garbage Collection Roots
A garbage collection root is an object that is accessible from outside the heap. The following reasons make an object a GC root:

System Class
Class loaded by bootstrap/system class loader. For example, everything from the rt.jar like java.util.* .
JNI Local
Local variable in native code, such as user defined JNI code or JVM internal code.
JNI Global
Global variable in native code, such as user defined JNI code or JVM internal code.
Thread Block
Object referred to from a currently active thread block.
Thread
A started, but not stopped, thread.
Busy Monitor
Everything that has called wait() or notify() or that is synchronized. For example, by calling synchronized(Object) or by entering a synchronized method. Static method means class, non-static method means object.
Java Local
Local variable. For example, input parameters or locally created objects of methods that are still in the stack of a thread.
Native Stack
In or out parameters in native code, such as user defined JNI code or JVM internal code. This is often the case as many methods have native parts and the objects handled as method parameters become GC roots. For example, parameters used for file/network I/O methods or reflection.
Finalizable
An object which is in a queue awaiting its finalizer to be run.
Unfinalized
An object which has a finalize method, but has not been finalized and is not yet on the finalizer queue.
Unreachable
An object which is unreachable from any other root, but has been marked as a root by MAT to retain objects which otherwise would not be included in the analysis.
Java Stack Frame
A Java stack frame, holding local variables. Only generated when the dump is parsed with the preference set to treat Java stack frames as objects.
Unknown
An object of unknown root type. Some dumps, such as IBM Portable Heap Dump files, do not have root information. For these dumps the MAT parser marks objects which are have no inbound references or are unreachable from any other root as roots of this type. This ensures that MAT retains all the objects in the dump.

软引用,弱引用

软引用:定义一些还有用但并非必须的对象。对于软引用关联的对象,GC不会直接回收,而是在系统将要内存溢出之前才会触发GC将这些对象进行回收。

弱引用:同样定义非必须对象。被弱引用关联的对象在GC执行时会被直接回收。
在这里插入图片描述

Handler导致的内存泄露的场景

我们开发中经常会看到这句提示:This Handler class should be static or leaks might occur.
大致意思就是说:Handler类应该定义成静态类,否则可能会发生内存泄露。

具体提示内容如下:
This Handler class should be static or leaks might occur.

Issue: Ensures that Handler classes do not hold on to a reference to an outer class.

In Android, Handler classes should be static or leaks might occur. Messages enqueued on the application thread’s MessageQueue also retain their target Handler. If the Handler is an inner class, its outer class will be retained as well. To avoid leaking the outer class, declare the Handler as a static nested class with a WeakReference to its outer class。

简单翻译:
Handler类应该定义成静态类,否则可能会发生内存泄露。
确保Handler类不会持有外部类的引用。
在Android中, Handler类应是static的,否则可能会发生泄漏。应用线程的消息队列中的Message也持有对Handler对象的引用。如果Handler是内部类,那么(回收内存时)其外部类也会被保留。(使用了Handler的Service和Activity也就无法被回收,这就可能导致内存泄露。) 为了避免泄露其外部类,可将Handler声明为static并持有其外部类的WeakReference(弱引用)。

简单解释下弱引用:
JVM的垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了某对象如果只被弱引用持有,不管当前内存空间是否足够都会回收该对象。简单理解就是JVM没有那么强的要求希望垃圾回收器将只被弱引用持有的对象保留在内存中。

public class MemoryLeakActivity extends Activity {

    private int i = 10;//成员属性i

    private Handler mHandler = new Handler();


    /**
     * 1.如果不传外部类引用则不能访问外部类的成员属性i
     */
    static class Runnable1 implements Runnable {

        @Override
        public void run() {
            //但是静态内部类不能访问外部类的成员属性i,怎么办?将外部类引用传递给静态内部类
            //System.out.println("i=" + i);
        }
    }

    /**
     * 2.如果直接传递外部类引用则依然会造成内存泄露
     */
    static class Runnable2 implements Runnable {
        //但是不能直接将外部类引用传递进来,因为虽然可以访问外部类的成员属性,但是还是会造成内存泄露
        MemoryLeakActivity activity;

        public Runnable2(MemoryLeakActivity activity) {
            this.activity = activity;
        }

        @Override
        public void run() {
            System.out.println(activity.i);
        }
    }


    /**
     * 3.正确解决方案:使用弱引用,即定义一个静态内部类的成员属性,持有外部类的弱引用
     */
    static class Runnable3 implements Runnable {
        /**
         * 注意这里应该使用WeakReference,而不是SoftReference,
         * 虽然使用SoftReference也不会造成OOM,但是当我们退出Activity时是希望Activity尽快被回收的,
         * 所以使用WeakReference更合适,因为被WeakReference关联的对象在GC执行时会被直接回收,
         * 而对于SoftReference关联的对象,GC不会直接回收,而是在系统将要内存溢出之前才会触发GC将这些对象进行回收。
         */
        WeakReference<MemoryLeakActivity> activityWeakReference;

        public Runnable3(MemoryLeakActivity activity) {
            this.activityWeakReference = new WeakReference<MemoryLeakActivity>(activity);
        }

        @Override
        public void run() {
            if (activityWeakReference.get() != null) {
                int i = activityWeakReference.get().i;
                System.out.println("i=" + i);
            }
        }
    }
    	
	/**
     * 4.正确解决方案:使用弱引用,即定义一个静态内部类的成员属性,持有外部类的弱引用
     * 定义静态的Handler
     */
    static class MyHandler extends Handler {

        /**
         * 注意这里应该使用WeakReference,而不是SoftReference,
         * 虽然使用SoftReference也不会造成OOM,但是当我们退出Activity时是希望Activity尽快被回收的,
         * 所以使用WeakReference更合适,因为被WeakReference关联的对象在GC执行时会被直接回收,
         * 而对于SoftReference关联的对象,GC不会直接回收,而是在系统将要内存溢出之前才会触发GC将这些对象进行回收。
         */
        WeakReference<MemoryLeakActivity> activityWeakReference;

        public MyHandler(MemoryLeakActivity activity) {
            this.activityWeakReference = new WeakReference<MemoryLeakActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            if (activityWeakReference.get() != null) {
                int i = activityWeakReference.get().i;
                System.out.println("i=" + i);
            }
        }

    }


    private Handler mHandler2 = new MyHandler(this);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memoryleak);


        /**
         * Handler的内存泄露场景
         */
        mHandler.postDelayed(new Runnable() {//这里创建了一个匿名内部类对象

            @Override
            public void run() {
                //匿名内部类对象持有外部类的引用(即持有Activity的引用),所以可以直接访问外部类的的成员属性i
                //所以会造成内存泄露
                System.out.println("i=" + i);
            }
        }, 1_000);


        //解决方案1:在onDestroy()方法里调用 mHandler.removeCallbacksAndMessages(null);将消息队列里的Message全部移除

        //解决方案2:使用静态内部类,静态内部类不会持有外部类的引用,而内部类和匿名内部类会持有外部类的引用
        mHandler.postDelayed(new Runnable3(this), 10_000);

    }


    @Override
    protected void onDestroy() {
        super.onDestroy();

        //解决方案1:在onDestroy()方法里调用 mHandler.removeCallbacksAndMessages(null);将消息队列里的Message全部移除
        mHandler.removeCallbacksAndMessages(null);
    }

Handler导致内存泄露的原因

相关Android知识点:

  • 1.在Java中,非静态内部类(即内部类或者匿名内部类)对象持有外部类的引用,而静态内部类不会持有外部类的引用。(这个例子中,即Handler对象持有外部类Activity的引用,所以可以直接访问外部类Activity的成员属性)。
  • 2.当App启动的时候,App的主线程会创建一个Looper对象(具体执行操作在ActivityThread类中),Looper对象中维护了一个MessageQueue消息队列,Looper会不断的从消息队列中循环取出消息进行处理。Looper对象在App的整个生命周期中都存在。
  • 3.当Handler对象在主线程中创建时,它会被关联到主线程的Looper对象,当调用Handler的sendMessage()方法发送消息即将该消息发送到所关联的Looper对象的消息队列中,消息队列中的消息有个target引用持有着发送该消息的Handler对象,当Looper对象处理消息时会调用Handler的handleMessage()方法进行消息回调处理。

Handler导致内存泄露的原因:
当Activity退出时,延迟消息继续在消息队列中排队等待处理,消息队列中的排队消息持有目标Handler的引用(即Message的target属性 Handler target;),如果Handler定义为内部类或者匿名内部类,则Handler对象会持有外部类Activity的引用,所以存在一条引用链:MessageQueue->Message->Handler->Activity,所以Activity不会被GC回收,发生了内存泄漏。

为了避免Activity发生内存泄漏,可以将Handler定义为static,我们知道,静态内部类不会持有外部类的引用。

国外论坛也有很多人讨论这个问题,相关内容可以参考:
https://www.androiddesignpatterns.com/2013/01/inner-class-handler-memory-leak.html
This Handler class should be static or leaks might occur: IncomingHandler
【Android】Handler Class Should be Static or Leaks Occur 警告原因及处理

Handler的内存泄露的解决方案

方法1:将Handler定义为static类,然后定义一个弱引用(WeakReference)属性来持有外部类Activity对象。
当然如果Handler发送的是Runnable对象,那么也可以将Runnable定义为static类,然后同样定义一个弱引用(WeakReference)属性来持有外部类Activity对象。

方法2:通过完善代码逻辑来避免内存泄露。既然是由于MessageQueue中的Message持有了Handler的引用,那么在Activity的onDestroy()方法里调用 mHandler.removeCallbacksAndMessages(null); 将MessageQueue里的Message全部移除就可以了。

MAT检测内存泄漏实战

1.打开Android studio的profiler的Memory视图
2.点击按钮进入SecondActivity,退出,再次点击按钮进入SecondActivity,退出。
在这里插入图片描述

3.点击dump按钮生成内存快照
在这里插入图片描述
dump完成后Android studio会开启一个新的视图显示当前内存中的所有对象
在这里插入图片描述
点击Arrange by package选项让所有类按照包进行组织,方便找到自己写的类,找到SecondActivity,可以看到内存中有2个SecondActivity对象仍然存活,但这能代表SecondActivity就发生内存泄漏吗?是不能的,因为只有强引用指向SecondActivity对象时才代表发生内存泄露,如果是软引用、弱引用、虚引用指向SecondActivity对象是不会发生内存泄漏的,但是Android studio的profiler无法判断是强引用、软引用、弱引用、虚引用的,需要借助MAT工具。
4. 导出hprof文件,点击导出按钮将Android studio中的hprof文件导出到自己的文件夹,用于后续在MAT工具中分析。
在这里插入图片描述
5.导出的hprof文件是无法直接在MAT工具中分析的,如果直接在MAT中打开这个文件会报错,需要进行转换,转换工具是sdk/platform-tools里面的hprof-conv.exe工具,执行命令进行转换:
在这里插入图片描述

6.在MAT中打开转换后的文件,打开后选择Histogram视图,输入SecondActivity进行过滤,可以看到SecondActivity对象有2个,右键选择SecondActivity对象,选择merge shortest paths to gc roots -> exclude all phantom/weak/soft etc. references,排除掉虚引用/弱引用/软引用,只保留强引用,因为只有强引用才会导致内存泄露。在这里插入图片描述
7.分析强引用的引用链:
在这里插入图片描述
ValueAnimator的mUpdateListeners成员属性持有IOSStyleLoadingView的内部类对象,该内部类对象持有外部类的引用this$0(即持有IOSStyleLoadingView的引用),而IOSStyleLoadingView是一个View,它的context属性(IOSStyleLoadingView中定义的)和mContext属性(View中定义的,View的mContext属性默认持有View所在Activity的引用)持有SecondActivity的引用。

分析下来原因就是valueAnimator添加的监听器没有移除:

    public void startAnimation() {
        valueAnimator = ValueAnimator.ofInt(7, 0);
        valueAnimator.setDuration(400);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                if ((int) animation.getAnimatedValue() != currentColor) {
//                    String b[] = new String[color.length];//移动后的数组
//                    for (int c = 0, size = color.length - 1; c < size; c++) {
//                        b[c + 1] = color[c];
//                    }
//                    b[0] = color[color.length - 1];
//                    color = b;
                    invalidate();
                    currentColor = (int) animation.getAnimatedValue();
                }
            }
        });
        valueAnimator.start();
    }

解决方法:在onDetachedFromWindow中移除监听器,并取消动画

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (valueAnimator != null) {
            valueAnimator.removeAllUpdateListeners();//移除监听器,否则会造成内存泄漏
            valueAnimator.cancel();//取消动画
        }
    }

8.重新执行1-7步,可以看到内存中SecondActivity还是有2个在这里插入图片描述
这是否意味着仍然存在内存泄露?右键选择SecondActivity对象,选择merge shortest paths to gc roots -> exclude all phantom/weak/soft etc. references

在这里插入图片描述
在这里插入图片描述
可以看到已经没有强引用的引用链了,说明不会发生内存泄露。
那么到底是谁引用着SecondActivity?接下来选择merge shortest paths to gc roots -> exclude weak references:
在这里插入图片描述

在这里插入图片描述
说明只有FinalizerReference引用着SecondActivity,即SecondActivity已经即将被回收了。

9.如果有很多页面,那是否要每个页面都执行上述操作?这需要大量的操作,有没有更好的办法进行一次性检测?有,可以进行内存快照的比对,如何操作?
首次进入主页面时dump一次内存快照,然后不断进出各个页面,再次返回到主页面时又dump一次内存快照,然后比对这两次的hprof文件。
在这里插入图片描述
在这里插入图片描述
图中显示3.hprof比1.hprof的相应对象多了多少个。

内存泄漏问题常见场景

1. 集合类

当使用集合时,只有添加元素,没有对应的删除元素。如EventBus只有注册没有注销!

2. 静态成员/单例

作为GC ROOT,持有短生命周期对象的引用(如持有Activity对象的引用)导致短生命周期对象无法释放。

/**
 * 内存泄漏问题常见场景
 * 静态成员/单例:作为GC ROOT,持有短生命周期对象的引用(如持有Activity对象的引用)导致短生命周期对象无法释放。
 */
public class SingletonManager {
    //GC ROOT:静态属性所引用的对象,这里new SingletonManager()创建的SingletonManager对象就是属于GC ROOT
    private static final SingletonManager ourInstance = new SingletonManager();

    private Context mContext;

    public static SingletonManager getInstance() {
        return ourInstance;
    }

    private SingletonManager() {
    }

    /**
     * 解决办法:mContext传递ApplicationContext,而不是传递Activity
     * 否则静态对象SingletonManager的成员属性mContext会一直持有Activity对象,导致Activity内存泄露
     *
     * @param context
     */
    public void init(Context context) {
        mContext = context;
    }

public class MemoryMainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_main);

        SingletonManager.getInstance().init(getApplicationContext());


    }
}

3. 未关闭/释放资源

如FileOutputStream未close。

    /**
     * 未关闭/释放资源导致的内存泄露
     */
    private void write() {
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(new File(""));
            fos.write(1);//fos.write(1);可能会出现异常,导致fos.close()不能执行,从而资源未关闭
            fos.close();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 正确写法
     */
    private void write1() {
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(new File(""));
            fos.write(1);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fos != null){
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

4. 非静态内部类

如Handler postDelayed一个匿名Runnable,退出Activity时有延时消息在消息队列中没处理完。

5. 系统Bug

WebView、InputMethodManager等

    /**
     * 系统BUG:InputMethodManager导致的内存泄露。
     */
    private void resolveInputMethodManagerMemoryLeak() {
        // 系统BUG:InputMethodManager导致的内存泄露。
        // InputMethodManager是一个static单例对象(所以InputMethodManager是GC root),InputMethodManager的mCurRootView、mNextServedView、mServedView持有DecorView的引用,
        // DecorView持有Activity的引用,导致Activity不能被回收。
        //解决办法:将InputMethodManager的mCurRootView、mNextServedView、mServedView置为null
        InputMethodManager im = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
        try {
            Field mCurRootView = InputMethodManager.class.getDeclaredField("mCurRootView");
            mCurRootView.setAccessible(true);
            mCurRootView.set(im, null);

            Field mNextServedView = InputMethodManager.class.getDeclaredField("mNextServedView");
            mNextServedView.setAccessible(true);
            mNextServedView.set(im, null);

            Field mServedView = InputMethodManager.class.getDeclaredField("mServedView");
            mServedView.setAccessible(true);
            mServedView.set(im, null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值