記一個「隱藏」的內存泄露

文章背景

這是一個在項目中遇到的一個內存泄露,由於隱藏的較深,定位與解決花費了近兩天時間[大哭]。特記錄其排查與解決過程。
複製代碼

發現問題

由於如今的Android應用大多要適配android6.0新增的運行時權限檢查,因此一般都會在首次啓動時,Splash 閃屏頁進行權限申請。而大多都用了開源庫作這件事。公司某項目就用了com.yanzhenjie.permission:support:2.0.1來進行權限申請。一次我檢查Bitmap OOM的問題時,用了AndroidStudio 的profile 內存監控工具, 發現有一張佔用大的圖片。 html

結合寬高看,定位到是閃屏的廣告圖,全屏,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都沒有被釋放掉:java

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:android

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:git

/**
 * 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,那麼只能本身嘗試解決此問題。)github

查找泄露方式

這裏插一下檢測activity泄露的幾種方式。
一. 本身註冊lifeCycler檢測,日誌輸出泄露對象。代碼以下bash

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 置爲空。想到兩種辦法,ide

  • 用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 結束,該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是否存在的代碼 一. 本身註冊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();
            }
        });

複製代碼

若是把act=null; 這行代碼註釋掉,猜猜會發生什麼現象?能夠本身實驗一下。 出現的現象就是一個activity 都釋放不了,爲何??

解釋:

若是去掉這行,那麼Activity act=weak.get(); 這行代碼就會有一個子線程引用指向activity 對象,而後休眠一秒,在此過程當中,就算主線程無任何引用,發生GC,發現這個對象還有引用,因此不會釋放,休眠結束,又當即從弱引用中取出對象,又建立引用。因此致使對象永遠沒法釋放,因此act=null,這行代碼必須加上。這算是多線程引用的問題。

總結

雖然咱們在其它時候或多或少都學習過線程間的問題,如可見性等等,可是在碰到實際問題時,卻不會常常往這方面去想,結合實際問題,才能記得更牢,經過此問題,也算是對之前線程的知識複習了一下。

推薦個git項目:
這是我的開源的Android庫,能夠用來優雅的、精準的埋點:Tracker,但願你們提點Issue與star。。

相關文章
相關標籤/搜索