對於現今市面上針對於用戶交互的應用,都有使用列表去展現信息。列表對於用戶來講是十分好的瀏覽、接收信息的一個控件。對於產品來講,列表流暢度的重要性就不言而喻了。而流暢度的好壞,對一個產品的基本體驗和口碑有着極大的影響。然而Android手機與iPhone手機對比,第一點每每就是流暢度的問題,對於技術來講,咱們的Google親爹,不斷對這個詬病進行優化,包括GPU硬件加速、將Dalvik虛擬機換成ART等等,咱們的代碼也不斷從ListView換成RecyclerView。當咱們沾沾自喜地說咱們的產品也能像飄柔那樣順滑,咱們的產品經理說出一個詞「競品對比」,來了一個JSON數據結構是三層數組嵌套,直觀來講就是嵌套三層RecyclerView,懵逼臉?固然咱們編碼確定不會這樣作。好了回到咱們的流暢度問題。javascript
對於流暢度,咱們首先會重點說到FPS問題,致使流暢度不足。FPS是Frames Per Second,Frame(畫面、幀),p就是Per(每),s就是Second(秒)。人類大腦與眼睛對一個畫面的連貫性感知有一個邊界值,譬如咱們看電影會以爲畫面很天然連貫,其幀率一般爲 24fps。java
但通過國內BAT專業測試團隊研究,咱們常說的FPS與流暢度的關係並不許確。此刻咱們要先理解60FPS、16ms這倆名詞。咱們在其餘的文章裏總會看到的名詞。那它到底是什麼意思呢?它們出於官方出的性能優化視頻Android Performance Patterns: Why 60fps? 對其解釋說:android
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的畫面更新。api
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提交到屏幕的幀數。數據結構
上面所說的繪製指的是Android的繪製機制。咱們要從View的建立、View的測量、View的佈局、View的繪製對整一個繪製流程有一個基本的理解,下面咱們更好地探究如何流暢,爲何卡頓。app
大多數用戶感受卡頓等性能的問題根源就是渲染性能(Render Performance)。此時要從VSync機制開始。VSync機制是Android4.1引入的是Vertical Synchronization(垂直同步)的縮寫。咱們能夠把它看做是一種定時中斷,其目的是爲了改善android的流暢程度。 ide
清晰理解上面咱們所述的概念後,接着去理解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 上層事件處理核心機制 Choreographer和Android Choreographer 源碼分析等文章參考理解,從上面Choreographer的構造方法咱們理解到:
然而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最高繪製能力。
因此經過VSync機制來描述流暢度是一個連續的過程,而在APP靜止某個界面時,流暢度很高,但FPS很低,流暢度更加客觀地描述APP的卡頓狀況。
經過一個漫長的理論分析,咱們即將在下一篇對引發卡頓緣由的代碼實操。咱們先預先認知一下如下幾點引發卡頓的緣由: