天天看點

短信清單滑動慢、卡頓---性能優化總結

前提

以前面試中,面試官問到我優化了那些點。我當時緊張,隻回答了說一個日期格式化的優化。是以從這個經曆來說,我個人對自己做過的優化沒有總結,優化的方法沒有仔細地考慮原因,以及這個優化會有什麼其他的副作用。今天特意來總結,因為最近學習了JVM記憶體布局、并發、鎖等方面的知識,通過這些知識來更好的了解當初的優化方法。當初優化通過工具分析到卡頓點,找出了優化方法,而沒有分析為什麼這樣優化會達到效果。

短信應用是android原生應用,平台是MTK android o的代碼。短信中存儲了5000條短信(通過工具批量插入)。

問題:滑動(快慢)短信清單會出現幀率小于60fps.

分析

  1. 首先我們使用systrace工具,檢視是否有卡頓現象。測試人員是通過高速攝影機來判斷丢祯率。那麼我們開發人員通過systrace工具來檢視到底是在那個環節耗時比較多。
    短信清單滑動慢、卡頓---性能優化總結
    通過上面的圖可以看出,短信應用在滑動時确實耗時偏高。我們檢視其中一祯:
短信清單滑動慢、卡頓---性能優化總結

工具的分析結果是建議我們提高擴充卡的getView綁定資料的效率。從這個建議當中我們無法确切的知道問題出在哪行代碼上,之所給我們一個大概的方向。通過systrace工具能夠讓開發人員檢測界面是否有卡頓,給出一個指導意見。在特别小的差别面前,我們人的感覺是無法差別的。

  1. Method Profiling 方法分析工具

    因為systrace無法定位到确切的代碼。還好我們有Method Profiling 工具可以分析方法的執行情況。理論上可以通過列印時間戳來追蹤耗時方法,但是這個方法比較費事。

    在android device monitor工具中點選“start Method Profiling”,開始檢測->滑動短信清單->“stop Method Profiling”,

    短信清單滑動慢、卡頓---性能優化總結
    那麼上面的圖中,我們如何找到耗時方法呢?可以在過濾器中輸入我們的包名,這樣,我們的耗時類就在最上面了。
    短信清單滑動慢、卡頓---性能優化總結
    第一個耗時高的方法是Conversation.from();我們再點選進去,發現是Conversation中的内部類Cache.put方法耗時非常多。而Cache.put方法是間接調用了ConcurrentHashMap.put方法。HashMap添加元素的步驟是,根據key的hash散列碼來放到桶中。如果桶中沒有元素則将元素放到這裡,如果有值,則比較是否是同一個元素,是則更新,否則在後面添加(分離連結法)。最終發現是equals方法耗時較多。那麼我看一下Conversation的equals方法是如何實作的。
/*
     * The primary key of a conversation is its recipient set; override
     * equals() and hashCode() to just pass through to the internal
     * recipient sets.
     */
    @Override
    public synchronized boolean equals(Object obj) {
        try {
            Conversation other = (Conversation) obj;
            /** M: contains method of ConcurrentHashMap use equals to judge element existence.
             *  we take wap push message and cb message into seperate thread.
             *  they may be has the same recipients as the common thread, but different.
             */
            return (mRecipients.equals(other.mRecipients)) && (mType == other.mType);
        } catch (ClassCastException e) {
            return false;
        }
    }

           

這裡面有synchronized關鍵字,那麼是不是這個導緻慢呢?首先在并發這塊,synchronized關鍵字是重量級鎖,一定要擷取鎖線程才能進入執行。即使單線程執行,也會在這個地方卡頓。是以我們嘗試将synchronized關鍵字去除,看效果怎樣。

  • 改了以後會有點效果,在contains()方法中從開始的占用85%的時間降低到61%.

    還沒有達到我們的預期,還有一個方法(mRecipients.equals(other.mRecipients)) ,這個中我們看一下是如何實作equals方法的。

  • mRecipients是一個ContactList,ContactList繼承自Arraylist,在ContactList的equals方法中,他循環周遊A中的接收者是否在B中,是以這個比較耗時。
  • 看它的注釋中有 wap push message and cb message,wap push message這個在短信中已經沒有了,而cb message我們已經分離了在另外一個apk中。是以我們可以把比較條件改為(mThreadId == other.mThreadId) ,通過測試,eqauls方法在contains()方法中從開始的占用61%的時間降低到15%.
短信清單滑動慢、卡頓---性能優化總結

那麼優化後我們發現還有下面的問題。

短信清單滑動慢、卡頓---性能優化總結

Conversation.from()方法的耗時比較長。那麼我們最終下去,看到底什麼地方出了問題。

定位到有一行代碼是删除字元串中的特色字元:

workingNumberOrEmail = workingNumberOrEmail.replaceAll(" ", "")
                    .replaceAll("-", "").replaceAll("[()]", "");
           

上面代碼的意思是将字元串中的空格,橫杠,小括号、中括号去除。但是它調用了三次replaceAll方法。是以我們能否讓它調用一次來達到效果。replaceAll的第一個String參數是可以為正規表達式。而代碼中沒有用到這個特性。是以我們的可以如下修改。

workingNumberOrEmail = workingNumberOrEmail.replaceAll(
                   "[\\ ]*[\\-]*[\\[]*[\\(]*[\\)]*[\\]]*", "");
           

這樣我們就完成了優化。

代碼中另外還有一個點。程式中通過一個異步線程去更新聯系人資料,任務放在runnable對象中,再将runnable對象發送給任務線程,但是任務線程通過ArrayList來存儲,運作時從ArrayList中去除第一個元素,增加時直接調用add方法。源碼:

private static class TaskStack {
             Thread mWorkerThread;
            private final ArrayList<Runnable> mThingsToLoad;
...
                              if (mThingsToLoad.size() > 0) {
                                    r = mThingsToLoad.remove(0);
...
             public void push(Runnable r) {
                synchronized (mThingsToLoad) {
                     mThingsToLoad.add(r);
...
           

而我們知道,ArrayList在添加元素時,将給數組擴容,然後将原資料拷貝到新空間中,再将新資料放在末尾。删除時申請性空間,将數組拷貝的新空間中。無論是删除還是添加,都需要進行整個數組的移動,是以會帶來很大的計算量。其實在這種情況下我們可以使用queue的資料結構可以很好的解決這個數組拷貝的問題。比如android中Handler的消息就是通過隊列來組織的(Handler消息)

3.

測試定位問題。

工具使用

卡頓原因分析。解決政策。

更好的方法

多線程、并發、鎖

硬體加速