Chromium网页绘图表面(Output Surface)创建过程分析

发表于 5年以前  | 总阅读数:2248 次

在Chromium中,Render进程在绘制网页之前,要为网页创建一个绘图表面。绘图表面描述的是网页经过渲染之后得到的输出。这个输出需要交给Browser进程处理,才能显示在屏幕上。在硬件加速渲染条件下,这个输出有可能是一个OpenGL纹理,也有可能是一系列需要进一步进行绘制的OpenGL纹理,取决于Render进程使用直接渲染器还是委托渲染器。本文接下来就对网页的绘图表面的创建过程进行详细分析。

《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

关于网页绘图表面的更详细描述,可以参考Chromium硬件加速渲染的OpenGL上下文绘图表面创建过程分析一文。本文的重点是分析Chromium的CC模块是如何触发网页绘图表面的创建的,以作为Chromium硬件加速渲染的OpenGL上下文绘图表面创建过程分析一文的补充。

网页绘图表面是由CC模块的调度器触发创建的,如图1所示:

图1 网页绘图表面的创建时机

CC模块内部的状态机一旦检测到网页的Layer Tree创建和初始化完毕,就会通知调度器触发一个创建绘图表面的操作。CC模块在为网页创建绘图表面的过程中,也有伴随着网页分块管理器、资源池和光栅化工作者线程池等基础设施的创建。一旦这些基础设施准备完毕,网页才能开始进行绘制,也就是在图1中,第1步完成后,第2到第6步才可以周而复始地执行。

从前面Chromium网页Layer Tree创建过程分析一文可以知道,当网页的Graphics Layer Tree的根节点创建出来之后,WebKit就会通知Chromium的Content层初始化一个CC Layer Tree,如下所示:

void RenderWidget::initializeLayerTreeView() {  
      compositor_ = RenderWidgetCompositor::Create(  
          this, is_threaded_compositing_enabled_);  
      ......  
      if (init_complete_)  
        StartCompositor();  
    }  

这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。

初始化CC Layer Tree的工作是通过调用RenderWidgetCompositor类的静态成员函数Create实现的。在前面Chromium网页Layer Tree创建过程分析一文中,我们分析了此时RenderWidget类的成员变量init_complete_的值等于true,因此接下来RenderWidget类的成员函数initializeLayerTreeView会调用另外一个成员函数StartCompositor为网页创建绘图表面。

RenderWidget类的成员函数StartCompositor的实现如下所示:

void RenderWidget::StartCompositor() {
      ......
      compositor_->setSurfaceReady();
    }

这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。

RenderWidget类的成员变量compositor_指向的是一个RenderWidgetCompositor对象,RenderWidget类的成员函数StartCompositor调用这个RenderWidgetCompositor对象的成员函数setSurfaceReady,用来通知它可以为网页创建绘图表面了。

RenderWidgetCompositor类的成员函数setSurfaceReady的实现如下所示:

void RenderWidgetCompositor::setSurfaceReady() {
      layer_tree_host_->SetLayerTreeHostClientReady();
    }

这个函数定义在文件external/chromium_org/content/renderer/gpu/render_widget_compositor.cc中。

RenderWidgetCompositor类的成员变量layer_tree_host_指向的是一个LayerTreeHost对象。这个LayerTreeHost对象就是用来管理CC Layer Tree的。RenderWidgetCompositor类的成员函数setSurfaceReady调用这个LayerTreeHost对象的成员函数SetLayerTreeHostClientReady,用来通知调度器为网页创建绘图表面。

LayerTreeHost类的成员函数SetLayerTreeHostClientReady的实现如下所示:

void LayerTreeHost::SetLayerTreeHostClientReady() {
      proxy_->SetLayerTreeHostClientReady();
    }

这个函数定义在文件external/chromium_org/cc/trees/layer_tree_host.cc中。

LayerTreeHost类的成员变量proxy_指向的是一个ThreadProxy对象,LayerTreeHost类的成员函数SetLayerTreeHostClientReady调用这个ThreadProxy对象的成员函数SetLayerTreeHostClientReady,用来调度器为网页创建绘图表面。

ThreadProxy类的成员函数SetLayerTreeHostClientReady的实现如下所示:

void ThreadProxy::SetLayerTreeHostClientReady() {
      ......
      Proxy::ImplThreadTaskRunner()->PostTask(
          FROM_HERE,
          base::Bind(&ThreadProxy::SetLayerTreeHostClientReadyOnImplThread,
                     impl_thread_weak_ptr_));
    }

这个函数定义在文件external/chromium_org/cc/trees/thread_proxy.cc中。

ThreadProxy类的成员函数SetLayerTreeHostClientReady向Compositor线程的消息队列发送一个Task。这个Task绑定了ThreadProxy类的成员函数SetLayerTreeHostClientReadyOnImplThread。因此接下来ThreadProxy类的成员函数SetLayerTreeHostClientReadyOnImplThread就会在Compositor线程中执行。

ThreadProxy类的成员函数SetLayerTreeHostClientReadyOnImplThread的实现如下所示:

void ThreadProxy::SetLayerTreeHostClientReadyOnImplThread() {
      ......
      impl().scheduler->SetCanStart();
    }

这个函数定义在文件external/chromium_org/cc/trees/thread_proxy.cc中。

ThreadProxy类的成员函数SetLayerTreeHostClientReadyOnImplThread将调度器设置为启动状态,这是通过调用Scheduler类的成员函数SetCanStart实现的,如下所示:

