Android 界面滑動卡頓分析與解決方案(入門)


Android 界面滑動卡頓分析與解決方案(入門)

導致Android界面滑動卡頓主要有兩個原因:
1.UI線程(main)有耗時操作
2.視圖渲染時間過長,導致卡頓

目前只講第1點,第二點相對比較復雜待以后慢慢研究。

眾所周知,界面的流暢度主要依賴FPS這個值,這個值是通過(1s/渲染1幀所花費的時間)計算所得,FPS值越大視頻越流暢,所以就需要渲染1幀的時間能盡量縮短。正常流暢度的FPS值在60左右,即渲染一幀的時間不應大於17ms。

先看一個例子:

Video1,該應用通過一組URL加載網絡上的圖片並顯示在ListView中。從視頻中可以看到滑動ListView存在一定的卡頓的現象。

分析工具1:Android systrace

打開Android開發工具中的DDMS,選中應用所在的進程並點擊倒數第二個圖標出現如下界面:
android systrace

填上相關信息后點擊OK,此時Android systrace已經開始工作,你只需要正常操作手機復現卡頓現象即可。

Android systrace 停止以后就會根據你上圖中的設置生成trace.xml文件(注:此文件在火狐瀏覽器中無法正常瀏覽,需使用chrome)。

打開trace.xml后觀察surfaceflinger存在很多斷斷續續,分布不夠均勻。
2

放大看
3
在某間隔出的時間是48ms,這遠遠的大於17ms,正是這個原因直接導致了界面的卡頓感。

接着再看:
4
此處也存在不規則分布,可以看到主要的耗時操作在:

5

obtainView和decodeBitmap上面,看到這兩個方法似乎是找到了罪魁禍首。那我們就看看這兩個方法是在哪里被調用的。

通過查找源代碼知道obtainView這個方法被定義在AbsListView.java這個類中,主要作用就是獲得一個和數據綁定過的視圖,在這個方法中調用到了

 mAdapter.getView(position, transientView, this);

這個方法大家再熟悉不過了,因為在自定義Adapter的時候都要去重寫getView方法,那我們可以推測卡頓的原因很有可能出自getView。畢竟這個方法里有我們自己寫的很大一坨代碼。

接着再看getView中最主要的方法

 mCacheWrapper.getBitmapFromCache(url, mHandler, position,2);

具體實現是:

    public void getBitmapFromCache(final String urlString, Handler handler,
            final int position, final int scale) {

        final String key = hashKeyForString(urlString);
        final Message msg = handler.obtainMessage();
        Bitmap bitmapCache = mMemoryCache.get(key);

        if (bitmapCache != null) {
            msg.arg1 = position;
            msg.obj = bitmapCache;
            msg.sendToTarget();
            Log.d(TAG, "memory include the key");
            return;
        }

        //InputStream is = getInputStreamFromCache(key);
        //if (is != null) {
        //    Bitmap bitmap = getBitmap(is, key, scale);
        //    if (bitmap != null) {
        //        mMemoryCache.put(key, bitmap);
        //        msg.arg1 = position;
        //        msg.obj = bitmap;
        //        msg.sendToTarget();
        //        Log.d(TAG, "disk include the key");
        //    }
        //    return;
        //}
        mExecutor.execute(new Runnable() {
            @Override
            public void run() {

                if (mCurrentTask.contains(key)) {
                    Log.d(TAG, "the key of task is still execute");
                    return;
                }
                mCurrentTask.add(key);
               

                boolean isOK = write2Cache(urlString);
                if (isOK) {
                    InputStream is = getInputStreamFromCache(key);
                    Log.d(TAG, "the file is write to disk cache");
                    is = getInputStreamFromCache(key);
                    if (is != null) {
                        Bitmap bitmap = getBitmap(is, key, scale);
                        if (bitmap != null) {
                            mMemoryCache.put(key, bitmap);
                            msg.arg1 = position;
                            msg.obj = bitmap;
                            msg.sendToTarget();
                        }
                    }
                }
                mCurrentTask.remove(key);
            }
        });
    }

看上面這段代碼發現加注釋代碼存在一定的嫌疑,因為它在主線程中做了IO操作和bitmap的decode操作。

我們稍微修改一下上面這段代碼:

    public void getBitmapFromCache(final String urlString, Handler handler,
            final int position, final int scale) {

        final String key = hashKeyForString(urlString);
        final Message msg = handler.obtainMessage();
        Bitmap bitmapCache = mMemoryCache.get(key);

        if (bitmapCache != null) {
            msg.arg1 = position;
            msg.obj = bitmapCache;
            msg.sendToTarget();
            Log.d(TAG, "memory include the key");
            return;
        }

        mExecutor.execute(new Runnable() {
            @Override
            public void run() {

                if (mCurrentTask.contains(key)) {
                    Log.d(TAG, "the key of task is still execute");
                    return;
                }
                //mCurrentTask.add(key);
                //InputStream is = getInputStreamFromCache(key);
                //if (is != null) {
                //    Bitmap bitmap = getBitmap(is, key, scale);
                //    if (bitmap != null) {
                //        mMemoryCache.put(key, bitmap);
                //        msg.arg1 = position;
                //        msg.obj = bitmap;
                //        msg.sendToTarget();
                //        Log.d(TAG, "disk include the key");
                //    }
                //    mCurrentTask.remove(key);
                //    return;
                //}

                boolean isOK = write2Cache(urlString);
                if (isOK) {
                    Log.d(TAG, "the file is write to disk cache");
                    is = getInputStreamFromCache(key);
                    if (is != null) {
                        Bitmap bitmap = getBitmap(is, key, scale);
                        if (bitmap != null) {
                            mMemoryCache.put(key, bitmap);
                            msg.arg1 = position;
                            msg.obj = bitmap;
                            msg.sendToTarget();
                        }
                    }
                }
                mCurrentTask.remove(key);
            }
        });
    }

將置灰出的代碼移動到主線程以外,在看看滑動流暢讀video2.mp4
可以看到卡頓現象已經沒有了,那罪魁禍首就是在主線程中有IO操作和bitmap的decode操作引起的。

上述分析過程跳躍性比較大,這里再推薦一種簡單直觀的方法:

分析工具2:Method Profiling.

還是打開DDMS,選中你的應用,點擊第六個圖標,
6

這邊默認OK
7

點擊OK開始抓取,接着滑動手機復現卡頓現象。最后再次點擊第六個鈕即可。
8

這里只看上圖中的main就可以了。
9

點擊main方法后會展開它的父方法(即調用main的方法)和它的子方法(即在main中調用的方法)。這里一般點擊后面百分數較大的那個子方法(百分數表示方法執行所占用的cpu時間)。
接着要做的就是一步一步往下點,直到找到我們要找的耗時操作。

最后我們還是來到了這邊
10

上圖已經定位了decodeStream方法,再往下走也是進到framework沒意義了。剩下的就是怎么將decodeStream放到主線程以外的線程的事情了。

總結:

主線程中不要放置耗時的操作,耗時操作可以扔Thread再通過Handler與主線程同步或使用AsyncTask來完成耗時操作。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM