Google 為何把 SurfaceView 設計的這么難用?
本文作者
作者:卻把清梅嗅
鏈接:https://juejin.cn/post/7140191497982312455
本文由作者授權發(fā)布。
如果你有過 SurfaceView 的使用經歷,那么你一定和我一樣,曾經被它所引發(fā)出 層出不窮的異狀 折磨的 懷疑人生—— 畢竟,作為一個有理想的開發(fā)者,在深入了解 SurfaceView 之前,你很難想通這樣一個問題:
為什么 Google 把 SurfaceView 設計的這么難用?
不支持 transform 動畫; 不支持半透明混合; 移動,大小改變,隱藏/顯示操作引發(fā)的各種問題;
另一方面,即使你對 SurfaceView 使用不多,圖形系統(tǒng) 的這朵烏云依然籠罩在每一位 Android 開發(fā)者的頭頂,來看 Google 對其的 描述:
-
SurfaceView 的設計初衷是為了解決什么問題? -
實際開發(fā)中,SurfaceView 這么 難用 的根本原因是什么? -
為了解決這些問題,Google 的工程師進行了哪些 嘗試 ?
接下來,讀者可帶著這些問題,跟隨筆者一起,再次回顧 SurfaceView 設計和實現(xiàn)的精彩歷程。
在了解 SurfaceView 的設計初衷之前,讀者首先需要對 Android 現(xiàn)有的圖形架構有一個基本的了解。
截至目前一切正常,但需要指出的是,現(xiàn)有圖形系統(tǒng)的架構設計中還藏了一個線程相關的 隱患 。
1.線程問題
2.動機
現(xiàn)在,我們引用官方文檔的描述,再次重申適用 SurfaceView 的場景:
在需要渲染到單獨的 Surface(例如,使用 Camera API 或 OpenGL ES 上下文進行渲染)時,使用 SurfaceView 進行渲染很有幫助。使用 SurfaceView 進行渲染時,SurfaceFlinger 會直接將緩沖區(qū)合成到屏幕上。 如果沒有 SurfaceView,您需要將緩沖區(qū)合成到屏幕外的 Surface,然后該 Surface 會合成到屏幕上,而使用 SurfaceView 進行渲染可以省去額外的工作。
3.具體思路
-
SurfaceView 與宿主視圖樹結構的關系,以及 挖洞 過程的實現(xiàn); -
SurfaceView 與系統(tǒng)服務的通信創(chuàng)建 Surface的實現(xiàn); SurfaceView 具體繪制流程的實現(xiàn)。
1. 視圖樹與挖洞
// /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
// /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
//...
}
2. 子圖層類型
// /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;
// ...
}
// /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;
}
}
3.子圖層類型-插曲
// /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;
}
}
4. 令人頭大的黑屏問題
// /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.
}
}
// /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ù),都會有大大小小的一些問題。
細心的讀者應該能夠發(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
2. Android官方文檔-圖形架構
https://source.android.com/docs/core/graphics
3. Android視圖SurfaceView的實現(xiàn)原理分析 @羅升陽
https://www.kancloud.cn/alex_wsc/androids/473787
4. Android 5.0(Lollipop)中的SurfaceTexture,TextureView, SurfaceView和GLSurfaceView @ariesjzj
-- END --
進技術交流群,掃碼添加我的微信:Byte-Flow
獲取相關資料和源碼
推薦:
