<kbd id="5sdj3"></kbd>
<th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>

    Google 為何把 SurfaceView 設計的這么難用?

    共 16224字,需瀏覽 33分鐘

     ·

    2023-10-12 12:45


    本文作者


    作者:卻把清梅嗅

    鏈接:https://juejin.cn/post/7140191497982312455

    本文由作者授權發(fā)布。

    1
    啟程


    如果你有過 SurfaceView 的使用經歷,那么你一定和我一樣,曾經被它所引發(fā)出 層出不窮的異狀 折磨的 懷疑人生—— 畢竟,作為一個有理想的開發(fā)者,在深入了解 SurfaceView 之前,你很難想通這樣一個問題:


    為什么 Google 把 SurfaceView 設計的這么難用?
    • 不支持 transform 動畫;
    • 不支持半透明混合;
    • 移動,大小改變,隱藏/顯示操作引發(fā)的各種問題;

    另一方面,即使你對 SurfaceView 使用不多,圖形系統(tǒng) 的這朵烏云依然籠罩在每一位 Android 開發(fā)者的頭頂,來看 Google 對其的 描述:

    https://source.android.com/docs/core/graphics/architecture
    最終我嘗試走近這片迷霧,并一點點去思考下列問題的答案:
    • SurfaceView 的設計初衷是為了解決什么問題?
    • 實際開發(fā)中,SurfaceView 這么 難用 的根本原因是什么?
    • 為了解決這些問題,Google 的工程師進行了哪些 嘗試 ?

    接下來,讀者可帶著這些問題,跟隨筆者一起,再次回顧 SurfaceView 設計和實現(xiàn)的精彩歷程。


    2
    世界觀

    在了解 SurfaceView 的設計初衷之前,讀者首先需要對 Android 現(xiàn)有的圖形架構有一個基本的了解。

    Android 系統(tǒng)采用一種稱為 Surface 的圖形架構,簡而言之,每一個 Activity 都關聯(lián)有至少一個 Window(窗口),每一個 Window 都對應有一個 Surface。
    Surface 這里直譯過來叫做 繪圖表面 ,顧名思義,其可在內存中生成一個圖形緩沖區(qū)隊列,用于描述 UI,經與系統(tǒng)服務的WindowServiceManager 通信后、通過 SurfaceFlinger 服務持續(xù)合成并送顯到顯示屏。
    讀者可通過下圖,在印象上對整個流程建立一個簡單的輪廓:
    由此可見,通常情況下,一個 Activity 的 UI 渲染本質是 系統(tǒng)提供一塊內存,并創(chuàng)建一個圖形緩沖區(qū)進行維護;這塊內存就是 Surface,最終頁面所有 View 的 UI 狀態(tài)數(shù)據,都會被填充到同一個 Surface 中。

    截至目前一切正常,但需要指出的是,現(xiàn)有圖形系統(tǒng)的架構設計中還藏了一個線程相關的 隱患 。


    3
    設計起源


    1.線程問題

    問題點在于:我們還需保證 Surface 內部 Buffer 緩沖區(qū)的 線程安全。
    這樣的描述,對于讀者似乎太過飄渺,但從結論來說,最終,一條 Android開發(fā)者 耳熟能詳 的規(guī)則因此而誕生:
    主線程不能執(zhí)行耗時操作。
    我們知道, UI 的所有操作,一定會涉及到視圖(View 樹) 內部大量狀態(tài)的維護,而 Surface 內部的緩沖區(qū)也會不斷地被讀寫,并交給系統(tǒng)渲染。因此,如果 UI 相關的操作,放在不同的線程中執(zhí)行,而多線程對這一塊內存區(qū)域的讀寫,勢必會引發(fā)內部狀態(tài)的混亂。
    為了避免這個問題,設計者就需要通過某種手段保證線程同步(比如加鎖),而這種同步所帶來的巨大開銷,對于開發(fā)者而言,是不可接受的。
    因此,最合理的方案就是保證所有UI相關操作都在同一個線程,而這個線程也被稱作 主線程 或 UI 線程。
    現(xiàn)在,我們將UI操作限制到主線程去執(zhí)行,以解決了本小節(jié)開始時提到的線程問題,但開發(fā)者仍需小心—— 眾所周知,主線程除了執(zhí)行UI相關的操作之外,還負責接收各種各樣的 輸入事件(比如觸摸、按鍵等),因此,為了保證用戶的輸入事件能夠及時得到響應,我們就要保證 UI 操作的 穩(wěn)定高效,盡可能避免耗時的 UI 操作。

    2.動機

    挑戰(zhàn)隨之而來。
    當渲染的緩沖數(shù)據來自外部的其它系統(tǒng)服務或API時——比如系統(tǒng)媒體解碼器的音視頻數(shù)據,或者 Camera API 的相機數(shù)據等,這時 UI 渲染的效率要求會變得非常高。
    開發(fā)者有了新的訴求:能否有這樣一種特殊的視圖,它擁有獨立的 Surface ,這樣就可以脫離現(xiàn)有 Activity 宿主的限制,在一個獨立的線程中進行繪制。
    由于該視圖不會占用主線程資源,一方面可以實現(xiàn)復雜而高效的 UI 渲染,另一方面可以及時響應用戶其它輸入事件。
    因此,SurfaceView 應運而生:與常規(guī)視圖控件不同,SurfaceView 擁有獨立的 Surface,如果我們將一個 Surface 理解為一個層級 (Layer),最終 SurfaceFlinger 會將前后兩者的2個 Layer 進行 合成 和 渲染 :

    現(xiàn)在,我們引用官方文檔的描述,再次重申適用 SurfaceView 的場景:

    在需要渲染到單獨的 Surface(例如,使用 Camera API 或 OpenGL ES 上下文進行渲染)時,使用 SurfaceView 進行渲染很有幫助。使用 SurfaceView 進行渲染時,SurfaceFlinger 會直接將緩沖區(qū)合成到屏幕上。
    如果沒有 SurfaceView,您需要將緩沖區(qū)合成到屏幕外的 Surface,然后該 Surface 會合成到屏幕上,而使用 SurfaceView 進行渲染可以省去額外的工作。


    3.具體思路

    根據當前的設想,我們針對  SurfaceView 設計思路進行細化。
    首先,我們需對現(xiàn)有的視圖樹結構進行改造。為了便于使用,我們允許開發(fā)者將 SurfaceView 直接加入到現(xiàn)有的視圖樹中(即作為控件,它受限于宿主 View Hierachy的結構關系),但在系統(tǒng)服務端中,對于 SurfaceFlinger 而言,SurfaceView 又是完全與宿主完全分離開的:
    在上圖中,我們可以看到,在 z 軸上,SurfaceView 默認是低于 DecorView 的,也就是說,SurfaceView 通??偸翘幱诋斍绊撁娴淖钕路?。
    這似乎有些違反直覺,但仔細考慮 SurfaceView 的應用場景,無論是 Camera 相機應用、音視頻播放頁,亦或者是渲染游戲畫面等,SurfaceView 承載的畫面似乎總應該在頁面的最下面。
    實際設計中也是如此,用來描述 SurfaceView 的 Layer 或者 LayerBuffer 的 z 軸位置默認是低于宿主窗口的。與此同時,為了便于最底層的視圖可見, SurfaceView 在宿主 Activity 的窗口上設置了一塊透明區(qū)域(挖了一個洞)。
    最終,SurfaceFlinger 把所有的 Layer 通過用統(tǒng)一流程來繪制和合成對應的 UI。
    在整個過程中,我們需更進一步深入研究幾個細節(jié):
    1. SurfaceView 與宿主視圖樹結構的關系,以及 挖洞 過程的實現(xiàn);
    2. SurfaceView 與系統(tǒng)服務的通信創(chuàng)建 Surface的實現(xiàn);
    3. SurfaceView 具體繪制流程的實現(xiàn)。



    4
    施工



    1. 視圖樹與挖洞

    一句話總結 SurfaceView 與視圖樹的關系: 在視圖樹內部,但又沒完全在內部 。
    首先,SurfaceView 的設計依然遵循 Android 的 View 體系,繼承了 View,這意味著使用時,它可以聲明在 xml 布局文件中:
       
       
    // /frameworks/base/core/java/android/view/SurfaceView.java
    public class SurfaceView extends View  { }


    出于安全性的考量,SurfaceView 相關源碼并未直接開放出來,開發(fā)者只能看到自動生成的一個接口類,源碼可以借助梯子在 這里 查閱。
    http://aospxref.com/android-10.0.0_r47/xref/frameworks/base/core/java/android/view/SurfaceView.java
    LayoutInflater 布局填充階段,按既有的布局填充流程,將 SurfaceView 構造并加入到視圖樹的某個結點;接下來,根布局會通過深度遍歷依次執(zhí)行 onAttachedToWindow() 處理視圖掛載窗口的事件:
       
       
    // /frameworks/base/core/java/android/view/SurfaceView.java
    @Override
    protected void onAttachedToWindow() {
        // ...
        mParent.requestTransparentRegion(SurfaceView.this);   // 1.
        ViewTreeObserver observer = getViewTreeObserver();
        observer.addOnPreDrawListener(mDrawListener);         // 2.
    }

    @UnsupportedAppUsage
    private final ViewTreeObserver.OnPreDrawListener mDrawListener = new ViewTreeObserver.OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
            updateSurface();                                 // 3.
            return true;
        }
    };

    protected void updateSurface() {
      // ...
      mSurfaceSession = new SurfaceSession();
      mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession);    // 4
      //...
    }


    步驟 1 中,SurfaceView 會向父視圖依次向上請求創(chuàng)造一份透明區(qū)域,根視圖統(tǒng)計到最終的信息后,通過 Binder 通知 WindowManagerService 將對應區(qū)域設置為透明。
    步驟 2、3、4 是在同一個方法的調用棧中,由此可見,SurfaceView 向系統(tǒng)請求透明區(qū)域后,會立即創(chuàng)建一個與繪圖表面的連接 SurfaceSession ,并創(chuàng)建一個對應的控制器 SurfaceControl,便于對這個獨立的繪圖表面進行直接通信。
    由此可見,Android 自有的視圖樹體系中,SurfaceView 作為一個普通的 View 被掛載上去之后,通過 Binder 通信,WindowManagerService 將其所在區(qū)域設置為透明(挖洞);并建立了與獨立繪圖表面的連接,后續(xù)便可與其直接通信。

    2. 子圖層類型

    在闡述繪制流程之前,讀者需簡單了解 子圖層類型 的概念。
    上文說到,SurfaceView 的絕大多數(shù)使用場景中,其 z 軸的位置通常是在頁面的 最下方 。但在實際開發(fā)中,隨著業(yè)務場景復雜度的上升,仍然有部分場景是無法被滿足的,比如:在頁面的最上方播放一條全屏的視頻廣告。
    因此,SurfaceView 的設計中引入了一個 子圖層類型 的概念,用于定義這個獨立的 Surface 相比較當前頁面窗口 (即Activity) 的位置:
       
       
    // /frameworks/base/core/java/android/view/SurfaceView.java
    public class SurfaceView extends View {

      // SurfaceView 的子圖層類型
      int mSubLayer = APPLICATION_MEDIA_SUBLAYER;

      // SurfaceView 是否展示在當前窗口的最上方
      // 該方法在挖洞和繪制流程中都有使用,最終影響到用戶的視覺效果
      private boolean isAboveParent() {
        return mSubLayer >= 0;
      }
    }

    // /frameworks/base/core/java/android/view/WindowManagerPolicyConstants.java
    public interface WindowManagerPolicyConstants {
      // ...
      int APPLICATION_MEDIA_SUBLAYER = -2;
      int APPLICATION_MEDIA_OVERLAY_SUBLAYER = -1;
      int APPLICATION_PANEL_SUBLAYER = 1;
      int APPLICATION_SUB_PANEL_SUBLAYER = 2;
      int APPLICATION_ABOVE_SUB_PANEL_SUBLAYER = 3
      // ...
    }


    如代碼所示,mSubLayer 默認值為 -2,這表示 SurfaceView 默認總是在 Activity 的下方,想要讓 SurfaceView 展示在 Activity 上方,可以調用 setZOrderOnTop(true) 以修改 mSubLayer 的值:
       
       
    // /frameworks/base/core/java/android/view/SurfaceView.java
    public class SurfaceView extends View {

      public void setZOrderOnTop(boolean onTop) {
          if (onTop) {
              mSubLayer = APPLICATION_PANEL_SUBLAYER;
          } else {
              mSubLayer = APPLICATION_MEDIA_SUBLAYER;
          }
      }

      public void setZOrderMediaOverlay(boolean isMediaOverlay) {
        mSubLayer = isMediaOverlay ? APPLICATION_MEDIA_OVERLAY_SUBLAYER : APPLICATION_MEDIA_SUBLAYER;
      }
    }


    現(xiàn)在,無論是將 SurfaceView 放在頁面的上方還是下方,都輕而易舉。
    但這仍然無法滿足所有訴求,比如針對具有 alpha 通道的透明視頻進行渲染時,產品希望其所在的圖層位置能夠更靈活(在兩個 View 之間),但由于 SurfaceView 自身設計的原因,其并無法與視圖樹融合,這也正是 SurfaceView 飽受詬病的主要原因之一。
    通過辯證的觀點來看, SurfaceView 的這種設計雖然滿足不了嚴苛的業(yè)務訴求,但在絕大多數(shù)場景下,獨立繪圖表面 這種設計都能夠保證足夠的渲染性能,同時不影響主線程輸入事件的處理,絕對是一個優(yōu)秀的設計。

    3.子圖層類型-插曲

    值得一提的是,在 SurfaceView的設計中,設計者還考慮到了音視頻渲染時,字幕相關業(yè)務的場景,因此額外提供了一個 setZOrderMediaOverlay() 方法:
       
       
    // /frameworks/base/core/java/android/view/SurfaceView.java
    public class SurfaceView extends View {
      public void setZOrderMediaOverlay(boolean isMediaOverlay) {
        mSubLayer = isMediaOverlay ? APPLICATION_MEDIA_OVERLAY_SUBLAYER : APPLICATION_MEDIA_SUBLAYER;
      }
    }


    該方法的設計說明了2點:
    首先,由于 APPLICATION_MEDIA_SUBLAYER 和 APPLICATION_MEDIA_OVERLAY_SUBLAYER 都小于0,因此,無論如何,字幕始終被渲染在頁面的下方。又因為視頻理應渲染在字幕的下方,所以 不推薦 開發(fā)者在使用 SurfaceView 渲染視頻時調用 setZOrderOnTop(true),將視頻放在頁面視圖的頂層。
    其次,同時具有 setZOrderOnTop() 和 setZOrderMediaOverlay() 方法,顯然是提供給兩個不同 SurfaceView 分別使用的,以定義不同的渲染層級,因此同一個頁面存在多個 SurfaceView 是正常的,開發(fā)者完全可以根據業(yè)務場景,合理運用。

    4. 令人頭大的黑屏問題

    在使用 SurfaceView 的過程中,筆者最終也遇到了 默認黑屏 的問題:
    由于視頻本身的加載和編解碼的耗時,用戶總是會先看到 SurfaceView 的黑色背景一閃而過,然后視頻才開始播放的情況,對于產品而言,這種交互體驗是 不可容忍 的。
    通過上文讀者知道,SurfaceView 擁有獨立的繪制表面,因此常規(guī)對付 View 的一些手段——比如 setVisibility() 、setAlpha() 、setBackgroundColor() 并不能解決上述問題;因此,想真正解決它,就必須先弄清楚 SurfaceView 底層的繪制流程。
    SurfaceView 雖然特殊,但其作為視圖樹的一個結點,其依然參與到了視圖樹常規(guī)繪制流程,這里我們直接看 SurfaceView 的 draw() 方法:
       
       
    // /frameworks/base/core/java/android/view/SurfaceView.java
    public class SurfaceView extends View {

      //...
      @Override
      public void draw(Canvas canvas) {
          if (mDrawFinished && !isAboveParent()) {             // 1.
              if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) {
                  clearSurfaceViewPort(canvas);
              }
          }
          super.draw(canvas);
      } 

      private void clearSurfaceViewPort(Canvas canvas) {
          // ...
          canvas.drawColor(0, PorterDuff.Mode.CLEAR);         // 2.
      }
    }

    由此可見,當滿足 !isAboveParent() 的條件——即 SurfaceView 的子圖層類型位于宿主視圖的下方時,SurfaceView 默認會將繪圖表面的顏色指定為黑色。
    顯然,該問題最簡單的解決方式就是對源碼進行hook或者反射,遺憾的是,上文我們也提到了,出于安全性的考量,SurfaceView 的源碼是沒有公開暴露的。
    設計者其實也想到了這個問題,因此額外提供了一個 SurfaceHolder 的 API 接口,通過該接口,開發(fā)者可以直接拿到獨立繪圖表面的 Canvas 對象,以及對這個畫布進行繪制操作:
    // /frameworks/base/core/java/android/view/SurfaceHolder.java
    public interface SurfaceHolder {
      // ...
      public Canvas lockCanvas();

      public void unlockCanvasAndPost(Canvas canvas);
      //...
    }

    遺憾的是,即使拿到 Canvas,開發(fā)者仍然會受到限制:

       
       
    // /frameworks/base/core/java/com/android/internal/view/BaseSurfaceHolder.java
    public abstract class BaseSurfaceHolder implements SurfaceHolder {

       private final Canvas internalLockCanvas(Rect dirty, boolean hardware) {
        if (mType == SURFACE_TYPE_PUSH_BUFFERS) {
            throw new BadSurfaceTypeException("Surface type is SURFACE_TYPE_PUSH_BUFFERS");
        }
        // ...
      }
    }


    這里的代碼,筆者引用 羅升陽 的 這篇文章 中的一段來解釋:

    https://www.kancloud.cn/alex_wsc/androids/473787

    注意,只有在一個 SurfaceView 的繪圖表面的類型不是 SURFACE_TYPE_PUSH_BUFFERS 的時候,我們才可以自由地在上面繪制 UI。我們使用 SurfaceView 來顯示攝像頭預覽或者播放視頻時,一般就是會將它的繪圖表面的類型設置為 SURFACE_TYPE_PUSH_BUFFERS 。在這種情況下,SurfaceView 的繪圖表面所使用的圖形緩沖區(qū)是完全由攝像頭服務或者視頻播放服務來提供的,因此,我們就不可以隨意地去訪問該圖形緩沖區(qū),而是要由攝像頭服務或者視頻播放服務來訪問,因為該圖形緩沖區(qū)有可能是在專門的硬件里面分配的。

    由此可見,SurfaceView 黑屏問題的原因是綜合且復雜的,無論是通過 setZOrderOnTop() 等方法設置為背景透明(但是會在頁面層級的最上方),亦或者調整布局參數(shù),都會有大大小小的一些問題。


    5
    小結


    綜合來看,SurfaceView 這些飽受爭議的問題,從設計的角度來看,都是有其自身考量的。
    而為了解決這些問題,官方后續(xù)提供了 TextureView 以替換 SurfaceView,TextureView 的原理是和 View 一樣繪制到當前 Activity 的窗口上,因此不存在 SurfaceView 的這些問題。
    換個角度來看,由于 TextureView 渲染依賴于主線程,因此也會導致了新的問題出現(xiàn)。除了性能比較 SurfaceView 會有明顯下降外,還會有經常掉幀的問題,有機會筆者會另起一篇進行分享。
    6
    參考 & 感謝


    細心的讀者應該能夠發(fā)現(xiàn),關于 參考&感謝 一節(jié),筆者著墨越來越多,原因無他,筆者 從不認為 一篇文章就能夠講一個知識體系講解的面面俱到,本文亦如是。
    因此,讀者應該有選擇性查看其它優(yōu)質內容的權利,甚至是為其增加一些簡潔的介紹(因為標題大多都很相似),而不是文章末尾甩一堆 https 開頭的鏈接不知所云。
    這也是對這些內容創(chuàng)作者的尊重,如果你喜歡本文,也同樣希望你能夠喜歡下面這些文章。

    1. Android源碼-frameworks-SurfaceView

    http://aospxref.com/android-10.0.0_r47/xref/frameworks/base/core/java/android/view/SurfaceView.java

    閱讀源碼永遠是學習最有效的方式,如果你想更進一步深入了解 SurfaceView,選它就對了。

    2. Android官方文檔-圖形架構

    https://source.android.com/docs/core/graphics

    遺憾的是,在筆者學習的過程中,官方文檔并未給予到很大的幫助,相當一部分原因是因為文檔中的內容太 規(guī)范 了,保持內容 精煉 且 準確 的同時,也增加了讀者的理解成本。
    但無論如何,作為權威的官方文檔,仍適合作為復習資料,反復閱讀。

    3. Android視圖SurfaceView的實現(xiàn)原理分析 @羅升陽

    https://www.kancloud.cn/alex_wsc/androids/473787

    神作, 我認為它是 最適合 進階學習和研究 SurfaceView 源碼的文章。

    4. Android 5.0(Lollipop)中的SurfaceTexture,TextureView, SurfaceView和GLSurfaceView @ariesjzj

    https://blog.csdn.net/jinzhuojun/article/details/44062175
    在筆者摸索學習,困惑于標題中這些概念的階段,本文以淺顯易懂的方式對它們進行了簡單的總結,推薦。

    -- END --


    進技術交流群,掃碼添加我的微信:Byte-Flow



    獲取相關資料和源碼



    推薦:

    Android FFmpeg 實現(xiàn)帶濾鏡的微信小視頻錄制功能

    全網最全的 Android 音視頻和 OpenGL ES 干貨,都在這了

    瀏覽 768
    點贊
    評論
    收藏
    分享

    手機掃一掃分享

    分享
    舉報
    評論
    圖片
    表情
    推薦
    點贊
    評論
    收藏
    分享

    手機掃一掃分享

    分享
    舉報

    <kbd id="5sdj3"></kbd>
    <th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>
    日韩一级爱爱 | 黑人大鸡巴视频 | 麻豆久久3 | 97啪啪视频 | 水多多在线成人免费视频 |