1. 程式人生 > >Android應用優化之流暢度

Android應用優化之流暢度

前言

對於現今市面上針對於使用者互動的應用,都有使用列表去展示資訊。列表對於使用者來說是十分好的瀏覽、接收資訊的一個控制元件。對於產品來說,列表流暢度的重要性就不言而喻了。而流暢度的好壞,對一個產品的基本體驗和口碑有著極大的影響。然而Android手機與iPhone手機對比,第一點往往就是流暢度的問題,對於技術來說,我們的Google親爹,不斷對這個詬病進行優化,包括GPU硬體加速、將Dalvik虛擬機器換成ART等等,我們的程式碼也不斷從ListView換成RecyclerView。當我們沾沾自喜地說我們的產品也能像飄柔那樣順滑,我們的產品經理說出一個詞“競品對比”,來了一個JSON資料結構是三層陣列巢狀,直觀來說就是巢狀三層RecyclerView,懵逼臉?當然我們編碼肯定不會這樣做。好了回到我們的流暢度問題。

流暢度

對於流暢度,我們首先會重點說到FPS問題,導致流暢度不足。FPS是Frames Per Second,Frame(畫面、幀),p就是Per(每),s就是Second(秒)。人類大腦與眼睛對一個畫面的連貫性感知有一個邊界值,譬如我們看電影會覺得畫面很自然連貫,其幀率通常為 24fps。

但經過國內BAT專業測試團隊研究,我們常說的FPS與流暢度的關係並不準確。此刻我們要先理解60FPS、16ms這倆名詞。我們在其他的文章裡總會看到的名詞。那它究竟是什麼意思呢?它們出於官方出的效能優化視訊Android Performance Patterns: Why 60fps? 對其解釋說:

While 60 frames per second is actually the sweet pot. Great, smooth motion without all the tricks. And most humans can’t perceive the benefits of going higher than this number.
60fps是最恰當的幀率,是使用者能感知到的最流暢的幀率,而且人眼和大腦之間的協作無法感知到超過 60fps的畫面更新。

Now, it’s worth noting that the human eye is very discerning when it comes to inconsistencies in these frame rates
但值得注意的是人眼卻能夠感受到重新整理頻率不一致帶來的卡頓現象,比如一會60fps,一會30fps,這是能夠被感受到的,即卡頓。

As an app developer, your goal is clear. Keep your app at 60 frames per second. that’s means you have got 16 milliseconds per frame to do all of your work.That is input, computing, network and rendering every frame to stay fluid for your users.
所以作為開發者,你必須要保證你的所有操作在16ms**(1000毫秒/60幀)**內完成包括輸入、計算、網路、渲染等這些操作,才能保證應用使用過程的流暢性。

基礎概念

Android應用程式顯示原理是:手機螢幕顯示的內容是通過Android系統的SurfaceFLinger類,把當前系統裡所有程序需要顯示的資訊經過測量、佈局和繪製後的Surface渲染合成一幀,然後交到螢幕進行顯示。

FPS就是1s內SurfaceFLinger提交到螢幕的幀數。

  • SurfaceFLinger:Android系統服務,負責管理Android系統的幀緩衝區,即顯示螢幕。
  • Surface:Android應用的每個視窗對應一個畫布(Canvas),即Surface,可以理解為Android應用程式的一個視窗。

Android應用程式的顯示重點有繪製、渲染。

上面所說的繪製指的是Android的繪製機制。我們要從View的建立View的測量View的佈局View的繪製對整一個繪製流程有一個基本的理解,下面我們更好地探究如何流暢,為什麼卡頓。

大多數使用者感覺卡頓等效能的問題根源就是渲染效能(Render Performance)。此時要從VSync機制開始。VSync機制是Android4.1引入的是Vertical Synchronization(垂直同步)的縮寫。我們可以把它看作是一種定時中斷,其目的是為了改善android的流暢程度。

清晰理解上面我們所述的概念後,接著去理解VSync機制。下圖是VSync機制下的繪製顯示過程,從下圖中看到CPU、GPU處理時間都很快,都是少於一個VSync間隔,也就是16ms,都能在16ms的VSync內display顯示對應的內容。

這裡寫圖片描述

上圖是一個相當理想狀態下的情況,但是當我們要完成一些酷炫、複雜的介面時,CPU、GPU處理時間會出現較慢的情況。就如下圖所示的情況。

這裡寫圖片描述

在上圖我們看到Display有兩個A、B,這裡涉及到另外一個概念,在我們在繪製UI的時候,會採用一種稱為“雙緩衝”的技術。雙緩衝意思是使用兩個緩衝區(SharedBufferStack中),其中一個稱為Front Buffer,另外一個稱為Back Buffer。UI總是先在Back Buffer中繪製,然後再和Front Buffer交換,渲染到顯示裝置中。理想情況下,這樣一個重新整理會在16ms內完成(60FPS),上圖就是描述的這樣一個重新整理過程(Display處理前Front Buffer,CPU、GPU處理Back Buffer。

從上圖我們看到CPU、GPU的處理情況,已經大於一個VSync的間隔(16ms),我們看到在Display本應顯示B幀,但卻因為GPU還在處理B幀,導致A幀被重複顯示,這就會讓視覺產生不協調,達不到60FPS,於是出現了丟幀(Skipped Frame,SF)現象。

另外在上圖第二個16ms時間段內,CPU無所事事,因為A Buffer被Display在使用。B Buffer被GPU在使用。注意,一旦過了VSYNC時間點,CPU就不能被觸發以處理繪製工作了。

此時就有一種想法,如果有第三個Buffer存在,那CPU此時也可以利用起來了。那在Android4.1引入了Triple Buffer,所以當雙Buffer不夠用時Triple Buffer的丟幀情況如下圖。

這裡寫圖片描述

所以從上圖可以看到,在第二個VSync,CPU是用了C Buffer繪圖。雖然還是會多顯示A幀一次,但後續顯示就比較順暢了。

可能有同學對上面的理解有點吃力,我們用通俗點的例子去描述一下。Vsync機制就像一臺轉速固定的發動機(60轉/s),每一轉都是處理一些UI的操作,但是不是每一轉都有事情幹,例如我們掛空擋的時候。而有時候因為一些阻力的原因,導致某一圈工作量過大,超過了16ms,那麼這發動機這秒內就不是60轉了,我們將這個轉速稱為流暢度。

獲取流暢度的值

上面描述到對於VSync機制,我們是理解為一種定時中斷,這個概念我們可以試著與Loop產生一種聯絡。在VSync機制中1s內Loop執行的次數。在這樣的機制下,我們在每一次的Loop執行前,我們記錄一下,就能獲取的流暢度的相對情況。

而Android中有一個叫畫圖的打雜工————Choreographer物件。Google的官方API描述是,它用於協調animations、input以及drawing的時序,並且每個Looper公用一個Choreographer物件。Choreographer中文翻譯過來是”舞蹈指揮”,字面上的意思就是優雅地指揮以上三個UI操作一起跳一支舞。

Choreographer的構造方法:

private Choreographer(Looper looper) {    
  mLooper = looper;    
  mHandler = new FrameHandler(looper);    
  mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper) : null;    
  mLastFrameTimeNanos = Long.MIN_VALUE;    
  mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());    
  mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];   
  for (int i = 0; i <= CALLBACK_LAST; i++) {        
   mCallbackQueues[i] = new CallbackQueue();    
  }
}

通過深入分析UI 上層事件處理核心機制 ChoreographerAndroid Choreographer 原始碼分析等文章參考理解,從上面Choreographer的構造方法我們理解到:

  1. Choreographer根據一個Looper來生成,Looper和執行緒是一對一的關係,因此對於每一條執行緒都有對應的一個Choreographer。
  2. 初始化FrameHandler。接收處理訊息。
  3. 初始化FrameDisplayEventReceiver。FrameDisplayEventReceiver用來接收垂直同步脈衝,就是VSync訊號,VSync訊號是一個時間脈衝,一般為60HZ,用來控制系統同步操作。
  4. 初始化mLastFrameTimeNanos(標記上一個frame的渲染時間)以及mFrameIntervalNanos(幀率,fps,一般手機上為1s/60)。
  5. 初始化CallbackQueue,callback佇列,將在下一幀開始渲染時回撥。

然而Choreographer的主要工作在doFrame中,我們針對來看doFrame函式:

void doFrame(long frameTimeNanos, int frame) {    
  final long startNanos;    
  synchronized (mLock) {        
    if (!mFrameScheduled) { //判斷是否有callback需要執行,mFrameScheduled會在postCallBack的時候置為true,一次frame執行時置為false       
      return; // no work to do        
    }
    \\\\列印跳frame時間        
    if (DEBUG_JANK && mDebugPrintNextFrameTimeDelta) {            
      mDebugPrintNextFrameTimeDelta = false;            
      Log.d(TAG, "Frame time delta: "                    
              + ((frameTimeNanos - mLastFrameTimeNanos) *  0.000001f) + " ms");        
    }
    //設定當前frame的Vsync訊號到來時間        
    long intendedFrameTimeNanos = frameTimeNanos;        
    startNanos = System.nanoTime();//實際開始執行當前frame的時間
    //時間差        
    final long jitterNanos = startNanos - frameTimeNanos;        
    if (jitterNanos >= mFrameIntervalNanos) {
      //時間差大於一個時鐘週期,認為跳frame            
      final long skippedFrames = jitterNanos / mFrameIntervalNanos;
      //跳frame數大於預設值,列印警告資訊,預設值為30            
      if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {                
         Log.i(TAG, "Skipped " + skippedFrames + " frames!  "                        
                    + "The application may be doing too much work on its main thread.");            
      }
      //計算實際開始當前frame與時鐘訊號的偏差值            
      final long lastFrameOffset = jitterNanos % mFrameIntervalNanos; 
      //列印偏差及跳幀資訊           
      if (DEBUG_JANK) {                
        Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "                        
                  + "which is more than the frame interval of "                        
                  + (mFrameIntervalNanos * 0.000001f) + " ms!  "                        
                  + "Skipping " + skippedFrames + " frames and setting frame "                        
                  + "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");            
       }
       //修正偏差值,忽略偏差,為了後續更好地同步工作            
       frameTimeNanos = startNanos - lastFrameOffset;        
    }
    ···
}

我截取了其中一段關於繪製和丟幀處理和判斷,後面的是回撥CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL;對我們的討論的目的過於深奧就不全部截取了。

我們利用Choreographer中的一個回撥介面,FrameCallback。

    public interface FrameCallback {
        /**
         * Called when a new display frame is being rendered.
         * ···
         */
        public void doFrame(long frameTimeNanos);
    }

doFrame()的註釋翻譯意思是:當新的一幀被繪製的時候被呼叫。因此我們利用這個特性,可以統計兩幀繪製的時間間隔。

主要流程如下:

1.實現Choreographer.FrameCallback介面;
2.在doFrame中統計兩幀繪製的時間;
3.啟動監測和處理資料;

@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
public class SMFrameCallback implements Choreographer.FrameCallback {
    private String TAG = "#SMFrameCallback";
    public static final float deviceRefreshRateMs = 16.6f;
    public static long lastFrameTimeNanos = 0;//納秒為單位
    public static long currentFrameTimeNanos = 0;
    public static SMFrameCallback sInstance;

    public void start() {
        Choreographer.getInstance().postFrameCallback(SMFrameCallback.getInstance());
    }

    public static SMFrameCallback getInstance() {
        if (sInstance == null) {
            sInstance = new SMFrameCallback();
        }
        return sInstance;
    }

    @Override
    public void doFrame(long frameTimeNanos) {
        if (lastFrameTimeNanos == 0) {
            lastFrameTimeNanos = frameTimeNanos;
            Choreographer.getInstance().postFrameCallback(this);
            return;
        }
        currentFrameTimeNanos = frameTimeNanos;
        // 計算兩次doFrame的時間間隔
        long value = (currentFrameTimeNanos - lastFrameTimeNanos) / 1000000;

        int skipFrameCount = skipFrameCount(lastFrameTimeNanos, currentFrameTimeNanos, deviceRefreshRateMs);

        Log.e(TAG, "兩次繪製時間間隔value=" + value + "  frameTimeNanos=" + frameTimeNanos + "  currentFrameTimeNanos=" + currentFrameTimeNanos + "  skipFrameCount=" + skipFrameCount + "");

        lastFrameTimeNanos = currentFrameTimeNanos;
        Choreographer.getInstance().postFrameCallback(this);
    }

    /**
     * 計算跳過多少幀
     */
    private int skipFrameCount(long start, long end, float devRefreshRate) {
        int count = 0;
        long diffNs = end - start;

        long diffMs = TimeUnit.MILLISECONDS.convert(diffNs, TimeUnit.MILLISECONDS);
        long dev = Math.round(devRefreshRate);
        if (diffMs > dev) {
            long skipCount = diffMs / dev;
            count = (int) skipCount;
        }
        return count;
    }
}

通過上述的工具類,我們在需要檢測的Activity中呼叫啟動程式碼即可。

 SMFrameCallback.getInstance().start();

一般情況下,我們會寫在我們的BaseActivity或者Activitylifecyclecallbacks中去呼叫。

自定義MyActivityLifeCycle實現Application.ActivityLifecycleCallbacks。

public class MyActivityLifeCycle implements Application.ActivityLifecycleCallbacks {
    private Handler mHandler = new Handler(Looper.getMainLooper());
    private boolean mPaused = true;
    private Runnable mCheckForegroundRunnable;
    private boolean mForeground = false;
    private static MyActivityLifeCycle sInstance;
    //當前Activity的弱引用
    private WeakReference<Activity> mActivityReference;

    protected final String TAG = "#MyActivityLifeCycle";

    public static final int ACTIVITY_ON_RESUME = 0;
    public static final int ACTIVITY_ON_PAUSE = 1;

    private MyActivityLifeCycle() {
    }

    public static synchronized MyActivityLifeCycle getInstance() {
        if (sInstance == null) {
            sInstance = new MyActivityLifeCycle();
        }
        return sInstance;
    }

    public Activity getCurrentActivity() {
        if (mActivityReference != null) {
            return mActivityReference.get();
        }
        return null;
    }

    public boolean isForeground() {
        return mForeground;
    }

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        mActivityReference = new WeakReference<>(activity);
    }

    @Override
    public void onActivityStarted(Activity activity) {

    }

    @Override
    public void onActivityResumed(Activity activity) {
        String activityName = activity.getClass().getName();
        notifyActivityChanged(activityName, ACTIVITY_ON_RESUME);
        mPaused = false;
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {
            FrameSkipMonitor.getInstance().setActivityName(activityName);
            FrameSkipMonitor.getInstance().OnActivityResume();
            if (!mForeground) {
                FrameSkipMonitor.getInstance().start();
            }
        }
        mForeground = true;
        if (mCheckForegroundRunnable != null) {
            mHandler.removeCallbacks(mCheckForegroundRunnable);
        }
        mActivityReference = new WeakReference<Activity>(activity);
    }

    @Override
    public void onActivityPaused(Activity activity) {
        notifyActivityChanged(activity.getClass().getName(), ACTIVITY_ON_PAUSE);
        mPaused = true;
        if (mCheckForegroundRunnable != null) {
            mHandler.removeCallbacks(mCheckForegroundRunnable);
        }
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN)
            FrameSkipMonitor.getInstance().OnActivityPause();

        mHandler.postDelayed(mCheckForegroundRunnable = new Runnable() {
            @Override
            public void run() {
                if (mPaused && mForeground) {
                    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {
                        FrameSkipMonitor.getInstance().report();
                    }
                    mForeground = false;
                }
            }
        }, 1000);
    }
}

然後在自定義的Application中呼叫。

public class MyApplication extends Application {

    ···

    @Override
    public void onCreate() {
        super.onCreate();
        registerActivityLifecycleCallbacks(MyActivityLifeCycle.getInstance());
    }
    ···
}

除了上述的Choreographer幀率檢測外,還有loop()列印日誌等方法來對幀率進行統計監測。這裡就不一一舉例了。

總結:

根據瞭解Google文件,我們理解到Android4.1引入了VSync機制,通過其Loop來了解當前App最高繪製能力。

  • 固定每隔16.6ms執行一次;
  • 如果沒有事件的時候,同樣會執行一個Loop;
  • 這個Loop在1s之內運行了多少次,可以表示為當前App繪製最高能力,即Android App卡頓程度;
  • 如果一次Loop執行時間超過16.6ms,即出現了丟幀情況。

所以通過VSync機制來描述流暢度是一個連續的過程,而在APP靜止某個介面時,流暢度很高,但FPS很低,流暢度更加客觀地描述APP的卡頓情況。

通過一個漫長的理論分析,我們即將在下一篇對引起卡頓原因的程式碼實操。我們先預先認知一下以下幾點引起卡頓的原因:

  1. 佈局Layout過於複雜,無法在16ms內完成渲染;
  2. View過度繪製,導致某些畫素在同一幀時間內被繪製多次,從而使CPU或GPU負載過重;
  3. View頻繁的觸發measure、layout,導致measure、layout累計耗時過多及整個View頻繁的重新渲染;
  4. 人為在UI執行緒中做輕微耗時操作,導致UI執行緒卡頓;
  5. 同一時間動畫執行的次數過多,導致CPU或GPU負載過重;
  6. 記憶體頻繁觸發GC過多(同一幀中頻繁建立記憶體),導致暫時阻塞渲染操作;
  7. 冗餘資源及邏輯等導致載入和執行緩慢;
  8. 工作執行緒優先順序未設定為Process.THREAD_PRIORITY_BACKGROUND導致後臺執行緒搶佔UI執行緒cpu時間片,阻塞渲染操作;
  9. 引起記憶體抖動、記憶體洩漏