由淺到深,深刻分析APP卡頓和優化

從這篇文章中你能得到這些料:java

  • 知道setContentView()以後發生了什麼?
  • 知道Android到底是如何在屏幕上顯示咱們指望的畫面的?
  • 對Android的視圖架構有總體把握。
  • 學會從根源處分析畫面卡頓的緣由。
  • 掌握如何編寫一個流暢的App的技巧。
  • 從源碼中學習Android的細想。
  • 收穫兩張自制圖,幫助你理解Android的視圖架構。

文末有資料大綱c++

一.從setContentView()提及

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

上面這段代碼想必Androider們大都已經不能再熟悉的更多了。可是你知道這樣寫了以後發生什麼了嗎?這個佈局到底被添加到哪了?個人天,知識點來了!數組

可能不少同窗也知道這個佈局是被放到了一個叫作DecorView的父佈局裏,可是我仍是要再說一遍。且看下圖ヾ(❀ω)ノ゙ 緩存

這個圖可能和夥伴們在書上或者網上常見的不太同樣,爲何不太同樣呢?由於是我本身畫的,哈哈哈...bash

下面就來看着圖捋一捋Android最基本的視圖框架架構

1.PhoneWindow

估計不少同窗都知道,每個Activity都擁有一個Window對象的實例。這個實例實際是PhoneWindow類型的。那麼PhoneWindow從名字很容易看出,它應該是Window的兒子(即子類)!app

知識點:每個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中的。ide

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(事實上它只是個代理,後面會講它代理的是誰)。

2.DecorView

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

從圖中能夠看到,DecorView繼承了FrameLayout,而且通常狀況下,它會在先添加一個預設的佈局。好比DecorCaptionView,它是從上到下放置本身的子佈局的,至關於一個LinearLayout。一般它會有一個標題欄,而後有一個容納內容的mContentRoot,這個佈局的類型視狀況而定。咱們但願顯示的佈局就是放到了mContentRoot中。

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

3.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(this, window);
        //建立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被添加到窗口上。

4.ViewRootImpl

ViewRootImpl可以和系統的WindowManagerService進行交互,而且管理着DecorView的繪製和窗口狀態。很是的重要。趕忙在圖中找到對應位置吧!

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

到此,咱們已經一塊兒把第一張圖擼了一遍了,如今童鞋們因該對Android視圖框架有了大體的瞭解。下面將更進一步的去了解Android的繪製機制。

二.App老是卡頓究竟是什麼緣由?

下面將會詳細的講解爲何咱們設置的視圖可以被繪製到屏幕上?這中間究竟隱藏着怎樣的離奇?看完以後,你天然就可以從根源知道爲何你的App會那麼卡,以及開始有思路着手解決這些卡頓。

一樣用一張圖來展現這個過程。因爲Android繪製機制確實有點複雜,因此第一眼看到的時候你的心裏中可能蹦騰了一萬隻草泥馬😂。不要怕!咱們從源頭開始,一點一點的梳理這個看似複雜的繪製機制。爲何說看似複雜呢?由於這個過程只須要幾分鐘。Just Do It!

1.CPU、GPU是搞什麼鬼的?

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

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

2.RefreshRate刷新率和FrameRate幀率

RefreshRate刷新率是屏幕每秒刷新的次數,是一個與硬件有關的固定值。在Android平臺上,這個值通常爲60HZ,即屏幕每秒刷新60次。

FrameRate幀率是每秒繪製的幀數。一般只要幀數和刷新率保持一致,就可以看到流暢的畫面。在Android平臺,咱們應該儘可能維持60FPS的幀率。但有時候因爲視圖的複雜,它們可能就會出現不一致的狀況。

如圖,當幀率小於刷新率時,好比圖中的30FPS < 60HZ,就會出現相鄰兩幀看到的是同一個畫面,這就形成了卡頓。這就是爲何咱們總會說,要儘可能保證一幀畫面可以在16ms內繪製完成,就是爲了和屏幕的刷新率保持同步。

下面將會介紹Android是如何來確保刷新率和幀率保持同步的。

3.Vsync(垂直同步)是什麼?

你可能在遊戲的設置中見過Vsync,開啓它一般可以提升遊戲性能。在Android中,一樣使用Vsync垂直同步來提升顯示性能。它可以使幀率FrameRate和硬件的RefreshRate刷新強制保持一致。

3.1HWComposer與Vsync不得不說的事

看圖啦看圖啦。首先在最左邊咱們看到有個叫HWComposer的類,這是一個c++編寫的類。它Android系統初始化時就被建立,而後開始配合硬件產生Vsync信號,也就是圖中的HW_Vsync信號。固然它不是一直不停的在產生,這樣會致使Vsync信號的接收者不停的接收到繪製、渲染命令,即便它們並不須要,這樣會帶來嚴重的性能損耗,由於進行了不少無用的繪製。因此它被設計設計成可以喚醒和睡眠的。這使得HWComposer在須要時才產生Vsync信號(好比當屏幕上的內容須要改變時),不須要時進入睡眠狀態(好比當屏幕上的內容保持不變時,此時屏幕每次刷新都是顯示緩衝區裏沒發生變化的內容)。

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

