【爬坑】一次OOM爬坑之旅

耳東_發表於2018-07-27

記一次 OOM 引起的爬坑之旅。

問題

測試反饋首頁在重新整理的時候有偶爾崩潰的情況,但是情況描述的不是很清楚,因為不是能一直復現的。

復現

找到測試妹子要來出問題的手機自測。根據測試的描述既然是在重新整理的時候崩潰,所以就一直重新整理首頁看看能不能復現,果然復現了出來,在進行多次重新整理以後首頁出現了崩潰,然後再嘗試幾次並且統計各種資料發現每到第 12 次重新整理的時候就出現崩潰。崩潰日誌:

坑一 Fatal signal 6 (SIGABRT), code -6

首先需要說明的是首頁是一個 fragment,它的佈局大概是這樣的

<RelativeLayout>
    <SmartRefreshLayout>
        <ObservableNestedScrollView>
            <LinearLayout>
                ...
                <ViewPager>
                    <GridView/>
                </ViewPager>
                ...
            </LinearLayout>
            <FrameLayout>
        </ObservableNestedScrollView>
    </SmartRefreshLayout>
</RelativeLayout>

可以看到巢狀很深,而且很複雜又是下拉重新整理又是 scrollview 而且 FrameLayout 裡面又巢狀了一個 fragment 並且載入了 webView。然後根據上面的錯誤第一時間想到的是 webView 引起的問題,因為這個錯誤日誌給出的資訊很少,只能看出是在 native 層出現了一些問題,網上類似的問題有很多,解決方案也很多,一一實驗都沒有解決問題。

在這個上面困擾了兩三個小時,個人感覺不應該這麼詭異,再來梳理程式碼:

  • 被巢狀的 fragment 是一個封裝好的專門用來載入 webview 的 fragment
  • 被巢狀的 fragment 在很多其他頁面也有使用,只不過是沒有使用在這麼複雜的頁面中
  • 這裡幾乎沒有對 webview 做任何操作只是做了一個載入而已
  • 程式碼中看不出任何在 webView 中可能出現的問題

乾脆做個試驗,就單獨把這個 fragment 巢狀進一個空白的 fragment 裡面,不做其他操作只做和首頁一樣的邏輯即可,然後進行實驗。

既然說是坑了,結果當然是沒有任何問題

因為在首頁中沒有任何其他的業務邏輯有和被巢狀的 fragment 有關聯,所以可以確認問題不在這裡。那為什麼會報這個錯誤呢?能猜到的就是其他問題引起程式崩潰然後導致 webview 載入的時候出現問題,但是這些崩潰沒有被在日誌中顯示出來。

既然這樣就把巢狀 fragment 這一部分程式碼去掉執行一下看看有什麼問題。

坑二 Could not read input channel file descriptors from parcel

繼續踩坑,把 fragment 這一部分程式碼去掉執行以後果然發生了變化,同樣的重新整理次數同樣的崩潰但是這次崩潰的日誌不一樣了:

java.lang.RuntimeException: Could not read input channel file descriptors from parcel.
at android.view.InputChannel.nativeReadFromParcel(Native Method)
at android.view.InputChannel.readFromParcel(InputChannel.java:148)
at android.view.IWindowSession$Stub$Proxy.addToDisplay(IWindowSession.java:690)
at android.view.ViewRootImpl.setView(ViewRootImpl.java:502)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:259)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
at android.widget.Toast$TN.handleShow(Toast.java:405)
at android.widget.Toast$TN$1.run(Toast.java:313)
at android.os.Handler.handleCallback(Handler.java:733)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:136)
at android.app.ActivityThread.main(ActivityThread.java:5017)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:779)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:595)
at dalvik.system.NativeStart.main(Native Method)

同樣也是沒有任何有用的資訊,不過還是要比之前的要詳細一些,隨手 google 一下這個問題網上給出的答案几乎一致:

  • (1)RemoteView中新增的圖片太大了,超過40K會報這個異常
  • (2)Intent傳遞的資料太大了超過1M也會報這個錯誤
  • (3)FileDescripter 太多而且沒有關閉,looper太多沒有quit
  • (4)試試在AndroidManefest.xml中對當前Activity配置configchange=“orientation|keyboardHidden”(不曉得有沒有寫對)強制在Activity橫豎屏切換的時候不重新onCreate。
  • (5)谷歌原生BUG很多人都遇到這個問題而且沒有得到解決

根據業務邏輯首先排除了1、2、4,實際上5的可能性也不大,那問題可能就是出現在3上面了。

FileDescripter

FileDescripter 是個什麼東西?在官方文件裡解釋如下:

Instances of the file descriptor class serve as an opaque handle to the underlying machine-specific structure representing an open file, an open socket, or another source or sink of bytes. The main practical use for a file descriptor is to create a FileInputStream or FileOutputStream to contain it.

簡單來說就是一個在底層的控制程式碼,用來開啟檔案、套接字或者其他什麼什麼的東西。它是來自於 linux 的一個概念,android 中每個程式預設可以開啟的控制程式碼數量是 1024 個,任何一個 IO 操作都會使用一個 FD。有一些文章還提到一個程式開啟的 FD 不會超過 100 個,使用WebView的也不會超過200個,如果到達了500以上基本都存在洩露問題。可以使用命令來確定程式使用的 FD 的數量:

adb shell lsof | grep | wc -l

既然這樣了就查一下吧,開啟手機執行命令結果是0!不管執行多少次結果都是0。繼續 google 發現這個命令需要 root 許可權才行,GG 。沒辦法了只能找來一個測試機 root 以後繼續排查(這裡不得不吐槽一下vivo的手機root起來真麻煩),果然發現隨著重新整理頁面開啟的控制程式碼數量在暴增:

