Chromium网页滑动和捏合手势处理过程分析

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

从前面一文可以知道,Chromium的Browser进程从Touch事件中检测到滑动和捏合手势之后,就会将它们发送给Render进程处理。滑动手势对应于修改网页的Viewport,而捏合手势对应于设置网页的缩放因子。通常我们比较两个浏览器的流畅程度,就是比较它们的滑动和捏合性能。因此,浏览器必须要快速地响应用户的滑动和捏合手势。本文接下来就详细分析Chromium快速响应网页滑动和捏合手势的过程。

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

从前面Chromium网页输入事件捕捉和手势检测过程分析一文可以知道,Browser进程是通过一个类型为InputMsg_HandleInputEvent的IPC消息将网页的滑动和捏合手势发送给Render进程处理的。Render进程通过一个Input Event Filter截获这两个手势操作,并且分发给Compositor线程处理,如图1所示:

图1 Compositor线程处理滑动和捏合手势的过程

从前面Chromium网页渲染机制简要介绍和学习计划这个系列的文章可以知道,Compositor线程为网页维护了一个CC Active Layer Tree。这个CC Active Layer Tree描述的是网页当前正在显示的内容。由于滑动和捏合手势并没有改变网页的内容,仅仅是改变了它的位置和缩放因子,因此Compositor线程会将滑动和捏合手势直接应用在CC Active Layer Tree。这样就可以快速地响应用户的滑动和捏合操作了。

将滑动和捏合手势应用在网页的CC Active Layer Tree之后,就会造成它与网页的CC Layer Tree不一致。通常情况下,我们都是将CC Layer Tree的变化同步到CC Active Layer Tree中去的。但是在网页被滑动和捏合的情况下,就刚好反过来,我们需要将CC Active Layer Tree的变化同步到CC Layer Tree中去,以维持两个Tree的一致性。

那么,CC Active Layer Tree的变化什么时候会同步到CC Layer Tree去的呢?Compositor线程将滑动和捏合手势应用在CC Active Layer Tree之后,会执行两个操作。第一个操作是请求马上对CC Active Layer Tree进行渲染。第二个操作是请求执行下一次Commit,这将会触发CC Layer Tree被重新绘制。CC Layer Tree在重新绘制的过程中,就会将之前应用在CC Active Layer Tree上的滑动和捏合操作也应用在CC Layer Tree上,从而可以将CC Active Layer Tree的变化同步到CC Layer Tree中去。

接下来,我们就从Render进程截获Browser进程发送过来的输入事件开始,分析它快速响应网页的滑动和捏合手势的过程。

从前面Chromium网页Frame Tree创建过程分析一文可以知道,Render进程在加载网页之后,会通过调用RenderThreadImpl类的成员函数EnsureWebKitInitialized初始化WebKit,如下所示:

void RenderThreadImpl::EnsureWebKitInitialized() {
      ......

      bool enable = command_line.HasSwitch(switches::kEnableThreadedCompositing);
      if (enable) {
        ......

        InputHandlerManagerClient* input_handler_manager_client = NULL;
        ......
        if (!input_handler_manager_client) {
          input_event_filter_ =
              new InputEventFilter(this, compositor_message_loop_proxy_);
          AddFilter(input_event_filter_.get());
          input_handler_manager_client = input_event_filter_.get();
        }
        input_handler_manager_.reset(
            new InputHandlerManager(compositor_message_loop_proxy_,
                                    input_handler_manager_client));
      }

      ......
    }

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

在初始化WebKit的过程中,RenderThreadImpl类的成员函数EnsureWebKitInitialized将会创建一个Input Event Filter截获Browser进程发送过来的网页输入事件。这个Input Event Filter保存在RenderThreadImpl类的成员变量input_event_filter_中。接下来,我们就分析这个Input Event Filter的创建过程。

Input Event Filter是用来将滑动和捏合手势分发给Render进程的Compositor线程处理的,因此只有Render进程存在Compositor线程的情况下,才会创建Input Event Filter。当Render进程指定了switches::kEnableThreadedCompositing(即enable-threaded-compositing)启动选项时,Render进程就会存在Compositor线程。这时候RenderThreadImpl类的成员函数EnsureWebKitInitialized将会创建一个Input Event Filter。

创建出来的Input Event Filter保存在RenderThreadImpl类的成员变量input_event_filter_中,并且在创建的时候,需要指定一个Compositor线程消息循环代理对象。这个消息循环代理对象由RenderThreadImpl类的另外一个成员变量compositor_message_loop_proxy_描述。

接下来我们分析InputEventFilter类的构造函数的实现,以便了解Input Event Filter的创建过程,如下所示:

InputEventFilter::InputEventFilter(
        IPC::Listener* main_listener,
        const scoped_refptr<base::MessageLoopProxy>& target_loop)
        : main_loop_(base::MessageLoopProxy::current()),
          main_listener_(main_listener),
          ......,
          target_loop_(target_loop),
          ...... {
      ......
    }

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

从前面的调用过程可以知道,参数main_listener指向的是一个RenderThreadImpl对象,它将会保存在InputEventFilter类的成员变量main_listener_中。以后Compositor线程会通过这个RenderThreadImpl对象将截获到的其它输入事件转发给Main线程处理。此外,参数target_loop描述的Compositor线程消息循环代理对象将会保存在InputEventFilter类的另外一个成员变量targetloop。以后Input Event Filter就会通过这个消息循环代理对象将滑动和捏合手势分发给Compositor线程处理。

InputEventFilter类的构造函数还是调用base::MessageLoopProxy类的静态成员函数current获得当前线程(Main线程)的消息循环代理对象,并且将这个消息循环代理对象保存在InputEventFilter类的成员变量mainloop。以后Compositor线程也会通过这个消息循环代理对象将截获到的其它输入事件转发给Main线程处理。

回到RenderThreadImpl类的成员函数EnsureWebKitInitialized中,它创建了一个Input Event Filter之后,接着又要这个Input Event Filter以及Compositor线程的消息循环代理对象创建一个InputHandlerManager对象,并且保存在RenderThreadImpl类的成员变量input_handler_manager_中。

接下来我们继续分析InputHandlerManager对象的创建过程,也就是InputHandlerManager类的构造函数的实现,如下所示:

InputHandlerManager::InputHandlerManager(
        const scoped_refptr<base::MessageLoopProxy>& message_loop_proxy,
        InputHandlerManagerClient* client)
        : message_loop_proxy_(message_loop_proxy),
          client_(client) {
      DCHECK(client_);
      client_->SetBoundHandler(base::Bind(&InputHandlerManager::HandleInputEvent,
                                          base::Unretained(this)));
    }

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

InputHandlerManager类的构造函数首先分别将参数message_loop_proxy描述的Compositor线程消息循环代理对象和参数client描述的Input Event Filter保存成员变量message_loop_proxy_和client_中。

InputHandlerManager类的构造函数接下来又调用参数client描述的Input Event Filter的成员函数SetBoundHandler,用来给后者设置一个Handler,如下所示:

void InputEventFilter::SetBoundHandler(const Handler& handler) {
      ......
      handler_ = handler;
    }

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

从前面的调用过程可以知道,参数handler描述的Hanlder绑定了InputHandlerManager类的成员函数HandleInputEvent,它被保存在InputEventFilter类的成员变量handler_中。这个Handler以后用来处理网页的滑动和捏合手势。

从前面Chromium网页Layer Tree创建过程分析一文可以知道,网页的CC Layer Tree是在RenderViewImpl类的成员函数initializeLayerTreeView中创建的。创建之后,RenderViewImpl类的成员函数initializeLayerTreeView将会获得一个Input Handler。这个Input Handler将会注册到前面创建的Input Handler Manager中,用来将网页滑动和捏合手势操作应用在网页的CC Active Layer Tree中。

接下来,我们就继续分析上述Input Handler的获取和注册过程,如下所示:

void RenderViewImpl::initializeLayerTreeView() {
      RenderWidget::initializeLayerTreeView(); 
      RenderWidgetCompositor* rwc = compositor();
      ......

    #if !defined(OS_MACOSX)  // many events are unhandled - http://crbug.com/138003
      RenderThreadImpl* render_thread = RenderThreadImpl::current();
      // render_thread may be NULL in tests.
      InputHandlerManager* input_handler_manager =
          render_thread ? render_thread->input_handler_manager() : NULL;
      if (input_handler_manager) {
        input_handler_manager->AddInputHandler(
            routing_id_, rwc->GetInputHandler(), AsWeakPtr());
      }
    #endif
    }

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