3.2Vsync 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文件,就能夠對這兩個值進行調節。

4.回到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中。

5.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。

如今來看看前面代碼中調用的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的繪製過程當中具備重要的做用,因此必定要結合圖梳理下這個流程。

6.SurfaceFlinger和Surface簡單說

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

6.1隱藏在背後的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,找到它所對應的內存區域中的繪製數據,而後進行合成。

6.2合成師SurfaceFlinger

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

7.終於能夠說說你的App爲何這麼卡的緣由了

經過對Android繪製機制的瞭解,咱們知道形成應用卡頓的根源就在於16ms內不能完成繪製渲染合成過程,由於Android平臺的硬件刷新率爲60HZ,大概就是16ms刷新一次。若是沒能在16ms內完成這個過程,就會使屏幕重複顯示上一幀的內容,即形成了卡頓。在這16ms內,須要完成視圖樹的全部測量、佈局、繪製渲染及合成。而咱們的優化工做主要就是針對這個過程的。

7.1複雜的視圖樹

若是視圖樹複雜,會使整個Traversal過程變長。所以,咱們在開發過程當中要控制視圖樹的複雜程度。減小沒必要要的層級嵌套。好比使用RelativeLayout能夠減小複雜佈局的嵌套。好比使用SuperTextView,這個控件能夠減小既須要顯示文字,又須要圖片和特殊背景的需求的佈局複雜程度,全部的東西由一個控件實現。

7.2頻繁的requestlayout()

若是頻繁的觸發requestLayout(),就可能會致使在一幀的週期內,頻繁的發生佈局計算,這也會致使整個Traversal過程變長。有的ViewGroup類型的控件,好比RelativeLayout,在一幀的週期內會經過兩次layout()操做來計算確認子View的位置,這種少許的操做並不會引發可以被注意到的性能問題。可是若是在一幀的週期內頻繁的發生layout()計算,就會致使嚴重的性能,每次計算都是要消耗時間的!而requestLayout()操做,會向ViewRootImpl中一個名爲mLayoutRequesters的List集合裏添加須要從新Layout的View,這些View將在下一幀中所有從新layout()一遍。一般在一個控件加載以後,若是沒什麼變化的話,它不會在每次的刷新中都從新layout()一次,由於這是一個費時的計算過程。因此,若是每一幀都有許多View須要進行layout()操做,可想而知你的界面將會卡到爆!卡到爆!須要注意,setLayoutParams()最終也會調用requestLayout(),因此也不能爛用!同窗們在寫代碼的過程當中必定要謹慎注意那些可能引發requestLayout()的地方啊!

7.3UI線程被阻塞

若是UI線程受到阻塞,顯而易見的是,咱們的Traversal過程也將受阻塞!畫面卡頓是妥妥的發生啊。這就是爲何你們一直在強調不要在UI線程作耗時操做的緣由。一般UI線程的阻塞和如下緣由脫不了關係。

  • 在UI線程中進行IO讀寫數據的操做。這是一個很費時的過程好嗎?千萬別這麼幹。若是不想得到一個卡到爆的App的話,把IO操做通通放到子線程中去。
  • 在UI線程中進行復雜的運算操做。運算自己是一個耗時的操做,固然簡單的運算幾乎瞬間完成,因此不會讓你感覺到它在耗時。可是對於十分複雜的運算,對時間的消耗是十分辣眼睛的!若是不想得到一個卡到爆的App的話,把複雜的運算操做放到子線程中去。
  • 在UI線程中進行復雜的數據處理。我說的是好比數據的加密、解密、編碼等等。這些操做都須要進行復雜運算,特別是在數據比較複雜的時候。若是不想得到一個卡到爆的App的話,把複雜數據的處理工做放到子線程中去
  • 頻繁的發生GC,致使UI線程被頻繁中斷。在Java中,發生GC(垃圾回收)意味着Stop-The-World,就是說其它線程所有會被暫停啊。好可怕!正常的GC致使偶然的畫面卡頓是能夠接受的,可是頻繁發生就讓人很蛋疼了!頻繁GC的罪魁禍首是 內存抖動簡單的說就是在短期內頻繁的建立大量對象,致使達到GC的閥值,而後GC就發生了。若是不想得到一個卡到爆的App的話,把內存的管理作好,即便這是Java。
  • 故意阻塞UI線程。好吧,相信沒人會這麼幹吧。好比sleep()一下?

## 三.總結

抽出空餘時間寫文章分享須要動力,還請各位看官動動小手點個贊,鼓勵下嘍

相關文章
相關標籤/搜索