adb shell lsof | grep 29344 | wc -l
123
adb shell lsof | grep 29344 | wc -l
195

確認問題

經過上面分析最終確認了崩潰的原因,那麼回過頭來繼續看程式碼,首先確認一下在哪裡使用了這個 FileDescripter。根據前面 FileDescripter 的功能然後結合程式碼可以確定在這個頁面中如果需要用到它就只可能出現在載入圖片上。這個時候首先想到的問題可能出現在圖片載入框架上,因為圖片載入及快取的框架是公司自己封裝的基礎元件,還是有可能出現一些隱藏 bug 的。

所以不如先來做個試驗,寫一個簡單 demo 一次性載入了上千次圖片發現並沒有崩潰。所以問題不出在圖片載入框架上。而且,首頁載入的圖片不超過10張,怎麼會引起控制程式碼數量過多呢,唯一的可能就是哪裡有迴圈或者多次的重複載入導致的,首頁經過程式碼分析可能出現的地方就是在 listView 或者 gradview 的 adapter的 gitView 方法中,因為如果巢狀過多或者高度問題會導致 getView 方法執行多次。那麼就 debug 看看吧。果然發現在其中一個 adapter 的 gietView 方法執行很多次,只載入六張圖片的情況下竟然執行了七十多次,這是很不正常的。而 70 多這個數字很有意思,結合之前崩潰的統計 77 * 12 = 924 再加上一些其他地方開啟的 FD 數量差不多就是 1024 引發崩潰,那麼基本上問題就出現在這裡了。

經過上面的分析基本確定了問題出現的地方,但是還沒有找到具體出錯的程式碼,不過既然找到了出錯的方法也就好辦了。因為程式碼很簡單,把圖片載入相關的去掉再進行測試果然沒有問題。最終確認引起錯誤的程式碼:

// 其實就是一個很簡單的圖片載入
finalViewHolder.mgifMySelectImg.setImageResource(R.drawable.parent_grow_select_img_default);

其實就是一個很簡單的圖片載入,但是問題出現在這上面,因為 mgifMySelectImg 是一個 GifImageView ,他是一個引用的第三方庫地址(新版中這個庫的問題已經解決了)。

解決

既然知道原因瞭解決起來也很簡單。因為這部分程式碼不是自己寫的,和同事溝通以後把 mgifMySelectImg 修改為 ImageView 解決了問題。

後續

當然解決問題很簡單,不過在這過程中也發現了一些問題同樣要解決。

首先就是 gitView 執行過多的問題,因為它執行次數過多勢必會導致頁面卡頓,所以做了簡單的優化:

  • 減少佈局層級
  • 對 gradView 做了一層封裝,判斷在 onMeasure 時 getView 及時跳出中不進行資源載入

    關於 getView 的跳出時機需要自己根據需求來定,實測結果如果跳出的時機不對會造成佈局錯亂。因此建議只在耗時的資源載入時做這個判斷。

listView 優化隨手 google 一下有很多,也很詳細。

然後就是為什麼會出現記憶體洩露呢,GifImageView 出現記憶體洩露的問題在哪裡?這個沒辦法只能去追蹤原始碼看一下也許以後開發中能避免出現此問題,簡單說明一下:

說明:當前 GifImageView 版本 1.2.3,根據實測 1.2.15 版本沒有此問題。

在 setImageResource 方法中它呼叫了 GifViewUtils.setResource 方法。

    static boolean setResource(ImageView view, boolean isSrc, int resId) {
        Resources res = view.getResources();
        if (res != null) {
            try {
                GifDrawable d = new GifDrawable(res, resId);
                if (isSrc) {
                    view.setImageDrawable(d);
                } else if (VERSION.SDK_INT >= 16) {
                    view.setBackground(d);
                } else {
                    view.setBackgroundDrawable(d);
                }
                return true;
            } catch (NotFoundException | IOException var5) {
                ;
            }
        }
        return false;
    }

在 setResource 中 new 了一個新的 GifDrawable。在它的構造方法中呼叫了建立了一個 GifInfoHandle 物件,

    GifInfoHandle(AssetFileDescriptor afd) throws IOException {
        try {
            this.gifInfoPtr = openFd(afd.getFileDescriptor(), afd.getStartOffset());
        } finally {
            try {
                afd.close();
            } catch (IOException var8) {
                ;
            }
        }
    }

問題就出在這裡 GifInfoHandle 的 openFD 方法上,它是一個 native 方法,由於 github 上沒有找到 1.2.3 版本關於這一部分的 native 程式碼,所以也就沒辦法再繼續下去了。

那麼就到此為止吧。當然這個庫還是不錯的,它通過在 native 層來做一些事情的方式來實現載入,這與傳統的使用webView 或者 Movie 來載入 gif 圖片的方式不同。當然具體的原理還沒有去研究。

目前在專案中已經把這一個gif載入的庫升級到最新,因為其他地方也在使用如果不進行升級肯定存在隱患。

總結

到此一個因為 OOM 而引起的爬坑之旅算是結束了,前後花費七八個小時最終的問題解決起來卻很簡單——不得不說搞開發就是這麼奇妙。坑爬出來了總要有所收穫,這裡也簡單總結一下,算是一次經驗積累吧。

  • 遇到奇怪,隱晦的問題的時候不要急,通過刪程式碼的方式能比較好的找到真正出問題的地方;

  • 多做實驗,把懷疑可能出問題的地方單獨拿出來試驗一下;

  • 準備一個有 root 許可權的手機,因為很多時候由於工程的限制沒法用模擬器;

  • 不光要解決問題,更要知道問題的所在。

參考

相關文章