歡迎光臨
每天分享高質量文章

用兩張圖告訴你,為什麼你的App會卡頓?

作者:CoorChice
https://www.jianshu.com/p/df4d5ec779c8

有什麼料?

從這篇文章中你能獲得這些內容:

知道setContentView()之後發生了什麼?
知道Android究竟是如何在螢幕上顯示我們期望的畫面的?
對Android的檢視架構有整體把握。
學會從根源處分析畫面卡頓的原因。
掌握如何編寫一個流暢的App的技巧。
從原始碼中學習Android的細想。
收穫兩張自製圖,幫助你理解Android的檢視架構。

從setContentView()說起

public class AnalyzeViewFrameworkActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_analyze_view_framwork);
  }
}

上面這段程式碼想必Androider們大都已經不能再熟悉的更多了。但是你知道這樣寫了之後發生什麼了嗎?這個佈局到底被新增到哪了?我的天,知識點來了!

可能很多同學也知道這個佈局是被放到了一個叫做DecorView的父佈局裡,但是我還是要再說一遍。且看下圖。

這個圖可能和夥伴們在書上或者網上常見的不太一樣,為什麼不太一樣呢?因為是我自己畫的,哈哈哈…
下麵就來看著圖捋一捋Android最基本的檢視框架。

PhoneWindow

估計很多同學都知道,每一個Activity都擁有一個Window物件的實體。這個實體實際是PhoneWindow型別的。那麼PhoneWindow從名字很容易看出,它應該是Window的兒子(即子類)!

知識點:每一個Activity都有一個PhoneWindow物件。

那麼,PhoneWindow有什麼用呢?它在Activity充當什麼角色呢?下麵我就姑且把PhoneWindow等同於Window來稱呼吧。
Window從字面看它是一個視窗,意思和PC上的視窗概念有點像。但也不是那麼準確。看圖說。可以看到,我們要顯示的佈局是被放到它的屬性mDecor中的,這個mDecor就是DecorView的一個實體。下麵會專門擼DecorView,現在先把關註點放到Window上。Window還有一個比較重要的屬性mWindowManager,它是WindowManager(這是個介面)的一個實現類的一個實體。我們平時透過getWindowManager()方法獲得的東西就是這個mWindowManager。顧名思義,它是Window的管理者,負責管理著視窗及其中顯示的內容。它的實際實現類是WindowManagerImpl。可能童鞋們現在正在PhoneWindow中尋找著這個mWindowManager是在哪裡實體化的,是不是上下來回滾動著這個類都找不見?STOP!mWindowManager是在它爹那裡就實體化好的。下麵程式碼是在Window.java中的。

public void setWindowManager(WindowManager wm, 
    IBinder appToken, 
    String appName, 
    boolean hardwareAccelerated)
 
{
        ...
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
            //獲取了一個WindowManager
        }
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
        //透過這裡我們可以知道,上面獲取到的wm實際是WindowManagerImpl型別的。
    }

透過上面的介紹,我們已經知道了Window中有負責承載佈局的DecorView,有負責管理的WindowManager(事實上它只是個代理,後面會講它代理的是誰)。

DecorView

前面提到過,在Activity的onCreate()中透過setContentView()設定的佈局實際是被放到DecorView中的。我們在圖中找到DecorView。

從圖中可以看到,DecorView繼承了FrameLayout,並且一般情況下,它會在先新增一個預設的佈局。比如DecorCaptionView,它是從上到下放置自己的子佈局的,相當於一個LinearLayout。通常它會有一個標題欄,然後有一個容納內容的mContentRoot,這個佈局的型別視情況而定。我們希望顯示的佈局就是放到了mContentRoot中。

知識點:透過setContentView()設定的佈局是被放到DecorView中,DecorView是檢視樹的最頂層。

WindowManager

前面已經提到過,WindowManager在Window中具有很重要的作用。我們先在圖中找到它。這裡需要先說明一點,在PhoneWindow中的mWindowManager實際是WindowManagerImpl型別的。WindowManagerImpl自然就是介面WindowManager的一個實現類嘍。這一點是我沒有在圖中反映的。

WindowManager是在Activity執行attach()時被建立的,attach()方法是在onCreate()之前被呼叫的。
Activity.java

final void attach(Context context, ActivityThread aThread,
    Instrumentation instr, IBinder token, int ident,
    Application application, Intent intent, ActivityInfo info,
    CharSequence title, Activity parent, String id,
    NonConfigurationInstances lastNonConfigurationInstances,
    Configuration config, String referrer, IVoiceInteractor voiceInteractor,
    Window window){
        ...
        mWindow = new PhoneWindow(thiswindow);
        //建立Window
        ...
        mWindow.setWindowManager(
         (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
         mToken, mComponent.flattenToString(),
         (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        //註意!這裡就是在建立WindowManager。
        //這個方法在前面已經說過了。
        if (mParent != null) {
           mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();
            }

繼續看圖。WindowManagerImpl持有了PhoneWindow的取用,因此它可以對PhoneWindow進行管理。同時它還持有一個非常重要的取用mGlobal。這個mGlobal指向一個WindowManagerGlobal型別的單例物件,這個單例每個應用程式只有唯一的一個。在圖中,我說明瞭WindowManagerGlobal維護了本應用程式內所有Window的DecorView,以及與每一個DecorView對應關聯的ViewRootImpl。這也就是為什麼我前面提到過,WindowManager只是一個代理,實際的管理功能是透過WindowManagerGlobal實現的。我們來看個原始碼的例子就比較清晰了。開始啦!

WimdowManagerImpl.java

public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params{
    ...
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    //實際是透過WindowManagerGlobal實現的。
}

從上面的程式碼可以看出,WindowManagerImpl確實只是WindowManagerGlobal的一個代理而已。同時,上面這個方法在整個Android的檢視框架流程中十分的重要。我們知道,在Activity執行onResume()後介面就要開始渲染了。原因是在onResume()時,會呼叫WindowManager的addView()方法(實際最後呼叫的是WindowManagerGlobal的addView()方法),把檢視新增到視窗上。
ActivityThread.java

final void handleResumeActivity(IBinder token,
    boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason)
 
{
    ...
    ViewManager wm = a.getWindowManager();
    //獲得WindowManager,實際是WindowManagerImpl
    ...
    wm.addView(decor, l);
    //新增檢視
    ...
    wm.updateViewLayout(decor, l);
    //需要掃清的時候會走這裡
    ...
}

從上面可以看到,當Activity執行onResume()的時候就會新增檢視,或者掃清檢視。需要解釋一點:WindowManager實現了ViewManager介面。

如圖中所說,WindowManagerGlobal呼叫addView()的時候會把DecorView新增到它維護的陣列中去,並且會建立另一個關鍵且極其重要的ViewRootImpl(這個必須要專門講一下)型別的物件,並且也會把它存到一個陣列中維護。
WindowManagerGlobal.java

public void addView(View view, ViewGroup.LayoutParams params,
    Display display, Window parentWindow
{
    ...
    root = new ViewRootImpl(view.getContext(), display);
    //重要角色登場
    view.setLayoutParams(wparams);
    mViews.add(view);
    mRoots.add(root);
    //儲存起來維護
    mParams.add(wparams);
    ...
    root.setView(view, wparams, panelParentView);
    //設定必要屬性view是DecorView,panelParentView是PhoneWindow
    ...
}

可以看出ViewRootImpl是在Activity執行onResume()的時候才被建立的,並且此時才把DecorView傳進去讓它管理。

知識點:WindowManager是在onCreate()時被建立。它對視窗的管理能力實際是透過WindowManagerGlobal實現的。在onResume()是檢視才透過WindowManager被新增到視窗上。

ViewRootImpl

ViewRootImpl能夠和系統的WindowManagerService進行互動,並且管理著DecorView的繪製和視窗狀態。非常的重要。趕緊在圖中找到對應位置吧!

ViewRootImpl並不是一個View,而是負責管理檢視的。它配合系統來完成對一個Window內的檢視樹的管理。從圖中也可以看到,它持有了DecorView的取用,並且檢視樹它是檢視樹繪製的起點。因此,ViewRootImpl會稍微複雜一點,需要我們更深入的去瞭解,在圖中我標出了它比較重要的組成Surface和Choreographer等都會在後面提到。

到此,我們已經一起把第一張圖擼了一遍了,現在童鞋們因該對Android檢視框架有了大致的瞭解。下麵將更進一步的去瞭解Android的繪製機制。

App總是卡頓到底是什麼原因?

下麵將會詳細的講解為什麼我們設定的檢視能夠被繪製到螢幕上?這中間究竟隱藏著怎樣的離奇?看完之後,你自然就能夠從根源知道為什麼你的App會那麼卡,以及開始有思路著手解決這些卡頓。

同樣用一張圖來展示這個過程。由於Android繪製機制確實有點複雜,所以第一眼看到的時候你的內心中可能蹦騰了一萬隻草泥馬?。不要怕!我們從源頭開始,一點一點的梳理這個看似複雜的繪製機制。為什麼說看似複雜呢?因為這個過程只需要幾分鐘。Just Do It!

CPU、GPU是什麼?

整天聽到CPU、GPU的,你知道他們是乾什麼的嗎?這裡簡單的提一下,幫助理解後面的內容。

在Android的繪製架構中,CPU主要負責了檢視的測量、佈局、記錄、把內容計算成Polygons多邊形或者Texture紋理,而GPU主要負責把Polygons或者Textture進行Rasterization柵格化,這樣才能在螢幕上成像。在使用硬體加速後,GPU會分擔CPU的計算任務,而CPU會專註處理邏輯,這樣減輕CPU的負擔,使得整個系統效率更高。
image

RefreshRate掃清率和FrameRate幀率

RefreshRate掃清率是螢幕每秒掃清的次數,是一個與硬體有關的固定值。在Android平臺上,這個值一般為60HZ,即螢幕每秒掃清60次。

FrameRate幀率是每秒繪製的幀數。通常只要幀數和掃清率保持一致,就能夠看到流暢的畫面。在Android平臺,我們應該儘量維持60FPS的幀率。但有時候由於檢視的複雜,它們可能就會出現不一致的情況。

如圖,當幀率小於掃清率時,比如圖中的30FPS < 60HZ,就會出現相鄰兩幀看到的是同一個畫面,這就造成了卡頓。這就是為什麼我們總會說,要儘量保證一幀畫面能夠在16ms內繪製完成,就是為了和螢幕的掃清率保持同步。

下麵將會介紹Android是如何來確保掃清率和幀率保持同步的。

Vsync(垂直同步)是什麼?

你可能在遊戲的設定中見過Vsync,開啟它通常能夠提高遊戲效能。在Android中,同樣使用Vsync垂直同步來提高顯示效能。它能夠使幀率FrameRate和硬體的RefreshRate掃清強制保持一致。

HWComposer與Vsync不得不說的事

看圖啦看圖啦。首先在最左邊我們看到有個叫HWComposer的類,這是一個c++編寫的類。它Android系統初始化時就被建立,然後開始配合硬體產生Vsync訊號,也就是圖中的HW_Vsync訊號。當然它不是一直不停的在產生,這樣會導致Vsync訊號的接收者不停的接收到繪製、渲染命令,即使它們並不需要,這樣會帶來嚴重的效能損耗,因為進行了很多無用的繪製。所以它被設計設計成能夠喚醒和睡眠的。這使得HWComposer在需要時才產生Vsync訊號(比如當螢幕上的內容需要改變時),不需要時進入睡眠狀態(比如當螢幕上的內容保持不變時,此時螢幕每次掃清都是顯示緩衝區裡沒發生變化的內容)。

如圖,Vsync的兩個接收者,一個是SurfaceFlinger(負責合成各個Surface),一個是Choreographer(負責控制檢視的繪製)。我們稍後再介紹,現在先知道它們是乾什麼的就行了。

Vsync offset機制

為了提高效率,儘量減少卡頓,在Android 4.1時引入了Vsync機制,併在隨後的4.4版本中加入Vsync offset偏移機制。

圖1. 為4.1時期的Vsync機制。可以看到,當一個Vsync訊號到來時,SurfaceFlinger和UI繪製行程會同時啟動,導致它們競爭CPU資源,而CPU分配資源會耗費時間,著降低系統效能。同時當收到一個Vsync訊號時,第N幀開始繪製。等再收到一個Vsync訊號時,第N幀才被SurfaceFlinger合成。而需要顯示到螢幕上,需要等都第三個Vsync訊號。這是比較低效率。於是才有了圖2. 4.4版本加入的Vsync offset機制。

圖2. Google加入Vsync offset機制後,原本的HW_Vsync訊號會經過DispSync會分成Vsync和SF_Vsync兩個虛擬化的Vsync訊號。其中Vsync訊號會傳送到Choreographer中,而SF_Vsync會傳送到SurfaceFlinger中。理論上只要phase_app和phase_sf這兩個偏移引數設定合理,在繪製階段消耗的時間控制好,那麼畫面就會像圖2中的前幾幀那樣有序流暢的進行。理想總是美好的。實際上很難一直維持這種有序和流暢,比如frame_3是比較複雜的一幀,它的繪製完成的時間超過了SurfaceFlinger開始合成的時間,所以它必須要等到下一個Vsync訊號到來時才能被合成。這樣便造成了一幀的丟失。但即使是這樣,如你所見,加入了Vsync offset機制後,繪製效率還是提高了很多。

從圖中可以看到,Vsync和SF_Vsync的偏移量分別由phase_app和phase_sf控制,這兩個值是可以調節的,預設為0,可為負值。你只需要找到BoardConfig.mk檔案,就可以對這兩個值進行調節。

回到ViewRootImpl

前面介紹了幾個關鍵的概念,現在我們回到ViewRootImpl中去,在圖中找到ViewRootImpl的對應位置。

前面說過,ViewRootImpl控制著一個Window中的整個檢視樹的繪製。那它是如何進行控制的呢?一次繪製究竟是如何開始的呢?

在ViewRootImpl建立的時候,會獲取到前面提到過過的一個關鍵物件Choreographer。Choreographer在一個執行緒中僅存在一個實體,因此在UI執行緒只有一個Choreographer存在。也就說,通常情況下,它相當於一個應用中的單例。

在ViewRootImpl初始化時,會實現一個Choreographer.FrameCallback(這是一個Choreographer中的內部類),並向Choreographer中post。顧名思義,FrameCallback會在每次接收到Vsync訊號時被回呼。
Choreographer.java

public interface FrameCallback {
    public void doFrame(long frameTimeNanos);
    //一旦註冊到CallbackQueue中,那麼
    //每次Choreographer接收到Vsync訊號時都會回呼。
    }

FrameCallback一旦被註冊,那麼每次收到Vsync訊號時它都會被回呼。利用它,我們可以實現會幀率的監聽。

ViewRootImpl.java

//這個方法只有在ViewRootImpl初始化時才會被呼叫
private void profileRendering(boolean enabled) {
    ...
    mRenderProfiler = new Choreographer.FrameCallback() {
    @Override
    public void doFrame(long frameTimeNanos) {
        ...
        scheduleTraversals();
        //請求一個Vsync訊號,後面還會提到這個方法
        mChoreographer.postFrameCallback(mRenderProfiler);
        //每次回呼時,重新將FrameCallback post到Choreographer中
        ...
    }
    };
    ...
    mChoreographer.postFrameCallback(mRenderProfiler);
    //將FrameCallback post到Choreographer中
    ...
}

上面程式碼出現了一個重要方法scheduleTraversals()。下麵我們看看它究竟為何重要。
ViewRootImpl.java

void scheduleTraversals({
    ...
    mChoreographer.postCallback(
        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    //向Choreographer中post一個TraversalRunnable
    //這又是一個十分重要的物件
    ...
    }

可以看出scheduleTraversals()每次呼叫時會向Choreographer中post一個TraversalRunnable,它會促使Choreographer去請求一個Vsync訊號。所以這個方法的作用就是用來請求一次Vsync訊號掃清介面的。事實上,你可以看到,在invalidate()、requestLayout()等操作中,都能夠看到它被呼叫。原因就是這些操作需要掃清介面,所以需要請求一個Vsync訊號來出發新介面的繪製。

ViewRootImpl.java

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
        //開始遍歷檢視樹,這意味著開始繪製一幀內容了
    }
}

從圖中可以看到,每當doTraversal()被呼叫時,一系列的測量、佈局和繪製操作就開始了。在繪製時,會透過Surface來獲取一個Canvas記憶體塊交給DecorView,用於檢視的繪製。整個View檢視的內容都是被繪製到這個Canvas中。

Choreographer中的風起雲湧

前面反覆提到向Choreographer中post回呼,那麼post過去發生了些什麼呢?從圖中可以看到,所有的post操作最終都進入到postCallbackDelayedInternal()中。

Choreographer.java

private void postCallbackDelayedInternal(int callbackType,
    Object action, Object token, long delayMillis)
 
{
    ...
    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
        //將Callback新增到CallbackQueue[]中
        if (dueTime <= now) {
            scheduleFrameLocked(now);
            //如果回呼時間到了,請求一個Vsync訊號
            //在接收到後會呼叫doFrame()回呼這個Callback。
        } else {
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            //非同步訊息,避免被攔截器攔截
            mHandler.sendMessageAtTime(msg, dueTime);
            //如果還沒到回呼的時間,向FrameHandelr中傳送
            //MSG_DO_SCHEDULE_CALLBACK訊息
        }
    }
    ...
}

上面這段程式碼會把post到Choreographer中的Callback新增到Callback[]中,並且當它因該被回呼時,請求一個Vsync訊號,在接收到下一個Vsync訊號時回呼這個Callback。如果沒有到回呼的時間,則向FrameHandler中傳送一個MSG_DO_SCHEDULE_CALLBACK訊息,但最終還是會請求一個Vsync訊號,然後回呼這個Callback。

簡單說一下CallbackQueue。它和MessageQueue差不多,都是單連結串列結構。事實上,算上每種型別的單連結串列結構,它更像是二維陣列的樣子。簡單點描述,假設有一個MessageQueue[]陣列,裡面存了幾個MessageQueue。來看看它的建立你可能就明白,它是在Choreographer初始化時建立的。

private Choreographer(Looper looper) {
    mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
    //CALLBACK_LAST值為3。
    for (int i = 0; i <= CALLBACK_LAST; i++) {
        mCallbackQueues[i] = new CallbackQueue();
    }
}

現在來看看前面程式碼中呼叫的scheduleFrameLocked()是如何請求一個Vsync訊號的。

private void scheduleFrameLocked(long now) {
    ...
    //先判斷當前是不是在UI執行緒
    if (isRunningOnLooperThreadLocked()) {
        scheduleVsyncLocked();
        //是UI執行緒就請求一個Vsync訊號
    } else {
        Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
        msg.setAsynchronous(true);
        mHandler.sendMessageAtFrontOfQueue(msg);
        //不在UI執行緒向FrameHandler傳送一個MSG_DO_SCHEDULE_VSYNC訊息
        //來請求一個Vsync訊號
    }
}

private void scheduleVsyncLocked() {
    mDisplayEventReceiver.scheduleVsync();
    //透過DisplayEventReceiver請求一個Vsync訊號
    //這是個恨角色,待會兒會聊聊它。
    //MSG_DO_SCHEDULE_VSYNC訊息也是透過呼叫這個方法請求Vsync訊號的。
}

上面我們提到過,Choreographer在一個執行緒中只有一個。所以,如果在其它執行緒,需要透過Handler來切換到UI執行緒,然後再請求Vsync訊號。

下麵看看剛剛出場的mDisplayEventReceiver是個什麼鬼?

private final class FrameDisplayEventReceiver extends DisplayEventReceiver
    implements Runnable 
{

    //這個方法用於接收Vsync訊號
    public void onVsync(){
        ...
        Message msg = Message.obtain(mHandler, this);
        msg.setAsynchronous(true);
        mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        //這裡並沒有設定訊息的型別
        //其實就是預設為0,即MSG_DO_FRAME型別的訊息
        //它其實就是通知Choreographer開始回呼CallbackQueue[]中的Callback了
        //也就是開始繪製下一幀的內容了
    }

    //這個方法是在父類中的,寫在這方便看
    public void scheduleVsync() {
        ...
        nativeScheduleVsync(mReceiverPtr);
        //請求一個Vsync訊號
    }
}

這給類功能比較明確,而且很重要!

上面一直在說向FrameHandler中發訊息,搞得神神秘秘的。接下來就來看看FrameHandler本尊。請在圖中找到對應位置哦。

private final class FrameHandler extends Handler {
    public FrameHandler(Looper looper) {
        super(looper);
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_DO_FRAME:
                //開始回呼Callback,以開始繪製下一幀內容
                doFrame(System.nanoTime(), 0);
                break;
            case MSG_DO_SCHEDULE_VSYNC:
                //請求一個Vsync訊號
                doScheduleVsync();
                break;
            case MSG_DO_SCHEDULE_CALLBACK:
                //實際也是請求一個Vsync訊號
                doScheduleCallback(msg.arg1);
                break;
            }
        }
    }

FrameHandler主要在UI執行緒處理3種型別的訊息。

MSG_DO_FRAME:值為0。當接收到一個Vsync訊號時會傳送該種型別的訊息,然後開始回呼CallbackQueue[]中的Callback。比如上面說過,在ViewRootImpl有兩個重要的Callback,FrameCallback(請求Vsync並再次註冊回呼)和TraversalRunnable(執行doTraversal()開始繪製介面)頻繁被註冊。
MSG_DO_SCHEDULE_VSYNC:值為1。當需要請求一個Vsync訊息(即螢幕上的內容需要更新時)會傳送這個訊息。接收到Vsync後,同上一步。
MSG_DO_SCHEDULE_CALLBACK:值為2。請求回呼一個Callback。實際上會先請求一個Vsync訊號,然後再傳送MSG_DO_FRAME訊息,然後再回呼。
FrameHandler並不複雜,但在UI的繪製過程中具有重要的作用,所以一定要結合圖梳理下這個流程。

SurfaceFlinger和Surface簡單說

在介紹Vsync的時候,我們可能已經看到了,現在Android系統會將HW_VSYNC虛擬化為兩個Vsync訊號。一個是VSYNC,被髮送給上面一直在講的Choreographer,用於觸發檢視樹的繪製渲染。另一個是SF_VSYNC,被髮送給我接下來要講的SurfaceFlinger,用於觸發Surface的合成,即各個Window視窗畫面的合成。接下來我們就簡單的看下SurfaceFlinger和Surface。由於這部分基本是c++編寫的,我著重講原理。

隱藏在背後的Surface

平時同學們都知道,我們的檢視需要被繪製。那麼它們被繪製到那了呢?也許很多童鞋腦海裡立即浮現出一個詞:Canvas。但是,~沒錯!就是繪製到了Canvas上。那麼Canvas又是怎麼來的呢?是的,它可以New出來的。但是前面提到過,我們Window中的檢視樹都是被繪製到一個由Surface提供的Canvas上。忘了的童鞋面壁思過。

Canvas實際代表了一塊記憶體,用於儲存繪製出來的資料。在Canvas的建構式中你可以看到:

public Canvas({
    ...
    mNativeCanvasWrapper = initRaster(null);
    //申請一塊記憶體,並且傳回該記憶體的一個long型別的標記或者索引。
    ...
}

可以看到,Canvas實際主要就是持有了一塊用於繪製的記憶體塊的索引long mNativeCanvasWrapper。每次繪製時就透過這個索引找到對應的記憶體塊,然後將資料繪製到記憶體中。比如:

public void drawRect(@NonNull RectF rect, @NonNull Paint paint) {
    native_drawRect(mNativeCanvasWrapper,
        rect.left, rect.top, rect.right, rect.bottom, paint.getNativeInstance());
    //在mNativeCanvasWrapper標記的記憶體中繪製一個矩形。
    }

簡單的說一下。Android繪製圖形是透過圖形庫Skia(主要針對2D)或OpenGL(主要針對3D)進行。圖形庫是個什麼概念?就好比你在PC上用畫板畫圖,此時畫板就相當於Android中的圖形庫,它提供了一系列標準化的工具供我們畫圖使用。比如我們drawRect()實際就是操作圖形庫在記憶體上寫入了一個矩形的資料。

扯多了,我們繼續回到Surface上。當ViewRootImpl執行到draw()方法(即開始繪製圖形資料了),會根據是否開啟了硬體(從Android 4.0開始預設是開啟的)加速來決定是使用CPU軟繪製還是使用GPU硬繪製。如果使用軟繪製,圖形資料會繪製在Surface預設的CompatibleCanvas上(和普通Canvas的唯一區別就是對Matrix進行了處理,提高在不同裝置上的相容性)。如果使用了硬繪製,圖形資料會被繪製在DisplayListCanvas上。DisplayListCanvas會透過GPU使用openGL圖形庫進行繪製,因此具有更高的效率。

前面也簡單說了一下,每一個Window都會有一個自己的Surface,也就是說一個應用程式中會存在多個Surface。透過上面的講解,童鞋們也都知道了Surface的作用就是管理用於繪製檢視樹的Canvas的。這個Surface是和SurfaceFlinger共享,從它實現了Parcelable介面也可以才想到它會被序列化傳遞。事實上,Surface中的繪製資料是透過匿名共享記憶體的方式和SurfaceFlinger共享的,這樣SurfaceFlinger可以根據不同的Surface,找到它所對應的記憶體區域中的繪製資料,然後進行合成。

合成師SurfaceFlinger

SurfaceFlinger是系統的一個服務。前面也一直在提到它專門負責把每個Surface中的內容合成快取,以待顯示到螢幕上。SurfaceFlinger在合成Surface時是根據Surface的Z-order順序一層一層進行。比如一個Dialog的Surface就會在Activity的Surface上面。然後這個東西不多提了。

終於可以說說你的App為什麼這麼卡的原因了

透過對Android繪製機制的瞭解,我們知道造成應用卡頓的根源就在於16ms內不能完成繪製渲染合成過程,因為Android平臺的硬體掃清率為60HZ,大概就是16ms掃清一次。如果沒能在16ms內完成這個過程,就會使螢幕重覆顯示上一幀的內容,即造成了卡頓。在這16ms內,需要完成檢視樹的所有測量、佈局、繪製渲染及合成。而我們的最佳化工作主要就是針對這個過程的。

複雜的檢視樹

如果檢視樹複雜,會使整個Traversal過程變長。因此,我們在開發過程中要控制檢視樹的複雜程度。減少不必要的層級巢狀。比如使用RelativeLayout可以減少複雜佈局的巢狀。

頻繁的requestlayout()

如果頻繁的觸發requestLayout(),就可能會導致在一幀的週期內,頻繁的發生佈局計算,這也會導致整個Traversal過程變長。有的ViewGroup型別的控制元件,比如RelativeLayout,在一幀的週期內會透過兩次layout()操作來計算確認子View的位置,這種少量的操作並不會引起能夠被註意到的效能問題。但是如果在一幀的週期內頻繁的發生layout()計算,就會導致嚴重的效能,每次計算都是要消耗時間的!而requestLayout()操作,會向ViewRootImpl中一個名為mLayoutRequesters的List集合裡新增需要重新Layout的View,這些View將在下一幀中全部重新layout()一遍。通常在一個控制元件載入之後,如果沒什麼變化的話,它不會在每次的掃清中都重新layout()一次,因為這是一個費時的計算過程。所以,如果每一幀都有許多View需要進行layout()操作,可想而知你的介面將會卡到爆!卡到爆!需要註意,setLayoutParams()最終也會呼叫requestLayout(),所以也不能爛用!同學們在寫程式碼的過程中一定要謹慎註意那些可能引起requestLayout()的地方啊!

UI執行緒被阻塞

如果UI執行緒受到阻塞,顯而易見的是,我們的Traversal過程也將受阻塞!畫面卡頓是妥妥的發生啊。這就是為什麼大家一直在強調不要在UI執行緒做耗時操作的原因。通常UI執行緒的阻塞和以下原因脫不了關係。

在UI執行緒中進行IO讀寫資料的操作。這是一個很費時的過程好嗎?千萬別這麼乾。如果不想獲得一個卡到爆的App的話,把IO操作統統放到子執行緒中去。
在UI執行緒中進行複雜的運算操作。運算本身是一個耗時的操作,當然簡單的運算幾乎瞬間完成,所以不會讓你感受到它在耗時。但是對於十分複雜的運算,對時間的消耗是十分辣眼睛的!如果不想獲得一個卡到爆的App的話,把複雜的運算操作放到子執行緒中去。
在UI執行緒中進行複雜的資料處理。我說的是比如資料的加密、解密、編碼等等。這些操作都需要進行複雜運算,特別是在資料比較複雜的時候。如果不想獲得一個卡到爆的App的話,把複雜資料的處理工作放到子執行緒中去。
頻繁的發生GC,導致UI執行緒被頻繁中斷。在Java中,發生GC(垃圾回收)意味著Stop-The-World,就是說其它執行緒全部會被暫停啊。好可怕!正常的GC導致偶然的畫面卡頓是可以接受的,但是頻繁發生就讓人很蛋疼了!頻繁GC的罪魁禍首是記憶體抖動.
故意阻塞UI執行緒。好吧,相信沒人會這麼乾吧。比如sleep()一下?

參考資料:

  1. Implementing VSYNC:https://source.android.com/devices/graphics/implement-vsync

  2. SurfaceFlinger and Hardware Composer:https://source.android.com/devices/graphics/arch-sf-hwc

  3. Surface and SurfaceHolder:https://source.android.com/devices/graphics/arch-sh

  4. Implementing the Hardware Composer HAL:https://source.android.com/devices/graphics/implement-hwc

  5. 可能是史上最簡單的!一張圖3分鐘讓你明白Activity啟動流程,不看後悔!http://www.jianshu.com/p/9ecea420eb52

  6. 驚天秘密!從Thread開始,揭露Android執行緒通訊的詭計和主執行緒的陰謀http://www.jianshu.com/p/8862bd2b6a29

  7. 震驚!這個控制元件絕對值得收藏。輕鬆實現圓角、文字描邊、狀態指示等效果http://www.jianshu.com/p/cfe18cbc6924

  8. Android記憶體基礎——記憶體抖動http://www.jianshu.com/p/69e6f894c698

  9. Android效能最佳化之渲染篇http://hukai.me/android-performance-render/

  10. Android硬體加速原理與實現簡介http://tech.meituan.com/hardware-accelerate.html

  11. Android SurfaceFlinger對VSync訊號的處理過程分析http://blog.csdn.net/yangwen123/article/details/17001405

  12. Android Vsync 原理http://www.10tiao.com/html/431/201601/401709603/1.html

  13. Android Choreographer 原始碼分析http://www.jianshu.com/p/996bca12eb1d?utm_campaign=hugo&utm;_medium=reader_share&utm;_content=note

  14. Android應用程式視窗(Activity)的檢視物件(View)的建立過程分析:http://blog.csdn.net/luoshengyang/article/details/8245546

  15. Android 4.4(KitKat)中VSync訊號的虛擬化http://blog.csdn.net/jinzhuojun/article/details/17293325

  16. Understanding necessity of Android VSYNC signals:http://stackoverflow.com/questions/27947848/understanding-necessity-of-android-vsync-signals


●編號337,輸入編號直達本文

●輸入m獲取到文章目錄

推薦↓↓↓

Java程式設計

更多推薦18個技術類公眾微信

涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

贊(0)

分享創造快樂

© 2024 知識星球   網站地圖