网页的CC Layer Tree是通过调用RenderViewImpl的父类RenderWidget的成员函数initializeLayerTreeView创建的。在创建CC Layer Tree的过程中,将会创建一个RenderWidgetCompositor对象。这个RenderWidgetCompositor对象可以通过调用RenderViewImpl类的成员函数compositor获得。同时,调用这个RenderWidgetCompositor对象的成员函数GetInputHandler,可以获得一个Input Handler,如下所示:

const base::WeakPtr<cc::InputHandler>&
    RenderWidgetCompositor::GetInputHandler() {
      return layer_tree_host_->GetInputHandler();
    }

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

从前面Chromium网页Layer Tree创建过程分析一文可以知道,RenderWidgetCompositor类的成员变量layer_tree_host_指向的是一个LayerTreeHost对象。这个LayerTreeHost对象负责管理网页的CC Layer Tree。RenderWidgetCompositor类的成员函数GetInputHandler调用这个LayerTreeHost对象的成员函数GetInputHandler获得一个Input Handler,如下所示:

class CC_EXPORT LayerTreeHost {
     public:
      ......

      LayerTreeHostClient* client() { return client_; }
      const base::WeakPtr<InputHandler>& GetInputHandler() {
        return input_handler_weak_ptr_;
      }

      ......

     private:
      ......

      base::WeakPtr<InputHandler> input_handler_weak_ptr_;

      ......
    };

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

从这里可以看到,LayerTreeHost类的成员函数GetInputHandler返回的是成员变量input_handler_weak_ptr_指向的一个Input Handler。这个Input Handler的创建过程如下所示:

scoped_ptr<LayerTreeHostImpl> LayerTreeHost::CreateLayerTreeHostImpl(
        LayerTreeHostImplClient* client) {
      ......
      scoped_ptr<LayerTreeHostImpl> host_impl =
          LayerTreeHostImpl::Create(settings_,
                                    client,
                                    proxy_.get(),
                                    rendering_stats_instrumentation_.get(),
                                    shared_bitmap_manager_,
                                    id_);
      ......

      input_handler_weak_ptr_ = host_impl->AsWeakPtr();
      return host_impl.Pass();
    }

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

LayerTreeHost类的成员函数CreateLayerTreeHostImpl的调用过程可以参考前面Chromium网页Layer Tree创建过程分析一文。从这里我们可以看到,LayerTreeHost类的成员变量input_handler_weak_ptr_指向的实际上是一个LayerTreeHostImpl对象。这个LayerTreeHostImpl对象负责管理网页的CC Pending Layer Tree和CC Active Layer Tree。

回到RenderViewImpl类的成员函数initializeLayerTreeView中,它获得了一个类型为LayerTreeHostImpl的Input Handler之后,会将它注册到前面创建的Input Handler Manager中,这是通过调用InputHandlerManager类的成员函数AddInputHandler进行的,如下所示:

void InputHandlerManager::AddInputHandler(
        int routing_id,
        const base::WeakPtr<cc::InputHandler>& input_handler,
        const base::WeakPtr<RenderViewImpl>& render_view_impl) {
      if (message_loop_proxy_->BelongsToCurrentThread()) {
        AddInputHandlerOnCompositorThread(routing_id,
                                          base::MessageLoopProxy::current(),
                                          input_handler,
                                          render_view_impl);
      } else {
        message_loop_proxy_->PostTask(
            FROM_HERE,
            base::Bind(&InputHandlerManager::AddInputHandlerOnCompositorThread,
                       base::Unretained(this),
                       routing_id,
                       base::MessageLoopProxy::current(),
                       input_handler,
                       render_view_impl));
      }
    }

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

从前面的分析可以知道,InputHandlerManager类的成员变量message_loop_proxy_描述的是Compositor线程的消息循环代理对象。通过这个消息循环代理对象,InputHandlerManager类的成员函数AddInputHandler可以判断当前线程是否是Compositor线程。如果是的话,就直接调用InputHandlerManager类的成员函数AddInputHandlerOnCompositorThread将参数input_handler描述的类型为LayerTreeHostImpl的Input Handler注册在内部。否则的话,就会向Compositor线程发送一个Task,这个Task绑定了InputHandlerManager类的成员函数AddInputHandlerOnCompositorThread。这意味着接下来InputHandlerManager类的成员函数AddInputHandlerOnCompositorThread也会在Compositor线程中被调用。

InputHandlerManager类的成员函数AddInputHandlerOnCompositorThread的实现如下所示:

void InputHandlerManager::AddInputHandlerOnCompositorThread(
        int routing_id,
        const scoped_refptr<base::MessageLoopProxy>& main_loop,
        const base::WeakPtr<cc::InputHandler>& input_handler,
        const base::WeakPtr<RenderViewImpl>& render_view_impl) {
      ......

      input_handlers_.add(routing_id,
          make_scoped_ptr(new InputHandlerWrapper(this,
              routing_id, main_loop, input_handler, render_view_impl)));
    }

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

InputHandlerManager类的成员变量input_handlers_描述的是一个Hash Map。这个Hash Map保存了一个系列的Input Handler Wrapper。每一个Input Handler Wrapper在内部都封装了一个Input Handler,并且都是以该Input Handler对应的网页的Routing ID为键值进行保存的。也就是说,在同一个Render进程中加载的每一个网页都会注册一个Input Handler到Input Handler Manager中,分别用来处理各自的滑动和捏合手势操作。

这一步执行完成之后,Render进程就创建了一个Input Event Filter和一个Input Handler Manager,并且往创建出来的Input Handler Manager注册一个类型为LayerTreeHostImpl的Input Handler。这个Input Handler以后将负责将网页的滑动和捏合手势操作应用网页的CC Active Layer中。

从前面Chromium的IPC消息发送、接收和分发机制分析一文可以知道,注册到IPC消息通道中的Filter是最先接收到其他进程发送过来的IPC消息的。这意味着Browser进程发送给Render进程的InputMsg_HandleInputEvent消息会被前面所创建的Input Event Filter截获,也就是Render进程会将接收到的InputMsg_HandleInputEvent消息交给它的成员函数OnMessageReceived处理,如下所示:

bool InputEventFilter::OnMessageReceived(const IPC::Message& message) {
      if (!RequiresThreadBounce(message))
        return false;

      ......

      target_loop_->PostTask(
          FROM_HERE,
          base::Bind(&InputEventFilter::ForwardToHandler, this, message));
      return true;
    }

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

InputEventFilter类的成员函数OnMessageReceived首先调用另外一个成员函数RequiresThreadBounce判断参数message描述的消息是否是一个输入事件消息。如果不是的话,那么就地返回一个false值给调用者,表示它不处理该消息。

从前面的分析可以知道,参数message描述的是一个InputMsg_HandleInputEvent消息。这是一个输入事件消息,因此InputEventFilter类的成员函数OnMessageReceived会对它进行处理,也就是将它封装在一个Task中,并且通过成员变量target_loop_将它发送到Compositor线程的消息队列中去等待处理。这个Task绑定了InputEventFilter类的成员函数ForwardToHandler。这意味着接下来InputEventFilter类的成员函数ForwardToHandler接下来将会在Compositor线程中被调用。

InputEventFilter类的成员函数ForwardToHandler的实现如下所示:

void InputEventFilter::ForwardToHandler(const IPC::Message& message) {
      ......

      int routing_id = message.routing_id();
      InputMsg_HandleInputEvent::Param params;
      if (!InputMsg_HandleInputEvent::Read(&message, ¶ms))
        return;
      const WebInputEvent* event = params.a;
      ui::LatencyInfo latency_info = params.b;
      .......

      InputEventAckState ack_state = handler_.Run(routing_id, event, &latency_info);

      if (ack_state == INPUT_EVENT_ACK_STATE_NOT_CONSUMED) {
        ......
        IPC::Message new_msg = InputMsg_HandleInputEvent(
            routing_id, event, latency_info, is_keyboard_shortcut);
        main_loop_->PostTask(
            FROM_HERE,
            base::Bind(&InputEventFilter::ForwardToMainListener,
                       this, new_msg));
        return;
      }

      ......

      InputHostMsg_HandleInputEvent_ACK_Params ack;
      ack.type = event->type;
      ack.state = ack_state;
      ack.latency = latency_info;
      ack.overscroll = overscroll_params.Pass();
      SendMessage(scoped_ptr<IPC::Message>(
          new InputHostMsg_HandleInputEvent_ACK(routing_id, ack)));
    }

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

