记一个“隐藏”的内存泄露

3,937 阅读8分钟

文章背景

这是一个在项目中遇到的一个内存泄露,因为隐藏的较深,定位与解决花费了近两天时间[大哭]。特记录其排查与解决过程。

发现问题

因为现在的Android应用大多要适配android6.0新增的运行时权限检查,所以通常都会在首次启动时,Splash 闪屏页进行权限申请。而大多都用了开源库做这件事。公司某项目就用了com.yanzhenjie.permission:support:2.0.1来进行权限申请。一次我检查Bitmap OOM的问题时,用了AndroidStudio 的profile 内存监控工具, 发现有一张占用大的图片。

结合宽高看,定位到是闪屏的广告图,全屏,750*1334大小,按占用内存算约等于2M,而且是2张(总共三张引导,因为用了viewPager,在滑动到第三张时,第一张释放)。然后查看getDrawable源码时发现,里面是用cache 池的,如果池中有,直接从池中取,而池是WeakReference引用的,见:

abstract class ThemedResourceCache<T> {
    private ArrayMap<ThemeKey, LongSparseArray<WeakReference<T>>> mThemedEntries;
    private LongSparseArray<WeakReference<T>> mUnthemedEntries;
    private LongSparseArray<WeakReference<T>> mNullThemedEntries;

而弱引用是当发现GC时,会直接回收的。所以这张Bitmap就不应在内存中,然后又发现原来是整个SplashActivity都没有被释放掉:

Activity 没有释放导致view没有释放,再导致图片没释放。 这个时候肯定要先用LeakCanary跑一遍了,因为会直接告诉你泄露的点。 leakcanary 报告图片:

到这里,原来是AndPermission这个权限库导致的问题,使用如下:

AndPermission.with(this).runtime().permission(permissions)
                .onGranted(data -> {
                    requestReadPhonePermission();
                }).onDenied(permissions1 -> {
            if (AndPermission.hasAlwaysDeniedPermission(SplashActivity.this, permissions1)) {
                Toast.makeText(SplashActivity.this, "拒绝", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(SplashActivity.this, "永不允许", Toast.LENGTH_SHORT).show();
            }
        }).start();

定位问题

然后我想看看为什么泄露,就先看了这个库的源码,大致原理如下。所有的请求会包装成BridgeRequest对象,里面有activity引用 里面有一个单例RequestManager,里有一个线程RequestExecutor,那么这线程也是单例的。 线程有一个队列private final BlockingQueue<BridgeRequest> mQueue;线程中不断从queue取数据,取出,注册广播,并且启动一个透明activity:BridgeActivity,在bridgeAct中申请权限,那么结果也是bridge中onRequestPermissionsResult 中,然后发送一个广播,这边再收到广播,onCallback回调给我们的调用上层。 核心代码如下: RequestManager:

public class RequestManager {

    private static RequestManager sManager;

    public static RequestManager get() {
        if (sManager == null) {
            synchronized (RequestManager.class) {
                if (sManager == null) {
                    sManager = new RequestManager();
                }
            }
        }
        return sManager;
    }

    private final BlockingQueue<BridgeRequest> mQueue;

    private RequestManager() {
        this.mQueue = new LinkedBlockingQueue<>();

        new RequestExecutor(mQueue).start();
    }

    public void add(BridgeRequest request) {
        mQueue.add(request);
    }
}

RequestExecutor:

/**
 * Created by Zhenjie Yan on 2/13/19.
 */
final class RequestExecutor extends Thread implements Messenger.Callback {

    private final BlockingQueue<BridgeRequest> mQueue;
    private BridgeRequest mRequest;
    private Messenger mMessenger;

    public RequestExecutor(BlockingQueue<BridgeRequest> queue) {
        this.mQueue = queue;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                try {
                    mRequest = mQueue.take();
                } catch (InterruptedException e) {
                    continue;
                }

                mMessenger = new Messenger(mRequest.getSource().getContext(), this);
                mMessenger.register();
                executeCurrent();

                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void executeCurrent() {
        switch (mRequest.getType()) {
            case BridgeRequest.TYPE_PERMISSION: {
                Intent intent = new Intent(source.getContext(), BridgeActivity.class);
                intent.putExtra(KEY_TYPE, BridgeRequest.TYPE_PERMISSION);
                intent.putExtra(KEY_PERMISSIONS, permissions);
                source.startActivity(intent);
                break;
            }

        }
    }

    @Override
    public void onCallback() {
        synchronized (this) {
            mMessenger.unRegister();
            mRequest.getCallback().onCallback();
            notify();
        }
    }
}

而上而说了,此线程是单例一直存活的,而mRequest里有activity引用,关键就是在onCallbackl回调中没有把mRequest置为空,而该git上也有人提相同问题,解决办法也是把mRequest置为空。(这里说一下,在2.0.3版本是解决了此问题的,但是2.0.2以上不再兼容supportV7,只兼容了androidX,因为项目中还没有升级替换androidX,那么只能自己尝试解决此问题。)

查找泄露方式

这里插一下检测activity泄露的几种方式。
一. 自己注册lifeCycler检测,日志输出泄露对象。代码如下

        registerActivityLifecycleCallbacks(new EmptyActivityLifecycleCallbacks() {

            @Override
            public void onActivityDestroyed(Activity activity) {
                super.onActivityDestroyed(activity);

                ReferenceQueue<Activity> refer=new ReferenceQueue<>();
                WeakReference<Activity> weak=new WeakReference<>(activity,refer);
                new Thread(){
                    @Override
                    public void run() {
                        super.run();
                        while (true){
                            try {
                                Activity act=weak.get();
                                Reference<Activity> refe= (Reference<Activity>) refer.poll();

                                Log.d(TAG,"weak ="+ act+" "+refe);
                                if(act==null && refe==null){
                                    return;
                                }
                                act=null;
                                Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }.start();
            }
        });

二. leakcanary。输出泄露源
三. 在androidProfile中看有没有这个activity对象。

其实leakcanary检测内存泄露的步骤和1差不多,就是在onDestory后用弱引用,对activity检测,正常的话GC后,引用中对象会为空,一段时间过后,发现对象还在,认为泄露,再dump内存,分析引用链,输出报告。如果要验证某页面的是否泄露的话,用日志的方法会快一点。

尝试解决

好,我们自己在onCallback 中把这两个mRequest,mMessenger 置为空。想到两种办法,

  • 用AOP框架AspectJ,在onCallback执行后,用反射把这个变量为空。

  • 把项目2.0.1源码拷下来,直接在源码上修改。

看起来此问题好像就要解决了。如果真是如此,就没必要记录了。 这里先用了AOP,发现不行,再用了直接改源码,发现还是存在泄露。。。

这里无论用那种方式,对象都确实存在,我们直接看leakcanary的报告。

再上profiler:

可以看到,mRequest与mMessenger确实为null啊。为何SplashActivity还是存在??? 在确认了没有其它可疑的引用后,然后各种猜想,查各种文章。感觉上就像有一个隐藏的对象引用着它,无影无踪。就在这条猜想后,立马百度了一下 "线程 隐藏变量",看有无相关线索,然后看到了

线程间的可见性

java的每个线程有独立的工作内存,他们的工作方式是从主内存将变量读取到自己的工作内存,然后在工作内存中进行逻辑或者自述运算再把变量写回到主内存中。正常情况下各线程的工作内存之间是相互隔离的、不可见的。(参考自:JAVA多线程可见性

线程间的不可见性会导致我们在多线程中操作同一变量会引发的问题,如同一变量i,两个线程同时加,会导致数不等于总和,这个大家应该都知道。

做个Java小实验

public class JavaTest {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.reset();
        System.out.println("main t=" + t.p2);
    }

    static class MyThread extends Thread {
        private Person p2;

        public MyThread() {
            this.p2 = new Person();
        }
        @Override
        public void run() {
            super.run();
            while (p2 != null) {
//                System.out.println("MyThread " + p2);
            }
        }
        public void reset() {
            p2 = null;
        }
    }
}

在这个实验中,子线程MyThread有一变量,子线程通过p2!=null做循环检查。100毫秒后,主线程中调用子线程reset方法把p2置空,那么按照理论,子线程会结束循环,实验结果如下图:

出现这个红色小方块说明程序未结束,因为有一个子线程未结束。而我们在主线程中这行 System.out.println("main 2=" + p + " t=" + t.p2); 结果是t.p2 确实为null,这个问题是不是和上面那个很像呢。是在主线程调用的onCallback()中置空。

这个就是线程的不可见性导致的,主线程和子线程都有一份这个变量,主线程调用置null,而子线程中的变量没有从主内存中更新,所以对于子线程而言,依然不为null,解决办法就是对这个变量加上volatile 关键字,当更新后,使得子线程立即从主内存中更新。
加上volatile 后:

程序正常结束了。

解决问题

当找到这问题原因,再回到Android中,那么就好解决了,加上volative就可解决,还有一种就是原作者的解决办法是用了系统的线程池,然后把RequestExecutor当一个runnable 用,这样,线程调度runnable,run回调完,该runnable 结束,该runnable对象也就被释放了,内部属性也同样被释放了。

如下:

public class RequestManagerFix {
    private static RequestManagerFix sManager;
    public static RequestManagerFix get() {
        if (sManager == null) {
            synchronized (RequestManagerFix.class) {
                if (sManager == null) {
                    sManager = new RequestManagerFix();
                }
            }
        }
        return sManager;
    }
    private final Executor mExecutor;
    private RequestManagerFix() {
        this.mExecutor = Executors.newCachedThreadPool();
    }
    public void add(BridgeRequest request) {
        mExecutor.execute(new RequestExecutorFix(request));
    }
}

final class RequestExecutor implements Messenger.Callback, Runnable {

    private BridgeRequest mRequest;
    private Messenger mMessenger;

    public RequestExecutor(BridgeRequest queue) {
        this.mRequest = queue;
    }
    @Override
    public void run() {
        mMessenger = new Messenger(mRequest.getSource().getContext(), this);
        mMessenger.register();
        executeCurrent();
    }
    private void executeCurrent() {
        。。。
    }
    @Override
    public void onCallback() {
        synchronized (this) {
            mMessenger.unRegister();
            mRequest.getCallback().onCallback();
            mRequest = null;
            mMessenger = null;
        }
    }
}

原RequestExecutor 做的事情就是类似线程池,而确实没有必要自己写一套线程池。

附带的线程问题

在我测试过程中,我自己写的这套检测activity是否存在的代码,代码如下

        registerActivityLifecycleCallbacks(new EmptyActivityLifecycleCallbacks() {

            @Override
            public void onActivityDestroyed(Activity activity) {
                super.onActivityDestroyed(activity);

                ReferenceQueue<Activity> refer=new ReferenceQueue<>();
                WeakReference<Activity> weak=new WeakReference<>(activity,refer);
                new Thread(){
                    @Override
                    public void run() {
                        super.run();
                        while (true){
                            try {
                                Activity act=weak.get();
                                Reference<Activity> refe= (Reference<Activity>) refer.poll();

                                Log.d(TAG,"weak ="+ act+" "+refe);
                                if(act==null && refe==null){
                                    return;
                                }
                                act=null;
                                Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }.start();
            }
        });

如果把act=null; 这行代码注释掉,猜猜会发生什么现象?可以自己实验一下。 出现的现象就是一个activity 都释放不了,为什么??

解释:

如果去掉这行,那么Activity act=weak.get(); 这行代码就会有一个子线程引用指向activity 对象,然后休眠一秒,在此过程中,就算主线程无任何引用,发生GC,发现这个对象还有引用,所以不会释放,休眠结束,又立即从弱引用中取出对象,又创建引用。所以导致对象永远无法释放,所以act=null,这行代码必须加上。这算是多线程引用的问题。

总结

虽然我们在其它时候或多或少都学习过线程间的问题,如可见性等等,但是在碰到实际问题时,却不会经常往这方面去想,结合实际问题,才能记得更牢,通过此问题,也算是对以前线程的知识复习了一下。

推荐个git项目:
这是个人开源的Android库,可以用来优雅的、精准的埋点:Tracker,希望大家提点Issue与star。。