void Scheduler::SetCanStart() {
      state_machine_.SetCanStart();
      ProcessScheduledActions();
    }

这个函数定义在文件external/chromium_org/cc/scheduler/scheduler.cc中。

Scheduler类的成员函数SetCanStart首先将内部的状态机设置为启动状态,这是通过调用SchedulerStateMachine类的成员函数SetCanStart实现的,如下所示:

class CC_EXPORT SchedulerStateMachine {
     public:
      ......

      // Set that we can create the first OutputSurface and start the scheduler.
      void SetCanStart() { can_start_ = true; }

      ......
    };

这个函数定义在文件external/chromium_org/cc/scheduler/scheduler_state_machine.h中。

SchedulerStateMachine类的成员函数SetCanStart将成员变量can_start_的值设置为true,这样就会触发调度器为网页创建绘图表面。

回到Scheduler类的成员函数SetCanStart中,它将内部的状态机设置为启动状态之后,接着调用另外一个成员函数ProcessScheduledActions检查下一个要执行的操作,这个操作即为创建绘图表面。

Scheduler类的成员函数ProcessScheduledActions的实现如下所示:

void Scheduler::ProcessScheduledActions() {
      ......

      SchedulerStateMachine::Action action;
      do {
        action = state_machine_.NextAction();
        ......
        state_machine_.UpdateState(action);
        ......
        switch (action) {
          ......
          case SchedulerStateMachine::ACTION_BEGIN_OUTPUT_SURFACE_CREATION:
            client_->ScheduledActionBeginOutputSurfaceCreation();
            break;
          ......
        }
      } while (action != SchedulerStateMachine::ACTION_NONE);

      SetupNextBeginFrameIfNeeded();
      ......

      if (state_machine_.ShouldTriggerBeginImplFrameDeadlineEarly()) {
        ......
        ScheduleBeginImplFrameDeadline(base::TimeTicks());
      }
    }

这个函数定义在文件external/chromium_org/cc/scheduler/scheduler_state_machine.cc中。

Scheduler类的成员函数ProcessScheduledAction的详细实现可以参考前面Chromium网页渲染调度器(Scheduler)实现分析一文,这里我们只关注它是如何触发网页绘图表面的创建的。

Scheduler类的成员函数ProcessScheduledAction首先调用SchedulerStateMachine类的成员函数NextAction询问状态机下一个应该执行的操作,后者的实现如下所示:

SchedulerStateMachine::Action SchedulerStateMachine::NextAction() const {
      if (ShouldUpdateVisibleTiles())
        return ACTION_UPDATE_VISIBLE_TILES;
      if (ShouldActivatePendingTree())
        return ACTION_ACTIVATE_PENDING_TREE;
      if (ShouldCommit())
        return ACTION_COMMIT;
      if (ShouldAnimate())
        return ACTION_ANIMATE;
      if (ShouldDraw()) {
        if (PendingDrawsShouldBeAborted())
          return ACTION_DRAW_AND_SWAP_ABORT;
        else if (forced_redraw_state_ == FORCED_REDRAW_STATE_WAITING_FOR_DRAW)
          return ACTION_DRAW_AND_SWAP_FORCED;
        else
          return ACTION_DRAW_AND_SWAP_IF_POSSIBLE;
      }
      if (ShouldManageTiles())
        return ACTION_MANAGE_TILES;
      if (ShouldSendBeginMainFrame())
        return ACTION_SEND_BEGIN_MAIN_FRAME;
      if (ShouldBeginOutputSurfaceCreation())
        return ACTION_BEGIN_OUTPUT_SURFACE_CREATION;
      return ACTION_NONE;
    }

这个函数定义在文件external/chromium_org/cc/scheduler/scheduler_state_machine.cc中。

在我们这个情景中,SchedulerStateMachine类的成员函数NextAction在调用到另外一个成员函数ShouldBeginOutputSurfaceCreation时得到的返回值为true,后者的实现如下所示:

bool SchedulerStateMachine::ShouldBeginOutputSurfaceCreation() const {
      // Don't try to initialize too early.
      if (!can_start_)
        return false;

      // We only want to start output surface initialization after the
      // previous commit is complete.
      if (commit_state_ != COMMIT_STATE_IDLE)
        return false;

      // Make sure the BeginImplFrame from any previous OutputSurfaces
      // are complete before creating the new OutputSurface.
      if (begin_impl_frame_state_ != BEGIN_IMPL_FRAME_STATE_IDLE)
        return false;

      // We want to clear the pipline of any pending draws and activations
      // before starting output surface initialization. This allows us to avoid
      // weird corner cases where we abort draws or force activation while we
      // are initializing the output surface.
      if (active_tree_needs_first_draw_ || has_pending_tree_)
        return false;

      // We need to create the output surface if we don't have one and we haven't
      // started creating one yet.
      return output_surface_state_ == OUTPUT_SURFACE_LOST;
    }

这个函数定义在文件external/chromium_org/cc/scheduler/scheduler_state_machine.cc中。

SchedulerStateMachine类的成员函数ShouldBeginOutputSurfaceCreation返回true要满足以下六个条件:

1. 网页的CC Layer Tree已经创建和初始化完成。这时候SchedulerStateMachine类的成员变量can_start_的值等于true。

2. 状态机的CommitState状态等于COMMIT_STATE_IDLE。这意味着当前Main线程不是正在提交变化给Compositor线程渲染。