InputEventFilter类的成员函数ForwardToHandler首先获得封装在参数message描述的InputMsg_HandleInputEvent消息中的输入事件,也就是一个WebInputEvent对象event。接下来将这个输入事件交给成员变量handler_描述的一个Handler处理,也就是调用这个Handler的成员函数Run处理。

从前面的分析可以知道,InputEventFilter类的成员变量handler_描述的Handler绑定了InputHandlerManager类的成员函数HandleInputEvent。这意味着InputHandlerManager类的成员函数HandleInputEvent将会在Compositor线程中被调用,用来处理WebInputEvent对象event描述的输入事件。

InputHandlerManager类的成员函数HandleInputEvent执行完成后,会有一个返回值,表示它是否已经对分发给它的输入事件进行了处理。如果已经处理,那么这个输入事件就不会再分发给Main线程处理。在这种情况下,InputEventFilter类的成员函数ForwardToHandler需要发送一个类型为InputHostMsg_HandleInputEvent_ACK的IPC消息给Browser进程,用来ACK之前Browser进程给它发送的类型为InputHostMsg_HandleInputEvent的IPC消息。

另一方面,如果InputHandlerManager类的成员函数HandleInputEvent没有处理分发给它的输入事件,那么这个输入事件就会继续分发给Main线程处理。这时候会先将输入事件重新封装在一个类型为InputHostMsg_HandleInputEvent的IPC消息中,然后再将这个IPC消息封装在一个Task中,并且通过成员变量main_loop_发送到Main线程的消息队列中去。这个Task绑定了InputEventFilter类的成员函数ForwardToMainListener。这意味着在这种情况下,InputEventFilter类的成员函数ForwardToMainListener接下来会在Main线程中被调用,用来处理Compositor线程不处理的输入事件。

接下来我们就继续分析InputHandlerManager类的成员函数HandleInputEvent的实现,以便了解Compositor线程处理输入事件的过程,如下所示:

InputEventAckState InputHandlerManager::HandleInputEvent(
        int routing_id,
        const WebInputEvent* input_event,
        ui::LatencyInfo* latency_info) {
      .....

      InputHandlerMap::iterator it = input_handlers_.find(routing_id);
      ......

      InputHandlerProxy* proxy = it->second->input_handler_proxy();
      return InputEventDispositionToAck(
          proxy->HandleInputEventWithLatencyInfo(*input_event, latency_info));
    }

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

参数routing_id描述的是一个Routing ID。这个Routing ID表示另外一个参数input_event描述的输入事件是来自哪一个网页。InputHandlerManager类的成员函数HandleInputEvent会根据这个Routing ID在成员变量input_handlers_描述的一个Hash Map中检查这个网页是否注册了一个Input Handler Wrapper。如果注册了,那么就说明网页要对该输入事件进行处理。

我们假设参数routing_id描述的Routing ID所对应的网页注册了一个Input Handler Wrapper。接下来InputHandlerManager类的成员函数HandleInputEvent就会调用这个Input Handler Wrapper的成员函数input_handler_proxy获得一个InputHandlerProxy对象,并且调用这个InputHandlerProxy对象的成员函数HandleInputEventWithLatencyInfo处理参数input_event描述的输入事件。

InputHandlerProxy类的成员函数HandleInputEventWithLatencyInfo的返回值是一个类型为InputHandlerProxy::EventDisposition的枚举值。这个枚举值有三个取值,分别是InputHandlerProxy::DID_HANDLE、InputHandlerProxy::DID_NOT_HANDLE和InputHandlerProxy::DROP_EVENT,用来表示分发给 InputHandlerProxy类的成员函数HandleInputEventWithLatencyInfo的输入事件是否已经被处理,或者需要进行丢弃。InputHandlerManager类的成员函数HandleInputEvent会调用函数InputEventDispositionToAck会将这个枚举值转化为另外一个类型为InputEventAckState的枚举值。后者同样是用来告诉InputHandlerManager类的成员函数HandleInputEvent的调用者,它分发给InputHandlerManager类的成员函数HandleInputEvent是否已经被处理的。

接下来,我们就继续分析InputHandlerProxy类的成员函数HandleInputEventWithLatencyInfo的实现,以便了解Compositor线程处理输入事件的过程,如下所示:

InputHandlerProxy::EventDisposition
    InputHandlerProxy::HandleInputEventWithLatencyInfo(
        const WebInputEvent& event,
        ui::LatencyInfo* latency_info) {
      ......

      InputHandlerProxy::EventDisposition disposition = HandleInputEvent(event);
      return disposition;
    }

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

InputHandlerProxy类的成员函数HandleInputEventWithLatencyInfo主要是将参数event描述的输入事件分给另外一个成员函数HandleInputEvent处理,如下所示:

InputHandlerProxy::EventDisposition InputHandlerProxy::HandleInputEvent(
        const WebInputEvent& event) {
      ......

      if (event.type == WebInputEvent::MouseWheel) {
        ......
      } else if (event.type == WebInputEvent::GestureScrollBegin) {
        ......
      } else if (event.type == WebInputEvent::GestureScrollUpdate) {
        ......

        const WebGestureEvent& gesture_event =
            *static_cast<const WebGestureEvent*>(&event);
        bool did_scroll = input_handler_->ScrollBy(
            gfx::Point(gesture_event.x, gesture_event.y),
            gfx::Vector2dF(-gesture_event.data.scrollUpdate.deltaX,
                           -gesture_event.data.scrollUpdate.deltaY));
        return did_scroll ? DID_HANDLE : DROP_EVENT;
      } else if (event.type == WebInputEvent::GestureScrollEnd) {
        ......
      } else if (event.type == WebInputEvent::GesturePinchBegin) {
        ......
      } else if (event.type == WebInputEvent::GesturePinchEnd) {
        ......
      } else if (event.type == WebInputEvent::GesturePinchUpdate) {
        ......

        const WebGestureEvent& gesture_event =
            *static_cast<const WebGestureEvent*>(&event);
        input_handler_->PinchGestureUpdate(
            gesture_event.data.pinchUpdate.scale,
            gfx::Point(gesture_event.x, gesture_event.y));
        return DID_HANDLE;
      } else if (event.type == WebInputEvent::GestureFlingStart) {
        ......
      } else if (event.type == WebInputEvent::GestureFlingCancel) {
        ......
      } else if (event.type == WebInputEvent::TouchStart) {
        ......
      } else if (WebInputEvent::isKeyboardEventType(event.type)) {
        ......
      } else if (event.type == WebInputEvent::MouseMove) {
        ......
      }

      return DID_NOT_HANDLE;
    }

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

InputHandlerProxy类的成员函数HandleInputEvent主要处理七种类型的输入事件:Mouse Wheel、Mouse Move、Keyboard、Touch Start、Gesture Scroll、Gesture Pinch和Gesture Fling。其中,后面三种属于手势操作。Fling手势和Scroll手势类似,不过前者是一个快速滑动操作,并且滑动后会松开。本文我们只关注Scroll和Pinch这两种手势操作。

Scroll和Pinch这两种手势操作操作又分为Begin、Update和End三种状态,分别表示手势操作刚刚开始、正在执行和已经结束。这里我们只关注手势操作正在执行的过程。InputHandlerProxy类的成员变量input_hander_描述的是一个类型为LayerTreeHostImpl的Input Handler。前面我们已经分析过这个Input Handler的注册过程。对于正在执行的Scroll和Pinch手势操作,InputHandlerProxy类的成员函数HandleInputEvent将为分别分发给上述Input Handler的成员函数ScrollBy和PinchGestureUpdate处理,也就是LayerTreeHostImpl类的成员函数ScrollBy和PinchGestureUpdate处理。

接下来我们就分别分析LayerTreeHostImpl类的成员函数ScrollBy和PinchGestureUpdate的实现,以便了解滑动手势和捏合手势的处理过程。

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

bool LayerTreeHostImpl::ScrollBy(const gfx::Point& viewport_point,
                                     const gfx::Vector2dF& scroll_delta) {
      ......

      gfx::Vector2dF pending_delta = scroll_delta;
      .....
      bool did_scroll_x = false;
      bool did_scroll_y = false;
      bool did_scroll_top_controls = false;
      ......

      bool consume_by_top_controls =
          top_controls_manager_ &&
          (((CurrentlyScrollingLayer() == InnerViewportScrollLayer() ||
             CurrentlyScrollingLayer() == OuterViewportScrollLayer()) &&
            InnerViewportScrollLayer()->MaxScrollOffset().y() > 0) ||
           scroll_delta.y() < 0);

      for (LayerImpl* layer_impl = CurrentlyScrollingLayer();
           layer_impl;
           layer_impl = layer_impl->parent()) {
        if (!layer_impl->scrollable())
          continue;

        if (layer_impl == InnerViewportScrollLayer()) {
          ......
          gfx::Vector2dF applied_delta;
          gfx::Vector2dF excess_delta;
          if (consume_by_top_controls) {
            excess_delta = top_controls_manager_->ScrollBy(pending_delta);
            applied_delta = pending_delta - excess_delta;
            pending_delta = excess_delta;
            // Force updating of vertical adjust values if needed.
            if (applied_delta.y() != 0) {
              did_scroll_top_controls = true;
              ......
            }
          }
          ......
        }

        gfx::Vector2dF applied_delta;
        // Gesture events need to be transformed from viewport coordinates to local
        // layer coordinates so that the scrolling contents exactly follow the
        // user's finger. In contrast, wheel events represent a fixed amount of
        // scrolling so we can just apply them directly.
        if (!wheel_scrolling_) {
          float scale_from_viewport_to_screen_space = device_scale_factor_;
          applied_delta =
              ScrollLayerWithViewportSpaceDelta(layer_impl,
                                                scale_from_viewport_to_screen_space,
                                                viewport_point, pending_delta);
        } else {
          applied_delta = ScrollLayerWithLocalDelta(layer_impl, pending_delta);
        }

        ......

        // If the layer wasn't able to move, try the next one in the hierarchy.
        bool did_move_layer_x = std::abs(applied_delta.x()) > kEpsilon;
        bool did_move_layer_y = std::abs(applied_delta.y()) > kEpsilon;
        did_scroll_x |= did_move_layer_x;
        did_scroll_y |= did_move_layer_y;

        ......

        // Allow further movement only on an axis perpendicular to the direction in
        // which the layer moved.
        gfx::Vector2dF perpendicular_axis(-applied_delta.y(), applied_delta.x());
        pending_delta = MathUtil::ProjectVector(pending_delta, perpendicular_axis);

        if (gfx::ToRoundedVector2d(pending_delta).IsZero())
          break;
      }

      bool did_scroll_content = did_scroll_x || did_scroll_y;
      if (did_scroll_content) {
        client_->SetNeedsCommitOnImplThread();
        SetNeedsRedraw();
        ......
      }

      ......

      return did_scroll_content || did_scroll_top_controls;
    }

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

滑动手势在Begin的时候,会根据触摸点确定需要进行Scroll的Layer。这个Layer可以通过调用LayerTreeHostImpl类的成员函数CurrentlyScrollingLayer获得。参数scroll_delta描述的是这个Layer要滑动的距离。但是这个Layer由于自身大小的原因,可能不能滑动指定的距离。这时候剩下未能滑动的距离将由它的可滑动的Parent Layer进行消化。这个过程会一直持续下去,直到参数scroll_delta描述的距离滑动完成为止。

在CC Active Layer Tree中,有两个特殊的Layer。一个称为Inner Viewport Scroll Layer,另一个称为Outer Viewport Scroll Layer,它们分别可以通过调用LayerTreeHostImpl类的成员函数InnerViewportScrollLayer和OuterViewportScrollLayer获得。其中,Inner Viewport Scroll Layer是一定会存在的,用来对网页进行整体滑动。Outer Viewport Scroll Layer只有在浏览器设置了"enable-pinch-virtual-viewport"启动选项时才会存在,用来支持一种称为Pinch Virtual Viewport的特性。这个特性可以参考官方文档:Layer-based Solution for Pinch Zoom / Fixed Position。当这两个Layer都存在时,Outer Viewport Scroll Layer是Inner Viewport Scroll Layer的子Layer。

代表网页内容的Layer属于Outer Viewport Scroll Layer的子Layer。因此,当网页从代表网页内容的Layer开始滑动时,会沿着Parent向Outer Viewport Scroll Layer滑动,Outer Viewport Scroll Layer又会再沿着Parent向Inner Viewport Scroll Layer滑动,直到中间的某一个Layer能够完全消化参数scroll_delta描述的滑动距离。

如果网页存在Top Controls,并且网页是从Inner Viewport Scroll Layer或者Outer Viewport Scroll Layer开始滑动,那么当滑动到Inner Viewport Scroll Layer时,Top Controls也会进行相应的滑动。Top Controls描述的就是包含地址栏的控件。一般情况下,网页在滑动的时候,地址栏是保持不变的。在移动平台中,例如在Android平台中,为了充分利用屏幕来显示网页,可以在浏览器启动时,设置"enable-top-controls-position-calculation"和"top-controls-height"启动选项,用来在滑动Inner Viewport Scroll Layer时,相应地滑动Top Controls。最终得到的效果就是使得Top Controls可以自动进行隐藏或者出现,以充分利用屏幕来显示网页。

滑动网页是一个复杂的过程,上面我们只是描述了一个大概的流程,有兴趣的读者可以自己详细分析一下LayerTreeHostImpl类的成员函数ScrollBy的实现。具体到每一个Layer来说,当轮到它进行滑动时,LayerTreeHostImpl类的成员函数ScrollBy就会调用另外一个成员函数ScrollLayerWithViewportSpaceDelta或者ScrollLayerWithLocalDelta对它进行滑动。其中,LayerTreeHostImpl类的成员函数ScrollLayerWithViewportSpaceDelta用来处理Touch事件触发的滑动操作,而LayerTreeHostImpl类的成员函数ScrollLayerWithLocalDelta用来处理鼠标中键触发的滑动操作。前者需要将滑动大小和位置从Viewport转化为Screen Space,后者不需要。

最后,如果代表网页内容的Layer发生了滑动,那么LayerTreeHostImpl类的成员函数ScrollBy就会做两件事情。第一件事情是调用成员变量client_指向的一个ThreadProxy对象的成员函数SetNeedsCommitOnImplThread请求执行一次同步操作,也就是重新对CC Layer Tree进行绘制,以及将其同步为一个新的CC Pending Layer Tree。这样做的目的为了将当前应用在CC Active Layer Tree的滑动操作也应用到CC Layer Tree中去,以便保持两者的一致性。这个过程可以参考前面Chromium网页Layer Tree绘制过程分析Chromium网页Layer Tree同步为Pending Layer Tree的过程分析这两篇文章。第二件事情是调用另外一个成员函数SetNeedsRedraw请求对刚刚被应用了滑动操作的CC Active Layer Tree进行渲染,以便可以对网页的滑动手势作出响应。

接下来我们就以Touch事件触发的滑动手势为例,继续分析CC Active Layer Tree中的Layer被滑动的过程,也就是LayerTreeHostImpl类的成员函数ScrollLayerWithViewportSpaceDelta的实现,如下所示:

gfx::Vector2dF LayerTreeHostImpl::ScrollLayerWithViewportSpaceDelta(
        LayerImpl* layer_impl,
        float scale_from_viewport_to_screen_space,
        const gfx::PointF& viewport_point,
        const gfx::Vector2dF& viewport_delta) {
      ......

      gfx::Transform inverse_screen_space_transform(
          gfx::Transform::kSkipInitialization);
      ......

      gfx::PointF screen_space_point =
          gfx::ScalePoint(viewport_point, scale_from_viewport_to_screen_space);

      gfx::Vector2dF screen_space_delta = viewport_delta;
      screen_space_delta.Scale(scale_from_viewport_to_screen_space);

      // First project the scroll start and end points to local layer space to find
      // the scroll delta in layer coordinates.
      bool start_clipped, end_clipped;
      gfx::PointF screen_space_end_point = screen_space_point + screen_space_delta;
      gfx::PointF local_start_point =
          MathUtil::ProjectPoint(inverse_screen_space_transform,
                                 screen_space_point,
                                 &start_clipped);
      gfx::PointF local_end_point =
          MathUtil::ProjectPoint(inverse_screen_space_transform,
                                 screen_space_end_point,
                                 &end_clipped);
      ......

      // local_start_point and local_end_point are in content space but we want to
      // move them to layer space for scrolling.
      float width_scale = 1.f / layer_impl->contents_scale_x();
      float height_scale = 1.f / layer_impl->contents_scale_y();
      local_start_point.Scale(width_scale, height_scale);
      local_end_point.Scale(width_scale, height_scale);

      // Apply the scroll delta.
      gfx::Vector2dF previous_delta = layer_impl->ScrollDelta();
      layer_impl->ScrollBy(local_end_point - local_start_point);

      // Get the end point in the layer's content space so we can apply its
      // ScreenSpaceTransform.
      gfx::PointF actual_local_end_point = local_start_point +
                                           layer_impl->ScrollDelta() -
                                           previous_delta;
      gfx::PointF actual_local_content_end_point =
          gfx::ScalePoint(actual_local_end_point,
                          1.f / width_scale,
                          1.f / height_scale);

      // Calculate the applied scroll delta in viewport space coordinates.
      gfx::PointF actual_screen_space_end_point =
          MathUtil::MapPoint(layer_impl->screen_space_transform(),
                             actual_local_content_end_point,
                             &end_clipped);
      ......

      gfx::PointF actual_viewport_end_point =
          gfx::ScalePoint(actual_screen_space_end_point,
                          1.f / scale_from_viewport_to_screen_space);
      return actual_viewport_end_point - viewport_point;
    }

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

LayerTreeHostImpl类的成员函数ScrollLayerWithViewportSpaceDelta首先将触摸点位置viewport_point从Viewport Space转换到Screen Space,再从Screen Space转换到Local Layer Space上,最后得到参数layer_impl描述的Layer的滑动距离为(local_end_point - local_start_point)。这个滑动距离通过调用LayerImpl类的成员函数ScrollBy应用在参数layer_impl描述的Layer中。

LayerTreeHostImpl类的成员函数ScrollLayerWithViewportSpaceDelta最后又将参数layer_impl描述的Layer滑动后的终点actual_local_end_point从Local Layer Space转换到Screen Space,再从Screen Space转换到Viewport Space,这样得到的actual_viewport_end_point减去原先的触摸点位置viewport_point,就得到参数layer_impl描述的Layer所消耗的滑动距离,剩下的未消耗滑动距离将被LayerTreeHostImpl类的成员函数ScrollBy应用在父Parent Layer中。

接下来我们继续分析LayerImpl类的成员函数ScrollBy的实现,以便了解CC Active Layer Tree中的Layer的滑动过程,如下所示:

gfx::Vector2dF LayerImpl::ScrollBy(const gfx::Vector2dF& scroll) {
      DCHECK(scrollable());
      gfx::Vector2dF min_delta = -scroll_offset_;
      gfx::Vector2dF max_delta = MaxScrollOffset() - scroll_offset_;
      // Clamp new_delta so that position + delta stays within scroll bounds.
      gfx::Vector2dF new_delta = (ScrollDelta() + scroll);
      new_delta.SetToMax(min_delta);
      new_delta.SetToMin(max_delta);
      gfx::Vector2dF unscrolled =
          ScrollDelta() + scroll - new_delta;
      SetScrollDelta(new_delta);

      return unscrolled;
    }

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

从前面的调用过程可以知道,参数scroll表示当前正在处理的Layer新增的滑动量。

LayerImpl类的成员变量scroll_offset_表示当前正在处理的Layer的滑动偏移值。LayerImpl类还有另外一个成员变量scrolldelta,用来表示当前正在处理的Layer还没有同步到CC Layer Tree中去的滑动量,它的值可以通过调用LayerImpl类的成员函数ScrollDelta获得。此外,通过调用LayerImpl类的成员函数MaxScrollOffset还可以获得当前正在处理的Layer的最大滑动偏移值。

有了前面的数据之后,就可以计算得到当前正在处理的Layer可以滑动的范围[min_delta, max_delta],以及累计还没有同步到CC Layer Tree中去的滑动量new_delta。其中,滑动量new_delta会被限制在滑动范围[min_delta, max_delta]之内。

LayerImpl类的成员函数ScrollBy接下来又会计算当前正在处理的Layer未能消化的滑动量unscrolled。这个未消化的滑动量unscrolled将由当前正在处理的Layer的Parent Layer进行处理。

LayerImpl类的成员函数ScrollBy最后还会调用另外一个成员函数SetScrollDelta处理累计还没有同步到CC Layer Tree中去的滑动量new_delta,如下所示:

void LayerImpl::SetScrollDelta(const gfx::Vector2dF& scroll_delta) {
      SetScrollOffsetAndDelta(scroll_offset_, scroll_delta);
    }

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

LayerImpl类的成员函数SetScrollDelta调用另外一个成员函数SetScrollOffsetAndDelta设置当前正在处理的Layer的滑动偏移值和未同步到CC Layer Tree中去的滑动量,如下所示:

void LayerImpl::SetScrollOffsetAndDelta(const gfx::Vector2d& scroll_offset,
                                            const gfx::Vector2dF& scroll_delta) {
      bool changed = false;

      last_scroll_offset_ = scroll_offset;

      if (scroll_offset_ != scroll_offset) {
        changed = true;
        scroll_offset_ = scroll_offset;

        if (scroll_offset_delegate_)
          scroll_offset_delegate_->SetTotalScrollOffset(TotalScrollOffset());
      }

      if (ScrollDelta() != scroll_delta) {
        changed = true;
        if (layer_tree_impl()->IsActiveTree()) {
          LayerImpl* pending_twin =
              layer_tree_impl()->FindPendingTreeLayerById(id());
          if (pending_twin) {
            // The pending twin can't mirror the scroll delta of the active
            // layer.  Although the delta - sent scroll delta difference is
            // identical for both twins, the sent scroll delta for the pending
            // layer is zero, as anything that has been sent has been baked
            // into the layer's position/scroll offset as a part of commit.
            DCHECK(pending_twin->sent_scroll_delta().IsZero());
            pending_twin->SetScrollDelta(scroll_delta - sent_scroll_delta());
          }
        }

        if (scroll_offset_delegate_) {
          scroll_offset_delegate_->SetTotalScrollOffset(scroll_offset_ +
                                                        scroll_delta);
        } else {
          scroll_delta_ = scroll_delta;
        }
      }

      if (changed) {
        NoteLayerPropertyChangedForSubtree();
        ......
      }
    }

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

LayerImpl类的成员函数SetScrollOffsetAndDelta主要是将当前正在处理的Layer的滑动偏移值和未同步到CC Layer Tree中去的滑动量分别记录在成员变量scroll_offset_和scroll_delta_中。

当Chromium用来实现WebView时,LayerImpl类的成员变量scroll_offset_delegate_指向一个ScrollOffsetDelegate对象。这个ScrollOffsetDelegate对象用来通知嵌入WebView的窗口,它通过WebView加载的网页发生了滑动,它可以做相应的处理。本文只考虑Chromium用来实现独立浏览器的情况,因此可以认为LayerImpl类的成员变量scroll_offset_delegate_的值等于NULL。

如果当前处理的Layer是属于CC Active Layer Tree的,那么当未同步到CC Layer Tree中去的滑动量发生变化时,那么LayerImpl类的成员函数SetScrollOffsetAndDelta还会将这个滑动量同步到它在CC Pending Layer Tree中对应的Twin Layer中去,以保持两者的一致性。关于Twin Layer的更多知识,可以参考前面Chromium网页Layer Tree同步为Pending Layer Tree的过程分析一文。

在将CC Pending Layer Tree激活为CC Active Layer Tree的过程中,CC Pending Layer Tree中的Layer也会通过LayerImpl类的成员函数SetScrollOffsetAndDelta记录它的滑动偏移值和未与CC Layer Tree同步的滑动量。在这种情况下,当前正在处理的Layer是属于CC Pending Layer Tree中的,因此 LayerImpl类的成员函数SetScrollOffsetAndDelta需要判断当前正在处理的Layer是否属于CC Active Layer Tree的。如果不是,那么就不需要将未与CC Layer Tree同步的滑动量同步到CC Active Layer Tree中去,因为后者已经执行过该同步操作了。

只要当前正在处理的Layer的滑动偏移值或者未同步到CC Layer Tree中去的滑动量发生变化,那么本地变量changed的值就会被设置为true。在这种情况下,LayerImpl类的成员函数SetScrollOffsetAndDelta会调用另外一个成员函数NoteLayerPropertyChangedForSubtree通知当前正在处理的Layer所在的Tree,它的绘制属性发生了变化,下次在执行Tree同步操作时,需要执行绘制属性同步操作。

LayerImpl类的成员函数NoteLayerPropertyChangedForSubtree的实现如下所示:

void LayerImpl::NoteLayerPropertyChangedForSubtree() {
      layer_property_changed_ = true;
      layer_tree_impl()->set_needs_update_draw_properties();
      for (size_t i = 0; i < children_.size(); ++i)
        children_[i]->NoteLayerPropertyChangedForDescendantsInternal();
      SetNeedsPushProperties();
    }

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

LayerImpl类的成员函数NoteLayerPropertyChangedForSubtree执行四个操作:

1. 将当前正在处理的Layer的绘制属性标记为发生了变化。这是通过将LayerImpl类的成员变量layer_property_changed_设置为true实现的。

2. 将当前正在处理的Layer所属的Tree的绘制属性标记为需要更新。当前正在处理的Layer所属的Tree要么是CC Pending Layer Tree,要么是CC Active Layer Tree,它可以通过调用LayerImpl类的成员函数layer_tree_impl获得。获得之后,就可以调用它的成员函数set_needs_update_draw_properties进行标记了。

3. 将当前正在处理的Layer的所有子Layer的绘制属性也标记为发生了变化。这是因为当一个Layer发生滑动时,它的所有子Layer也会跟着滑动。当前正在处理的Layer的所有子Layer都保存在LayerImpl类的成员变量children_描述的一个List中。通过遍历这个List,以及调用这些子Layer的成员函数NoteLayerPropertyChangedForDescendantsInternal就可以对它们进行递归标记。

4. 将当前正在处理的Layer的绘制属性标记为需要同步到Twin Layer中去。这是通过调用LayerImpl类的成员函数SetNeedsPushProperties实现的。

这一步执行完成之后,Compositor线程就将滑动手势操作应用在CC Active Layer Tree中了,并且也将滑动手势引发的滑动量同步到CC Pending Layer Tree中去了。接下来还需要将滑动手势引发的滑动量同步到CC Layer Tree中去。前面提到,这个同步操作是通过请求执行下一个Commit操作实现的。这一点我们后面再分析。

回到前面分析的InputHandlerProxy类的成员函数HandleInputEvent中,我们继续分析它调用LayerTreeHostImpl类的成员函数PinchGestureUpdate处理捏合手势的过程,如下所示:

void LayerTreeHostImpl::PinchGestureUpdate(float magnify_delta,
                                               const gfx::Point& anchor) {
      ......

      // Keep the center-of-pinch anchor specified by (x, y) in a stable
      // position over the course of the magnify.
      float page_scale_delta = active_tree_->page_scale_delta();
      gfx::PointF previous_scale_anchor =
          gfx::ScalePoint(anchor, 1.f / page_scale_delta);
      active_tree_->SetPageScaleDelta(page_scale_delta * magnify_delta);
      page_scale_delta = active_tree_->page_scale_delta();
      gfx::PointF new_scale_anchor =
          gfx::ScalePoint(anchor, 1.f / page_scale_delta);
      gfx::Vector2dF move = previous_scale_anchor - new_scale_anchor;

      previous_pinch_anchor_ = anchor;

      move.Scale(1 / active_tree_->page_scale_factor());
      // If clamping the inner viewport scroll offset causes a change, it should
      // be accounted for from the intended move.
      move -= InnerViewportScrollLayer()->ClampScrollToMaxScrollOffset();

      // We manually manage the bubbling behaviour here as it is different to that
      // implemented in LayerTreeHostImpl::ScrollBy(). Specifically:
      // 1) we want to explicit limit the bubbling to the outer/inner viewports,
      // 2) we don't want the directional limitations on the unused parts that
      //    ScrollBy() implements, and
      // 3) pinching should not engage the top controls manager.
      gfx::Vector2dF unused = OuterViewportScrollLayer()
                                  ? OuterViewportScrollLayer()->ScrollBy(move)
                                  : move;

      if (!unused.IsZero()) {
        InnerViewportScrollLayer()->ScrollBy(unused);
        InnerViewportScrollLayer()->ClampScrollToMaxScrollOffset();
      }

      ......

      client_->SetNeedsCommitOnImplThread();
      SetNeedsRedraw();
      ......
    }

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

LayerTreeHostImpl类的成员变量active_tree_指向的是一个LayerTreeImpl对象。这个LayerTreeImpl对象描述的就是网页的CC Active Layer Tree。调用这个LayerTreeImpl对象的成员函数page_scale_delta可以获得还未同步到CC Layer Tree去的缩放因子page_scale_delta。参数magnify_delta描述的是当前的捏合手势对网页引发的缩放量。将page_scale_delta乘以magnify_delta就可以得到新的未同步到CC Layer Tree去的缩放因子。这个缩放因子会通过调用LayerTreeImpl类的成员函数SetPageScaleDelta设置到CC Active Layer Tree中去。

捏合手势除了会引发网页的缩放因子发生变化之外,还会引发网页发生滑动。因此,LayerTreeHostImpl类的成员函数PinchGestureUpdate接下来还会计算当前的捏合手势对网页引发的滑动量。注意,这个滑动量是应用在整个网页上的,不是网页的某个Layer上。网页的Outer Viewport Scroll Layer和Inner Viewport Scroll Layer代表的整个网页,因此当前的捏合手势对网页引发的滑动量将会应用在这两个Layer上:首先是应用在Outer Viewport Scroll Layer上;如果Outer Viewport Scroll Layer不能完全消化这个滑动量,那么剩余的滑动量将应用在Inner Viewport Scroll Layer上。

重新计算和设置好CC Active Layer Tree的缩放因子和滑动量之后,LayerTreeHostImpl类的成员函数PinchGestureUpdate像前面分析的成员函数ScrollBy一样,会做两件事情。第一件事情是调用成员变量client_指向的一个ThreadProxy对象的成员函数SetNeedsCommitOnImplThread请求执行一次同步操作,以保持CC Active Layer Tree和CC Layer Tree的缩放因子一致。第二件事情是调用另外一个成员函数SetNeedsRedraw请求对刚刚被应用了缩放操作的CC Active Layer Tree进行渲染,以便可以对网页的捏合手势作出响应。

接下来我们继续分析LayerTreeImpl类的成员函数SetPageScaleDelta的实现,以便了解CC Active Layer Tree的缩放因子的计算过程,如下所示:

void LayerTreeImpl::SetPageScaleDelta(float delta) {
      SetPageScaleValues(page_scale_factor_, min_page_scale_factor_,
          max_page_scale_factor_, delta);
    }

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

LayerTreeImpl类的成员变量page_scale_factor_描述的就是CC Active Layer Tree当前的缩放因子,另外两个成员变量min_page_scale_factor_和max_page_scale_factor_描述的是CC Active Layer Tree允许设置的最小和最大缩放因子。

LayerTreeImpl类的成员函数SetPageScaleDelta调用另外一个成员函数SetPageScaleValues更新CC Active Layer Tree未同步到CC Layer Tree去的缩放因子,如下所示:

void LayerTreeImpl::SetPageScaleValues(float page_scale_factor,
          float min_page_scale_factor, float max_page_scale_factor,
          float page_scale_delta) {
      bool page_scale_changed =
          min_page_scale_factor != min_page_scale_factor_ ||
          max_page_scale_factor != max_page_scale_factor_ ||
          page_scale_factor != page_scale_factor_;

      min_page_scale_factor_ = min_page_scale_factor;
      max_page_scale_factor_ = max_page_scale_factor;
      page_scale_factor_ = page_scale_factor;

      float total = page_scale_factor_ * page_scale_delta;
      if (min_page_scale_factor_ && total < min_page_scale_factor_)
        page_scale_delta = min_page_scale_factor_ / page_scale_factor_;
      else if (max_page_scale_factor_ && total > max_page_scale_factor_)
        page_scale_delta = max_page_scale_factor_ / page_scale_factor_;

      if (page_scale_delta_ == page_scale_delta && !page_scale_changed)
        return;

      if (page_scale_delta_ != page_scale_delta) {
        page_scale_delta_ = page_scale_delta;

        if (IsActiveTree()) {
          LayerTreeImpl* pending_tree = layer_tree_host_impl_->pending_tree();
          if (pending_tree) {
            DCHECK_EQ(1, pending_tree->sent_page_scale_delta());
            pending_tree->SetPageScaleDelta(
                page_scale_delta_ / sent_page_scale_delta_);
          }
        }

        set_needs_update_draw_properties();
      }

      ......
    }

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

LayerTreeImpl类的成员函数SetPageScaleValues会根据参数更新CC Active Layer Tree的以下四个缩放因子:

1. 当前缩放因子page_scalefactor

2. 最小缩放因子min_page_scalefactor

3. 最大缩放因子max_page_scalefactor

4. 未同步到CC Layer Tree去的缩放因子page_scaledelta

在我们这个情景中,前面3个缩放因子是保持不变的。发生变化的是第四个缩放因子。当前缩放因子page_scale_factor_乘以新的未同步到CC Layer Tree去的缩放因子page_scale_delta,将会得到CC Active Layer Tree新的缩放因子。这个缩放因子不能小于最小缩放因子min_page_scalefactor,以及不能大于最大缩放因子max_page_scalefactor。因此,LayerTreeImpl类的成员函数SetPageScaleValues会对这一点进行检查,并且相应地调整参数page_scale_delta的值。

LayerTreeImpl类不仅用来描述网页的CC Active Layer Tree,也用来描述网页的CC Pending Layer Tree。如果当前正在处理的LayerTreeImpl对象描述的是网页的CC Active Layer Tree,那么LayerTreeImpl类的成员函数SetPageScaleValues还会将未同步到CC Layer Tree的缩放因子也设置到CC Pending Layer Tree中去,以保持两者的一致性。

最后,如果未同步到CC Layer Tree去的缩放因子发生了变化,那么LayerTreeImpl类的成员函数SetPageScaleValues会调用另外一个成员函数set_needs_update_draw_properties将CC Active Layer Tree的绘制属性标记为发生了变化,以便接下来对CC Active Layer Tree进行渲染时,应用新的缩放因子,以反映当前的捏合手势对网页产生的变化。

这样,我们就分析了滑动和捏合手势应用在网页的CC Active Layer Tree的过程,主要是引起了CC Active Layer Tree位置和缩放因子的变化。这些变化会触发CC Active Layer Tree进行重新渲染,也就是执行一个Redraw操作。这个操作的执行过程可以参考前面Chromium网页Pending Layer Tree激活为Active Layer Tree的过程分析一文。

从前面的分析可以知道,CC Active Layer Tree在响应滑动和捏合手势的时候,会将得到的新的滑动量和缩放因子同步给CC Pending Layer Tree,但是还没有将它们同步给CC Layer Tree。因此,Compositor线程还会触发一个Commit操作。在执行这个操作之前,CC模块的调度器会先对网页的CC Layer Tree进行重新绘制。在绘制的过程中,就会将CC Active Layer Tree的滑动量和缩放因子同步到CC Layer Tree中去,以保持两者的一致性。接下来我们就继续分析这个同步过程。

从前面Chromium网页Layer Tree绘制过程分析一文可以知道,CC模块的调度器会调用ThreadProxy类的成员函数ScheduledActionSendBeginMainFrame请求Main线程对CC Layer Tree进行重新绘制,如下所示:

void ThreadProxy::ScheduledActionSendBeginMainFrame() {
      ......

      scoped_ptr<BeginMainFrameAndCommitState> begin_main_frame_state(
          new BeginMainFrameAndCommitState);
      ......
      begin_main_frame_state->scroll_info =
          impl().layer_tree_host_impl->ProcessScrollDeltas();

      ......

      Proxy::MainThreadTaskRunner()->PostTask(
          FROM_HERE,
          base::Bind(&ThreadProxy::BeginMainFrame,
                     main_thread_weak_ptr_,
                     base::Passed(&begin_main_frame_state)));

      ......
    }

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

ThreadProxy类的成员函数ScheduledActionSendBeginMainFrame首先会调用LayerTreeHostImpl类的成员函数ProcessScrollDeltas收集CC Active Layer Tree未同步到CC Layer Tree去的滑动量和缩放因子,然后再请求Main线程重新绘制CC Layer Tree,也就是在Main线程中调用ThreadProxy类的成员函数BeginMainFrame。

接下来我们先分析LayerTreeHostImpl类的成员函数ProcessScrollDeltas收集CC Active Layer Tree未同步到CC Layer Tree去的滑动量和缩放因子的过程,如下所示:

scoped_ptr<ScrollAndScaleSet> LayerTreeHostImpl::ProcessScrollDeltas() {
      scoped_ptr<ScrollAndScaleSet> scroll_info(new ScrollAndScaleSet());

      CollectScrollDeltas(scroll_info.get(), active_tree_->root_layer());
      scroll_info->page_scale_delta = active_tree_->page_scale_delta();
      active_tree_->set_sent_page_scale_delta(scroll_info->page_scale_delta);

      return scroll_info.Pass();
    }

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

LayerTreeHostImpl类的成员函数ProcessScrollDeltas首先是调用另外一个函数CollectScrollDeltas在CC Active Layer Tree中收集未同步到CC Layer Tree中去的滑动量。这是以Layer为单位进行收集的。

LayerTreeHostImpl类的成员函数ProcessScrollDeltas接下来又会调用成员变量active_tree_指向的一个LayerTreeImpl对象的成员函数page_scale_delta获得CC Active Layer Tree未同步到CC Layer Tree中去的缩放因子,并且又会调用该LayerTreeImpl对象的成员函数set_sent_page_scale_delta将上述获得的缩放因子设置为CC Active Layer Tree的Sent Page Scale Delta。这个Sent Page Scale Delta描述的是CC Active Layer Tree已经同步到CC Layer Tree的缩放因子。

前面从CC Active Layer Tree收集到的滑动量和缩放因子均保存在一个ScrollAndScaleSet对象。这个ScrollAndScaleSet对象将会返回给调用者,也就是ThreadProxy类的成员函数ScheduledActionSendBeginMainFrame进行处理。

接下来我们继续分析函数CollectScrollDeltas在CC Active Layer Tree中收集未同步到CC Layer Tree中去的滑动量的过程,如下所示:

static void CollectScrollDeltas(ScrollAndScaleSet* scroll_info,
                                    LayerImpl* layer_impl) {
      if (!layer_impl)
        return;

      gfx::Vector2d scroll_delta =
          gfx::ToFlooredVector2d(layer_impl->ScrollDelta());
      if (!scroll_delta.IsZero()) {
        LayerTreeHostCommon::ScrollUpdateInfo scroll;
        scroll.layer_id = layer_impl->id();
        scroll.scroll_delta = scroll_delta;
        scroll_info->scrolls.push_back(scroll);
        layer_impl->SetSentScrollDelta(scroll_delta);
      }

      for (size_t i = 0; i < layer_impl->children().size(); ++i)
        CollectScrollDeltas(scroll_info, layer_impl->children()[i]);
    }

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

参数layer_impl指向的是一个LayerImpl对象。这个LayerImpl对象描述的是要从中收集未同步到CC Layer Tree中去的滑动量的Layer。从前面的调用过程可以知道,这个LayerImpl对象描述的是CC Active Layer Tree的Root Layer。通过前面分析滑动手势应用在CC Active Layer Tree的过程可以知道,调用这个LayerImpl对象的成员函数ScrollDelta即可以获得它未同步到CC Layer Tree中去的滑动量。这个滑动量会记录在参数scroll_info指向的一个ScrollAndScaleSet对象中。

同时,前面获得的未同步到CC Layer Tree中去的滑动量也会通过调用LayerImpl类的成员函数SetSentScrollDelta设置为参数layer_impl所描述的Layer的Sent Scroll Delta,用来表示当前它已经同步到CC Layer Tree的缩放因子。

