反思 系列博客是一种看似 “内卷” ,但却 效果显著 的学习方法,该系列来源和目录请参阅 这儿 。

起程

假如你有过 SurfaceView 的运用阅历,那么你必定和我相同,从前被它所引发出 层出不穷的异状 折磨的 置疑人生—— 究竟,作为一个有抱负的开发者,在深化了解 SurfaceView 之前,你很难想通这样一个问题:

为什么 GoogleSurfaceView 规划的这么难用?

  • 不支持 transform 动画;
  • 不支持半通明混合;
  • 移动,大小改动,隐藏/显现操作引发的各种问题;

另一方面,即便你对 SurfaceView 运用不多,图形系统 的这朵乌云仍然笼罩在每一位 Android 开发者的头顶,来看 Google 对其的 描绘:

反思:Google 为何把 SurfaceView 设计的这么难用?

终究我测验走近这片迷雾,并一点点去思考下列问题的答案:

    1. SurfaceView 的规划初衷是为了处理什么问题?
    1. 实践开发中,SurfaceView 这么 难用 的根本原因是什么?
    1. 为了处理这些问题,Google 的工程师进行了哪些 测验

接下来,读者可带着这些问题,跟随笔者一同,再次回忆 SurfaceView 规划和完成的精彩进程。

一、世界观

在了解 SurfaceView 的规划初衷之前,读者首要需求对 Android 现有的图形架构有一个根本的了解。

Android 系统采用一种称为 Surface 的图形架构,简而言之,每一个 Activity 都相关有至少一个 Window(窗口),每一个 Window 都对应有一个 Surface

Surface 这儿直译过来叫做 绘图外表 ,顾名思义,其可在内存中生成一个图形缓冲区行列,用于描绘 UI,经与系统服务的WindowServiceManager 通讯后、经过 SurfaceFlinger 服务持续组成并送显到显现屏。

读者可经过下图,在形象上对整个流程树立一个简略的轮廓:

反思:Google 为何把 SurfaceView 设计的这么难用?

由此可见,通常情况下,一个 ActivityUI 烘托实质是 系统供给一块内存,并创立一个图形缓冲区进行保护;这块内存便是 Surface,终究页面一切 ViewUI 状况数据,都会被填充到同一个 Surface 中。

截至目前一切正常,但需求指出的是,现有图形系统的架构规划中还藏了一个线程相关的 隐患

二、规划来源

1.线程问题

问题点在于:咱们还需确保 Surface 内部 Buffer 缓冲区的 线程安全

这样的描绘,关于读者似乎过分飘渺,但从结论来说,终究,一条 Android开发者 耳熟能详 的规则因而而诞生:

主线程不能履行耗时操作

咱们知道, UI 的一切操作,必定会涉及到视图(View 树) 内部很多状况的保护,而 Surface 内部的缓冲区也会不断地被读写,并交给系统烘托。因而,假如 UI 相关的操作,放在不同的线程中履行,而多线程对这一块内存区域的读写,势必会引发内部状况的混乱。

为了避免这个问题,规划者就需求经过某种手法确保线程同步(比方加锁),而这种同步所带来的巨大开支,关于开发者而言,是不行接受的。

因而,最合理的计划便是确保一切UI相关操作都在同一个线程,而这个线程也被称作 主线程UI 线程。

现在,咱们将UI操作约束到主线程去履行,以处理了本末节开始时提到的线程问题,但开发者仍需当心—— 众所周知,主线程除了履行UI相关的操作之外,还负责接收各式各样的 输入事情(比方接触、按键等),因而,为了确保用户的输入事情可以及时得到呼应,咱们就要确保 UI 操作的 安稳高效,尽可能避免耗时的 UI 操作。

2.动机

应战随之而来。

当烘托的缓冲数据来自外部的其它系统服务或API时——比方系统媒体解码器的音视频数据,或许 Camera API 的相机数据等,这时 UI 烘托的效率要求会变得十分高。

开发者有了新的诉求:能否有这样一种特别的视图,它具有独立的 Surface ,这样就可以脱离现有 Activity 宿主的约束,在一个独立的线程中进行制作。

由于该视图不会占用主线程资源,一方面可以完成杂乱而高效的 UI 烘托,另一方面可以及时呼运用户其它输入事情

因而,SurfaceView 应运而生:与惯例视图控件不同,SurfaceView 具有独立的 Surface,假如咱们将一个 Surface 了解为一个层级 (Layer),终究 SurfaceFlinger 会将前后两者的2Layer 进行 组成烘托

反思:Google 为何把 SurfaceView 设计的这么难用?

现在,咱们引证官方文档的描绘,再次重申适用 SurfaceView 的场景:

在需求烘托到单独的 Surface(例如,运用 Camera APIOpenGL ES 上下文进行烘托)时,运用 SurfaceView 进行烘托很有协助。运用 SurfaceView 进行烘托时,SurfaceFlinger 会直接将缓冲区组成到屏幕上。

假如没有 SurfaceView,您需求将缓冲区组成到屏幕外的 Surface,然后该 Surface 会组成到屏幕上,而运用 SurfaceView 进行烘托可以省去额外的作业。

3.详细思路

依据当时的想象,咱们针对 SurfaceView 规划思路进行细化。

首要,咱们需对现有的视图树结构进行改造。为了便于运用,咱们答应开发者将 SurfaceView 直接加入到现有的视图树中(即作为控件,它受限于宿主 View Hierachy的结构联系),但在系统服务端中,关于 SurfaceFlinger 而言,SurfaceView 又是彻底与宿主彻底分离开的:

反思:Google 为何把 SurfaceView 设计的这么难用?

在上图中,咱们可以看到,在 z 轴上,SurfaceView 默许是低于 DecorView 的,也便是说,SurfaceView 通常总是处于当时页面的最下方。

这似乎有些违背直觉,但仔细考虑 SurfaceView 的运用场景,无论是 Camera 相机运用、音视频播映页,亦或许是烘托游戏画面等,SurfaceView 承载的画面似乎总应该在页面的最下面。

实践规划中也是如此,用来描绘 SurfaceViewLayer 或许 LayerBufferz 轴位置默许是低于宿主窗口的。与此一起,为了便于最底层的视图可见, SurfaceView 在宿主 Activity 的窗口上设置了一块通明区域(挖了一个洞)。

终究,SurfaceFlinger 把一切的 Layer 经过用统一流程来制作和组成对应的 UI

在整个进程中,咱们需更进一步深化研讨几个细节:

  1. SurfaceView 与宿主视图树结构的联系,以及 挖洞 进程的完成;
  2. SurfaceView 与系统服务的通讯创立 Surface的完成;
  3. SurfaceView 详细制作流程的完成。

三、施工

1. 视图树与挖洞

一句话总结 SurfaceView 与视图树的联系: 在视图树内部,但又没彻底在内部

首要,SurfaceView 的规划仍然遵循 AndroidView 系统,继承了 View,这意味着运用时,它可以声明在 xml 布局文件中:

// /frameworks/base/core/java/android/view/SurfaceView.java
public class SurfaceView extends View  { }

出于安全性的考量,SurfaceView 相关源码并未直接敞开出来,开发者只能看到自动生成的一个接口类,源码可以借助梯子在 这儿 查阅。

LayoutInflater 布局填充阶段,按既有的布局填充流程,将 SurfaceView 构造并加入到视图树的某个结点;接下来,根布局会经过深度遍历顺次履行 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 会向父视图顺次向上恳求创造一份通明区域,根视图统计到终究的信息后,经过 Binder 通知 WindowManagerService 将对应区域设置为通明。

过程 2、3、4 是在同一个办法的调用栈中,由此可见,SurfaceView 向系统恳求通明区域后,会立即创立一个与绘图外表的衔接 SurfaceSession ,并创立一个对应的控制器 SurfaceControl,便于对这个独立的绘图外表进行直接通讯。

由此可见,Android 自有的视图树系统中,SurfaceView 作为一个普通的 View 被挂载上去之后,经过 Binder 通讯,WindowManagerService 将其地点区域设置为通明(挖洞);并树立了与独立绘图外表的衔接,后续便可与其直接通讯。

2. 子图层类型

在阐述制作流程之前,读者需简略了解 子图层类型 的概念。

上文提到,SurfaceView 的绝大多数运用场景中,其 z 轴的位置通常是在页面的 最下方 。但在实践开发中,跟着事务场景杂乱度的上升,仍然有部分场景是无法被满意的,比方:在页面的最上方播映一条全屏的视频广告。

因而,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;
  }
}

现在,无论是将 SurfaceView 放在页面的上方仍是下方,都垂手可得。

但这仍然无法满意一切诉求,比方针对具有 alpha 通道的通明视频进行烘托时,产品希望其地点的图层位置可以更灵活(在两个 View 之间),但由于 SurfaceView 本身规划的原因,其并无法与视图树交融,这也正是 SurfaceView 饱尝诟病的主要原因之一。

经过辩证的观点来看, SurfaceView 的这种规划尽管满意不了苛刻的事务诉求,但在绝大多数场景下,独立绘图外表 这种规划都可以确保满足的烘托功能,一起不影响主线程输入事情的处理,绝对是一个优秀的规划。

3.子图层类型-插曲

值得一提的是,在 SurfaceView 的规划中,规划者还考虑到了音视频烘托时,字幕相关事务的场景,因而额外供给了一个 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_SUBLAYERAPPLICATION_MEDIA_OVERLAY_SUBLAYER 都小于0,因而,无论如何,字幕一直被烘托在页面的下方。又由于视频理应烘托在字幕的下方,所以 不引荐 开发者在运用 SurfaceView 烘托视频时调用 setZOrderOnTop(true),将视频放在页面视图的顶层。