3. 状态机的BeginImplFrameState状态等于BEGIN_IMPL_FRAME_STATE_IDLE。这意味着调度器已经向Compositor线程发出了一个渲染请求。

4. Compositor线程的CC Pending Layer Tree已经激活为CC Active Layer Tree。这时候SchedulerStateMachine类的成员变量has_pending_tree_的值等于false。

5. Compositor线程的CC Active Layer Tree被激活后,已经被执行过至少一次渲染操作了。这时候SchedulerStateMachine类的成员变量active_tree_needs_first_draw_的值等于false。

6. 状态机的OutputSurfaceState状态等于OUTPUT_SURFACE_LOST。

从前面的分析可以知道,第1个条件是满足的。由于这时候状态机也是刚刚初始化完成,因此后面五个条件也得到满足的。因此SchedulerStateMachine类的成员函数ShouldBeginOutputSurfaceCreation会返回true值给调用者。

同时,从SchedulerStateMachine类的成员函数ShouldBeginOutputSurfaceCreation的实现也可以看出,如果网页绘图表面在创建之后,由于其它原因失效了,那么要等到网页的渲染管线达到一个稳定状态之后,才可以重新创建。失效的原因可以参考Chromium网页渲染调度器(Scheduler)实现分析一文。稳定状态指的是Main线程已经将当前的网页变化提交给了Compositor线程,而Compositor线程也已经处理这些变化引起的渲染工作。总的来说,就是图1所示的第2到第6个操作均已经连贯执行完成。

回到SchedulerStateMachine类的成员函数NextAction中,当调用另外一个成员函数ShouldBeginOutputSurfaceCreation得到的返回值等于true的时候,它会返回一个ACTION_BEGIN_OUTPUT_SURFACE_CREATION值给调用者,也就是调度器,表示现在需要为网页创建绘图表面。

回到Scheduler类的成员函数ProcessScheduledActions中,它知道了下一个要执行的操作是ACTION_BEGIN_OUTPUT_SURFACE_CREATION之后,接下来会先调用SchedulerStateMachine类的成员函数UpdateState更新状态机的状态,如下所示:

void SchedulerStateMachine::UpdateState(Action action) {
      switch (action) {
        ......

        case ACTION_BEGIN_OUTPUT_SURFACE_CREATION:
          ......
          output_surface_state_ = OUTPUT_SURFACE_CREATING;

          ......
          return;

          ......
      }
    }

这个函数定义在文件external/chromium_org/cc/scheduler/scheduler_state_machine.cc中。

当调度器接下要执行的操作是ACTION_BEGIN_OUTPUT_SURFACE_CREATION时,SchedulerStateMachine类的成员函数UpdateState主要就是修改状态机的OutputSurfaceState状态,也就是将它的状态从OUTPUT_SURFACE_LOST迁移至OUTPUT_SURFACE_CREATING,表示正在为网页创建绘图表面。

回到Scheduler类的成员函数ProcessScheduledActions中,它更新了状态机的OutputSurfaceState状态之后,接下来就会调用成员变量client_指向的一个ThreadProxy对象的成员函数ScheduledActionBeginOutputSurfaceCreation为网页创建绘图表面。

ThreadProxy类的成员函数ScheduledActionBeginOutputSurfaceCreation的实现如下所示:

void ThreadProxy::ScheduledActionBeginOutputSurfaceCreation() {
      ......
      Proxy::MainThreadTaskRunner()->PostTask(
          FROM_HERE,
          base::Bind(&ThreadProxy::CreateAndInitializeOutputSurface,
                     main_thread_weak_ptr_));
    }

这个函数定义在文件external/chromium_org/cc/trees/thread_proxy.cc中。

ThreadProxy类的成员函数ScheduledActionBeginOutputSurfaceCreation向Main线程的消息队列发送了一个Task,这个Task绑定了ThreadProxy类的成员函数CreateAndInitializeOutputSurface。因此,接下来ThreadProxy类的成员函数CreateAndInitializeOutputSurface就会在Main线程中执行。

ThreadProxy类的成员函数CreateAndInitializeOutputSurface的实现如下所示:

void ThreadProxy::CreateAndInitializeOutputSurface() {
      ......
      DCHECK(IsMainThread());

      scoped_ptr<OutputSurface> output_surface =
          layer_tree_host()->CreateOutputSurface();

      if (output_surface) {
        Proxy::ImplThreadTaskRunner()->PostTask(
            FROM_HERE,
            base::Bind(&ThreadProxy::InitializeOutputSurfaceOnImplThread,
                       impl_thread_weak_ptr_,
                       base::Passed(&output_surface)));
        return;
      }

      ......
    }

这个函数定义在文件external/chromium_org/cc/trees/thread_proxy.cc中。

ThreadProxy类的成员函数CreateAndInitializeOutputSurface首先调用另外一个成员函数layer_tree_host获得一个LayerTreeHost对象。然后再调用这个LayerTreeHost对象的成员函数CreateOutputSurface为网页创建绘图表面。

ThreadProxy类的成员函数CreateAndInitializeOutputSurface接下来又向Compositor线程的消息队列发送一个Task。这个Task绑定的函数是ThreadProxy类的成员函数InitializeOutputSurfaceOnImplThread,用来在Compositor线程中初始化前面创建好的绘图表面。

接下来我们先分析LayerTreeHost类的成员函数CreateOutputSurface的实现,接着再分析ThreadProxy类的成员函数InitializeOutputSurfaceOnImplThread的实现。

LayerTreeHost类的成员函数CreateOutputSurface的实现如下所示:

scoped_ptr<OutputSurface> LayerTreeHost::CreateOutputSurface() {
      return client_->CreateOutputSurface(num_failed_recreate_attempts_ >= 4);
    }

这个函数定义在文件external/chromium_org/cc/trees/layer_tree_host.cc中。

从前面Chromium网页Layer Tree创建过程分析一文可以知道,LayerTreeHost类的成员变量client_指向的是一个RenderWidgetCompositor对象。LayerTreeHost类的成员函数CreateOutputSurface调用这个RenderWidgetCompositor对象的成员函数CreateOutputSurface为网页创建绘图表面。

RenderWidgetCompositor类的成员函数CreateOutputSurface的实现如下所示:

scoped_ptr<cc::OutputSurface> RenderWidgetCompositor::CreateOutputSurface(
        bool fallback) {
      return widget_->CreateOutputSurface(fallback);
    }

这个函数定义在文件external/chromium_org/content/renderer/gpu/render_widget_compositor.cc中。

从前面Chromium网页Layer Tree创建过程分析一文可以知道,RenderWidgetCompositor类的成员变量widget_指向的是一个RenderViewImpl对象。RenderWidgetCompositor类的成员函数CreateOutputSurface调用这个RenderViewImpl对象的成员函数CreateOutputSurface为网页创建绘图表面。

RenderViewImpl类的成员函数CreateOutputSurface是从父类RenderWidget继承下来的。RenderWidget类的成员函数CreateOutputSurface为网页创建绘图表面的过程可以参考前面Chromium的GPU进程启动过程分析一文。这个过程实际上就是Render进程请求与GPU进程建立GPU通道的过程。GPU通道建立起来之后,Render进程就可以发送GPU命令给GPU进程执行了。注意,在Render进程中加载的每一个网页都会与GPU进程建立一个GPU通道,并且每一个GPU通道在GPU进程又对应有一个OpenGL上下文。我们也可以认为Render进程的一个绘图表面对应于GPU进程中的一个OpenGL上下文。

假设网页A与GPU进程建立的GPU通道为G1,并且GPU通道为G1在GPU进程中对应的OpenGL上下文C1。当Render进程要渲染网页A的UI时,它就会通过GPU通道G1向GPU进程发送相应的GPU命令。GPU进程接收到这些GPU命令之后,就会激活OpenGL上下文C1,然后执行接收到的GPU命令。通过这种方式,GPU进程就可以同时接收不同网页的渲染命令,并且它们不会互相干扰,因为不同网页的渲染命令都是在各自的OpenGL上下文中执行的。这些都是Chromium硬件加速渲染机制相关的知识,可以参考前面Chromium硬件加速渲染机制基础知识简要介绍和学习计划这个系列的文章。

回到ThreadProxy类的成员函数CreateAndInitializeOutputSurface中,它调用LayerTreeHost类的成员函数CreateOutputSurface为网页创建了一个绘图表面之后,接下来会请求Compositor线程初始化这个绘图表面。这是通过在Compositor线程中调用ThreadProxy类的成员函数InitializeOutputSurfaceOnImplThread实现的。

ThreadProxy类的成员函数InitializeOutputSurfaceOnImplThread的实现如下所示:

void ThreadProxy::InitializeOutputSurfaceOnImplThread(
        scoped_ptr<OutputSurface> output_surface) {
      ......
      DCHECK(IsImplThread());

      LayerTreeHostImpl* host_impl = impl().layer_tree_host_impl.get();
      bool success = host_impl->InitializeRenderer(output_surface.Pass());
      ......

      if (success)
        impl().scheduler->DidCreateAndInitializeOutputSurface();
    }

这个函数定义在文件external/chromium_org/cc/trees/thread_proxy.cc。

ThreadProxy类的成员函数InitializeOutputSurfaceOnImplThread首先获得一个LayerTreeHostImpl对象,接着调用这个LayerTreeHostImpl对象的成员函数InitializeRenderer初始化参数output_surface描述的绘图表面。从前面Chromium的GPU进程启动过程分析一文可以知道,参数output_surface指向的实际上是一个CompositorOutputSurface对象。也就是说,网页的绘图表面是通过一个CompositorOutputSurface对象来描述的。

初始化完成后,ThreadProxy类的成员函数InitializeOutputSurfaceOnImplThread会调用Scheduler类的成员函数DidCreateAndInitializeOutputSurface通知调度器,它之前调度执行的ACTION_BEGIN_OUTPUT_SURFACE_CREATION操作已经执行完成。这时候调度器就会修改状态机的OutputSurfaceState状态。

接下来我们先分析LayerTreeHostImpl类的成员函数InitializeRenderer的实现,接下来再分析Scheduler类的成员函数DidCreateAndInitializeOutputSurface的实现。

LayerTreeHostImpl类的成员函数InitializeRenderer的实现如下所示:

bool LayerTreeHostImpl::InitializeRenderer(
        scoped_ptr<OutputSurface> output_surface) {
      ......

      if (!output_surface->BindToClient(this))
        return false;

      output_surface_ = output_surface.Pass();
      resource_provider_ =
          ResourceProvider::Create(output_surface_.get(),
                                   shared_bitmap_manager_,
                                   settings_.highp_threshold_min,
                                   settings_.use_rgba_4444_textures,
                                   settings_.texture_id_allocation_chunk_size,
                                   settings_.use_distance_field_text);

      ......

      CreateAndSetRenderer();

      ......

      if (settings_.impl_side_painting)
        CreateAndSetTileManager();

      ......

      return true;
    }

这个函数定义在文件external/chromium_org/cc/trees/layer_tree_host_impl.cc中。

LayerTreeHostImpl类的成员函数InitializeRenderer首先调用参数output_surface指向的一个CompositorOutputSurface对象的成员函数BindToClient,让这个CompositorOutputSurface对象为它所描述的绘图表面在GPU进程中创建一个OpenGL上下文,如下所示:

bool CompositorOutputSurface::BindToClient(
        cc::OutputSurfaceClient* client) {
      ......

      if (!cc::OutputSurface::BindToClient(client))
        return false;

      ......

      return true;
    }

这个函数定义在文件external/chromium_org/content/renderer/gpu/compositor_output_surface.cc中。

CompositorOutputSurface类的成员函数BindToClient调用父类OutputSurface的成员函数BindToClient来为当前正在处理的绘图表面在GPU进程中创建一个OpenGL上下文,如下所示:

bool OutputSurface::BindToClient(OutputSurfaceClient* client) {
      DCHECK(client);
      client_ = client;
      bool success = true;

      if (context_provider_) {
        success = context_provider_->BindToCurrentThread();
        if (success)
          SetUpContext3d();
      }

      if (!success)
        client_ = NULL;

      return success;
    }

这个函数定义在文件external/chromium_org/cc/output/output_surface.cc中。

从前面的调用过程可以知道,参数client指向的是一个LayerTreeHostImpl对象。OutputSurface类的成员函数BindToClient将这个LayerTreeHostImpl对象保存在成员变量client_中。

OutputSurface类的成员函数BindToClient接下来调用成员变量context_provider_指向的一个ContextProviderCommandBuffer对象的成员函数BindToCurrentThread为当前正在处理的绘图表面在GPU进程中创建一个OpenGL上下文。这个ContextProviderCommandBuffer对象的创建过程可以参考前面Chromium的GPU进程启动过程分析一文。

ContextProviderCommandBuffer类的成员函数BindToCurrentThread的实现如下:

bool ContextProviderCommandBuffer::BindToCurrentThread() {
      ......

      if (!context3d_->makeContextCurrent())
        return false;

      ......

      return true;
    }

这个函数定义在文件external/chromium_org/content/common/gpu/client/context_provider_command_buffer.cc中。

ContextProviderCommandBuffer类的成员变量context3d_指向的是一个WebGraphicsContext3DCommandBufferImpl对象。这个WebGraphicsContext3DCommandBufferImpl对象的创建过程可以参考前面Chromium的GPU进程启动过程分析一文。

ContextProviderCommandBuffer类的成员函数BindToCurrentThread调用成员变量context3d_指向的WebGraphicsContext3DCommandBufferImpl对象的成员函数makeContextCurrent在GPU进程中创建一个OpenGL上下文。这个OpenGL上下文与前面创建的绘图表面相对应。

WebGraphicsContext3DCommandBufferImpl类的成员函数makeContextCurrent请求GPU进程创建OpenGL上下文的过程可以参考前面Chromium硬件加速渲染的OpenGL上下文创建过程分析一文。

回到OutputSurface类的成员函数BindToClient中,它为当前正在处理的绘图表面在GPU进程中创建了一个OpenGL上下文之后,接下来会调用另外一个成员函数SetUpContext3d设置一些GPU进程回调通知,如下所示:

void OutputSurface::SetUpContext3d() {
      ......

      context_provider_->SetLostContextCallback(
          base::Bind(&OutputSurface::DidLoseOutputSurface,
                     base::Unretained(this)));
      context_provider_->ContextSupport()->SetSwapBuffersCompleteCallback(
          base::Bind(&OutputSurface::OnSwapBuffersComplete,
                     base::Unretained(this)));
      context_provider_->SetMemoryPolicyChangedCallback(
          base::Bind(&OutputSurface::SetMemoryPolicy,
                     base::Unretained(this)));
    }

这个函数定义在文件external/chromium_org/cc/output/output_surface.cc中。

OutputSurface类的成员函数SetUpContext3d设置了三个GPU进程回调通知:Lost Context Callback、Swap Buffers Complete Callback、Memory Policy Changed Callback,分别是GPU通道失效通知、网页UI渲染完成通知和内存策略变化通知。当这三个回调通知发生时,OutputSurface类的成员函数DidLoseOutputSurface、OnSwapBuffersComplete和SetMemoryPolicy就会被调用。其中,OutputSurface类的成员函数DidLoseOutputSurface会通知调度器将状态机的OutputSurfaceState状态修改为OUTPUT_SURFACE_LOST,这样调度器以后就可以重新为网页创建一个绘图表面;OutputSurface类的成员函数OnSwapBuffersComplete会通知调度器前面向GPU进程发出的一个SwapBuffers操作已经完成,也就是网页的UI已经被Browser进程合成完毕。

回到LayerTreeHostImpl类的成员函数InitializeRenderer中,它为参数output_surface描述的绘图表面在GPU进程中创建了一个OpenGL上下文之后,接下来会将这个参数指向的CompositorOutputSurface对象保存在成员变量output_surface_中,并且还会调用ResourceProvider类的静态成员函数Create创建一个ResourceProvider对象,保存在另外一个成员变量resource_provider_中。这个ResourceProvider对象在后面的网页渲染过程中,用来创建各种GPU资源。我们在后面的文章中就会看到这一点。

LayerTreeHostImpl类的成员函数InitializeRenderer接下来调用另外一个成员函数CreateAndSetRenderer为网页创建渲染器。创建出来的渲染器决定了网页的UI以什么样的方式合成到浏览器窗口中。

LayerTreeHostImpl类的成员函数CreateAndSetRenderer的实现如下所示:

void LayerTreeHostImpl::CreateAndSetRenderer() {
      ......

      if (output_surface_->capabilities().delegated_rendering) {
        renderer_ = DelegatingRenderer::Create(
            this, &settings_, output_surface_.get(), resource_provider_.get());
      } else if (output_surface_->context_provider()) {
        renderer_ = GLRenderer::Create(this,
                                       &settings_,
                                       output_surface_.get(),
                                       resource_provider_.get(),
                                       texture_mailbox_deleter_.get(),
                                       settings_.highp_threshold_min);
      } else if (output_surface_->software_device()) {
        renderer_ = SoftwareRenderer::Create(
            this, &settings_, output_surface_.get(), resource_provider_.get());
      }

      ......
    }

这个函数定义在文件external/chromium_org/cc/trees/layer_tree_host_impl.cc中。

从前面Chromium硬件加速渲染的UI合成过程分析一文可以知道,当Render进程的启动参数设置了"enable-delegated-renderer"选项时,Render进程使用委托渲染器。这个委托渲染器可以通过调用DelegatingRenderer类的静态成员函数Create创建。所谓委托渲染器,就是它委托Browser进程渲染和合成网页的UI。具体来说,就是Compositor线程在渲染网页时,它并没有真的执行渲染操作,而是计算出一系列的Render Pass,然后传递给Browser进程的的渲染器直接合成在浏览器窗口中。Render Pass描述的是网页当前可见的一个分块,它本质上是一个纹理,并且指定了纹理坐标,以及纹理的其它渲染参数,Browser进程根据这些信息就可以将网页的UI渲染出来。与非委托渲染器相比,委托渲染器可以减少一次渲染操作。非委托渲染器的Compositor线程在渲染网页时,会将网页当前可见的分块全部渲染一个纹理上,然后再将这个纹理传递给Browser进程。Browser进程拿到这个纹理后,还要再执行一次渲染操作,也就是合成在浏览窗口上,网页的UI才能显示出来。这过程包含了两次渲染操作,一次发生Render进程,另一次发生在Browser进程。因此非委托渲染器比委托渲染器多执行了一次渲染操作。

如果Render进程的启动参数没有设置"enable-delegated-renderer"选项,但是设置了使用硬件方式渲染网页的UI,那么Render进程就会使用非委托渲染器。这个非委托渲染器可以通过调用GLRenderer类的静态成员函数Create创建。注意,Browser进程也是通过CC模块来渲染浏览器窗口的,也就是它也像Render进程一样,将浏览器窗口抽象成CC Layer Tree、CC Pending Layer Tree和CC Active Layer Tree,这时候网页的UI就是其中的一个Layer。由于Browser进程不能再委托别人渲染自己的UI,因此在采用硬件方式渲染浏览器窗口的情况下,它就只能使用非委托渲染器来渲染浏览器窗口UI。

如果Render进程使用软件方式渲染网页的UI,那么它使用的渲染器通过调用SoftwareRenderer类的静态成员函数Create创建。一般来说,在获取网页的截图时,才会使用软件方式渲染网页的UI。

更多关于委托渲染器和非委托渲染器的知识,可以参考Chromium硬件加速渲染的UI合成过程分析一文。

再回到LayerTreeHostImpl类的成员函数InitializeRenderer中,它为网页创建了渲染器之后,接下来判断网页是否使用线程化渲染方式。如果是的话,LayerTreeHostImpl类的成员变量setting_描述的一个LayerTreeSettings对象的成员变量impl_side_painting的值就会等于ture。这时候LayerTreeHostImpl类的成员函数InitializeRenderer就会调用另外一个成员函数CreateAndSetTileManager创建一个分块管理器(Tile Manager)。这是由于线程化渲染方式中,Main线程只是记录了每一个网页分块的绘制命令。在这种情况下,Compositor线程在渲染网页的UI之前,要先通过分块管理器执行每一个分块的绘制命令,以便得到一个图像。这个过程也就是光栅化过程。在非线程化渲染方式中,Main线程直接就将分块光栅化在一个图像中了,也就是它已经做了光栅化的操作了。

我们假设网页使用的是线程化渲染方式,因此接下来我们继续分析LayerTreeHostImpl类的成员函数CreateAndSetTileManager的实现,如下所示:

void LayerTreeHostImpl::CreateAndSetTileManager() {
      ......

      ContextProvider* context_provider = output_surface_->context_provider();
      ......

      if (use_gpu_rasterization_ && context_provider) {
        resource_pool_ =
            ResourcePool::Create(resource_provider_.get(),
                                 GL_TEXTURE_2D,
                                 resource_provider_->best_texture_format());

        raster_worker_pool_ =
            DirectRasterWorkerPool::Create(proxy_->ImplThreadTaskRunner(),
                                           resource_provider_.get(),
                                           context_provider);
        on_demand_task_graph_runner_ = &synchronous_task_graph_runner_;
      } else if (UseZeroCopyTextureUpload()) {
        resource_pool_ =
            ResourcePool::Create(resource_provider_.get(),
                                 GetMapImageTextureTarget(context_provider),
                                 resource_provider_->best_texture_format());

        raster_worker_pool_ =
            ImageRasterWorkerPool::Create(proxy_->ImplThreadTaskRunner(),
                                          RasterWorkerPool::GetTaskGraphRunner(),
                                          resource_provider_.get());
        on_demand_task_graph_runner_ = RasterWorkerPool::GetTaskGraphRunner();
      } else if (UseOneCopyTextureUpload()) {
        // We need to create a staging resource pool when using copy rasterizer.
        staging_resource_pool_ =
            ResourcePool::Create(resource_provider_.get(),
                                 GetMapImageTextureTarget(context_provider),
                                 resource_provider_->best_texture_format());
        resource_pool_ =
            ResourcePool::Create(resource_provider_.get(),
                                 GL_TEXTURE_2D,
                                 resource_provider_->best_texture_format());

        raster_worker_pool_ = ImageCopyRasterWorkerPool::Create(
            proxy_->ImplThreadTaskRunner(),
            RasterWorkerPool::GetTaskGraphRunner(),
            resource_provider_.get(),
            staging_resource_pool_.get());
        on_demand_task_graph_runner_ = RasterWorkerPool::GetTaskGraphRunner();
      } else {
        resource_pool_ = ResourcePool::Create(
            resource_provider_.get(),
            GL_TEXTURE_2D,
            resource_provider_->memory_efficient_texture_format());

        raster_worker_pool_ = PixelBufferRasterWorkerPool::Create(
            proxy_->ImplThreadTaskRunner(),
            RasterWorkerPool::GetTaskGraphRunner(),
            resource_provider_.get(),
            transfer_buffer_memory_limit_);
        on_demand_task_graph_runner_ = RasterWorkerPool::GetTaskGraphRunner();
      }

      tile_manager_ =
          TileManager::Create(this,
                              proxy_->ImplThreadTaskRunner(),
                              resource_pool_.get(),
                              raster_worker_pool_->AsRasterizer(),
                              rendering_stats_instrumentation_);

      ......
    }

这个函数定义在文件external/chromium_org/cc/trees/layer_tree_host_impl.cc中。

分块管理器通过一个TileManager对象描述。这个TileManager对象可以通过调用TileManager类的静态成员函数Create创建。

创建分块管理器一个Resource Pool和Raster Worker Pool。分块管理器的职责是光栅化分块,不同的光栅化方式需要不同类型的Resource Pool和Raster Worker Pool。

CC模块提供了四种光栅化方式。第一种是使用GPU执行光栅化操作,后面三种使用CPU执行光栅化操作。注意,在硬件方式渲染网页UI的情况下,经过CPU光栅化后的分块仍然是通过GPU进行渲染的。

当Render进程设置了"force-gpu-rasterization"和"enable-impl-side-painting"启动选项时,LayerTreeHostImpl类的成员变量use_gpu_rasterization_的值就会等于true,表示要使用GPU光栅化网页分块。但是只有在网页使用硬件方式渲染时,才会真的使用GPU光栅化网页分块。这是因为使用GPU光栅化分块时,分块就直接光栅化在一个纹理中。这个纹理可以被GPU继续渲染出来形成网页的UI。因此,只有在使用硬件方式渲染网页的情况下,使用GPU光栅化分块才有意义。否则的话,光栅化后的分块内容还需要从GPU读取出来再交给CPU渲染。这样效率将会极其低下。

LayerTreeHostImpl类的成员变量output_surface_描述的是网页的绘图表面。从前面的分析可以知道,这个绘图表面是通过一个CompositorOutputSurface对象描述的,也就是LayerTreeHostImpl类的成员变量output_surface_指向的是一个CompositorOutputSurface对象。当调用这个CompositorOutputSurface对象的成员函数context_provider获得一个不为NULL的ContextProvider对象时,就说明网页使用硬件方式渲染UI。

GPU将分块光栅化在一个类型为GL_TEXTURE_2D的纹理上,因此这时候分块管理器需要一个类型为GL_TEXTURE_2D的Resource Pool,也就是一个能够创建类型为GL_TEXTURE_2D的纹理的Resource Pool。与此同时,分块管理器通过一个名称为"Direct"的Raster Worker Pool执行GPU光栅化操作。这个Raster Worker Pool可以通过调用DirectRasterWorkerPool类的静态成员函数Create创建。"Direct"的意思就是直接将分块光栅化在GPU里面,以后可以继续通过GPU进行渲染。

当Render进程设置了"enable-zero-copy"启动选项,并且平台支持GPU和CPU共享内存时,调用LayerTreeHostImpl类的成员函数UseZeroCopyTextureUpload得到的返回值就等于true,这时候将会使用CPU光栅化网页分块。在这种情况下,CPU将分块光栅化一块GPU和CPU都能访问的内存上,这样就可以避免在光栅化操作完成后,将分块内容从CPU拷贝到GPU的过程。因此,这种光栅化方式称为"Zero Copy"光栅化。GPU和CPU都能访问的内存是一种特殊的内存。在Android平台上,这种内存称为Graphics Buffer。CPU光栅化完成后,GPU可以将这个Graphics Buffer当作一个类型为GL_TEXTURE_EXTERNAL_OES的纹理访问。因此,这时候分块管理器需要一个类型为GL_TEXTURE_EXTERNAL_OES的Resource Pool,也就是一个能够创建类型为GL_TEXTURE_EXTERNAL_OES的纹理的Resource Pool。与此同时,分块管理器通过一个名称为"Image"的Raster Worker Pool执行CPU光栅化操作。这个Raster Worker Pool可以通过调用ImageRasterWorkerPool类的静态成员函数Create创建。