函数CollectScrollDeltas收集完成参数layer_impl描述的Layer未同步到CC Layer Tree中去的滑动量之后,接下来会递归调用自己继续收集参数layer_impl描述的Layer的子Layer未同步到CC Layer Tree中去的滑动量,直到CC Active Layer Tree的所有Layer都收集完成为止。

这一步执行完成之的,回到前面ThreadProxy类的成员函数ScheduledActionSendBeginMainFrame中,这时候它就收集到了CC Active Layer Tree未同步到CC Layer Tree中去的滑动量和缩放因子。这些滑动量和缩放因子将会传递给ThreadProxy类的成员函数BeginMainFrame进行处理,如下所示:

void ThreadProxy::BeginMainFrame(
        scoped_ptr<BeginMainFrameAndCommitState> begin_main_frame_state) {
      ......

      layer_tree_host()->ApplyScrollAndScale(*begin_main_frame_state->scroll_info);
      ......

      layer_tree_host()->AnimateLayers(  
          begin_main_frame_state->monotonic_frame_begin_time);  

      ......  

      layer_tree_host()->Layout();  

      ......  

      scoped_ptr<resourceupdatequeue> queue =  
          make_scoped_ptr(new ResourceUpdateQueue);  
      bool updated = layer_tree_host()->UpdateLayers(queue.get());  

      ......  

      {  
        ......  

        CompletionEvent completion;  
        Proxy::ImplThreadTaskRunner()->PostTask(  
            FROM_HERE,  
            base::Bind(&ThreadProxy::StartCommitOnImplThread,  
                       impl_thread_weak_ptr_,  
                       &completion,  
                       queue.release()));  
        completion.Wait();  

        ......  
      }  

      ......  
    }

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

在前面Chromium网页Layer Tree绘制过程分析一文中,我们已经分析过了ThreadProxy类的成员函数BeginMainFrame的实现,它主要就是对网页的CC Layer Tree进行重新绘制。在绘制之前,会先对CC Layer Tree的动画和布局进行计算,以及在绘制之后,在Compositor线程中调用ThreadProxy类的成员函数StartCommitOnImplThread将绘制好的CC Layer Tree同步到CC Pending Layer Tree中去。

ThreadProxy类的成员函数BeginMainFrame在计算CC Layer Tree的动画之前,会先将前面从CC Active Layer Tree中收集到的滑动量和缩放因子应用在CC Layer Tree中。这是通过调用LayerTreeHost类的成员函数ApplyScrollAndScale实现的,如下所示:

void LayerTreeHost::ApplyScrollAndScale(const ScrollAndScaleSet& info) {
      ......

      gfx::Vector2d inner_viewport_scroll_delta;
      gfx::Vector2d outer_viewport_scroll_delta;

      for (size_t i = 0; i < info.scrolls.size(); ++i) {
        Layer* layer =
            LayerTreeHostCommon::FindLayerInSubtree(root_layer_.get(),
                                                    info.scrolls[i].layer_id);
        if (!layer)
          continue;
        if (layer == outer_viewport_scroll_layer_.get()) {
          outer_viewport_scroll_delta += info.scrolls[i].scroll_delta;
        } else if (layer == inner_viewport_scroll_layer_.get()) {
          inner_viewport_scroll_delta += info.scrolls[i].scroll_delta;
        } else {
          layer->SetScrollOffsetFromImplSide(layer->scroll_offset() +
                                             info.scrolls[i].scroll_delta);
        }
      }

      if (!inner_viewport_scroll_delta.IsZero() ||
          !outer_viewport_scroll_delta.IsZero() || info.page_scale_delta != 1.f) {
        ......

        inner_viewport_scroll_layer_->SetScrollOffsetFromImplSide(
            inner_viewport_scroll_layer_->scroll_offset() +
            inner_viewport_scroll_delta);
        if (outer_viewport_scroll_layer_) {
          outer_viewport_scroll_layer_->SetScrollOffsetFromImplSide(
              outer_viewport_scroll_layer_->scroll_offset() +
              outer_viewport_scroll_delta);
        }
        ApplyPageScaleDeltaFromImplSide(info.page_scale_delta);

        ......
      }
    }

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

从前面的分析可以知道,参数info描述的ScrollAndScaleSet对象记录了CC Active Layer Tree中未将滑动量同步到CC Layer Tree中去的每一个Layer。根据这些Layer的ID可以在CC Layer Tree中找到对应的Layer。找到对应的Layer之后,就可以调用它们的成员函数SetScrollOffsetFromImplSide给它们设置新的滑动量,从而使得CC Layer Tree和CC Active Layer保持一致。

从前面的分析还可以知道,参数info描述的ScrollAndScaleSet对象还记录了CC Active Layer Tree未同步到CC Layer Tree中去的缩放因子。这个缩放因子将会通过调用LayerTreeHost类的成员函数ApplyPageScaleDeltaFromImplSide应用在CC Layer Tree中。

接下来,我们就继续分析Layer类的成员函数SetScrollOffsetFromImplSide和LayerTreeHost类的成员函数ApplyPageScaleDeltaFromImplSide的实现,以便了解将CC Active Layer Tree的滑动量和缩放因子同步到CC Layer Tree的过程。

Layer类的成员函数SetScrollOffsetFromImplSide的实现如下所示:

void Layer::SetScrollOffsetFromImplSide(const gfx::Vector2d& scroll_offset) {
      ......

      scroll_offset_ = scroll_offset;
      SetNeedsPushProperties();

      ......
    }

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

参数scroll_offset描述的就是从CC Active Layer Tree中获得的未同步到CC Layer Tree中去的滑动量。这个滑动量会记录在当前正在处理的Layer的成员变量scroll_offset_中。接下来在绘制当前正在处理的Layer时,就可以应用这个滑动量。

Layer类的成员函数SetScrollOffsetFromImplSide最后还会调用另外一个成员函数SetNeedsPushProperties将当前正在处理的Layer的属性标记为发生了变化,以便接下来将CC Layer Tree同步为CC Pending Layer Tree时,可以将它的属性同步到CC Pending Layer Tree中对应的Layer去。

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

void LayerTreeHost::ApplyPageScaleDeltaFromImplSide(float page_scale_delta) {
      DCHECK(CommitRequested());
      page_scale_factor_ *= page_scale_delta;
    }

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

LayerTreeHost类的成员变量page_scale_factor_描述的是CC Layer Tree当前的缩放因子。参数page_scale_delta描述的是CC Active Layer Tree未同步到CC Layer Tree中去的缩放因子。将这两者相乘,就可以得到CC Layer Tree新的缩放因子。这个新的缩放因子在接下来绘制CC Layer Tree时,就会得到应用。

这样,我们就分析完成了CC Active Layer Tree在应用了滑动和捏合手势操作之后,将自己的滑动量和缩放因子同步到CC Layer Tree的过程。这个过程执行完成之后,就可以保持CC Active Layer Tree和CC Layer Tree的一致性。

至此,我们也分析完成了Chromium处理网页滑动和捏合手势的过程。这个处理过程的巧妙之处就是,滑动和捏合手势先直接应用在网页的CC Active Layer Tree上,然后再同步到CC Layer Tree中去。其他的输入事件的处理流程一般是先交给WebKit处理。WebKit处理完成之后,再请求Main线程重新绘制网页的CC Layer Tree。CC Layer Tree绘制完成之后,再同步到网页的CC Pending Layer Tree中去。最后CC Pending Layer Tree再激活为CC Active Layer Tree。CC Active Layer Tree被激活之后,就会被渲染。这时候用户的输入事件就会在UI上得到体现。

试想,如果网页的滑动和捏合手势与普通的输入事件走相同的处理流程,那么对它们的响应就是慢很多。这样用户就会觉得浏览器不够流畅。滑动和捏合手势的特点不会改变网页的内容,而仅仅是改变网页的Viewport和缩放因子,这完全可以在网页的CC Active Layer Tree上实现。因此,Chromium就会对它们进行特殊处理,以提高用户浏览网页时的流畅度。

在接下来的一篇文章中,我们就分析Chromium处理一般输入事件的过程,敬请关注!更多的信息也可以关注老罗的新浪微博: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次阅读  |  详细内容 »
 目录