其次,一起具有 setZOrderOnTop()setZOrderMediaOverlay() 办法,显着是供给给两个不同 SurfaceView 别离运用的,以界说不同的烘托层级,因而同一个页面存在多个 SurfaceView 是正常的,开发者彻底可以依据事务场景,合理运用。

4. 令人头大的黑屏问题

在运用 SurfaceView 的进程中,笔者终究也遇到了 默许黑屏 的问题:

由于视频本身的加载和编解码的耗时,用户总是会先看到 SurfaceView 的黑色背景一闪而过,然后视频才开始播映的情况,关于产品而言,这种交互体验是 不行容忍 的。

经过上文读者知道,SurfaceView 具有独立的制作外表,因而惯例抵挡 View 的一些手法——比方 setVisibility()setAlpha()setBackgroundColor() 并不能处理上述问题;因而,想真实处理它,就必须先弄清楚 SurfaceView 底层的制作流程。

SurfaceView 尽管特别,但其作为视图树的一个结点,其仍然参与到了视图树惯例制作流程,这儿咱们直接看 SurfaceViewdraw() 办法:

// /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 的源码是没有揭露暴露的。

规划者其实也想到了这个问题,因而额外供给了一个 SurfaceHolderAPI 接口,经过该接口,开发者可以直接拿到独立绘图外表的 Canvas 对象,以及对这个画布进行制作操作:

// /frameworks/base/core/java/android/view/SurfaceHolder.java
public interface SurfaceHolder {
  // ...
  public Canvas lockCanvas();
  public void unlockCanvasAndPost(Canvas canvas);
  //...
}

惋惜的是,即便拿到 Canvas,开发者仍然会受到约束:

// /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");
    }
    // ...
  }
}

这儿的代码,笔者引证 罗升阳 的 这篇文章 中的一段来解释:

留意,只有在一个 SurfaceView 的绘图外表的类型不是 SURFACE_TYPE_PUSH_BUFFERS 的时分,咱们才可以自由地在上面制作 UI。咱们运用 SurfaceView 来显现摄像头预览或许播映视频时,一般便是会将它的绘图外表的类型设置为 SURFACE_TYPE_PUSH_BUFFERS 。在这种情况下,SurfaceView 的绘图外表所运用的图形缓冲区是彻底由摄像头服务或许视频播映服务来供给的,因而,咱们就不行以随意地去拜访该图形缓冲区,而是要由摄像头服务或许视频播映服务来拜访,由于该图形缓冲区有可能是在专门的硬件里边分配的。

由此可见,SurfaceView 黑屏问题的原因是归纳且杂乱的,无论是经过 setZOrderOnTop() 等办法设置为背景通明(但是会在页面层级的最上方),亦或许调整布局参数,都会有大大小小的一些问题。

小结

归纳来看,SurfaceView 这些饱尝争议的问题,从规划的视点来看,都是有其本身考量的。

而为了处理这些问题,官方后续供给了 TextureView 以替换 SurfaceViewTextureView 的原理是和 View 相同制作到当时 Activity 的窗口上,因而不存在 SurfaceView 的这些问题。

换个视点来看,由于 TextureView 烘托依赖于主线程,因而也会导致了新的问题出现。除了功能比较 SurfaceView 会有显着下降外,还会有经常掉帧的问题,有机会笔者会另起一篇进行共享。

参阅 & 感谢

细心的读者应该可以发现,关于 参阅&感谢 一节,笔者着墨越来越多,原因无他,笔者 从不认为 一篇文章就可以讲一个常识系统解说的八面玲珑,本文亦如是。

因而,读者应该有选择性查看其它优质内容的权利,甚至是为其增加一些简洁的介绍(由于标题大多都很类似),而不是文章末尾甩一堆 https 开头的链接不知所云。

这也是对这些内容创作者的尊重,假如你喜爱本文,也同样希望你可以喜爱下面这些文章。

1. Android源码-frameworks-SurfaceView

阅览源码永远是学习最有效的方法,假如你想更进一步深化了解 SurfaceView,选它就对了。

2. Android官方文档-图形架构

惋惜的是,在笔者学习的进程中,官方文档并未给予到很大的协助,相当一部分原因是由于文档中的内容太 标准 了,坚持内容 精粹精确 的一起,也增加了读者的了解成本。

但无论如何,作为权威的官方文档,仍合适作为复习资料,反复阅览。

3. Android视图SurfaceView的完成原理分析 @罗升阳

神作, 我认为它是 最合适 进阶学习和研讨 SurfaceView 源码的文章。

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

在笔者摸索学习,困惑于标题中这些概念的阶段,本文以深入浅出的方法对它们进行了简略的总结,引荐。


关于我

Hello,我是 却把清梅嗅 ,假如您觉得文章写的很赞,欢迎 ❤️,假如您觉得文章更新太慢,欢迎经过 ❤️ 或许 谈论 鼓励我写出更好的文章。

  • 我的Android学习系统
  • 关于文章纠错
  • 关于常识付费
  • 关于《反思》系列