当Render进程设置了"enable-one-copy"启动选项,并且平台支持GPU和CPU共享内存时,调用LayerTreeHostImpl类的成员函数UseZeroCopyTextureUpload得到的返回值就等于true,这时候也会使用CPU光栅化网页分块。不过在这种情况下,CPU只是将分块光栅化一块临时的GPU和CPU都能访问的内存上,然后再将这个内存拷贝在一个类型为GL_TEXTURE_2D的纹理上。由于需要执行一次拷贝操作,因此这种光栅化方式称为"One Copy"光栅化。不过,这个拷贝是直接在GPU内完成的,并没有涉及到从CPU读取数据到GPU或者从GPU读取数据到CPU的操作,因此效率不是问题。这时候分块管理器需要两个Resource Pool,一个类型为GL_TEXTURE_EXTERNAL_OES,另一个类型为GL_TEXTURE_2D。其中,前者用来创建临时的GPU和CPU都能访问的内存,后者用来创建类型为GL_TEXTURE_2D的纹理。与此同时,分块管理器通过一个名称为"Image Copy"的Raster Worker Pool执行CPU光栅化操作。这个Raster Worker Pool可以通过调用ImageCopyRasterWorkerPool类的静态成员函数Create创建。

在其余情况下,分块管理器将使用CPU光栅化网页分块,并且是将分块光栅化在一个Pixel Buffer Object(PBO)中。这些PBO最后需要从CPU上传到GPU中,才能被GPU当作纹理访问。在这种情况下,分块管理器需要一个类型为GL_TEXTURE_2D的Resource Pool,用来创建类型为GL_TEXTURE_2D的纹理保存分块光栅化后得到的内容。与此同时,分块管理器通过一个名称为"Pixel Buffer"的Raster Worker Pool执行CPU光栅化操作。这个Raster Worker Pool可以通过调用PixelBufferRasterWorkerPool类的静态成员函数Create创建。

我们注意到,LayerTreeHostImpl类有一个成员变量on_demand_task_graphrunner,它指向的是一个TaskGraphRunner对象。这个TaskGraphRunner对象也是用来执行光栅化任务的,不过它只用在非委托渲染器中。在非委托渲染方式中,有时候会因为内存限制,使得不能够为所有的网页分块都分配一块独立的内存执行光栅化操作。这时候非委托渲染器会使用一块共用的内存来光栅化那些没有独立内存的分块。这些分块光栅化完成后就会马上渲染,以便将共用的内存释放出来给其它分块使用。因此,对于这些没有独立内存的分块,它们的光栅化过程是比较特殊的,需要通过LayerTreeHostImpl类的成员变量on_demand_task_graph_runner_描述的TaskGraphRunner对象进行。

关于网页分块的光栅化过程,我们在后面的文章中再进行详细分析。

这一步执行完成之后,网页的绘图表面就初始化完成了。回到ThreadProxy类的成员函数InitializeOutputSurfaceOnImplThread中,它接下来就会调用Scheduler类的成员函数DidCreateAndInitializeOutputSurface通知调度器修改状态机的OutputSurfaceState状态,如下所示:

void Scheduler::DidCreateAndInitializeOutputSurface() {
      ......
      state_machine_.DidCreateAndInitializeOutputSurface();
      ProcessScheduledActions();
    }

这个函数定义在文件external/chromium_org/cc/scheduler/scheduler.cc中。

Scheduler类的成员函数DidCreateAndInitializeOutputSurface调用SchedulerStateMachine类的成员函数DidCreateAndInitializeOutputSurface将状态机的OutputSurfaceState状态修改为OUTPUT_SURFACE_WAITING_FOR_FIRST_COMMIT,如下所示:

void SchedulerStateMachine::DidCreateAndInitializeOutputSurface() {
      DCHECK_EQ(output_surface_state_, OUTPUT_SURFACE_CREATING);
      output_surface_state_ = OUTPUT_SURFACE_WAITING_FOR_FIRST_COMMIT;

      ......
    }

这个函数定义在文件external/chromium_org/cc/scheduler/scheduler_state_machine.cc中。

当状态机的OutputSurfaceState状态为OUTPUT_SURFACE_WAITING_FOR_FIRST_COMMIT时,将会触发调度器尽快执行一个ACTION_SEND_BEGIN_MAIN_FRAME,也就是图1所示的第2个操作。这个操作将会请求Main线程绘制CC Layer Tree的内容。CC Layer Tree的内容绘制好之后,将会被同步到CC Pending Layer Tree中去执行光栅化操作,然后激活为CC Active Layer Tree。这个CC Active Layer Tree经过Compositor渲染后就得到网页的UI。

至此,我们就分析完成网页的绘图表面创建和初始化过程了。有了绘图表面之后,接下来就可以绘制和渲染网页的内容了。在接下来的一篇文章中,我们就继续分析网页的绘制过程,也就是Main线程绘制CC Layer Tree的过程,敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 目录