Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

Android WebView执行GPU命令的过程分析

$
0
0

       Android WebView使用的Chromium引擎,虽然没有自己的GPU进程或者线程,但是却可以执行GPU命令。原来,Android WebView会给它提供一个In-Process Command Buffer GL接口。通过这个接口,Chromium引擎就可以将GPU命令提交给App的Render Thread执行。本文接下来就详细分析Android WebView执行GPU命令的过程。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

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

       从前面Chromium硬件加速渲染的OpenGL命令执行过程分析这篇文章可以知道,Chromium渲染引擎在有自己的GPU进程或者线程的情况下,是通过一个Command Buffer GL接口执行GPU命令的。这个Command Buffer GL接口通过一个GLES2Implementation类描述。Android WebView给Chromium引擎提供的In-Process Command Buffer GL接口,同样是通过GLES2Implementation类描述的。这样,Chromium渲染引擎就不用关心它发出的GPU命令是如何执行的。

       在Chromium渲染引擎中,需要执行GPU命令的是Render端和Browser端。Render端执行GPU命令是为渲染网页的UI,而Browser端执行GPU命令是为了将Render端渲染的网页UI合成显示在屏幕上。对Android WebView来说,它的Render端会将网页抽象成一个CC Layer Tree,然后使用一个Synchronous Compositor将它渲染在一个Synchronous Compositor Output Surface上,如图1所示:


图1 Android WebView的Render端渲染网页UI的示意图

       Android WebView的Browser端同样会将自己要合成的UI抽象为一个CC Layer Tree,然后使用一个Hardware Renderer将它渲染在一个Parent Output Surface上,如图2所示:


图2 Android WebView的Browser端合成网页UI的示意图

       Browser端的CC Layer Tree比较特别,它只有两个节点。一个是根节点,另一个是根节点的子节点,称为Delegated Render Layer,它要渲染的内容来自于Render端的渲染输出。从前面Chromium硬件加速渲染的UI合成过程分析一文可以知道,Render端的渲染输出是一系列的Render Pass。每一个Render Pass都包含了若干个纹理。这些纹理是在Render端光栅化网页时产生的。Browser端的Hardware Renderer所要做的事情就是将这些纹理渲染在屏幕上。这个过程也就是Browser端合成网页UI的过程。

       不管是Render端,还是Browser端,当它们执行GPU命令的时候,都是通过GLES2Implementation类描述的一个In-Process Command Buffer GL接口写入到一个Command Buffer中去的。只不过在Android WebView的情况下,这些GPU命令会被一个DeferredGpuCommandService服务提交给App的Render Thread执行,如图3所示:


图3 Android WebView执行GPU命令的过程

       Render端和Browser端将要执行的GPU命令写入到Command Buffer之后,就会请求DeferredGpuCommandService服务在App的Render Thread中调度执行一个Task。这个Task绑定了InProcessCommandBuffer类的成员函数FlushOnGpuThread。InProcessCommandBuffer类的成员函数FlushOnGpuThread又是通过一个Gpu Scheduler按照上下文来执行请求的GPU命令,并且这些GPU命令在执行之前,先要经过一个GLES2 Decoder进行解码。这个过程可以参考前面Chromium硬件加速渲染的OpenGL命令执行过程分析一文。

       对于Browser端来说,它本来就是在App的Render Thread中请求执行GPU命令的。这时候DeferredGpuCommandService服务会直接在当前线程中调用InProcessCommandBuffer类的成员函数FlushOnGpuThread,以便执行请求的GPU命令。

       对于Render端来说,它在两种情景下需要请求执行GPU命令。第一种情景是光栅化网页的UI,第二种情景是绘制网页的UI。这两种情景都是发生在Render端的Compositor线程中。关于Render端的Compositor线程,以及网页UI的光栅化和绘制操作,可以参考前面Chromium网页渲染机制简要介绍和学习计划这个系列的文章。

       上述两种情景都会将要执行的GPU操作抽象为一个DrawGLFunctor对象。对于第一个情景,DrawGLFunctor对象会通过App的UI线程直接提交给App的Render Thread。App的Render Thread再通知该DrawGLFunctor对象执行GPU操作。

       对于第二个情景,DrawGLFunctor对象会被App的UI线程封装为一个DrawFunctorOp操作,并且写入到App UI的Display List中。 接下来App的UI线程会将Display List同步给App的Render Thread。随后这个Display List就会被App的Render Thread通过一个OpenGL Renderer进行Replay,这时候Display List中包含的DrawFunctorOp操作就会被执行。在执行的过程中,与它关联的DrawGLFunctor对象获得通知。DrawGLFunctor对象获得通知以后就会执行之前Render端请求的GPU操作了。

       DrawGLFunctor对象在执行GPU操作的时候,会调用到一个DrawGL函数。这个DrawGL函数是Android WebView在启动Chromium渲染引擎时注册的。它在执行的过程中,就会通过Browser端的Hardware Renderer通知DeferredGpuCommandService服务执行此前请求调度的Task。这时候InProcessCommandBuffer类的成员函数FlushOnGpuThread就会被调用,这时候Render端之前请求的GPU命令就会被执行。

       接下来,我们就结合源码,分析Chromium渲染引擎的Render端和Browser端创建In-Process Command Buffer GL接口的过程,以及以Render端使用GPU光栅化网页的过程为例,分析Chromium渲染引擎通过In-Process Command Buffer GL接口执行GPU命令的过程。不过,在分析这些过程之前,我们首先分析Android WebView向Chromium渲染引擎注册DrawGL函数的过程。这个过程发生在Android WebView启动Chromium渲染引擎的Browser端的过程中。

       从前面Android WebView启动Chromium渲染引擎的过程分析一文可以知道,Android WebView在启动Chromium渲染引擎的Browser端的过程中,会调用到WebViewChromiumFactoryProvider类的成员函数startChromiumLocked,如下所示:

public class WebViewChromiumFactoryProvider implements WebViewFactoryProvider {
    ......

    private void startChromiumLocked() {
        ......

        initPlatSupportLibrary();
        AwBrowserProcess.start(ActivityThread.currentApplication());
    
        ......
    }

    ......
}
       这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/WebViewChromiumFactoryProvider.java中。

       在调用AwBrowserProcess类的静态成员函数start动Chromium渲染引擎的Browser端之前,WebViewChromiumFactoryProvider类的成员函数startChromiumLocked先会调用成员函数initPlatSupportLibrary向Chromium渲染引擎注册一个DrawGL函数,如下所示:

public class WebViewChromiumFactoryProvider implements WebViewFactoryProvider {
    ......

    private void initPlatSupportLibrary() {
        DrawGLFunctor.setChromiumAwDrawGLFunction(AwContents.getAwDrawGLFunction());
        ......
    }

    ......
}

       这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/WebViewChromiumFactoryProvider.java中。

       WebViewChromiumFactoryProvider类的成员函数startChromiumLocked首先会调用AwContents类的静态成员函数getAwDrawGLFunction获得要注册的DrawGL函数,然后再调用DrawGLFunctor类的静态成员函数setChromiumAwDrawGLFunction将它注册到Chromium渲染引擎中。

       接下来,我们首先分析AwContents类的静态成员函数getAwDrawGLFunction获取要注册的DrawGL函数的过程,然后再分析DrawGLFunctor类的静态成员函数setChromiumAwDrawGLFunction注册DrawGL函数到Chromium渲染引擎的过程。

       AwContents类的静态成员函数getAwDrawGLFunction的实现如下所示:

public class AwContents {
    ......

    public static long getAwDrawGLFunction() {
        return nativeGetAwDrawGLFunction();
    }

    ......
}
       这个函数定义在文件external/chromium_org/android_webview/java/src/org/chromium/android_webview/AwContents.java中。

       AwContents类的静态成员函数getAwDrawGLFunction调用另外一个静态成员函数AwContents类的静态成员函数获取要注册的的DrawGL函数的过程。

       AwContents类的静态成员函数AwContents类的静态成员函数是一个JNI方法,它由C++层的函数Java_com_android_org_chromium_android_1webview_AwContents_nativeGetAwDrawGLFunction实现,如下所示:

__attribute__((visibility("default")))
jlong
    Java_com_android_org_chromium_android_1webview_AwContents_nativeGetAwDrawGLFunction(JNIEnv*
    env, jclass jcaller) {
  return GetAwDrawGLFunction(env, jcaller);
}
       这个函数定义在文件out/target/product/generic/obj/GYP/shared_intermediates/android_webview/jni/AwContents_jni.h中。

       函数Java_com_android_org_chromium_android_1webview_AwContents_nativeGetAwDrawGLFunction又通过调用另外一个函数GetAwDrawGLFunction获取要注册的DrawGL函数,如下所示:

static jlong GetAwDrawGLFunction(JNIEnv* env, jclass) {
  return reinterpret_cast<intptr_t>(&DrawGLFunction);
}
      这个函数定义在文件external/chromium_org/android_webview/native/aw_contents.cc中。

      函数GetAwDrawGLFunction返回的是定义在同文件中的函数DrawGLFunction。这个函数的地址将会返回到前面分析的WebViewChromiumFactoryProvider类的成员函数initPlatSupportLibrary向Chromium,后者再调用DrawGLFunctor类的静态成员函数setChromiumAwDrawGLFunction将它注册到Chromium渲染引擎中,如下所示:

class DrawGLFunctor {
    ......

    public static void setChromiumAwDrawGLFunction(long functionPointer) {
        nativeSetChromiumAwDrawGLFunction(functionPointer);
    }

    ......
}
       这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/DrawGLFunctor.java中。

       DrawGLFunctor类的静态成员函数setChromiumAwDrawGLFunction调用另外一个静态成员函数nativeSetChromiumAwDrawGLFunction将前面获得的函数DrawGLFunction注册在Chromium渲染引擎中。

       DrawGLFunctor类的静态成员函数nativeSetChromiumAwDrawGLFunction是一个JNI方法,它由C++层的函数SetChromiumAwDrawGLFunction实现,如下所示:

AwDrawGLFunction* g_aw_drawgl_function = NULL;

......

void SetChromiumAwDrawGLFunction(JNIEnv*, jclass, jlong draw_function) {
  g_aw_drawgl_function = reinterpret_cast<AwDrawGLFunction*>(draw_function);
}
      这个函数定义在文件frameworks/webview/chromium/plat_support/draw_gl_functor.cpp中。

      函数SetChromiumAwDrawGLFunction将参数draw_function描述的函数DrawGLFunction的地址保存在全局变量g_aw_drawgl_function中。这样,Android WebView就在启动Chromium渲染引擎的Browser端的过程中,向Chromium渲染引擎注册了一个DrawGL函数。

      接下来,我们分析Chromium渲染引擎为Render端创建In-Process Command Buffer GL接口的过程。Chromium渲染引擎为Render端创建的In-Process Command Buffer GL接口是封装在绘图表面(Output Surface)里面的。在Chromium中,每一个网页都关联一个Output Surface。在分析Render端的In-Process Command Buffer GL接口的创建之前,我们首先分析网页的Output Surface的创建过程。

      从前面Chromium网页绘图表面(Output Surface)创建过程分析Chromium的GPU进程启动过程分析这两篇文章可以知道,Render端在加载了网页之后,会为网页创建一个绘图表面,即一个Output Surface,最终是通过调用RenderWidget类的成员函数CreateOutputSurface进行创建的,如下所示:

scoped_ptr<cc::OutputSurface> RenderWidget::CreateOutputSurface(bool fallback) {
  ......

#if defined(OS_ANDROID)
  if (SynchronousCompositorFactory* factory =
      SynchronousCompositorFactory::GetInstance()) {
    return factory->CreateOutputSurface(routing_id());
  }
#endif

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

       在Android平台上,RenderWidget类的成员函数CreateOutputSurface首先会调用SynchronousCompositorFactory类的静态成员函数GetInstance检查当前进程是否存在一个用来创建Output Surface的工厂对象。如果存在,那么就会调用它的成员函数CreateOutputSurface为当前加载的网页创建一个Output Surface。

       SynchronousCompositorFactory类的静态成员函数GetInstance的实现如下所示:

SynchronousCompositorFactory* g_instance = NULL;

......

void SynchronousCompositorFactory::SetInstance(
    SynchronousCompositorFactory* instance) {
  ......

  g_instance = instance;
}

SynchronousCompositorFactory* SynchronousCompositorFactory::GetInstance() {
  return g_instance;
}

       这两个函数定义在文件external/chromium_org/content/renderer/android/synchronous_compositor_factory.cc。

       SynchronousCompositorFactory类的静态成员函数GetInstance返回的是全局变量g_instance指向的一个SynchronousCompositorFactoryImpl对象。这个SynchronousCompositorFactoryImpl对象是通过SynchronousCompositorFactory类的静态成员数SetInstance设置给全局变量g_instance的。

       这个SynchronousCompositorFactoryImpl对象是什么时候设置给全局变量g_instance的呢?回忆前面Android WebView启动Chromium渲染引擎的过程分析一文,Android WebView在启动Chromium渲染引擎的过程,会创建一个DeferredGpuCommandService服务,并且将这个DeferredGpuCommandService服务保存在一个SynchronousCompositorFactoryImpl对象的成员变量service_中。这个SynchronousCompositorFactoryImpl对象在创建的过程中,就会通过调用SynchronousCompositorFactory类的静态成员数SetInstance将自己保存在上述全局变量g_instance中,如下所示:

SynchronousCompositorFactoryImpl::SynchronousCompositorFactoryImpl()
    : record_full_layer_(true),
      num_hardware_compositors_(0) {
  SynchronousCompositorFactory::SetInstance(this);
}
      这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_factory_impl.cc中。

      回到前面分析的RenderWidget类的成员函数CreateOutputSurface中,这时候它调用SynchronousCompositorFactory类的静态成员函数GetInstance就会获得一个SynchronousCompositorFactoryImpl对象。有了这个SynchronousCompositorFactoryImpl对象之后,RenderWidget类的成员函数CreateOutputSurface就会调用它的成员函数CreateOutputSurface为当前正在加载的网页创建一个Output Surface,如下所示:

scoped_ptr<cc::OutputSurface>
SynchronousCompositorFactoryImpl::CreateOutputSurface(int routing_id) {
  scoped_ptr<SynchronousCompositorOutputSurface> output_surface(
      new SynchronousCompositorOutputSurface(routing_id));
  return output_surface.PassAs<cc::OutputSurface>();
}
      这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_factory_impl.cc中。

      SynchronousCompositorFactoryImpl类的成员函数CreateOutputSurface创建一个类型为Synchronous Compositor的Output Surface返回给调用者。从前面Chromium网页绘图表面(Output Surface)创建过程分析一文可以知道,这个Synchronous Compositor Output Surface将会返回给ThreadProxy类的成员函数CreateAndInitializeOutputSurface,如下所示:

void ThreadProxy::CreateAndInitializeOutputSurface() {  
  ......    
  
  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又会将这个Synchronous Compositor Output Surface传递给Render端的Compositor线程,让后者对它进行初始化。这个初始化操作是通过在Render端的Compositor线程中调用ThreadProxy类的成员函数InitializeOutputSurfaceOnImplThread实现的。

       从前面Chromium网页绘图表面(Output Surface)创建过程分析一文可以知道,ThreadProxy类的成员函数InitializeOutputSurfaceOnImplThread在初始化Synchronous Compositor Output Surface的过程中,会调用它的成员函数BindToClient,表示它已经被设置给一个网页使用。

       SynchronousCompositorOutputSurface类的成员函数BindToClient的实现如下所示:

bool SynchronousCompositorOutputSurface::BindToClient(
    cc::OutputSurfaceClient* surface_client) {
  ......

  SynchronousCompositorOutputSurfaceDelegate* delegate = GetDelegate();
  if (delegate)
    delegate->DidBindOutputSurface(this);

  return true;
}
       这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_output_surface.cc中。

       SynchronousCompositorOutputSurface类的成员函数BindToClient会调用另外一个成员函数GetDelegate获得一个Delegate对象,如下所示:

SynchronousCompositorOutputSurfaceDelegate*
SynchronousCompositorOutputSurface::GetDelegate() {
  return SynchronousCompositorImpl::FromRoutingID(routing_id_);
}
       这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_output_surface.cc中。

       SynchronousCompositorOutputSurface类的成员函数GetDelegate通过调用SynchronousCompositorImpl类的静态成员函数FromRoutingID获得一个Delegate对象,如下所示:

SynchronousCompositorImpl* SynchronousCompositorImpl::FromRoutingID(
    int routing_id) {
  return FromID(GetInProcessRendererId(), routing_id);
}
       这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_impl.cc中。

       SynchronousCompositorImpl类的静态成员函数FromRoutingID首先会调用静态成员函数GetInProcessRendererId获得当前正在处理的Render端的ID。有了这个ID之后,连同参数routing_id描述的网页ID,传递给SynchronousCompositorImpl类的另外一个静态成员函数FromID,如下所示:

SynchronousCompositorImpl* SynchronousCompositorImpl::FromID(int process_id,
                                                             int routing_id) {
  ......
  RenderViewHost* rvh = RenderViewHost::FromID(process_id, routing_id);
  ......
  WebContents* contents = WebContents::FromRenderViewHost(rvh);
  ......
  return FromWebContents(contents);
}
       这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_impl.cc中。

       SynchronousCompositorImpl类的静态成员函数FromID首先通过调用RenderViewHost类的静态成员函数FromID获得与参数process_id和routing_id对应的一个RenderViewHost对象。这个RenderViewHost对象用来在Browser端描述的一个网页。这个网页就是当前正在Android WebView中加载的网页。

       获得了与当前正在加载的网页对应的RenderViewHost对象之后,就可以调用WebContents类的静态成员函数FromRenderViewHost获得一个WebContents对象。在Chromium中,每一个网页在Content层又都是通过一个WebContents对象描述的。这个WebContents对象的创建过程可以参考前面Android WebView启动Chromium渲染引擎的过程分析一文。

       获得了用来描述当前正在加载的网页的WebContents对象之后,SynchronousCompositorImpl类的静态成员函数FromID就可以调用另外一个静态成员函数FromWebContents获得一个SynchronousCompositorImpl对象。这个SynchronousCompositorImpl对象的创建过程可以参考前面Android WebView启动Chromium渲染引擎的过程分析一文,它是负责用来渲染在Android WebView中加载的网页的UI的。

       SynchronousCompositorImpl类的静态成员函数FromID最后会将获得的SynchronousCompositorImpl对象返回给调用者,也就是前面分析的SynchronousCompositorOutputSurface类的成员函数BindToClient。SynchronousCompositorOutputSurface类的成员函数BindToClient接下来会调用这个SynchronousCompositorImpl对象的成员函数DidBindOutputSurface,表示它现在已经与一个Synchronous Compositor Output Surface建立了绑定关系,这样以后它就可以将网页的UI渲染在这个Synchronous Compositor Output Surface之上。

       SynchronousCompositorImpl类的成员函数DidBindOutputSurface的实现如下所示:

void SynchronousCompositorImpl::DidBindOutputSurface(
      SynchronousCompositorOutputSurface* output_surface) {
  ......
  output_surface_ = output_surface;
  if (compositor_client_)
    compositor_client_->DidInitializeCompositor(this);
}
       这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_impl.cc中。

       SynchronousCompositorImpl类的成员函数DidBindOutputSurface首先将参数output_surface描述的Synchronous Compositor Output Surface保存在成员变量output_surface_中。

       从前面Android WebView启动Chromium渲染引擎的过程分析一文可以知道,SynchronousCompositorImpl类的成员变量compositor_client_指向的是一个BrowserViewRenderer对象。SynchronousCompositorImpl类的成员函数DidBindOutputSurface最后会调用这个BrowserViewRenderer对象的成员函数DidInitializeCompositor,表示Chromium渲染引擎已经为它创建了一个用来渲染网页UI的Synchronous Compositor,如下所示:

void BrowserViewRenderer::DidInitializeCompositor(
    content::SynchronousCompositor* compositor) {
  ......

  compositor_ = compositor;
}
       这个函数定义在文件external/chromium_org/android_webview/browser/browser_view_renderer.cc中。

       BrowserViewRenderer类的成员函数DidInitializeCompositor会将参数compositor指向的一个SynchronousCompositorImpl对象保存在成员变量compositor_中。

       这一步执行完成之后,Chromium渲染引擎就为在Render端加载的网页创建了一个Synchronous Compositor Output Surface,并且会将这个Synchronous Compositor Output Surface设置给一个Synchronous Compositor。这个Synchronous Compositor又会设置给一个BrowserViewRenderer对象。这个BrowserViewRenderer对象负责绘制Android WebView的UI。

       接下来我们就开始分析Chromium渲染引擎为Render端创建In-Process Command Buffer GL接口的过程。这个In-Process Command Buffer GL接口是在Android WebView第一次执行硬件加速渲染之前创建的。从前面Android WebView启动Chromium渲染引擎的过程分析一文可以知道,Android WebView每次被绘制时,它在Chromium的android_webview模块中对应的AwContents对象的成员函数OnDraw都会被调用,如下所示:

bool AwContents::OnDraw(JNIEnv* env,
                        jobject obj,
                        jobject canvas,
                        jboolean is_hardware_accelerated,
                        jint scroll_x,
                        jint scroll_y,
                        jint visible_left,
                        jint visible_top,
                        jint visible_right,
                        jint visible_bottom) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  if (is_hardware_accelerated)
    InitializeHardwareDrawIfNeeded();
  return browser_view_renderer_.OnDraw(
      canvas,
      is_hardware_accelerated,
      gfx::Vector2d(scroll_x, scroll_y),
      gfx::Rect(visible_left,
                visible_top,
                visible_right - visible_left,
                visible_bottom - visible_top));
}
      这个函数定义在文件external/chromium_org/android_webview/native/aw_contents.cc。

      如果执行的是硬件加速渲染,那么AwContents类的成员函数OnDraw首先会调用另外一个成员函数InitializeHardwareDrawIfNeeded检查当前是否已经创建了一个DeferredGpuCommandService服务。如果还没有创建,那么就会进行创建。这个DeferredGpuCommandService服务的创建过程可以参考前面Android WebView启动Chromium渲染引擎的过程分析一文。

      AwContents类的成员函数OnDraw接下来会调用成员变量browser_view_renderer_描述的一个BrowserViewRenderer对象的成员函数OnDraw执行硬件加速渲染,如下所示:

bool BrowserViewRenderer::OnDraw(jobject java_canvas,
                                 bool is_hardware_canvas,
                                 const gfx::Vector2d& scroll,
                                 const gfx::Rect& global_visible_rect) {
  ......

  if (is_hardware_canvas && attached_to_window_)
    return OnDrawHardware(java_canvas);
  // Perform a software draw
  return OnDrawSoftware(java_canvas);
}
       这个函数定义在文件external/chromium_org/android_webview/browser/browser_view_renderer.cc中。

       在执行硬件加速渲染的情况下,BrowserViewRenderer对象的成员函数OnDraw会调用成员函数OnDrawHardware对Android WebView进行绘制,如下所示:

bool BrowserViewRenderer::OnDrawHardware(jobject java_canvas) {
  ......

  if (!hardware_enabled_) {
    hardware_enabled_ = compositor_->InitializeHwDraw();
    ......
  }

  ......

  scoped_ptr<DrawGLInput> draw_gl_input(new DrawGLInput);
  ......

  scoped_ptr<cc::CompositorFrame> frame =
      compositor_->DemandDrawHw(surface_size,
                                gfx::Transform(),
                                viewport,
                                clip,
                                viewport_rect_for_tile_priority,
                                transform_for_tile_priority);
  ......

  shared_renderer_state_->SetDrawGLInput(draw_gl_input.Pass());
  ......
  return client_->RequestDrawGL(java_canvas, false);
}
       这个函数定义在文件external/chromium_org/android_webview/browser/browser_view_renderer.cc中。

       当BrowserViewRenderer类的成员变量hardware_enabled_的值等于false时,表示Android WebView是第一次启用硬件加速渲染。在这种情况下,BrowserViewRenderer类的函数OnDraw首先会初始化一个硬件加速渲染环境,然后再对Android WebView进行绘制。

       从前面的分析可以知道,BrowserViewRenderer类的成员变量compositor_指向的是一个SynchronousCompositorImpl对象。BrowserViewRenderer类的函数OnDraw就是通过调用这个SynchronousCompositorImpl对象的成员函数InitializeHwDraw初始化一个硬件加速渲染环境的。在初始化这个硬件加速渲染环境的过程中,就会创建一个In-Process Command Buffer GL接口。

       接下来,我们就继续分析SynchronousCompositorImpl类的成员函数InitializeHwDraw初始化创建In-Process Command Buffer GL接口的过程。在接下来一篇文章中,我们再分析BrowserViewRenderer类的函数OnDraw绘制Android WebView的过程。

       SynchronousCompositorImpl类的成员函数InitializeHwDraw的实现如下所示:

base::LazyInstance<SynchronousCompositorFactoryImpl>::Leaky g_factory =
    LAZY_INSTANCE_INITIALIZER;

......

bool SynchronousCompositorImpl::InitializeHwDraw() {
  ......

  scoped_refptr<cc::ContextProvider> onscreen_context =
      g_factory.Get().CreateOnscreenContextProviderForCompositorThread();

  bool success = output_surface_->InitializeHwDraw(onscreen_context);

  ......
  return success;
}
       这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_impl.cc中。

       全局变量指向的就是前面提到的用来为网页创建Output Surface的SynchronousCompositorFactoryImpl对象。SynchronousCompositorImpl类的成员函数InitializeHwDraw调用这个SynchronousCompositorFactoryImpl对象的成员函数CreateOnscreenContextProviderForCompositorThread创建一个硬件加速渲染环境。

       创建出来的硬件加速渲染环境是通过一个ContextProviderInProcess对象描述的。这个ContextProviderInProcess对象又会设置给SynchronousCompositorImpl类的成员变量output_surface_描述的一个Synchronous Compositor Output Surface。Synchronous Compositor Output Surface有了硬件加速渲染环境之后,就可以执行GPU命令了。

       接下来,我们首先分析SynchronousCompositorFactoryImpl类的成员函数CreateOnscreenContextProviderForCompositorThread创建硬件加速渲染环境的过程,如下所示:

scoped_refptr<cc::ContextProvider> SynchronousCompositorFactoryImpl::
    CreateOnscreenContextProviderForCompositorThread() {
  ......
  return webkit::gpu::ContextProviderInProcess::Create(
      WrapContext(CreateContext(service_, share_context_.get())),
      "Child-Compositor");
}
       这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_factory_impl.cc中。

       SynchronousCompositorFactoryImpl类的成员函数CreateOnscreenContextProviderForCompositorThread首先调有函数CreateContext创建一个In-Process Command Buffer GL接口,然后再调用另外一个函数WrapContext将这个In-Process Command Buffer GL接口封装在一个WebGraphicsContext3DInProcessCommandBufferImpl对象,最后调用ContextProviderInProcess类的静态成员函数Create将该WebGraphicsContext3DInProcessCommandBufferImpl对象封装在一个ContextProviderInProcess对象中。

       从前面Android WebView启动Chromium渲染引擎的过程分析一文可以知道,SynchronousCompositorFactoryImpl类的成员变量service_指向的就是一个DeferredGpuCommandService服务。函数CreateContext在创建In-Process Command Buffer GL接口的时候,会使用到这个DeferredGpuCommandService服务,如下所示:

scoped_ptr<gpu::GLInProcessContext> CreateContext(
    scoped_refptr<gpu::InProcessCommandBuffer::Service> service,
    gpu::GLInProcessContext* share_context) {
  ......

  scoped_ptr<gpu::GLInProcessContext> context(
      gpu::GLInProcessContext::Create(service,
                                      NULL /* surface */,
                                      false /* is_offscreen */,
                                      gfx::kNullAcceleratedWidget,
                                      gfx::Size(1, 1),
                                      share_context,
                                      false /* share_resources */,
                                      in_process_attribs,
                                      gpu_preference));
  return context.Pass();
}
       这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_factory_impl.cc中。 

       函数调用GLInProcessContext类的静态成员函数Create创建了一个In-Process Command Buffer GL接口,如下所示:

GLInProcessContext* GLInProcessContext::Create(
    scoped_refptr<gpu::InProcessCommandBuffer::Service> service,
    scoped_refptr<gfx::GLSurface> surface,
    bool is_offscreen,
    gfx::AcceleratedWidget window,
    const gfx::Size& size,
    GLInProcessContext* share_context,
    bool use_global_share_group,
    const GLInProcessContextAttribs& attribs,
    gfx::GpuPreference gpu_preference) {
  ......

  scoped_ptr<GLInProcessContextImpl> context(new GLInProcessContextImpl());
  if (!context->Initialize(surface,
                           is_offscreen,
                           use_global_share_group,
                           share_context,
                           window,
                           size,
                           attribs,
                           gpu_preference,
                           service))
    return NULL;

  return context.release();
}
       这个函数定义在文件external/chromium_org/gpu/command_buffer/client/gl_in_process_context.cc中。

       GLInProcessContext类的静态成员函数Create首先创建了一个GLInProcessContextImpl对象,然后调用这个GLInProcessContextImpl对象的成员函数Initialize对其进行初始化。在初始化的过程中,就会创建一个In-Process Command Buffer GL接口,如下所示:

bool GLInProcessContextImpl::Initialize(
    scoped_refptr<gfx::GLSurface> surface,
    bool is_offscreen,
    bool use_global_share_group,
    GLInProcessContext* share_context,
    gfx::AcceleratedWidget window,
    const gfx::Size& size,
    const GLInProcessContextAttribs& attribs,
    gfx::GpuPreference gpu_preference,
    const scoped_refptr<InProcessCommandBuffer::Service>& service) {
  ......

  command_buffer_.reset(new InProcessCommandBuffer(service));
  ......

  if (!command_buffer_->Initialize(surface,
                                   is_offscreen,
                                   window,
                                   size,
                                   attrib_vector,
                                   gpu_preference,
                                   wrapped_callback,
                                   share_command_buffer)) {
    ......
    return false;
  }

  // Create the GLES2 helper, which writes the command buffer protocol.
  gles2_helper_.reset(new gles2::GLES2CmdHelper(command_buffer_.get()));
  ......

  // Create the object exposing the OpenGL API.
  gles2_implementation_.reset(new gles2::GLES2Implementation(
      gles2_helper_.get(),
      share_group,
      transfer_buffer_.get(),
      bind_generates_resources,
      attribs.lose_context_when_out_of_memory > 0,
      command_buffer_.get()));
  ......

  return true;
}
       这个函数定义在文件external/chromium_org/gpu/command_buffer/client/gl_in_process_context.cc中。

       GLInProcessContextImpl类的成员函数Initialize创建In-Process Command Buffer GL接口的过程与前面Chromium硬件加速渲染的OpenGL命令执行过程分析一文提到的Command Buffer GL接口的创建过程是类似的。首先是创建一个Command Buffer,然后再将该Command Buffer封装在一个GLES2CmdHelper对象中。以后通过个GLES2CmdHelper对象就可以将要执行的GPU命令写入到它封装的Command Buffer中去。最后又会使用前面封装得到的GLES2CmdHelper对象创建一个GLES2Implementation对象。这个GLES2Implementation对象就用来描述的一个In-Process Command Buffer GL或者Command Buffer GL接口。

       In-Process Command Buffer GL接口与Command Buffer GL接口的最大区别就在于它们使用了不同的Command Buffer。In-Process Command Buffer GL使用的Command Buffer是一个In-Process Command Buffer,而Command Buffer GL接口使用的Command Buffer是一个Command Buffer Proxy。In-Process Command Buffer会将要执行的GPU命令发送给App的Render Thread处理,而Command Buffer Proxy会将要执行的GPU命令发送给Chromium的GPU进程/线程处理。

       接下来,我们就重点分析In-Process Command Buffer的创建过程,以便后面可以更好地理解它是怎么将要执行的GPU命令发送给App的Render Thread处理的。

       从前面的调用过程可以知道,参数service描述的是一个DeferredGpuCommandService服务。这个DeferredGpuCommandService服务将会用来创建In-Process Command Buffer,如下所示:

InProcessCommandBuffer::InProcessCommandBuffer(
    const scoped_refptr<Service>& service)
    : ......,
      service_(service.get() ? service : GetDefaultService()),
      ...... {
  ......
}
       这个函数定义在文件external/chromium_org/gpu/command_buffer/service/in_process_command_buffer.cc中。

       在创建In-Process Command Buffer的过程中,如果指定了一个Service,那么以后就会通过这个Service执行GPU命令。在我们这个情景中,指定的Service即为一个DeferredGpuCommandService服务。因此,这时候InProcessCommandBuffer类的成员变量service_指向的是一个DeferredGpuCommandService服务。

       如果在创建In-Process Command Buffer的过程中,没有指定一个Service,那么InProcessCommandBuffer的构造函数就会通过调用另外一个成员函数GetDefaultService获得一个默认的Service,用来执行GPU命令。这个默认的Service实际上就是一个自行创建的GPU线程。

       回到GLInProcessContextImpl类的成员函数Initialize中,它创建了一个In-Process Command Buffer之后,接下来还会调用这个InProcessCommandBuffer类的成员函数Initialize对它进行初始化,如下所示:

bool InProcessCommandBuffer::Initialize(
    scoped_refptr<gfx::GLSurface> surface,
    bool is_offscreen,
    gfx::AcceleratedWidget window,
    const gfx::Size& size,
    const std::vector<int32>& attribs,
    gfx::GpuPreference gpu_preference,
    const base::Closure& context_lost_callback,
    InProcessCommandBuffer* share_group) {
  ......

  gpu::Capabilities capabilities;
  InitializeOnGpuThreadParams params(is_offscreen,
                                     window,
                                     size,
                                     attribs,
                                     gpu_preference,
                                     &capabilities,
                                     share_group);

  base::Callback<bool(void)> init_task =
      base::Bind(&InProcessCommandBuffer::InitializeOnGpuThread,
                 base::Unretained(this),
                 params);

  base::WaitableEvent completion(true, false);
  bool result = false;
  QueueTask(
      base::Bind(&RunTaskWithResult<bool>, init_task, &result, &completion));
  completion.Wait();

  ......
  return result;
}
       这个函数定义在文件external/chromium_org/gpu/command_buffer/service/in_process_command_buffer.cc中。

       InProcessCommandBuffer类的成员函数Initialize所做的事情就是通过成员变量service_指向的DeferredGpuCommandService服务请求在App的Render Thread中调用InProcessCommandBuffer类的成员函数InitializeOnGpuThread,以便对当前正在创建的In-Process Command Buffer进行初始化。

       后面分析Render端执行的GPU命令的过程时,我们就会清楚地看到DeferredGpuCommandService服务是如何请求在App的Render Thread执行一个操作的。现在我们主要关注In-Process Command Buffer的初始化过程,也就是InProcessCommandBuffer类的成员函数InitializeOnGpuThread的实现,如下所示:

bool InProcessCommandBuffer::InitializeOnGpuThread(
    const InitializeOnGpuThreadParams& params) {
  ......

  scoped_ptr<CommandBufferService> command_buffer(
      new CommandBufferService(transfer_buffer_manager_.get()));
  command_buffer->SetPutOffsetChangeCallback(base::Bind(
      &InProcessCommandBuffer::PumpCommands, gpu_thread_weak_ptr_));
  ......

  decoder_.reset(gles2::GLES2Decoder::Create(
      params.context_group
          ? params.context_group->decoder_->GetContextGroup()
          : new gles2::ContextGroup(NULL,
                                    NULL,
                                    NULL,
                                    service_->shader_translator_cache(),
                                    NULL,
                                    bind_generates_resource)));

  gpu_scheduler_.reset(
      new GpuScheduler(command_buffer.get(), decoder_.get(), decoder_.get()));
  ......
  command_buffer_ = command_buffer.Pass();

  decoder_->set_engine(gpu_scheduler_.get());

  ......
}
       这个函数定义在文件external/chromium_org/gpu/command_buffer/service/in_process_command_buffer.cc中。

       InProcessCommandBuffer类的成员函数InitializeOnGpuThread首先是创建了一个Command Buffer Service,并且保存在成员变量command_buffer_中。这个Command Buffer Service负责管理Command Buffer的状态,例如第一个等待执行的GPU命令的位置,以及最新写入的GPU命令的位置,等等。

       InProcessCommandBuffer类的成员函数InitializeOnGpuThread创建了一个Command Buffer Service,会调用它的成员函数SetPutOffsetChangeCallback,用来设置一个Put Offset Change Callback,如下所示:

void CommandBufferService::SetPutOffsetChangeCallback(
    const base::Closure& callback) {
  put_offset_change_callback_ = callback;
}
       这个函数定义在文件external/chromium_org/gpu/command_buffer/service/command_buffer_service.cc中。

       这个Callback指定为当前正在创建的In-Process Command Buffer的成员函数PumpCommands。当我们往Command Buffer写入了新的GPU命令时,这个Callback就会被执行,也就是InProcessCommandBuffer类的成员函数PumpCommands会被调用。

       InProcessCommandBuffer类的成员函数PumpCommands在调用的过程中,就会通过一个Gpu Scheduler和一个GLES2 Decoder执行新写入到Command Buffer中的GPU命令。Gpu Scheduler在执行一个GPU命令之前,会将当前的OpenGL上下文切换至该GPU命令所属的OpenGL上下文,这样它就可以同时支持多个OpenGL上下文。GLES2 Decoder负责从Command Buffer中解析出每一个待执行的GPU命令及其携带的参数,这样就可以通过调用对应的OpenGL函数执行它们。关于Gpu Scheduler和一个GLES2 Decoder执行GPU命令的过程,可以参考前面Chromium硬件加速渲染的OpenGL命令执行过程分析一文。

       回到前面分析的InProcessCommandBuffer类的成员函数InitializeOnGpuThread中,上述Gpu Scheduler和GLES2 Decoder也是在InProcessCommandBuffer类的成员函数InitializeOnGpuThread中创建的。创建出来之后,就分别保存在InProcessCommandBuffer类的成员变量gpu_scheduler_和decoder_中。

       这一步执行完成后,回到前面分析的SynchronousCompositorFactoryImpl类的成员函数CreateOnscreenContextProviderForCompositorThread中,这时候它就获得了一个In-Process Command Buffer GL接口。这个In-Process Command Buffer GL接口是封装在一个GLInProcessContextImpl对象中的。这个GLInProcessContextImpl对象接下来又会通过函数WrapContext封装在一个WebGraphicsContext3DInProcessCommandBufferImpl对象中,如下所示:

scoped_ptr<WebGraphicsContext3DInProcessCommandBufferImpl> WrapContext(
    scoped_ptr<gpu::GLInProcessContext> context) {
  ......

  return scoped_ptr<WebGraphicsContext3DInProcessCommandBufferImpl>(
      WebGraphicsContext3DInProcessCommandBufferImpl::WrapContext(
          context.Pass(), GetDefaultAttribs()));
}
       这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_factory_impl.cc中。

       函数WrapContext通过调用WebGraphicsContext3DInProcessCommandBufferImpl类的静态成员函数WrapContext将一个GLInProcessContextImpl对象封装在一个WebGraphicsContext3DInProcessCommandBufferImpl对象中,如下所示:

scoped_ptr<WebGraphicsContext3DInProcessCommandBufferImpl>
WebGraphicsContext3DInProcessCommandBufferImpl::WrapContext(
    scoped_ptr< ::gpu::GLInProcessContext> context,
    const blink::WebGraphicsContext3D::Attributes& attributes) {
  bool lose_context_when_out_of_memory = false;  // Not used.
  bool is_offscreen = true;                      // Not used.
  return make_scoped_ptr(new WebGraphicsContext3DInProcessCommandBufferImpl(
      context.Pass(),
      attributes,
      lose_context_when_out_of_memory,
      is_offscreen,
      gfx::kNullAcceleratedWidget /* window. Not used. */));
}
       这个函数定义在文件external/chromium_org/webkit/common/gpu/webgraphicscontext3d_in_process_command_buffer_impl.cc中。

       WebGraphicsContext3DInProcessCommandBufferImpl类的构造函数会将要封装的GLInProcessContextImpl对象保存在成员变量context_中,如下所示:

WebGraphicsContext3DInProcessCommandBufferImpl::
    WebGraphicsContext3DInProcessCommandBufferImpl(
        scoped_ptr< ::gpu::GLInProcessContext> context,
        const blink::WebGraphicsContext3D::Attributes& attributes,
        bool lose_context_when_out_of_memory,
        bool is_offscreen,
        gfx::AcceleratedWidget window)
    : ......,
      context_(context.Pass()) {
  ......
}
       这个函数定义在文件external/chromium_org/webkit/common/gpu/webgraphicscontext3d_in_process_command_buffer_impl.cc中。

       这一步执行完成后,继续回到前面分析的SynchronousCompositorFactoryImpl类的成员函数CreateOnscreenContextProviderForCompositorThread中,这时候它就获得了一个WebGraphicsContext3DInProcessCommandBufferImpl对象。这个WebGraphicsContext3DInProcessCommandBufferImpl对象通过一个GLInProcessContextImpl对象间接地保存了前面创建的In-Process Command Buffer GL接口

       SynchronousCompositorFactoryImpl类的成员函数CreateOnscreenContextProviderForCompositorThread最后又会调用ContextProviderInProcess类的静态成员函数Create将上述WebGraphicsContext3DInProcessCommandBufferImpl对象封装在一个ContextProviderInProcess对象中,如下所示:

scoped_refptr<ContextProviderInProcess> ContextProviderInProcess::Create(
    scoped_ptr<WebGraphicsContext3DInProcessCommandBufferImpl> context3d,
    const std::string& debug_name) {
  ......
  return new ContextProviderInProcess(context3d.Pass(), debug_name);
}
       这个函数定义在文件external/chromium_org/webkit/common/gpu/context_provider_in_process.cc中。

       ContextProviderInProcess对象会将要封装的WebGraphicsContext3DInProcessCommandBufferImpl对象保存在成员变量context_中,如下所示:

ContextProviderInProcess::ContextProviderInProcess(
    scoped_ptr<WebGraphicsContext3DInProcessCommandBufferImpl> context3d,
    const std::string& debug_name)
    : context3d_(context3d.Pass()),
      ...... {
  ......
}
      这个函数定义在文件external/chromium_org/webkit/common/gpu/context_provider_in_process.cc中。

      这一步执行完成后,SynchronousCompositorImpl类的成员函数InitializeHwDraw,这时候它就是初始化了一个硬件加速渲染环境。这个硬件加速渲染环境就是通过前面创建的ContextProviderInProcess对象描述。这个ContextProviderInProcess对象接来会设置给SynchronousCompositorImpl类的成员变量output_surface_描述的一个Synchronous Compositor Output Surface。这是通过调用SynchronousCompositorOutputSurface类的成员函数InitializeHwDraw实现的,如下所示:

bool SynchronousCompositorOutputSurface::InitializeHwDraw(
    scoped_refptr<cc::ContextProvider> onscreen_context_provider) {
  ......

  return InitializeAndSetContext3d(onscreen_context_provider);
}
       这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_output_surface.cc中。

       SynchronousCompositorOutputSurface类的成员函数InitializeHwDraw会调用另外一个成员函数InitializeAndSetContext3d将参数onscreen_context_provider指向的ContextProviderInProcess对象保存在内部。以后通过这个ContextProviderInProcess对象,就可以获得前面创建的In-Process Command Buffer GL接口了。

       SynchronousCompositorOutputSurface类的成员函数InitializeAndSetContext3d是从父类OutputSurface继承下来的,它的实现如下所示:

bool OutputSurface::InitializeAndSetContext3d(
    scoped_refptr<ContextProvider> context_provider) {
  ......

  bool success = false;
  if (context_provider->BindToCurrentThread()) {
    context_provider_ = context_provider;
    ......
    success = true;
  }

  ......

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

       OutputSurface类的成员函数InitializeAndSetContext3d首先会调用参数context_provider指向的ContextProviderInProcess对象的成员函数BindToCurrentThread将其引用的In-Process Command Buffer GL接口设置为当前线程所使用的GL接口。当前线程即为Render端的Compositor线程。有了这个GL接口之后,Render端的Compositor线程就可以执行GPU操作了。

       成功将参数context_provider指向的ContextProviderInProcess对象引用的In-Process Command Buffer GL接口设置为当前线程所使用的GL接口之后,该ContextProviderInProcess对象就会保存在OutputSurface类的成员变量context_provider_中。

       接下来我们继续分析ContextProviderInProcess类的成员函数BindToCurrentThread为当前线程设置Process Command Buffer GL接口的过程,如下所示:

bool ContextProviderInProcess::BindToCurrentThread() {
  ......

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

  ......
  return true;
}
       这个函数定义在文件external/chromium_org/webkit/common/gpu/context_provider_in_process.cc中。

       从前面的分析可以知道,ContextProviderInProcess类的成员变量context3d_指向的是一个WebGraphicsContext3DInProcessCommandBufferImpl对象。ContextProviderInProcess类的成员函数BindToCurrentThread会调用这个WebGraphicsContext3DInProcessCommandBufferImpl对象的成员函数makeContextCurrent将前面创建的In-Process Command Buffer GL接口设置为当前线程所使用的GL接口,如下所示:

bool WebGraphicsContext3DInProcessCommandBufferImpl::makeContextCurrent() {
  if (!MaybeInitializeGL())
    return false;
  ::gles2::SetGLContext(GetGLInterface());
  return context_ && !isContextLost();
}
       这个函数定义在文件external/chromium_org/webkit/common/gpu/webgraphicscontext3d_in_process_command_buffer_impl.cc中。

       WebGraphicsContext3DInProcessCommandBufferImpl类的成员函数makeContextCurrent首先调用成员函数MaybeInitializeGL检查是否已经为当前线程初始化过GL接口了。如果还没有初始化,那么就会进行初始化,如下所示:

bool WebGraphicsContext3DInProcessCommandBufferImpl::MaybeInitializeGL() {
  if (initialized_)
    return true;
  ......

  real_gl_ = context_->GetImplementation();
  setGLInterface(real_gl_);

  ......

  initialized_ = true;
  return true;
}
       这个函数定义在文件external/chromium_org/webkit/common/gpu/webgraphicscontext3d_in_process_command_buffer_impl.cc中。

       当WebGraphicsContext3DInProcessCommandBufferImpl类的成员变量initialized_的值等于true的时候,就表示当前线程初始化过GL接口了。另一方面,如果当前线程还没有初始化过GL接口,那么WebGraphicsContext3DInProcessCommandBufferImpl类的成员函数MaybeInitializeGL就会进行初始化。

       从前面的分析可以知道,WebGraphicsContext3DInProcessCommandBufferImpl类的成员变量context_指向的是一个GLInProcessContextImpl对象。调用这个GLInProcessContextImpl对象的成员函数可以获得它内部封装一个GLES2Implementation对象。这个GLES2Implementation对象描述的就是一个In-Process Command Buffer GL接口。

       WebGraphicsContext3DInProcessCommandBufferImpl类的成员函数MaybeInitializeGL获得了上述In-Process Command Buffer GL接口之后,会调用另外一个成员函数setGLInterface将其保存起来。

       WebGraphicsContext3DInProcessCommandBufferImpl类的成员函数setGLInterface是从父类WebGraphicsContext3DImpl继承下来的,它的实现如下所示:

class WEBKIT_GPU_EXPORT WebGraphicsContext3DImpl  
    : public NON_EXPORTED_BASE(blink::WebGraphicsContext3D) {  
 public:  
  ......  
  
  ::gpu::gles2::GLES2Interface* GetGLInterface() {  
    return gl_;  
  }  
  
 protected:  
  ......  
  
  void setGLInterface(::gpu::gles2::GLES2Interface* gl) {  
    gl_ = gl;  
  }  
  
  ......  
  
  ::gpu::gles2::GLES2Interface* gl_;  
  ......  
};  
       这个函数定义在文件external/chromium_org/webkit/common/gpu/webgraphicscontext3d_impl.h中。

       WebGraphicsContext3DImpl类的成员函数setGLInterface将参数描述的In-Process Command Buffer GL接口保存在成员变量gl_中。这个In-Process Command Buffer GL接口可以通过调用WebGraphicsContext3DImpl类的另外一个成员函数GetGLInterface获得。

       这一步执行完成后,回到前面分析的WebGraphicsContext3DInProcessCommandBufferImpl类的成员函数makeContextCurrent,它接下来又会调用从父类WebGraphicsContext3DImpl继承下来的成员函数GetGLInterface获得前面所保存的In-Process Command Buffer GL接口,并且将该In-Process Command Buffer GL接口设置为当前线程的GL接口,也就是OpenGL调用接口。这是通过调用函数gles2::SetGLContext实现的,如下所示:

static gpu::ThreadLocalKey g_gl_context_key;   

......
 
gpu::gles2::GLES2Interface* GetGLContext() {  
  return static_cast<gpu::gles2::GLES2Interface*>(  
    gpu::ThreadLocalGetValue(g_gl_context_key));  
}
 
void SetGLContext(gpu::gles2::GLES2Interface* context) {  
  gpu::ThreadLocalSetValue(g_gl_context_key, context);  
}  
       这两个函数定义在文件external/chromium_org/gpu/command_buffer/client/gles2_lib.cc中。

       参数context描述的In-Process Command Buffer GL接口将被保存在全局变量g_gl_context_key所描述的一个线程局部储存中,作为当前线程所使用的OpenGL接口。以后通过调用另外一个函数gles2::GetGLContext就可以获得保存在这个线程局部储存中的In-Process Command Buffer GL接口。

       这一步执行完成后,以后Render端的Compositor线程直接调用OpenGL函数glXXX时,就会通过In-Process Command Buffer GL接口执行指定的GPU的操作。从OpenGL函数glXXX调用到In-Process Command Buffer GL接口的原理可以参考前面Chromium网页GPU光栅化原理分析一文。

       Render端的Compositor线程除了可以通过OpenGL函数glXXX使用In-Process Command Buffer GL接口,还可以通过它为网页创建的Synchronous Compositor Output Surface使用In-Process Command Buffer GL接口。接下来,我们就以Render端的Compositor线程执行GPU光栅化操作为例,说明它执行GPU命令的过程。

       从前面Chromium网页GPU光栅化原理分析一文可以知道,当Render端的Compositor线程是通过一个Skia Canvas对网页UI进行GPU光栅化操作的。这个Skia Canvas又是通过一个类型为SkSurface_Gpu的Skia Surface获得的。这个类型为SkSurface_Gpu的Skia Surface是通过调用DirectRasterBuffer类的成员函数CreateSurface创建的,如下所示:

skia::RefPtr<SkSurface> ResourceProvider::DirectRasterBuffer::CreateSurface() {  
  skia::RefPtr<SkSurface> surface;  
  switch (resource()->type) {  
    case GLTexture: {  
      ......  
      class GrContext* gr_context = resource_provider()->GrContext();  
      if (gr_context) {  
        GrBackendTextureDesc desc;  
        ......
 
        skia::RefPtr<GrTexture> gr_texture =  
            skia::AdoptRef(gr_context->wrapBackendTexture(desc));  
        ......
  
        surface = skia::AdoptRef(SkSurface::NewRenderTargetDirect(  
            gr_texture->asRenderTarget(), text_render_mode));  
      }  
      break;  
    }  
    ......
  }  
  return surface;  
}
       这个函数定义在文件external/chromium_org/cc/resources/resource_provider.cc中。

       DirectRasterBuffer类的成员函数CreateSurface的详细实现可以参考前面Chromium网页GPU光栅化原理分析一文。这里我们所关注的重点是它通过调用ResourceProvider类的成员函数GrContext获得一个In-Process Command Buffer GL接口的过程。这个In-Process Command Buffer GL接口会传递给这里所创建的类型为SkSurface_Gpu的Skia Surface。有了In-Process Command Buffer GL接口之后,类型为SkSurface_Gpu的Skia Surface就可以通过GPU对网页的UI进行光栅化了。

       DirectRasterBuffer类的成员函数CreateSurface首先是调用成员函数resource_provider获得一个ResourceProvider对象。这个ResourceProvider对象负责管理网页在渲染过程中所要使用到的资源。有这个ResourceProvider对象之后,就可以调用它的成员函数GrContext获得一个In-Process Command Buffer GL接口,如下所示:

class GrContext* ResourceProvider::GrContext() const {
  ContextProvider* context_provider = output_surface_->context_provider();
  return context_provider ? context_provider->GrContext() : NULL;
}
      这个函数定义在文件external/chromium_org/cc/resources/resource_provider.cc中。

      ResourceProvider类的成员变量output_surface_指向的就是一个SynchronousCompositorOutputSurface对象。ResourceProvider类的成员函数GrContext会调用这个SynchronousCompositorOutputSurface对象的成员函数context_provider获得一个Context Provider。

      SynchronousCompositorOutputSurface对象的成员函数context_provider是从父类OutputSurface继承下来的,它的实现如下所示:

class CC_EXPORT OutputSurface {
  ......

  scoped_refptr<ContextProvider> context_provider() const {
    return context_provider_.get();
  }

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

       从前面的分析可以知道,此时OutputSurface类的成员变量context_provider_指向的是一个ContextProviderInProcess对象。OutputSurface类的成员函数context_provider将这个ContextProviderInProcess对象返回给调用者。

       回到前面分析的ResourceProvider类的成员函数GrContext中,这时候它就获得了一个ContextProviderInProcess对象。接下来它继续调用这个ContextProviderInProcess对象的成员函数GrContext获得一个GrContextForWebGraphicsContext3D对象,如下所示:

class GrContext* ContextProviderInProcess::GrContext() {
  ......

  if (gr_context_)
    return gr_context_->get();

  gr_context_.reset(
      new webkit::gpu::GrContextForWebGraphicsContext3D(context3d_.get()));
  return gr_context_->get();
}
       这个函数定义在文件external/chromium_org/webkit/common/gpu/context_provider_in_process.cc中。

       ContextProviderInProcess类的成员函数GrContext首先判断成员变量gr_context_是否指向了一个GrContextForWebGraphicsContext3D对象。如果已经指向,那么就会将该GrContextForWebGraphicsContext3D对象返回给调用者。

       另一方面,如果ContextProviderInProcesGrContextForWebGraphicsContext3Ds类的成员变量gr_context_还没有指向一个GrContextForWebGraphicsContext3D对象,那么ContextProviderInProcess类的成员函数GrContext就会先创建该GrContextForWebGraphicsContext3D对象,然后再将它返回给调用者。

       在创建GrContextForWebGraphicsContext3D对象的时候,会使用到ContextProviderInProcess类的成员变量context3d_。从前面的分析可以知道,ContextProviderInProcess类的成员变量context3d_指向的是一个WebGraphicsContext3DInProcessCommandBufferImpl对象。这个WebGraphicsContext3DInProcessCommandBufferImpl对象内部封装了一个In-Process Command Buffer GL接口。

       因此,ContextProviderInProcess类的成员函数GrContext创建出来的GrContextForWebGraphicsContext3D对象也会间接地引用了一个In-Process Command Buffer GL接口。前面创建的类型为SkSurface_Gpu的Skia Surface在光栅化网页UI的时候,就会使用到这个In-Process Command Buffer GL接口,也就是会将要执行的GPU命令写入到一个In-Process Command Buffer中去。

       写入到In-Process Command Buffer中的命令会被周期性地提交给DeferredGpuCommandService服务处理。这是通过调用InProcessCommandBuffer类的成员函数Flush实现的。或者我们也可以主动地调用In-Process Command Buffer GL接口提供的成员函数Flush将写入在In-Process Command Buffer中的命令会被周期性地提交给DeferredGpuCommandService服务。这个主动提交的操作最终也是通过调用InProcessCommandBuffer类的成员函数Flush实现的。这一点可以参考前面Chromium硬件加速渲染的OpenGL命令执行过程分析一文。

       接下来,我们就从InProcessCommandBuffer类的成员函数Flush开始,分析Render端在使用GPU光栅化网页UI的过程中,是如何执行GPU命令的,如下所示:

void InProcessCommandBuffer::Flush(int32 put_offset) {
  ......

  last_put_offset_ = put_offset;
  base::Closure task = base::Bind(&InProcessCommandBuffer::FlushOnGpuThread,
                                  gpu_thread_weak_ptr_,
                                  put_offset);
  QueueTask(task);
}
       这个函数定义在文件external/chromium_org/gpu/command_buffer/service/in_process_command_buffer.cc中。

       参数put_offset表示最新写入的GPU命令在In-Process Command Buffer中的位置。InProcessCommandBuffer类的成员函数Flush首先将这个位置记录在成员变量last_put_offset_中。

       InProcessCommandBuffer类的成员函数Flush接下来创建了一个Task。这个Task绑定了InProcessCommandBuffer类的成员函数FlushOnGpuThread。接下来这个Task会被提交给DeferredGpuCommandService服务处理。这是通过调用InProcessCommandBuffer类的成员函数QueueTask实现的,如下所示:

class GPU_EXPORT InProcessCommandBuffer : public CommandBuffer,
                                          public GpuControl {
 ......

 private:
  ......

  void QueueTask(const base::Closure& task) { service_->ScheduleTask(task); }

  ......
}
      这个函数定义在文件external/chromium_org/gpu/command_buffer/service/in_process_command_buffer.h中。

      从前面的分析可以知道,InProcessCommandBuffer类的成员变量service_描述的就是一个DeferredGpuCommandService服务。InProcessCommandBuffer类的成员函数QueueTask通过调用这个DeferredGpuCommandService服务的成员函数ScheduleTask调度执行参数task描述的Task。

      DeferredGpuCommandService类的成员函数ScheduleTask的实现如下所示:

void DeferredGpuCommandService::ScheduleTask(const base::Closure& task) {
  {
    base::AutoLock lock(tasks_lock_);
    tasks_.push(task);
  }
  if (ScopedAllowGL::IsAllowed()) {
    RunTasks();
  } else {
    RequestProcessGL();
  }
}
       这个函数定义在文件external/chromium_org/android_webview/browser/deferred_gpu_command_service.cc中。

       DeferredGpuCommandService类的成员函数ScheduleTask首先将参数task描述的Task保存在成员变量tasks_描述的一个std::queue中。

       DeferredGpuCommandService类的成员函数ScheduleTask接下来通过调用ScopedAllowGL类的静态成员函数IsAllowed判断当前线程是否允许直接执行GPU命令,也就是当前线程是否是一个GPU线程。如果是的话,那么就会调用成员函数RunTasks执行参数task描述的Task,如下所示:

void DeferredGpuCommandService::RunTasks() {
  bool has_more_tasks;
  {
    base::AutoLock lock(tasks_lock_);
    has_more_tasks = tasks_.size() > 0;
  }

  while (has_more_tasks) {
    base::Closure task;
    {
      base::AutoLock lock(tasks_lock_);
      task = tasks_.front();
      tasks_.pop();
    }
    task.Run();
    {
      base::AutoLock lock(tasks_lock_);
      has_more_tasks = tasks_.size() > 0;
    }
  }
}
       这个函数定义在文件external/chromium_org/android_webview/browser/deferred_gpu_command_service.cc中。

       DeferredGpuCommandService类的成员函数RunTasks会依次执行保存在成员变量tasks_描述的std::queue中的每一个Task。从前面的分析可以知道,这个std::queue保存了一个Task。这个Task绑定了InProcessCommandBuffer类的成员函数FlushOnGpuThread。因此,当该Task被执行的时候,InProcessCommandBuffer类的成员函数FlushOnGpuThread就会被调用。在调用的过程中,就会执行那些新写入到In-Process Command Buffer的GPU命令。

       DeferredGpuCommandService类的成员函数ScheduleTask可以直接调用成员函数RunTasks执行GPU命令的情况发生在Browser端合成网页UI的过程中。这时候Browser端运行在App的Render Thread中,并且它会通过ScopedAllowGL类标记App的Render Thread可以执行GPU命令。这样DeferredGpuCommandService类的成员函数ScheduleTask就可以知道它可以直接执行GPU命令了。

       在我们这个情景中,正在执行的是网页UI的光栅化操作。这个操作是发生在Render端的Compositor线程中的。Render端的Compositor线程不是一个GPU线程,因此这时候DeferredGpuCommandService类的成员函数ScheduleTask就不能直接调用成员函数RunTasks执行GPU命令,而是要通过调用另外一个成员函数RequestProcessGL请求App的Render Thread执行。

       另外一个情景,也就是Render端绘制网页UI的情景,也是发生在Render端的Compositor线程。这时候DeferredGpuCommandService类的成员函数ScheduleTask也需要调用成员函数RequestProcessGL请求App的Render Thread执行绘制网页UI所需要执行的GPU命令。

       接下来,我们就继续分析DeferredGpuCommandService类的成员函数RequestProcessGL,以便了解它请求App的Render Thread执行GPU命令的过程,如下所示:

void DeferredGpuCommandService::RequestProcessGL() {
  SharedRendererState* renderer_state =
      GLViewRendererManager::GetInstance()->GetMostRecentlyDrawn();
  ......
  renderer_state->ClientRequestDrawGL();
}
       这个函数定义在文件external/chromium_org/android_webview/browser/deferred_gpu_command_service.cc中。

       我们在前面Android WebView启动Chromium渲染引擎的过程分析一文中提到,在Chromium渲染引擎中,存在一个GLViewRendererManager单例对象。这个GLViewRendererManager单例对象记录了当前有哪些WebView是采用硬件加速方式绘制的。通过调用这个GLViewRendererManager对象的成员函数GetMostRecentlyDrawn可以获得一个SharedRendererState对象。这个SharedRendererState对象的创建过程可以参考前面Android WebView启动Chromium渲染引擎的过程分析一文,它记录了当前正在绘制的Android WebView的状态。

       获得了与当前正在绘制的Android WebView关联的一个SharedRendererState对象之后,DeferredGpuCommandService类的成员函数RequestProcessGL就可以调用它的成员函数ClientRequestDrawGL请求App的Render Thread执行GPU命令,如下所示:

void SharedRendererState::ClientRequestDrawGL() {
  if (ui_loop_->BelongsToCurrentThread()) {
    ......
    ClientRequestDrawGLOnUIThread();
  } else {
    ......
    base::Closure callback;
    {
      base::AutoLock lock(lock_);
      callback = request_draw_gl_closure_;
    }
    ui_loop_->PostTask(FROM_HERE, callback);
  }
}
       这个函数定义在文件external/chromium_org/android_webview/browser/shared_renderer_state.cc中。

       从前面Android WebView启动Chromium渲染引擎的过程分析一文可以知道,SharedRendererState类的成员变量ui_loop_描述的就是Chromium的Browser端的Native UI Message Loop。通过这个Native UI Message Loop可以判断当前线程是否就是App的UI线程。如果是的话,那么SharedRendererState类的成员函数ClientRequestDrawGL就会直接调用另外一个成员函数ClientRequestDrawGLOnUIThread请求App的Render Thread执行GPU命令。

       如果当前线程不是App的UI线程,那么SharedRendererState类的成员函数ClientRequestDrawGL就会向Chromium的Browser端的Native UI Message Loop发送一个Task。这个Task由SharedRendererState类的成员变量request_draw_gl_closure_描述,它绑定的函数为SharedRendererState类的成员函数ClientRequestDrawGLOnUIThread。这样做的目的是让SharedRendererState类的成员函数ClientRequestDrawGLOnUIThread运行在App的UI线程中,以便可以通过App的UI线程来请求App的Render Thread执行GPU命令。

       SharedRendererState类的成员函数ClientRequestDrawGLOnUIThread的实现如下所示:

void SharedRendererState::ClientRequestDrawGLOnUIThread() {
  ......

  if (!client_on_ui_->RequestDrawGL(NULL, false)) {
    ......
  }
}
       这个函数定义在文件external/chromium_org/android_webview/browser/shared_renderer_state.cc中。

       从前面Android WebView启动Chromium渲染引擎的过程分析一文可以知道,SharedRendererState类的成员变量client_on_ui_指向的是一个Native层的AwContents对象。SharedRendererState类的成员函数ClientRequestDrawGLOnUIThread通过调用这个AwContents对象的成员函数RequestDrawGL请求App的Render Thread执行GPU命令,如下所示:

bool AwContents::RequestDrawGL(jobject canvas, bool wait_for_completion) {
  ......
  JNIEnv* env = AttachCurrentThread();
  ScopedJavaLocalRef<jobject> obj = java_ref_.get(env);
  .......
  return Java_AwContents_requestDrawGL(
      env, obj.obj(), canvas, wait_for_completion);
}
       这个函数定义在文件external/chromium_org/android_webview/native/aw_contents.cc中。

       从前面Android WebView启动Chromium渲染引擎的过程分析一文可以知道,每一个Native层的AwContents对象在Java层都有一个对应的AwContents对象。这个Java层的AwContents对象就保存在Native层的AwContents对象的成员变量java_ref_中。AwContents类的成员函数RequestDrawGL通过JNI方法Java_AwContents_requestDrawGL调用上述的Java层AwContents对象的成员函数requestDrawGL,用来请求App的Render Thread执行GPU命令。

       Java层的AwContents类的成员函数requestDrawGL的实现如下所示:

public class AwContents {
    ......

    @CalledByNative
    private boolean requestDrawGL(Canvas canvas, boolean waitForCompletion) {
        return mNativeGLDelegate.requestDrawGL(canvas, waitForCompletion, mContainerView);
    }

    ......
}
      这个函数定义在文件external/chromium_org/android_webview/java/src/org/chromium/android_webview/AwContents.java中。

      从前面Android WebView启动Chromium渲染引擎的过程分析一文可以知道,AwContents类的成员变量mNativeGLDelegate指向的是一个WebViewNativeGLDelegate对象。AwContents类的成员函数requestDrawGL调用这个WebViewNativeGLDelegate对象的成员函数requestDrawGL请求App的Render Thread执行GPU命令,如下所示:

class WebViewChromium implements WebViewProvider,
          WebViewProvider.ScrollDelegate, WebViewProvider.ViewDelegate {
    ......

    private DrawGLFunctor mGLfunctor;
    ......

    private class WebViewNativeGLDelegate implements AwContents.NativeGLDelegate {
        @Override
        public boolean requestDrawGL(Canvas canvas, boolean waitForCompletion,
                View containerView) {
            if (mGLfunctor == null) {
                mGLfunctor = new DrawGLFunctor(mAwContents.getAwDrawGLViewContext());
            }
            return mGLfunctor.requestDrawGL(
                    (HardwareCanvas) canvas, containerView.getViewRootImpl(), waitForCompletion);
        }

        ......
    }

    ......
}
       这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/WebViewChromium.java中。

       WebViewNativeGLDelegate类的成员函数requestDrawGL首先判断外部类WebViewChromium的成员变量mGLFunctor是否指向了一个DrawGLFunctor对象。如果指向了,那么就会调用它的成员函数requestDrawGL请求App的Render Thread执行GPU命令。

       另一方面,如果WebViewChromium类的成员变量mGLFunctor还没有指向一个DrawGLFunctor对象,那么WebViewNativeGLDelegate类的成员函数requestDrawGL就会创建这个DrawGLFunctor对象。创建过程如下所示:

class DrawGLFunctor {
    ......

    public DrawGLFunctor(long viewContext) {
        mDestroyRunnable = new DestroyRunnable(nativeCreateGLFunctor(viewContext));
        ......
    }

    ......
}
      这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/DrawGLFunctor.java中。

      DrawGLFunctor类的构造函数首先会调用成员函数nativeCreateGLFunctor获得一个Native层的DrawGLFunctor对象,然后再使用这个Native层的DrawGLFunctor对象创建一个DestroyRunnable对象,如下所示:

class DrawGLFunctor {
    ......

    private static final class DestroyRunnable implements Runnable {
        ......
        long mNativeDrawGLFunctor;
        DestroyRunnable(long nativeDrawGLFunctor) {
            mNativeDrawGLFunctor = nativeDrawGLFunctor;
        }

        ......
    }

    ......
}
      这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/DrawGLFunctor.java中。

      DestroyRunnable类的构造函数主要就是将参数nativeDrawGLFunctor描述的Native层DrawGLFunctor对象保存在成员变量mNativeDrawGLFunctor中。

      接下来我们继续分析Native层的DrawGLFunctor对象的创建过程,也就是DrawGLFunctor类的成员函数nativeCreateGLFunctor的实现。wGLFunctor类的成员函数nativeCreateGLFunctor是一个JNI方法,它由C++层的函数CreateGLFunctor实现,如下所示:

jlong CreateGLFunctor(JNIEnv*, jclass, jlong view_context) {
  RaiseFileNumberLimit();
  return reinterpret_cast<jlong>(new DrawGLFunctor(view_context));
}
      这个函数定义在文件frameworks/webview/chromium/plat_support/draw_gl_functor.cpp中。

      从这里可以看到,函数CreateGLFunctor创建的是一个Native层的DrawGLFunctor对象。这个DrawGLFunctor对象是用来描述Chromium渲染引擎请求App的Render Thread执行的GPU操作集合。

      这一步执行完成后,回到前面分析的WebViewNativeGLDelegate类的成员函数requestDrawGL中,接下来我们继续分析它调用外部类WebViewChromium类的成员变量mGLFunctor指向一个Java层的DrawGLFunctor对象的成员函数requestDrawGL请求App的Render Thread执行GPU命令,如下所示:

class DrawGLFunctor {
    ......

    public boolean requestDrawGL(HardwareCanvas canvas, ViewRootImpl viewRootImpl,
            boolean waitForCompletion) {
        ......

        if (canvas == null) {
            viewRootImpl.invokeFunctor(mDestroyRunnable.mNativeDrawGLFunctor, waitForCompletion);
            return true;
        }

        canvas.callDrawGLFunction(mDestroyRunnable.mNativeDrawGLFunctor);
        ......

        return true;
    }

    ......
}

       这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/DrawGLFunctor.java中。

       从前面的调用过程可以知道,参数canvas的值为null,另外一个参数waitForCompletion的值等于false。

       当参数canvas的值为null的时候,表示Chromium渲染引擎直接请求App的Render Thread执行GPU命令。这种情况一般就是发生在Render端光栅化网页UI的过程中。

       当参数canvas的值不等于null时,它指向一个Hardware Canvas,表示Chromium渲染引擎请求App的UI线程先将要执行的GPU命令记录在App UI的Display List中。等到该Display List同步给App的Render Thread,并且被App的Render Thread重放的时候,再执行请求的GPU命令。这种情况发生在Render端绘制网页UI的过程中。

       另外一个参数waitForCompletion表示App的UI线程是否需要同步等待App的Render Thread执行完成请求的GPU命令。

       在我们这个情景中,DrawGLFunctor类的成员函数requestDrawGL将会直接请求App的Render Thread执行GPU命令。这是通过调用参数viewRootImpl指向的一个ViewRootImpl对象的成员函数invokeFunctor实现的,如下所示:

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
    ......

    public void invokeFunctor(long functor, boolean waitForCompletion) {
        ThreadedRenderer.invokeFunctor(functor, waitForCompletion);
    }

    ......
}
       这个函数定义在文件frameworks/base/core/java/android/view/ViewRootImpl.java中。

       ViewRootImpl类的成员函数invokeFunctor会调用ThreadedRenderer类的静态成员函数invokeFunctor请求App的Render Thread执行GPU命令,如下所示:

public class ThreadedRenderer extends HardwareRenderer {
    ......

    static void invokeFunctor(long functor, boolean waitForCompletion) {
        nInvokeFunctor(functor, waitForCompletion);
    }

    ......
}
       这个函数定义在文件frameworks/base/core/java/android/view/ThreadedRenderer.java中。

       ThreadedRenderer类的静态成员函数invokeFunctor调用另外一个静态成员函数nInvokeFunctor请求App的Render Thread执行GPU命令。

       ThreadedRenderer类的静态成员函数nInvokeFunctor是一个JNI方法,它由C++层的函数android_view_ThreadedRenderer_invokeFunctor实现,如下所示:

static void android_view_ThreadedRenderer_invokeFunctor(JNIEnv* env, jobject clazz,
        jlong functorPtr, jboolean waitForCompletion) {
    Functor* functor = reinterpret_cast<Functor*>(functorPtr);
    RenderProxy::invokeFunctor(functor, waitForCompletion);
}
       这个函数定义在文件frameworks/base/core/jni/android_view_ThreadedRenderer.cpp中。

       从前面的分析可以知道,参数functorPtr描述的是一个Native层的DrawGLFunctor对象。这个DrawGLFunctor对象是从Functor类继承下来的,因此函数android_view_ThreadedRenderer_invokeFunctor可以将它转换为一个Functor对象。这个Functor对象会通过RenderProxy类的静态成员函数invokeFunctor提交给App的Render Thread处理,如下所示:

CREATE_BRIDGE2(invokeFunctor, RenderThread* thread, Functor* functor) {
    CanvasContext::invokeFunctor(*args->thread, args->functor);
    return NULL;
}

void RenderProxy::invokeFunctor(Functor* functor, bool waitForCompletion) {
    ATRACE_CALL();
    RenderThread& thread = RenderThread::getInstance();
    SETUP_TASK(invokeFunctor);
    args->thread = &thread;
    args->functor = functor;
    if (waitForCompletion) {
        // waitForCompletion = true is expected to be fairly rare and only
        // happen in destruction. Thus it should be fine to temporarily
        // create a Mutex
        Mutex mutex;
        Condition condition;
        SignalingRenderTask syncTask(task, &mutex, &condition);
        AutoMutex _lock(mutex);
        thread.queue(&syncTask);
        condition.wait(mutex);
    } else {
        thread.queue(task);
    }
}
       这个函数定义在文件frameworks/base/libs/hwui/renderthread/RenderProxy.cpp中。

       App的Render Thread可以通过调用RenderThread类的静态成员函数getInstance获得。获得了App的Render Thread之后,RenderProxy类的静态成员函数invokeFunctor就可以往它的消息队列发送一个Task。这个Task封装参数funtor描述的一个Native层的DrawGLFunctor对象,并且它绑定了由宏CREATE_BRIDGE2定义的函数invokeFunctor。

       这意味着当上述Task被App的Render Thread调度执行的时候,函数invokeFunctor就会在App的Render Thread中执行,它主要就是通过调用CanvasContext类的静态成员函数invokeFunctor通知参数参数funtor描述的Native层DrawGLFunctor对象,现在可以执行GPU命令了。

       从前面的调用过程可以知道,参数waitForCompletion的值等于false,表示当前线程(也就是App的UI线程)不用等待App的Render Thread执行完成请求的GPU命令,于是它就可以马上返回。

       接下来我们就继续分析CanvasContext类的静态成员函数invokeFunctor的实现,如下所示:

void CanvasContext::invokeFunctor(RenderThread& thread, Functor* functor) {
    ATRACE_CALL();
    DrawGlInfo::Mode mode = DrawGlInfo::kModeProcessNoContext;
    if (thread.eglManager().hasEglContext()) {
        thread.eglManager().requireGlContext();
        mode = DrawGlInfo::kModeProcess;
    }

    thread.renderState().invokeFunctor(functor, mode, NULL);
}
       这个函数定义在文件frameworks/base/libs/hwui/renderthread/CanvasContext.cpp中。

       CanvasContext类的静态成员函数invokeFunctor主要就是通过调用一个RenderState对象的成员函数invokeFunctor通知参数funtor描述的一个Native层DrawGLFunctor对象执行GPU命令。这个RenderState对象记录了App的Render Thread的当前渲染状态,它可以通过调用参数thread描述的一个RenderThread对象的成员函数renderState获得。

       如果此时App的Render Thread已经初始化好了OpenGL环境,那么RenderState类的成员函数invokeFunctor将会通知参数funtor描述的Native层DrawGLFunctor对象以DrawGlInfo::kModeProcess的模式执行GPU命令。否则的话,就以DrawGlInfo::kModeProcessNoContext的模式行。由于App的Render Thread一开始就会初始化OpenGL环境,因此我们将认为RenderState类的成员函数invokeFunctor将会通知参数funtor描述的Native层DrawGLFunctor对象以DrawGlInfo::kModeProcess的模式执行GPU命令。

       RenderState类的成员函数invokeFunctor的实现如下所示:

void RenderState::invokeFunctor(Functor* functor, DrawGlInfo::Mode mode, DrawGlInfo* info) {
    interruptForFunctorInvoke();
    (*functor)(mode, info);
    resumeFromFunctorInvoke();
}
       这个函数定义在文件frameworks/base/libs/hwui/RenderState.cpp中。

       RenderState类的成员函数invokeFunctor主要就是调用了参数functor描述的一个Native层DrawGLFunctor对象的重载操作符函数(),用来通知它执行GPU命令,如下所示:

AwDrawGLFunction* g_aw_drawgl_function = NULL;

class DrawGLFunctor : public Functor {
 public:
  ......

  // Functor
  virtual status_t operator ()(int what, void* data) {
    ......

    AwDrawGLInfo aw_info;
    aw_info.version = kAwDrawGLInfoVersion;
    switch (what) {
      case DrawGlInfo::kModeDraw: {
        aw_info.mode = AwDrawGLInfo::kModeDraw;
        ......
        break;
      }
      case DrawGlInfo::kModeProcess:
        aw_info.mode = AwDrawGLInfo::kModeProcess;
        break;
      case DrawGlInfo::kModeProcessNoContext:
        aw_info.mode = AwDrawGLInfo::kModeProcessNoContext;
        break;
      case DrawGlInfo::kModeSync:
        aw_info.mode = AwDrawGLInfo::kModeSync;
        break;
      default:
        ALOGE("Unexpected DrawGLInfo type %d", what);
        return DrawGlInfo::kStatusDone;
    }

    // Invoke the DrawGL method.
    g_aw_drawgl_function(view_context_, &aw_info, NULL);

    return DrawGlInfo::kStatusDone;
  }

  ......
};
       这个函数定义在文件frameworks/webview/chromium/plat_support/draw_gl_functor.cpp中。

       DrawGLFunctor类的重载操作符函数()主要就是调用全局变量g_aw_drawgl_function描述的一个DrawGL函数执行GPU命令,并且告知该DrawGL函数,App的Render Thread当前处于什么状态。这个状态由参数what描述的GPU执行模式决定。

       App的Render Thread有四种状态:

       1. AwDrawGLInfo::kModeDraw:表示App的Render Thread正在重放App UI的Display List。

       2. DrawGlInfo::kModeProcess:表示App的Render Thread正在执行外界请求的GPU命令,并且App的Render Thread当前具有OpenGL上下文。

       3. AwDrawGLInfo::kModeProcessNoContext:表示App的Render Thread正在执行外界请求的GPU命令,但是App的Render Thread当前没有OpenGL上下文。

       4, AwDrawGLInfo::kModeSync:表示App的Render Thread正在同步的App的UI线程的Display List。

       在我们这个情景中,App的Render Thread正处于第2种状态。第3种状态几乎不会出现,我们不予考虑。第1种和第4种状态我们在接下来一篇文章分析Android WebView渲染网页UI的过程中再详细分析。

       从前面的分析可以知道,全局变量g_aw_drawgl_function描述的DrawGL函数是由Android WebView在启动Chromium渲染引擎时注册的,它指向的函数为DrawGLFunction,它的实现如下所示:

static void DrawGLFunction(long view_context,
                           AwDrawGLInfo* draw_info,
                           void* spare) {
  // |view_context| is the value that was returned from the java
  // AwContents.onPrepareDrawGL; this cast must match the code there.
  reinterpret_cast<android_webview::AwContents*>(view_context)
      ->DrawGL(draw_info);
}
      这个函数定义在文件external/chromium_org/android_webview/native/aw_contents.cc中。

      参数view_context描述的是一个Native层的AwContents对象。函数DrawGLFunction主要就是调用这个AwContents对象的成员函数DrawGL执行GPU命令,如下所示:

void AwContents::DrawGL(AwDrawGLInfo* draw_info) {
  if (draw_info->mode == AwDrawGLInfo::kModeSync) {
    if (hardware_renderer_)
      hardware_renderer_->CommitFrame();
    return;
  }

  ......

  ScopedAllowGL allow_gl;
  ......

  if (draw_info->mode != AwDrawGLInfo::kModeDraw) {
    ......
    return;
  }

  if (!hardware_renderer_) {
    hardware_renderer_.reset(new HardwareRenderer(&shared_renderer_state_));
    hardware_renderer_->CommitFrame();
  }

  hardware_renderer_->DrawGL(state_restore.stencil_enabled(),
                             state_restore.framebuffer_binding_ext(),
                             draw_info);
  ......
}

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

       AwContents类的成员函数DrawGL根据App的Render Thread的当前不同的状态执行不同的操作。在分析这些操作之前,我们首先介绍AwContents类的成员变量hardware_renderer_。如果它的值不等于NULL,那么它就是指向一个HardwareRenderer对象。这个HardwareRenderer对象是Browser端用来合成网页UI的。

       如果App的Render Thread的当前状态是AwDrawGLInfo::kModeSync,并且当前AwContents类的成员变量hardware_renderer_指向了一个HardwareRenderer对象,那么AwContents类的成员函数DrawGL就会调用这个HardwareRenderer对象的成员函数CommitFrame将Render端上一次绘制出来的网页UI同步到Browser端。

       如果App的Render Thread的当前状态是AwDrawGLInfo::kModeProcess,那么AwContents类的成员函数DrawGL就会构造一个ScopedAllowGL对象。这个ScopedAllowGL对象在构造的过程中,就会通知DeferredGpuCommandService服务执行Render端之前请求执行的GPU命令。

       如果App的Render Thread的当前状态是AwDrawGLInfo::kModeDraw,那么AwContents类的成员函数DrawGL同样先通过上述构造的ScopedAllowGL对象通知DeferredGpuCommandService服务执行Render端之前请求执行的GPU命令。此外,AwContents类的成员函数DrawGL还会调用成员变量hardware_renderer_指向了一个HardwareRenderer对象的成员函数DrawGL将从Render端同步过来的网页UI合成显示在屏幕中。如果这个HardwareRenderer对象还没有创建,那么就会进行创建,并且会在创建后将Render端上一次绘制出来的网页UI同步到Browser端来,以便接下来可以将它合成显示在屏幕中。

       从前面的分析可以知道,App的Render Thread的当前状态是AwDrawGLInfo::kModeProcess,因此接下来AwContents类的成员函数DrawGL就会通过构造一个ScopedAllowGL对象来通知DeferredGpuCommandService服务执行Render端之前请求执行的GPU命令。这些GPU命令是用来光栅化网页的UI的。

       ScopedAllowGL对象的构造过程如下所示:

base::LazyInstance<scoped_refptr<DeferredGpuCommandService> >
    g_service = LAZY_INSTANCE_INITIALIZER;
......

base::LazyInstance<base::ThreadLocalBoolean> ScopedAllowGL::allow_gl;
......

bool ScopedAllowGL::IsAllowed() {
  return allow_gl.Get().Get();
}

ScopedAllowGL::ScopedAllowGL() {
  DCHECK(!allow_gl.Get().Get());
  allow_gl.Get().Set(true);

  if (g_service.Get())
    g_service.Get()->RunTasks();
}
       这个函数定义在文件external/chromium_org/android_webview/browser/deferred_gpu_command_service.cc中。

       ScopedAllowGL类的构造函数首先是通过ScopedAllowGL类的静态成员变量allow_gl描述的一个线程局存储将当前线程标记为一个GPU线程,这样当前线程就可以直接执行GPU命令了。

       全局变量g_service描述的就是一个DeferredGpuCommandService服务。ScopedAllowGL类的构造函数接下来就会调用这个DeferredGpuCommandService服务的成员函数RunTasks调度执行保存在它内部的一个Task队列中的Task了。DeferredGpuCommandService类的成员函数RunTasks我们在前面已经分析过了,这里不再复述。

       从前面的分析可以知道,DeferredGpuCommandService服务内部的Task队列保存了一个Task。这个Task绑定了InProcessCommandBuffer类的成员函数FlushOnGpuThread。这意味着接下来InProcessCommandBuffer类的成员函数FlushOnGpuThread会在App的Render Thread中调用,它的实现如下所示:

void InProcessCommandBuffer::FlushOnGpuThread(int32 put_offset) {
  ......

  command_buffer_->Flush(put_offset);

  ......
}
       这个函数定义在文件external/chromium_org/gpu/command_buffer/service/in_process_command_buffer.cc中。

       从前面的分析可以知道,InProcessCommandBuffer类的成员变量command_buffer_指向的是一个CommandBufferService对象。InProcessCommandBuffer类的成员函数FlushOnGpuThread调用这个CommandBufferService对象的成员函数Flush执行GPU命令,如下所示:

void CommandBufferService::Flush(int32 put_offset) {
  ......

  put_offset_ = put_offset;

  if (!put_offset_change_callback_.is_null())
    put_offset_change_callback_.Run();
}
       这个函数定义在文件external/chromium_org/gpu/command_buffer/service/command_buffer_service.cc中。

       参数表示put_offset表示最新写入的GPU命令在Command Buffer中的位置。CommandBufferService类的成员函数Flush首先将这个位置记录在成员变量put_offset_中。

       CommandBufferService类的成员函数Flush接下来又会检查成员变量put_offset_change_callback_是否指向了一个Callback。如果指向了一个Callback,那么就会调用它所绑定的函数来执行GPU命令。

       从前面的分析可以知道,CommandBufferService类的成员变量put_offset_change_callback_指向了一个Callback。这个Callback绑定的函数为InProcessCommandBuffer类的成员函数PumpCommands。因此,接下来InProcessCommandBuffer类的成员函数PumpCommands会被调用。在调用期间,就会执行前面写入在Command Buffer中的GPU命令,如下所示:

void InProcessCommandBuffer::PumpCommands() {
  ......

  gpu_scheduler_->PutChanged();
}
       这个函数定义在文件external/chromium_org/gpu/command_buffer/service/in_process_command_buffer.cc中。

       从前面的分析可以知道,InProcessCommandBuffer类的成员变量gpu_scheduler_描述的是一个Gpu Scheduler。InProcessCommandBuffer类的成员函数PumpCommands调用这个Gpu Scheduler的成员函数PutChanged,用来通知它Command Buffer有新的GPU命令需要处理。这个Gpu Scheduler获得这个通知后,就会从Command Buffer中读出新写入的GPU命令,并且调用相应的OpenGL函数进行处理。这个处理过程可以参考前面Chromium硬件加速渲染的OpenGL命令执行过程分析一文。

       这样,我们就以Render端使用GPU光栅化网页UI的情景为例,分析了Android WebView的Render端执行GPU命令的过程。其它的情景,Render端也是通过In-Process Command Buffer GL接口请求App的Render Thread来执行GPU命令。

       最后,我们分析Android WebView为Browser端创建In-Process Command Buffer GL接口的过程。有了In-Process Command Buffer GL接口之后,Browser端就可以像Render端一样,在App的Render Thread中执行GPU命令了。

       Android WebView的Browser端的主要任务是将网页的UI合成在屏幕中。为了完成这个任务,Browser端会创建一个Hardware Renderer。Hardware Renderer又会为Browser端创建一个CC Layer Tree,目的是为了可以使用Chromium的CC模块来完成合成网页UI的任务。

      Hardware Renderer会将它为Browser端创建的CC Layer Tree绘制在一个Parent Output Surface上。这个Parent Output Surface内部封装了一个In-Process Command Buffer GL接口。以后Chromium的CC模块就会通过这个In-Process Command Buffer GL接口合成网页的UI。

      接下来,我们就从Browser端创建Hardware Renderer的过程开始,分析Browser端用来合成网页UI的In-Process Command Buffer GL接口的创建过程。

      前面分析,App的Render Thread在AwContents类的成员函数DrawGL时提到,当App的Render Thread的处于AwDrawGLInfo::kModeDraw状态时,会检查Android WebView的Browser端是否已经创建了一个Hardware Renderer。如果还没有创建,那么就会进行创建。创建过程如下所示:

HardwareRenderer::HardwareRenderer(SharedRendererState* state)
    : ......,
      root_layer_(cc::Layer::Create()),
      ...... {
  ......

  layer_tree_host_ =
      cc::LayerTreeHost::CreateSingleThreaded(this, this, NULL, settings);
  layer_tree_host_->SetRootLayer(root_layer_);
  ......
}
       这个函数定义在文件external/chromium_org/android_webview/browser/hardware_renderer.cc中。

       HardwareRenderer类的构造函数主要就是为Browser端创建一个LayerTreeHost对象,并且保存成员变量layer_tree_host_中。这个LayerTreeHost对象描述的就是一个CC Layer Tree。这个CC Layer Tree是通过调用LayerTreeHost类的静态成员函数CreateSingleThreaded创建的,并且会将这个CC Layer Tree的Client指定为当前正在创建的Hardware Renderer,如下所示:

scoped_ptr<LayerTreeHost> LayerTreeHost::CreateSingleThreaded(
    LayerTreeHostClient* client,
    LayerTreeHostSingleThreadClient* single_thread_client,
    SharedBitmapManager* manager,
    const LayerTreeSettings& settings) {
  scoped_ptr<LayerTreeHost> layer_tree_host(
      new LayerTreeHost(client, manager, settings));
  layer_tree_host->InitializeSingleThreaded(single_thread_client);
  return layer_tree_host.Pass();
}
       这个函数定义在文件external/chromium_org/cc/trees/layer_tree_host.cc中。

       LayerTreeHost类的静态成员函数CreateSingleThreaded首先是创建了一个LayerTreeHost对象。这个LayerTreeHost对象描述的就是一个CC Layer Tree,它的创建过程如下所示:

LayerTreeHost::LayerTreeHost(LayerTreeHostClient* client,
                             SharedBitmapManager* manager,
                             const LayerTreeSettings& settings)
    : ......,
      client_(client),
      ...... {
  ......
}
       这个函数定义在文件external/chromium_org/cc/trees/layer_tree_host.cc中。

       LayerTreeHost类的构造函数主要是将参数client描述的一个HardwareRenderer对象保存成员变量client_中,作为当前正在创建的CC Layer Tree的Client。以后当前正在创建的CC Layer Tree将会通过这个Client为自己创建一个Output Surface。

       回到前面分析的LayerTreeHost类的静态成员函数CreateSingleThreaded中,它接下来又会调用前面创建的LayerTreeHost对象的成员函数InitializeSingleThreaded,用来执行初始化工作,如下所示:

void LayerTreeHost::InitializeSingleThreaded(
    LayerTreeHostSingleThreadClient* single_thread_client) {
  InitializeProxy(SingleThreadProxy::Create(this, single_thread_client));
}
       这个函数定义在文件external/chromium_org/cc/trees/layer_tree_host.cc中。

       LayerTreeHost类的成员函数InitializeSingleThreaded首先是调用SingleThreadProxy类的静态成员函数Create创建了一个SingleThreadProxy对象,如下所示:

scoped_ptr<Proxy> SingleThreadProxy::Create(
    LayerTreeHost* layer_tree_host,
    LayerTreeHostSingleThreadClient* client) {
  return make_scoped_ptr(
      new SingleThreadProxy(layer_tree_host, client)).PassAs<Proxy>();
}
       这个函数定义在文件external/chromium_org/cc/trees/single_thread_proxy.cc中。

       从这里可以看到,SingleThreadProxy类的静态成员函数Create创建的是一个SingleThreadProxy对象。这个SingleThreadProxy对象在创建的过程中,会将参数layer_tree_host指向的一个LayerTreeHost对象保存在自己的成员变量layer_tree_host_中,如下所示:

SingleThreadProxy::SingleThreadProxy(LayerTreeHost* layer_tree_host,
                                     LayerTreeHostSingleThreadClient* client)
    : Proxy(NULL),
      layer_tree_host_(layer_tree_host),
      ...... {
  ......
}
       这个函数定义在文件external/chromium_org/cc/trees/single_thread_proxy.cc中。

       回到前面分析的LayerTreeHost类的成员函数InitializeSingleThreaded中,它获得了一个SingleThreadProxy对象之后,就会调用另外一个成员函数InitializeProxy对该SingleThreadProxy进行初始化,如下所示:

void LayerTreeHost::InitializeProxy(scoped_ptr<Proxy> proxy) {
  ......

  proxy_ = proxy.Pass();
  proxy_->Start();
}
      这个函数定义在文件external/chromium_org/cc/trees/layer_tree_host.cc中。

      LayerTreeHost类的成员函数InitializeProxy首先将参数proxy指向的一个SingleThreadProxy对象保存在成员变量proxy_中,然后再调用这个SingleThreadProxy对象的成员函数
Start创建一个LayerTreeHostImpl对象,如下所示:

void SingleThreadProxy::Start() {
  ......
  layer_tree_host_impl_ = layer_tree_host_->CreateLayerTreeHostImpl(this);
}
       这个函数定义在文件external/chromium_org/cc/trees/single_thread_proxy.cc中。

       从前面的分析可以知道,SingleThreadProxy类的成员变量layer_tree_host_指向的是一个LayerTreeHost对象。SingleThreadProxy类的成员函数
Start调用这个LayerTreeHost对象的成员函数CreateLayerTreeHostImpl创建了一个LayerTreeHostImpl对象,并且保存在成员变量layer_tree_host_impl_。

       从前面的分析就可以知道,Hardware Renderer在为Browser端创建CC Layer Tree的过程中,一共创建了LayerTreeHost、SingleThreadProxy和LayerTreeHostImpl三个对象。这三个对象一起描述了一个CC Layer Tree。

       从前面Chromium网页渲染机制简要介绍和学习计划这个系列的文章可以知道,Render端的CC Layer Tree是通过调用LayerTreeHost类的静态成员函数CreateThreaded创建的。在创建的过程中,同样会创建一个LayerTreeHost对象和一个LayerTreeHostImpl对象。不过,它不会创建一个SingleThreadProxy对象,而是一个ThreadProxy对象。这三个对象同样是描述了一个CC Layer Tree。

       ThreadProxy类和SingleThreadProxy类都是从Proxy类继承下来的。它们的最大区别在于前者描述的CC Layer Tree在绘制的过程中,会使用到两个线程。一个称为Main线程,另一个称为Compositor线程。这两个线程通过一个CC调度器进行协作,完成绘制网页UI的任务。后者描述的CC Layer Tree在绘制的过程中,只会使用一个线程,因此它不需要使用到CC调度器。

       从前面Chromium网页渲染机制简要介绍和学习计划这个系列的文章可以知道,对于复杂的UI来说,使用两个线程绘制效率会更高。不过,对Android WebView的Browser端来说,它的UI结构是非常简单的(CC Layer Tree只包含两个节点),因此,它就不需要使用两个线程来绘制了。

       回到前面分析的HardwareRenderer类的构造函数中,它除了为Browser端创建一个CC Layer Tree,还会调用Layer类的静态成员函数Create0创建一个CC Layer。这个CC Layer就作为Browser端的CC Layer Tree的根节点。

       确保Android WebView的Browser端已经具有一个Hardware Renderer之后,前面分析的AwContents类的成员函数DrawGL就会调用这个Hardware Renderer的成员函数DrawGL来合成网页的UI,如下所示:

void HardwareRenderer::DrawGL(bool stencil_enabled,
                              int framebuffer_binding_ext,
                              AwDrawGLInfo* draw_info) {
  ......

  {
    ......
    layer_tree_host_->Composite(gfx::FrameTime::Now());
  }
  
  ......
}
       这个函数定义在文件external/chromium_org/android_webview/browser/hardware_renderer.cc中。

       从前面的分析可以知道,HardwareRenderer类的成员变量layer_tree_host_指向的是一个LayerTreeHost对象。HardwareRenderer类的成员函数DrawGL调用这个LayerTreeHost对象的成员函数Composite合成网页的UI,如下所示:

void LayerTreeHost::Composite(base::TimeTicks frame_begin_time) {
  ......
  SingleThreadProxy* proxy = static_cast<SingleThreadProxy*>(proxy_.get());

  if (output_surface_lost_)
    proxy->CreateAndInitializeOutputSurface();
  ......

  proxy->CompositeImmediately(frame_begin_time);
}
        这个函数定义在文件external/chromium_org/cc/trees/layer_tree_host.cc中。

        LayerTreeHost类的成员变量output_surface_lost_是一个布尔变量。当它的值等于true的时候,就表示CC模块还没有为当前正在处理的LayerTreeHost对象描述的CC Layer Tree创建过Output Surface,或者以前创建过,但是现在失效了。在这种情况下,LayerTreeHost类的成员函数Composite合成网页UI之前,先创建创建一个Output Surface。

       从前面的分析可以知道,LayerTreeHost类的成员变量proxy_指向的是一个SingleThreadProxy对象。LayerTreeHost类的成员函数Composite就是通过调用这个SingleThreadProxy对象的成员函数CreateAndInitializeOutputSurface为当前正在处理的LayerTreeHost对象描述的CC Layer Tree创建Output Surface的。

       有了Output Surface之后,LayerTreeHost类的成员函数Composite再调用上述SingleThreadProxy对象的成员函数CompositeImmediately合成网页的UI。这个过程我们在接下来一篇文章中再详细分析。

       接下来,我们继续分析SingleThreadProxy类的成员函数CreateAndInitializeOutputSurface为Browser端的CC Layer Tree创建Output Surface的过程。在创建的过程中,就会创建一个In-Process Command Buffer GL接口,如下所示:

void SingleThreadProxy::CreateAndInitializeOutputSurface() {
  ......

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

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

       从前面的分析可以知道,SingleThreadProxy类的成员变量layer_tree_host_指向的是一个LayerTreeHost对象。SingleThreadProxy类的成员函数CreateAndInitializeOutputSurface调用这个LayerTreeHost对象的成员函数CreateOutputSurface为Browser端的CC Layer Tree创建一个Output Surface,如下所示:

scoped_ptr<OutputSurface> LayerTreeHost::CreateOutputSurface() {
  return client_->CreateOutputSurface(num_failed_recreate_attempts_ >= 4);
}
       这个函数定义在文件external/chromium_org/cc/trees/layer_tree_host.cc中。

       从前面的分析可以知道,LayerTreeHost类的成员变量client_指向的是一个HardwareRenderer对象。LayerTreeHost类的成员函数CreateOutputSurfaceBrowser调用这个HardwareRenderer对象的成员函数CreateOutputSurface为Browser端的CC Layer Tree创建一个Output Surface,如下所示:

scoped_ptr<cc::OutputSurface> HardwareRenderer::CreateOutputSurface(
    bool fallback) {
  ......

  scoped_refptr<cc::ContextProvider> context_provider =
      CreateContext(gl_surface_,
                    DeferredGpuCommandService::GetInstance(),
                    shared_renderer_state_->GetSharedContext());
  scoped_ptr<ParentOutputSurface> output_surface_holder(
      new ParentOutputSurface(context_provider));
  output_surface_ = output_surface_holder.get();
  return output_surface_holder.PassAs<cc::OutputSurface>();
}
       这个函数定义在文件external/chromium_org/android_webview/browser/hardware_renderer.cc。

       HardwareRenderer类的成员函数CreateOutputSurface为Browser端的CC Layer Tree创建的是一个Parent Output Surface。相应地,Render端的CC Layer Tree使用的Synchronous Compositor Output Surface也称为Child Output Surface。之所以将Render端的CC Layer Tree的Output Surface称为Child,而将Browser端的CC Layer Tree创建的是一个Parent,是因为Render端的CC Layer Tree的绘制结果会输出为Browser端的CC Layer Tree的一个节点的内容。这一点我们在接下来一篇文章分析Android WebView渲染网页UI的过程就会清楚地看到。

       HardwareRenderer类的成员函数CreateOutputSurface在为Browser端的CC Layer Tree创建Parent Output Surface的时候,需要用到一个ContextProvider对象。这个ContextProvider对象是通过调用函数CreateContext创建的,如下所示:

scoped_refptr<cc::ContextProvider> CreateContext(
    scoped_refptr<gfx::GLSurface> surface,
    scoped_refptr<gpu::InProcessCommandBuffer::Service> service,
    gpu::GLInProcessContext* share_context) {
  ......

  scoped_ptr<gpu::GLInProcessContext> context(
      gpu::GLInProcessContext::Create(service,
                                      surface,
                                      surface->IsOffscreen(),
                                      gfx::kNullAcceleratedWidget,
                                      surface->GetSize(),
                                      share_context,
                                      false /* share_resources */,
                                      in_process_attribs,
                                      gpu_preference));
  ......

  return webkit::gpu::ContextProviderInProcess::Create(
      WebGraphicsContext3DInProcessCommandBufferImpl::WrapContext(
          context.Pass(), attributes),
      "Parent-Compositor");
}
       这个函数定义在文件external/chromium_org/android_webview/browser/hardware_renderer.cc。

       函数ContextProvider首先是调用GLInProcessContext类的静态成员函数Create创建了一个In-Process Command Buffer GL接口。

       上述In-Process Command Buffer GL接口又会通过WebGraphicsContext3DInProcessCommandBufferImpl类的静态成员函数WrapContext封装在一个WebGraphicsContext3DInProcessCommandBufferImpl对象中。

       最后,前面得到的WebGraphicsContext3DInProcessCommandBufferImpl对象又会通过ContextProviderInProcess类的静态成员函数Create封装在一个ContextProviderInProcess对象中返回给调用者。调用者有了这个ContextProviderInProcess对象之后,就可以创建一个Output Surface了。

       GLInProcessContext类的静态成员函数Create、WebGraphicsContext3DInProcessCommandBufferImpl类的静态成员函数WrapContext和ContextProviderInProcess类的静态成员函数Create的实现,前面在分析Render端的CC Layer Tree使用的Synchronous Compositor Output Surface的创建过程时,已经分析过了,这里就不再复述。

       这样,我们就可以知道,Browser端的CC Layer Tree使用的Output Surface里面包含了一个In-Process Command Buffer GL接口。CC模块在绘制Browser端的CC Layer Tree时,就会通过这个In-Process Command Buffer GL接口来执行GPU命令。这与我们前面分析的Render端执行GPU命令的过程是一致的。

       至此,我们就分析了Android WebView为Render端和Browser端创建In-Process Command Buffer GL接口的过程,并且以Render端光栅化网页UI的情景为例,分析了Render端通过In-Process Command Buffer GL接口请求App的Render Thread执行GPU命令的过程。Browser端执行GPU命令的过程与Render端也是类似的。一旦Render端和Browser可以执行GPU命令,它们就可以使用硬件加速的方式渲染网页的UI。在接下来一篇文章中,我们就详细分析Android WebView使用硬件加速方式渲染网页UI的过程。敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/12/12 1:05:00 原文链接
阅读:34667 评论:6 查看评论

Android WebView硬件加速渲染网页UI的过程分析

$
0
0

      Android WebView作为App UI的一部分,当App UI以硬件加速方式渲染时,它也是以硬件加速方式渲染的。Android WebView的UI来自于网页,是通过Chromium渲染的。Chromium渲染网页UI的机制与Android App渲染UI的机制是不一样的。不过,它们会一起协作完成网页UI的渲染。本文接下来就详细分析Android WebView硬件加速渲染网页UI的过程。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

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

      从前面Android应用程序UI硬件加速渲染技术简要介绍和学习计划这个系列的文章可以知道,Android App在渲染UI一帧的过程中,经历以下三个阶段:

      1. 在UI线程中构建一个Display List,这个Display List包含了每一个View的绘制命令。

      2. 将前面构建的Display List同步给Render Thread。

      3. Render Thread对同步得到的Display List进行渲染,也就是使用GPU执行Display List的绘制命令。

      上述三个阶段如果能够在16ms内完成,那么App的UI给用户的感受就是流畅的。为了尽量地在16ms内渲染完成App的一帧UI,Android使用了以上方式对App的UI进行渲染。这种渲染机制的好处是UI线程和Render Thread可以并发执行,也就是Render Thread在渲染当前帧的Display List的时候,UI线程可以准备下一帧的Display List。它们唯一需要同步的地方发生第二阶段。不过,这个阶段是可以很快完成的。因此,UI线程和Render Thread可以认为是并发执行的。

      Android WebView既然是App UI的一部分,也就是其中的一个View,它的渲染也是按照上述三个阶段进行的,如下所示:


图1 Android WebView硬件加速渲染网页UI的过程

       在第一阶段,Android WebView会对Render端的CC Layer Tree进行绘制。这个CC Layer Tree描述的就是网页的UI,它会通过一个Synchronous Compositor绘制在一个Synchronous Compositor Output Surface上,最终得到一个Compositor Frame。这个Compositor Frame会保存在一个SharedRendererState对象中。

       在第二阶段,保存在上述SharedRendererState对象中的Compositor Frame会同步给Android WebView会对Browser端的CC Layer Tree。Browser端的CC Layer Tree只有两个节点。一个是根节点,另一个是根节点的子节点,称为一个Delegated Renderer Layer。Render端绘制出来的Compositor Frame就是作为这个Delegated Renderer Layer的输入的。

       在第三阶段,Android WebView会通过一个Hardware Renderer将Browser端的CC Layer Tree渲染在一个Parent Output Surface上,实际上就是通过GPU命令将Render端绘制出来的UI合成显示在App的UI窗口中。

       接下来,我们就按照以上三个阶段分析Android WebView硬件加速渲染网页UI的过程。

       从前面Android应用程序UI硬件加速渲染的Display List构建过程分析一文可以知道,在App渲染UI的第一阶段,Android WebView的成员函数onDraw会被调用。从前面Android WebView执行GPU命令的过程分析一文又可以知道,Android WebView在Native层有一个BrowserViewRenderer对象。当Android WebView的成员函数onDraw被调用时,并且App的UI以硬件加速方式渲染时,这个Native层BrowserViewRenderer对象的成员函数OnDrawHardware会被调用,如下所示:

bool BrowserViewRenderer::OnDrawHardware(jobject java_canvas) {   
  ......  
  
  scoped_ptr<DrawGLInput> draw_gl_input(new DrawGLInput);  
  ......  
  
  scoped_ptr<cc::CompositorFrame> frame =  
      compositor_->DemandDrawHw(surface_size,  
                                gfx::Transform(),  
                                viewport,  
                                clip,  
                                viewport_rect_for_tile_priority,  
                                transform_for_tile_priority);  
  ......  

  frame->AssignTo(&draw_gl_input->frame);
  ......
  shared_renderer_state_->SetDrawGLInput(draw_gl_input.Pass()); 
 
  ......  
  return client_->RequestDrawGL(java_canvas, false);  
}  

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

       从前面Android WebView执行GPU命令的过程分析一文可以知道,BrowserViewRenderer类的成员变量compositor_指向的是一个SynchronousCompositorImpl对象。BrowserViewRenderer对象的成员函数OnDrawHardware会调用这个SynchronousCompositorImpl对象的成员函数DemandDrawHw对网页的UI进行绘制。

       绘制的结果是得到一个Compositor Frame。这个Compositor Frame会保存在一个DrawGLInput对象中。这个DrawGLInput对象又会保存在BrowserViewRenderer类的成员变量shared_renderer_state_指向的一个SharedRendererState对象中。这是通过调用SharedRendererState类的成员函数SetDrawGLInput实现的。

       BrowserViewRenderer类的成员变量client_指向的是一个AwContents对象。BrowserViewRenderer对象的成员函数OnDrawHardware最后会调用这个AwContents对象的成员函数RequestDrawGL请求在参数java_canvas描述的一个Hardware Canvas中增加一个DrawFunctorOp操作。这个DrawFunctorOp操作最终会包含在App的UI线程构建的Display List中。

       接下来,我们首先分析SynchronousCompositorImpl类的成员函数DemandDrawHw绘制网页的UI的过程,如下所示:

scoped_ptr<cc::CompositorFrame> SynchronousCompositorImpl::DemandDrawHw(
    gfx::Size surface_size,
    const gfx::Transform& transform,
    gfx::Rect viewport,
    gfx::Rect clip,
    gfx::Rect viewport_rect_for_tile_priority,
    const gfx::Transform& transform_for_tile_priority) {
  ......

  scoped_ptr<cc::CompositorFrame> frame =
      output_surface_->DemandDrawHw(surface_size,
                                    transform,
                                    viewport,
                                    clip,
                                    viewport_rect_for_tile_priority,
                                    transform_for_tile_priority);
  ......
  return frame.Pass();
}
      这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_impl.cc中。

      从前面Android WebView执行GPU命令的过程分析一文可以知道,SynchronousCompositorImpl类的成员变量output_surface_指向的是一个SynchronousCompositorOutputSurface对象。SynchronousCompositorImpl类的成员函数DemandDrawHw调用这个SynchronousCompositorOutputSurface对象的成员函数DemandDrawHw绘制网页的UI,如下所示:

scoped_ptr<cc::CompositorFrame>
SynchronousCompositorOutputSurface::DemandDrawHw(
    gfx::Size surface_size,
    const gfx::Transform& transform,
    gfx::Rect viewport,
    gfx::Rect clip,
    gfx::Rect viewport_rect_for_tile_priority,
    const gfx::Transform& transform_for_tile_priority) {
  ......

  InvokeComposite(transform,
                  viewport,
                  clip,
                  viewport_rect_for_tile_priority,
                  transform_for_tile_priority,
                  true);

  return frame_holder_.Pass();
}
       这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_output_surface.cc中。

       SynchronousCompositorOutputSurface类的成员函数DemandDrawHw调用另外一个成员函数InvokeComposite绘制网页的UI。绘制完成后,就会得到一个Compositor Frame。这个Compositor Frame保存在SynchronousCompositorOutputSurface类的成员变量frame_holder_中。因此,SynchronousCompositorOutputSurface类的成员函数DemandDrawHw可以将这个成员变量frame_holder_指向的Compositor Frame返回给调用者。

       SynchronousCompositorOutputSurface类的成员函数InvokeComposite的实现如下所示:

void SynchronousCompositorOutputSurface::InvokeComposite(
    const gfx::Transform& transform,
    gfx::Rect viewport,
    gfx::Rect clip,
    gfx::Rect viewport_rect_for_tile_priority,
    gfx::Transform transform_for_tile_priority,
    bool hardware_draw) {
  ......

  client_->BeginFrame(cc::BeginFrameArgs::CreateForSynchronousCompositor());

  ......
}
       这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_output_surface.cc中。

       SynchronousCompositorOutputSurface类的成员变量client_是从父类OutputSurface继承下来的。从前面Chromium网页绘图表面(Output Surface)创建过程分析一文可以知道,它指向的是一个LayerTreeHostImpl对象。SynchronousCompositorOutputSurface类的成员函数InvokeComposite调用这个LayerTreeHostImpl对象的成员函数BeginFrame绘制网页的UI。 

       从前面Chromium网页渲染调度器(Scheduler)实现分析一文可以知道,当LayerTreeHostImpl类的成员函数BeginFrame被调用时,它就会CC模块的调度器执行一个BEGIN_IMPL_FRAME操作,也就是对网页的CC Layer Tree进行绘制。绘制的过程可以参考Chromium网页Layer Tree绘制过程分析Chromium网页Layer Tree同步为Pending Layer Tree的过程分析Chromium网页Pending Layer Tree激活为Active Layer Tree的过程分析这三篇文章。

       由于Android WebView的Render端使用的是Synchronous Compositor,当前线程(也就是App的UI线程)会等待Render端的Compositor线程绘制完成网页的CC Layer Tree。从前面Chromium硬件加速渲染的UI合成过程分析一文可以知道,Compositor线程在绘制完成网页的CC Layer Tree的时候,会调用网页的Output Surface的成员函数SwapBuffers。

       在我们这个情景中,网页的Output Surface是一个Synchronous Compositor Output Surface。这意味着当Compositor线程在绘制完成网页的CC Layer Tree时,会调用SynchronousCompositorOutputSurface类的成员函数SwapBuffers,如下所示:

void SynchronousCompositorOutputSurface::SwapBuffers(
    cc::CompositorFrame* frame) {
  ......

  frame_holder_.reset(new cc::CompositorFrame);
  frame->AssignTo(frame_holder_.get());

  ......
}
       这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_output_surface.cc中。

       参数frame指向的Compositor Frame描述的就是网页的绘制结果。从前面Chromium硬件加速渲染的UI合成过程分析一文可以知道,这个Compositor Frame包含了一系列的Render Pass。每一个Render Pass都包含了若干个纹理,以及每一个纹理的绘制参数。这些纹理是在Render端光栅化网页时产生的。Browser端的Hardware Renderer所要做的事情就是将这些纹理渲染在屏幕上。这个过程也就是Browser端合成网页UI的过程。

       SynchronousCompositorOutputSurface类的成员函数SwapBuffers会将参数frame描述的Compositor Frame的内容拷贝一份到一个新创建的Compositor Frame中去。这个新创建的Compositor Frame会保存在SynchronousCompositorOutputSurface类的成员变量frame_hodler_中。因此,前面分析的SynchronousCompositorOutputSurface类的成员函数InvokeComposite返回给调用者的就是当前绘制的网页的内容。

       这一步执行完成后,回到前面分析的BrowserViewRenderer类的成员函数OnDrawHardware中,这时候它就获得了一个Render端绘制网页的结果,也就是一个Compositor Frame。这个Compositor Frame会保存在一个DrawGLInput对象中。这个DrawGLInput对象又会保存在BrowserViewRenderer类的成员变量shared_renderer_state_指向的一个SharedRendererState对象中。这是通过调用SharedRendererState类的成员函数SetDrawGLInput实现的,如下所示:

void SharedRendererState::SetDrawGLInput(scoped_ptr<DrawGLInput> input) {
  ......
  draw_gl_input_ = input.Pass();
}
      这个函数定义在文件external/chromium_org/android_webview/browser/shared_renderer_state.cc中。

      SharedRendererState类的成员函数SetDrawGLInput将参数input指向的一个DrawGLInput对象保存成员变量draw_gl_input_中。

      这一步执行完成后,再回到前面分析的BrowserViewRenderer类的成员函数OnDrawHardware中,接下来它会调用成员变量client_指向的一个Native层AwContents对象的成员函数RequestDrawGL请求在参数java_canvas描述的一个Hardware Canvas中增加一个DrawFunctorOp操作,如下所示:

bool AwContents::RequestDrawGL(jobject canvas, bool wait_for_completion) {
  ......

  JNIEnv* env = AttachCurrentThread();
  ScopedJavaLocalRef<jobject> obj = java_ref_.get(env);
  if (obj.is_null())
    return false;
  return Java_AwContents_requestDrawGL(
      env, obj.obj(), canvas, wait_for_completion);
}
       这个函数定义在文件external/chromium_org/android_webview/native/aw_contents.cc中。

       在前面Android WebView执行GPU命令的过程分析一文中,我们已经分析过Native层AwContents类的成员函数RequestDrawGL的实现了,它主要就是调用Java层的AwContents类的成员函数requestDrawGL请求在参数canvas描述的Hardware Canvas中增加一个DrawFunctorOp操作。

       Java层的AwContents类的成员函数requestDrawGL最终会调用到DrawGLFunctor类的成员函数requestDrawGL在参数canvas描述的Hardware Canvas中增加一个DrawFunctorOp操作,如下所示:

class DrawGLFunctor {
    ......

    public boolean requestDrawGL(HardwareCanvas canvas, ViewRootImpl viewRootImpl,
            boolean waitForCompletion) {
        ......

        if (canvas == null) {
            viewRootImpl.invokeFunctor(mDestroyRunnable.mNativeDrawGLFunctor, waitForCompletion);
            return true;
        }

        canvas.callDrawGLFunction(mDestroyRunnable.mNativeDrawGLFunctor);
        ......

        return true;
    }

    ......
}
       这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/DrawGLFunctor.java中。

       在前面Android WebView执行GPU命令的过程分析一文中,DrawGLFunctor类的成员函数requestDrawGL是在Render端光栅化网页UI的过程中调用的。这时候参数canvas的值等于null,因此DrawGLFunctor类的成员函数requestDrawGL会通过调用参数viewRootImpl指向的一个ViewRootImpl对象的成员函数invokeFunctor直接请求App的Render Thread执行GPU命令。

       现在,当DrawGLFunctor类的成员函数requestDrawGL被调用时,它的参数canvas的值不等于null,指向了一个Hardware Canvas。在这种情况下,DrawGLFunctor类的成员函数requestDrawGL将会调用这个Hardware Canvas的成员函数callDrawGLFunction,将一个Native层DrawGLFunctor对象封装成一个DrawFunctorOp操作,写入到它描述一个Display List中去。

       被封装的Native层DrawGLFunctor对象,保存在Java层DrawGLFunctor类的成员变量mDestroyRunnable指向的一个DestroyRunnable对象的成员变量mNativeDrawGLFunctor中。这一点可以参考前面Android WebView执行GPU命令的过程分析一文。

       从前面Android应用程序UI硬件加速渲染的Display List渲染过程分析一文可以知道,参数canvas描述的Hardware Canvas是通过一个GLES20Canvas对象描述的,因此接下来它的成员函数callDrawGLFunction会被调用,用来将一个Native层DrawGLFunctor对象封装成一个DrawFunctorOp操作写入它描述一个Display List中去,如下所示:

class GLES20Canvas extends HardwareCanvas {
    ......

    @Override
    public int callDrawGLFunction(long drawGLFunction) {
        return nCallDrawGLFunction(mRenderer, drawGLFunction);
    }

    ......
}
       这个函数定义在文件frameworks/base/core/java/android/view/GLES20Canvas.java中。

       GLES20Canvas类的成员函数callDrawGLFunction调用另外一个成员函数nCallDrawGLFunction将参数drawGLFunction描述的一个Native层DrawGLFunctor对象封装成一个DrawFunctorOp操作写入到当前正在处理的GLES20Canvas对象描述一个Display List中去。

       GLES20Canvas类的成员函数nCallDrawGLFunction是一个JNI方法,它由C++层的函数android_view_GLES20Canvas_callDrawGLFunction实现,如下所示:

static jint android_view_GLES20Canvas_callDrawGLFunction(JNIEnv* env, jobject clazz,
        jlong rendererPtr, jlong functorPtr) {
    DisplayListRenderer* renderer = reinterpret_cast<DisplayListRenderer*>(rendererPtr);
    Functor* functor = reinterpret_cast<Functor*>(functorPtr);
    android::uirenderer::Rect dirty;
    return renderer->callDrawGLFunction(functor, dirty);
}
       这个函数定义在文件frameworks/base/core/jni/android_view_GLES20Canvas.cpp中。

       参数rendererPtr描述的是一个Native层的DisplayListRenderer对象。这个DisplayListRenderer对象负责构造App UI的Display List。函数android_view_GLES20Canvas_callDrawGLFunction所做的事情就是调用这个DisplayListRenderer对象的成员函数callDrawFunction将参数functionPtr描述的一个Native层DrawGLFunctor对象封装成一个DrawFunctorOp操作写入到App UI的Display List中去,如下所示:

status_t DisplayListRenderer::callDrawGLFunction(Functor *functor, Rect& dirty) {
    // Ignore dirty during recording, it matters only when we replay
    addDrawOp(new (alloc()) DrawFunctorOp(functor));
    mDisplayListData->functors.add(functor);
    return DrawGlInfo::kStatusDone; // No invalidate needed at record-time
}
       这个函数定义在文件frameworks/base/libs/hwui/DisplayListRenderer.cpp中。

       DisplayListRenderer类的成员变量mDisplayListData指向的是一个DisplayListData对象。这个DisplayListData对象描述的就是App UI的Display List。因此,DisplayListRenderer对象的成员函数callDrawFunction就会将参数functor描述的一个Native层DrawGLFunctor对象封装成一个DrawFunctorOp操作写入到它里面去。

       这一步执行完成后,Android WebView就在App渲染一个帧的第一个阶段通知Render端绘制完成了网页的UI,并且往App UI的Display List写入了一个DrawFunctorOp操作。在第二阶段,App UI的Display List就会从App的UI线程同步给App的Render Thread。从前面Android应用程序UI硬件加速渲染的Display List渲染过程分析一文可以知道,在同步的过程中,RenderNode类的成员函数pushStagingDisplayListChanges地被调用,如下所示:

void RenderNode::pushStagingDisplayListChanges(TreeInfo& info) {
    if (mNeedsDisplayListDataSync) {
        mNeedsDisplayListDataSync = false;
        ......
        if (mDisplayListData) {
            for (size_t i = 0; i < mDisplayListData->functors.size(); i++) {
                (*mDisplayListData->functors[i])(DrawGlInfo::kModeSync, NULL);
            }
        }
        ......
    }
}
       这个函数定义在文件frameworks/base/libs/hwui/RenderNode.cpp中。

       这时候包含在App UI的Display List中的每一个DrawFunctorOp操作关联的Native层DrawGLFunctor对象的重载操作符函数()都会被调用,目的是让它执行一些同步操作。在我们这个情景中,就是将Render端绘制出来的UI同步到给Browser端。

       在前面Android WebView执行GPU命令的过程分析一文中,我们已经分析过Native层DrawGLFunctor对象的重载操作符函数()的实现了,它最终会调用到Native层的AwContents类DrawGL将Render端绘制出来的UI同步到给Browser端,如下所示:

void AwContents::DrawGL(AwDrawGLInfo* draw_info) {
  if (draw_info->mode == AwDrawGLInfo::kModeSync) {
    if (hardware_renderer_)
      hardware_renderer_->CommitFrame();
    return;
  }

  ......
}
      这个函数定义在文件external/chromium_org/android_webview/native/aw_contents.cc中。

      这时候App的Render Thread处于AwDrawGLInfo::kModeSync状态,因此AwContents类的成员函数DrawGL接下来将会调用成员变量hardware_renderer_指向的一个HardwareRenderer对象的成员函数CommitFrame将Render端绘制出来的UI同步到给Browser端,如下所示:

void HardwareRenderer::CommitFrame() {
  scoped_ptr<DrawGLInput> input = shared_renderer_state_->PassDrawGLInput();
  ......

  if (!frame_provider_ || size_changed) {
    ......

    frame_provider_ = new cc::DelegatedFrameProvider(
        resource_collection_.get(), input->frame.delegated_frame_data.Pass());

    delegated_layer_ = cc::DelegatedRendererLayer::Create(frame_provider_);
    ......

    root_layer_->AddChild(delegated_layer_);
  } else {
    frame_provider_->SetFrameData(input->frame.delegated_frame_data.Pass());
  }
}
      这个函数定义在文件external/chromium_org/android_webview/browser/hardware_renderer.cc中。

      从前面的分析可以知道,Render端在第一阶段已经将绘制出来的网页UI,保存在一个DrawGLInput对象中。这个DrawGLInput又保存在一个SharedRendererState对象中。HardwareRenderer类的成员变量shared_renderer_state_描述的就是这个SharedRendererState对象。因此,HardwareRenderer类的成员函数CommitFrame可以通过调用这个SharedRendererState对象的成员函数PassDrawGLInput获得保存在它内部的DrawGLInput对象,如下所示:

scoped_ptr<DrawGLInput> SharedRendererState::PassDrawGLInput() {
  base::AutoLock lock(lock_);
  return draw_gl_input_.Pass();
}
      这个函数定义在文件external/chromium_org/android_webview/browser/shared_renderer_state.cc中。

      从前面的分析可以知道,用来描述Render端在第一阶段绘制出来的网页UI的DrawGLInput对象就保存在SharedRendererState类的成员变量draw_gl_input_中,因此SharedRendererState类的成员函数PassDrawGLInput就可以将这个员变量draw_gl_input_指向的DrawGLInput对象返回给调用者。

       回到前面分析的HardwareRenderer类的成员函数CommitFrame中,这时候它获得了一个Render端在第一阶段绘制出来的UI,也就是一个DrawGLInput对象,接下来它就会判断之前是否已经为Browser端的CC Layer Tree创建过一个Delegated Renderer Layer。

       如果还没有创建,或者以前创建过,但是现在Android WebView的大小发生了变化,那么HardwareRenderer类的成员函数CommitFrame就会创建用前面获得的DrawGLInput对象创建一个Delegated Renderer Layer,并且作为Browser端的CC Layer Tree的根节点的子节点。

       另一方面,如果已经创建,并且Android WebView的大小没有发生变化,那么HardwareRenderer类的成员函数CommitFrame就会使用前面获得的DrawGLInput对象更新Delegated Renderer Layer的内容。

       这一步执行完成后,Android WebView就在App渲染一个帧的第二个阶段将Render端绘制出来的网页UI同步给了Browser端。在第三阶段,Browser端就会将网页的UI合成在App的窗口中,这样就可以显示在屏幕中了。

       从前面Android应用程序UI硬件加速渲染的Display List渲染过程分析一文可以知道,App的Render Thread在第三阶段会通过一个OpenGL Renderer渲染从App的UI线程同步过来的Display List,也就是执行它里面包含的渲染操作。从前面的分析可以知道,Android WebView在第一阶段往这个Display List写入了一个DrawFunctorOp操作,OpenGL Renderer会通过调用它的成员函数callDrawGLFunction执行这个操作,如下所示:

status_t OpenGLRenderer::callDrawGLFunction(Functor* functor, Rect& dirty) {
    ......

    mRenderState.invokeFunctor(functor, DrawGlInfo::kModeDraw, &info);
    ......

    return DrawGlInfo::kStatusDrew;
}
      这个函数定义在文件frameworks/base/libs/hwui/OpenGLRenderer.cpp中。

      参数funtor描述的就是与当前要执行的DrawFunctorOp操作关联的Native层DrawGLFunctor对象。OpenGLRenderer类的成员函数callDrawGLFunction会通过调用成员变量mRenderState指向的一个RenderState对象的成员函数invokeFunctor调用这个Native层DrawGLFunctor对象的重载操作符函数(),告知它App的Render Thread当前处于第三阶段,也就是DrawGlInfo::kModeDraw状态,它可以执行相应的GPU命令。

      在前面Android WebView执行GPU命令的过程分析一文中,我们已经分析过RenderState类的成员函数invokeFunctor的实现了,它最终会调用到Native层的AwContents类DrawGL将绘制Browser端的CC Layer Tree,如下所示:

void AwContents::DrawGL(AwDrawGLInfo* draw_info) {
  if (draw_info->mode == AwDrawGLInfo::kModeSync) {
    ......
    return;
  }

  ......

  if (draw_info->mode != AwDrawGLInfo::kModeDraw) {
    ......
    return;
  }

  ......

  hardware_renderer_->DrawGL(state_restore.stencil_enabled(),
                             state_restore.framebuffer_binding_ext(),
                             draw_info);
  ......
}
       这个函数定义在文件external/chromium_org/android_webview/native/aw_contents.cc中。

       由于当前App的Render Thread处于AwDrawGlInfo::kModeDraw状态,因此AwContents类DrawGL会调用成员变量hardware_renderer_指向的一个HardwareRenderer对象的成员函数DrawGL,用来绘制Browser端的CC Layer Tree,如下所示:

void HardwareRenderer::DrawGL(bool stencil_enabled,
                              int framebuffer_binding_ext,
                              AwDrawGLInfo* draw_info) {
  ......

  {
    ......
    layer_tree_host_->Composite(gfx::FrameTime::Now());
  }

  ......
}
      这个函数定义在文件external/chromium_org/android_webview/browser/hardware_renderer.cc中。

      从前面Android WebView执行GPU命令的过程分析一文可以知道,HardwareRenderer类的成员变量layer_tree_host_指向的是一个LayerTreeHost对象。这个LayerTreeHost对象描述的就是Browser端的CC Layer Tree。HardwareRenderer类的成员函数DrawGL将会调用这个LayerTreeHost对象的成员函数Composite,以便绘制Browser端的CC Layer Tree,如下所示:

void LayerTreeHost::Composite(base::TimeTicks frame_begin_time) {
  ......
  SingleThreadProxy* proxy = static_cast<SingleThreadProxy*>(proxy_.get());

  .....

  proxy->CompositeImmediately(frame_begin_time);
}
      这个函数定义在文件external/chromium_org/cc/trees/layer_tree_host.cc中。

      从前面Android WebView执行GPU命令的过程分析一文可以知道,当前正在处理的LayerTreeHost对象的成员变量proxy_指向的是一个SingleThreadProxy对象,LayerTreeHost类的成员函数Composite调用这个SingleThreadProxy对象的成员函数CompositeImmediately绘制Browser端的CC Layer Tree,如下所示:

void SingleThreadProxy::CompositeImmediately(base::TimeTicks frame_begin_time) {
  ......

  LayerTreeHostImpl::FrameData frame;
  if (DoComposite(frame_begin_time, &frame)) {
    {
      ......
    }
    ......
  }
}
       这个函数定义在文件external/chromium_org/cc/trees/single_thread_proxy.cc中。

       SingleThreadProxy类的成员函数CompositeImmediately主要是调用另外一个成员函数DoComposite绘制Browser端的CC Layer Tree,如下所示:

bool SingleThreadProxy::DoComposite(
    base::TimeTicks frame_begin_time,
    LayerTreeHostImpl::FrameData* frame) {
  ......

  bool lost_output_surface = false;
  {
    ......

    if (!layer_tree_host_impl_->IsContextLost()) {
      layer_tree_host_impl_->PrepareToDraw(frame);
      layer_tree_host_impl_->DrawLayers(frame, frame_begin_time);
      ......
    }
    
    ......
  }

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

       从前面Android WebView执行GPU命令的过程分析一文可以知道,SingleThreadProxy类的成员变量layer_tree_host_impl_指向的是一个LayerTreeHostImpl对象。SingleThreadProxy类的成员函数DoComposite主要是调用这个LayerTreeHostImpl对象的成员函数PrepareToDraw和DrawLayers绘制Browser端的CC Layer Tree。

       LayerTreeHostImpl类的成员函数PrepareToDraw和DrawLayers绘制CC Layer Tree的过程可以参考前面Chromium硬件加速渲染的UI合成过程分析一文。从前面Chromium硬件加速渲染的UI合成过程分析一文我们还可以知道,Chromium的Browser端在内部是通过一个Direct Renderer绘制CC Layer Tree的,而Render端是通过一个Delegated Renderer绘制CC Layer Tree的。Delegated Renderer并不是真的绘制CC Layer Tree,而只是将CC Layer Tree的绘制命令收集起来,放在一个Compositor Frame中。这个Compositor Frame最终会交给Browser端的Direct Renderer处理。Direct Renderer直接调用OpenGL函数执行保存在Compositor Frame的绘制命令。因此,当Browser端绘制完成自己的CC Layer Tree之后,加载在Android WebView中的网页UI就会合成显示在App的窗口中了。

      至此,我们就分析完成了Android WebView硬件加速渲染网页UI的过程,也完成了对Android基于Chromium实现的WebView的学习。重新学习可以参考Android WebView简要介绍和学习计划一文。更多的学习信息可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/12/19 0:58:18 原文链接
阅读:38367 评论:5 查看评论

《Android系统源代码情景分析》连载回忆录:灵感之源

$
0
0

       上个月,在花了一年半时间之后,写了55篇文章,分析完成了Chromium在Android上的实现,以及Android基于Chromium实现的WebView。学到了很多东西,不过也挺累的,平均不到两个星期一篇文章。本来想休息一段时间后,再继续分析Chromium使用的JS引擎V8。不过某天晚上,躺在床上睡不着,鬼使神差想着去创建一个个人站点,用来连载《Android系统源代码情景分析》一书的内容。

       事情是这样的,躺在床上睡不着,就去申请了一个域名,0xcc0xcd.com。域名申请到了,总不能不用吧。用来做什么呢?想起我写的那本书《Android系统源代码情景分析》,从2012年10月出版至今,也有四年多的时间了,得到了大家的厚受。不过网络上也逐渐的出现了一些盗版PDF。不用说,质量肯定很差。干脆我把这本书的内容在我的个人站点上放出来吧。后面征得了出版社的同意,就着手开始干了。

       网站名称为“进击的程序员”,主要是为了配合0xcc0xcd.com这个域名。从Windows时代过来的老司机可能一眼就能看出这个域名是什么意思。看不懂的,如果大家有兴趣,后面我也可以详细说说,怀念一下逝去的青春。

       从开始有想法,到把网站建好,以及将书前三章(准备知识、硬件抽象层、智能指针)的内容放上去,花了不到一个月的时间。在这不到一个月的时间里,学习到了挺多东西:申请域名、云服务器、域名解析、域名邮箱、网站备案以及开发网站等等。因为我一直都是做客户端开发,刚毕业几年做的是Windows客户端,后面做的是Android端,没有做过网站相关的开发,包含前端和后端,所以学习过程还是有些小波折。不过总体上来说还是比较顺利的。这也跟网站的技术选型有关吧。

       现在不是提倡做全栈工程师吗?这个建站过程也算是小小地实践了一把。怕时间久了会忘记一些关键细节和踩过的坑,所以就计划把建站连载书的过程记录下来。也希望能够帮助到有兴趣做全栈工程师的同学们。

       网站使用的是LNMP架构,如下图1所示:


图1 进击的程序员网站架构

       网站运行在云服务器上,系统装的是Ubuntu 14.04,除了Nginx、PHP和MySQL,还搭了一个GIT仓库,用来管理网站源码。这个GIT仓库除了用来管理网站源码,还用来将源码分布到网站中去。

       具体是这样的,在本地用自己的电脑开发网站(其实就是用vim编辑网页和PHP)。测试没有问题之后,就用git push命令将源码上传到GIT仓库。然后再登录到云服务器上,在网站根目录用git pull命令从GIT仓库中获得最新网站源码。

       此外,在本地还搭建了一个管理后台。这个管理后台就是用来给管理员管理网站的。主要就是操作一下数据库,例如查看数据、插入数据、更新数据等等。正规的网站会专门提供一些页面供管理员操作。鉴于这个网站不是很正规,管理员又是一个技术控,于是就直接使用Python脚本来实现这个管理后台了,想要什么功能就直接写个脚本。

      Oracle提供了一个Python版的MySQL数据库驱动库MySQL Connector/Python,通过它很容易用Python脚本操作MySQL中的数据。这样一个简单的管理后台就搭建起来了。

      整个网站的架构非常简单,可以非常快上手,同时它又五脏俱全。网站的前端主要用Ajax、jQuery开发,后端没有用什么高大尚的框架,基本上是徒手写的PHP。主要是考虑这个网站要做的事情很简单,就是连载《Android系统源代码情景分析》的内容,基本功能就是浏览和评论。所以就以最简单最快的方式实现。

      为了让大家利用碎片时间更好地阅读书的内容,网站在提供PC版的同时,也提供了移动版。移动版和PC版的功能是一样的,只是它们的页面表现形式不一样。所以网站在设计之初,就考虑了模块化和代码复用,用最小的成本获得同时实现PC端和移动端的功能。

      不知道为什么,说起PHP, 总是会想起“PHP是最好的语言”这句话。从这一个月的经历看,PHP是不是最好的语言不知道,但是用来建网站,PHP的确是最好的语言。用PHP和JS开发网站,效率比用Java/OC开发App,高多了。不过,网站的体验不如App。所以移动开发目前还是王道。

       接下来,我会用一个系列的文章分享整个建站过程,包括:

       1. 域名、云服务器、域名解析、网站备案、域名邮箱、CA证书申请

       2. LNMP开发环境搭建,包括如何配置SSL加密的HTTPS站点

       3. 支持SSH访问的GIT仓库搭建

       4. 网站基本功能开发,PC版和移动版代码复用

       5. 基于MySQL Connector/Python的管理后台开发

       欢迎大家关注!想在线阅读《Android系统源代码情景分析》一书的,点击进击的程序员进入!

作者:Luoshengyang 发表于2017/1/10 22:42:11 原文链接
阅读:45244 评论:28 查看评论

react native学习笔记17——存储篇(2)SQLite

$
0
0

前言

对于存放数据量小且简易的数据我们可以通过AsyncStorage来存储,但对于数据结构复杂、数据量大的数据,我们可以使用移动开发中常用的SQLite来处理。
SQLite是一种轻型的数据库,多用于移动端开发,在原生应用开发中比较常见。

使用

React Native并没有提供使用sqlite的组件,我们可以通过使用第三方组件react-native-sqlite来使用原生的SQLiteDatabase。

Android版配置

1. 安装

在项目根目录下执行cmd命令:

npm install --save react-native-sqlite-storage

2. 修改settings.gradle配置

修改android/settings.gradle的配置

...

include ':react-native-sqlite-storage'
project(':react-native-sqlite-storage').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sqlite-storage/src/android')

3. 修改build.gradle配置

修改android/app/build.gradle的配置

...

dependencies {
    ...
    compile project(':react-native-sqlite-storage')
}

4. 在MainApplication.java注册模块

修改android/app/src/main/java/com/[YourAppName]/MainApplication.java的配置

import org.pgsqlite.SQLitePluginPackage;

public class MainApplication extends Application implements ReactApplication {
  ......

  /**
   * A list of packages used by the app. If the app uses additional views
   * or modules besides the default ones, add more packages here.
   */
    @Override
    protected List<ReactPackage> getPackages() {
      return Arrays.<ReactPackage>asList(
        new SQLitePluginPackage(),   // register SQLite Plugin here
        new MainReactPackage());
    }
}

iOS版配置

1. 安装

在项目根目录下执行cmd命令:

npm install --save react-native-sqlite-storage

2. XCode SQLite项目依赖安装

将SQLite项目作为一个库进行依赖到当前项目,如下:

3.XCode SQLite库依赖

将libSQLite.a添加到Libraries and Frameworks中,同时添加sqlite3.0.tbd (XCode 7) 或者libsqlite3.0.dylib (XCode 6 and earlier)到当前位置:

使用实例

新建一个数据库的工具组件SQLite.js,该模块类似于工具类,用于数据库的建表、增删改查等操作,不需要渲染任何界面,所以render return null。

import React from 'react';
import SQLiteStorage from 'react-native-sqlite-storage';

SQLiteStorage.DEBUG(true);
SQLiteStorage.DEBUG(true);
var database_name = "test.db";//数据库文件
var database_version = "1.0";//版本号
var database_displayname = "MySQLite";
var database_size = -1;
var db;
export default class SQLite extends Component {
  render(){
        return null;
    }
}

创建数据表
这里创建了一个用户信息表,表结构如下:

字段 类型 描述
id INTEGER 主键
name VARCHAR 姓名
age VARCHAR 年龄
sex VARCHAR 性别
phone VARCHAR 电话号码
email VARCHAR 邮箱
address VARCHAR 地址
 createTable(){
        if (!db) {
            this.open();
        }
        //创建用户表
        db.transaction((tx)=> {
            tx.executeSql('CREATE TABLE IF NOT EXISTS USER(' +
                'id INTEGER PRIMARY KEY  AUTOINCREMENT,' +
                'name VARCHAR,'+
                'age VARCHAR,' +
                'sex VARCHAR,' +
                'phone VARCHAR,' +
                'email VARCHAR,' +
                'address VARCHAR)'
                , [], ()=> {
                    this._successCB('executeSql');
                }, (err)=> {
                    this._errorCB('executeSql', err);
                });
        }, (err)=> {//所有的 transaction都应该有错误的回调方法,在方法里面打印异常信息,不然你可能不会知道哪里出错了。
            this._errorCB('transaction', err);
        }, ()=> {
            this._successCB('transaction');
        })
    }

定义打开数据open和关闭数据库close的方法

    open(){
        db = SQLiteStorage.openDatabase(
            database_name,
            database_version,
            database_displayname,
            database_size,
            ()=>{
                this._successCB('open');
            },
            (err)=>{
                this._errorCB('open',err);
            });
        return db;
    }
    close(){
        if(db){
            this._successCB('close');
            db.close();
        }else {
            console.log("SQLiteStorage not open");
        }
        db = null;
    }
    _successCB(name){
        console.log("SQLiteStorage "+name+" success");
    }
    _errorCB(name, err){
        console.log("SQLiteStorage "+name);
        console.log(err);
    }

插入数据

insertUserData(userData){
        let len = userData.length;
        if (!db) {
            this.open();
        }
        this.createTable();
        this.deleteData();
        db.transaction((tx)=>{
            for(let i=0; i<len; i++){
                var user = userData[i];
                let name= user.name;
                let age = user.age;
                let sex = user.sex;
                let phone = user.phone;
                let email = user.email;
                let address = user.address;
                let sql = "INSERT INTO user(name,age,sex,phone,email,address)"+
                    "values(?,?,?,?,?,?)";
                tx.executeSql(sql,[name,age,sex,phone,email,address],()=>{

                    },(err)=>{
                        console.log(err);
                    }
                );
            }
        },(error)=>{
            this._errorCB('transaction', error);
        },()=>{
            this._successCB('transaction insert data');
        });
    }

删除数据

deleteData(){
        if (!db) {
            this.open();
        }
        db.transaction((tx)=>{
            tx.executeSql('delete from user',[],()=>{

            });
        });
    }

删除表

dropTable(){
        db.transaction((tx)=>{
            tx.executeSql('drop table user',[],()=>{

            });
        },(err)=>{
            this._errorCB('transaction', err);
        },()=>{
            this._successCB('transaction');
        });
    }

SQLite.js的完整代码如下:

//SQLite.js
import React, { Component } from 'react';
import {
    ToastAndroid,
} from 'react-native';
import SQLiteStorage from 'react-native-sqlite-storage';

SQLiteStorage.DEBUG(true);
var database_name = "test.db";//数据库文件
var database_version = "1.0";//版本号
var database_displayname = "MySQLite";
var database_size = -1;
var db;

export default class SQLite extends Component {

    componentWillUnmount(){
        if(db){
            this._successCB('close');
            db.close();
        }else {
            console.log("SQLiteStorage not open");
        }
    }
    open(){
        db = SQLiteStorage.openDatabase(
            database_name,
            database_version,
            database_displayname,
            database_size,
            ()=>{
                this._successCB('open');
            },
            (err)=>{
                this._errorCB('open',err);
            });
        return db;
    }
    createTable(){
        if (!db) {
            this.open();
        }
        //创建用户表
        db.transaction((tx)=> {
            tx.executeSql('CREATE TABLE IF NOT EXISTS USER(' +
                'id INTEGER PRIMARY KEY  AUTOINCREMENT,' +
                'name varchar,'+
                'age VARCHAR,' +
                'sex VARCHAR,' +
                'phone VARCHAR,' +
                'email VARCHAR,' +
                'address VARCHAR)'
                , [], ()=> {
                    this._successCB('executeSql');
                }, (err)=> {
                    this._errorCB('executeSql', err);
                });
        }, (err)=> {//所有的 transaction都应该有错误的回调方法,在方法里面打印异常信息,不然你可能不会知道哪里出错了。
            this._errorCB('transaction', err);
        }, ()=> {
            this._successCB('transaction');
        })
    }
    deleteData(){
        if (!db) {
            this.open();
        }
        db.transaction((tx)=>{
            tx.executeSql('delete from user',[],()=>{

            });
        });
    }
    dropTable(){
        db.transaction((tx)=>{
            tx.executeSql('drop table user',[],()=>{

            });
        },(err)=>{
            this._errorCB('transaction', err);
        },()=>{
            this._successCB('transaction');
        });
    }
    insertUserData(userData){
        let len = userData.length;
        if (!db) {
            this.open();
        }
        this.createTable();
        this.deleteData();
        db.transaction((tx)=>{
            for(let i=0; i<len; i++){
                var user = userData[i];
                let name= user.name;
                let age = user.age;
                let sex = user.sex;
                let phone = user.phone;
                let email = user.email;
                let address = user.address;
                let sql = "INSERT INTO user(name,age,sex,phone,email,address)"+
                    "values(?,?,?,?,?,?)";
                tx.executeSql(sql,[name,age,sex,phone,email,address],()=>{

                    },(err)=>{
                        console.log(err);
                    }
                );
            }
        },(error)=>{
            this._errorCB('transaction', error);
        },()=>{
            this._successCB('transaction insert data');
        });
    }
    close(){
        if(db){
            this._successCB('close');
            db.close();
        }else {
            console.log("SQLiteStorage not open");
        }
        db = null;
    }
    _successCB(name){
        console.log("SQLiteStorage "+name+" success");
    }
    _errorCB(name, err){
        console.log("SQLiteStorage "+name);
        console.log(err);
    }


    render(){
        return null;
    }
}

调用工具类
在同一目录下新建SQLiteDemo.js调用SQLite.js中封装好的方法,注意使用时先引入SQLite.js

import React, { Component } from 'react';
import {
    AppRegistry,
    Text,
    View,
} from 'react-native';
import SQLite from './SQLite';
var sqLite = new SQLite();
var db;
export default class SQLiteDemo extends Component{
    constructor(props) {
        super(props);
        this.state = {
            name:"",
            age:"",
            phone:"",
            email:"",
            address:"",
        };
    }

    compennetDidUnmount(){
        //关闭数据库
        sqLite.close();
    }
    componentWillMount(){
        //开启数据库
        if(!db){
            db = sqLite.open();
        }
        //建表
        sqLite.createTable();
        //删除数据
        sqLite.deleteData();

        //模拟数据
        var userData = [];
        var user = {};
        user.name = "Mr.Onion";
        user.age = "26";
        user.sex = "男";
        user.phone = "12345678910";
        user.email = "123454321@qq.com";
        user.address = "A市B街111号C室";
        userData.push(user);
        //插入数据
        sqLite.insertUserData(userData);
        //查询
        db.transaction((tx)=>{
            tx.executeSql("select * from user", [],(tx,results)=>{
                var len = results.rows.length;
                for(let i=0; i<len; i++){
                    var u = results.rows.item(i);
                    this.setState({
                        name:u.name,
                        age:u.age,
                        phone:u.phone,
                        email:u.email,
                        address:u.address,
                    });
                }
            });
        },(error)=>{
            console.log(error);
        });
    }
    render(){
        return (
            <View>
                <Text>
                    姓名:{this.state.name}
                </Text>
                <Text>
                    年龄:{this.state.age}
                </Text>
                <Text>
                    电话:{this.state.phone}
                </Text>
                <Text>
                    Email:{this.state.email}
                </Text>
                <Text>
                    地址:{this.state.address}
                </Text>
            </View>
        );
    }
}
作者:teagreen_red 发表于2017/11/15 18:53:00 原文链接
阅读:256 评论:0 查看评论

安卓自定义相机拍照功能全解

$
0
0

全栈工程师开发手册 (作者:栾鹏)

安卓教程全解

安卓实现一个相机的基本功能。

启动和释放相机

由于拍照功能一般需要实时预览,所以比较耗电,因此在窗口的恢复和暂停函数中需要启动和释放相机

  private Camera camera;
//重写窗口函数,重启摄像头
  @Override
  protected void onResume() {
    super.onResume();
    camera = Camera.open();   //打开相机
    init();
    Log.v("拍照", "启动摄像头");
  }
  //重写窗口函数,释放摄像头
  @Override
  protected void onPause() {
    super.onPause();
    camera.release();
    Log.v("拍照", "释放摄像头");
  }

实时预览

预览视图需要使用SurfaceView,因此在你的xml布局文件中要定义一个SurfaceView。
并且要实现SurfaceHolder.Callback预览回调接口,当然一般你看到的教程都是在当前activity中实现这个接口。

public class Camera_Activity extends Activity implements SurfaceHolder.Callback {

  private Camera camera;   //定义一个相机

@Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.camera_activity);

    SurfaceView surface = (SurfaceView)findViewById(R.id.surfaceView);
    SurfaceHolder holder = surface.getHolder();
    holder.addCallback(this);   //在当前窗口中实现回调接口
    holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    holder.setFixedSize(400, 300);
    }

 //实现接口函数
  public void surfaceCreated(SurfaceHolder holder) { 
    try {
      camera.setPreviewDisplay(holder);  //设置使用哪个SurfaceView来显示取景图片
      camera.startPreview();   //开始预览取景
      camera.startFaceDetection();   //启动人脸识别
      //必要时在预览上进行绘制
    } catch (IOException e) {
      Log.v("拍照", e.getMessage());
    }
  }
  //实现接口函数
  public void surfaceDestroyed(SurfaceHolder holder) {
    camera.stopFaceDetection();  //停止人脸识别检测
    camera.stopPreview();    //停止预览取景
  }
  //实现接口函数
  public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
  }

相机的基本操作函数

camera.setDisplayOrientation(90);//设置方向
camera.setErrorCallback(cb);   //设置错误回调函数
camera.setFaceDetectionListener(listener);  //设置人脸检测监听
camera.setPreviewCallback(cb); //设置预览回调
camera.takePicture(shutter, raw, jpeg);  //拍照
camera.autoFocus(cb);  //自动聚焦
camera.setPreviewDisplay(holder);  //设置预览视图对象
camera.startPreview();//启动预览
camera.startFaceDetection();//启动人脸识别
camera.stopFaceDetection();//停止人脸识别
camera.stopPreview();  //停止预览
camera.getParameters();  //获取参数
camera.setParameters(params); //设置参数

拍照

拍照函数使用takePicture,需要传输的参数分别是快门按下回调函数,获得拍照的原始图片数据后的回调函数,获得压缩后的jpeg图片数据回调函数。

//拍照函数
  private void takePicture() {
    camera.takePicture(shutterCallback, rawCallback, jpegCallback);   //拍照,参数:快门函数、获取原始图片函数、获取压缩后图片函数
  }

  ShutterCallback shutterCallback = new ShutterCallback() {
    public void onShutter() {
      //快门关闭时执行的函数
        Log.v("拍照", "快门关闭");
    }
  };

  //拍照后raw原始图像数据
  PictureCallback rawCallback = new PictureCallback() {
    public void onPictureTaken(byte[] data, Camera camera) {
      //对图像的原始数据做一些处理
        Log.v("拍照", "获取拍照后的原始数据");
    }
  };

  //拍照后jpeg编码图像数据
  PictureCallback jpegCallback = new PictureCallback() {
    public void onPictureTaken(byte[] data, Camera camera) {
      // 将图像的jpeg数据保存到sd卡中
      FileOutputStream outStream = null;
      try {
        String path = Environment.getExternalStorageDirectory() + "\test.jpg";

        outStream = new FileOutputStream(path);
        outStream.write(data);
        outStream.close();
        Log.v("拍照", "获取拍照后的jpeg压缩数据");
      } catch (FileNotFoundException e) {
        Log.v("拍照", "没有发现文件", e);
      } catch (IOException e) {
        Log.v("拍照", "IO接口出错", e);
      }
    }
  };

自动聚焦

设置自动聚焦使用autoFocus函数,参数为聚焦完成回调函数,包含聚焦成功或失败。

  //聚焦函数
  private void focus()
  {
      camera.autoFocus(mycallfun);
  }
  //自定义聚焦完成回调函数
  AutoFocusCallback mycallfun = new AutoFocusCallback(){
      @Override
      public void onAutoFocus(boolean success, Camera camera)
      {
          if (success)
          {
              // success为true表示对焦成功,改变对焦状态图像
              Log.v("拍照", "聚焦成功");
          }
      }
  };

读取并修改EXIF数据

拍照后获取的jpeg图片,不仅包含像素信息,还包含拍摄日期,时间,摄像头设置,图片设置,以及图像描述和位置等信息。这些信息被叫做EXIF数据

  private void modifyExif() {
    File file = new File(Environment.getExternalStorageDirectory(),"test.jpg");

    try {
      ExifInterface exif = new ExifInterface(file.getCanonicalPath());
      //读取摄像头模型和位置属性
      String model = exif.getAttribute(ExifInterface.TAG_MODEL);
      Log.v("拍照", "Model: " + model);
      //设置摄像头的品牌
      exif.setAttribute(ExifInterface.TAG_MAKE, "My Phone");
    } catch (IOException e) {
      Log.v("拍照", "IO Exception", e);
    }
  }
作者:luanpeng825485697 发表于2017/11/15 20:45:44 原文链接
阅读:288 评论:0 查看评论

安卓自定义相机录像功能全解

$
0
0

全栈工程师开发手册 (作者:栾鹏)

安卓教程全解

安卓录制视频需要使用MediaRecorder来完成。并且在录制中一般还需要SurfaceHolder进行实时预览,所以在布局文件中进行预览的SurfaceView控件

启动和释放相机

 private Camera camera;

//重写窗口函数,启动摄像头,并直接设置到录像模式
  @Override
  protected void onResume() {
    super.onResume();
    camera = Camera.open();        //启动相机
    camera.setDisplayOrientation(90);   //设置方向
    //使用摄像头录制提示,告诉摄像头你只想录制音频/视频,而不是拍摄静态图片,可以缩短启动时间
    Camera.Parameters parameters = camera.getParameters();
    parameters.setRecordingHint(true);
    camera.setParameters(parameters);
  }

  //重写窗口函数,释放 mediaRecorder
  @Override
  protected void onPause() {
    super.onPause();
    //重置和释放 mediaRecorder
    mediaRecorder.reset();
    mediaRecorder.release();
    camera.lock();

    //释放摄像头
    camera.release();
  }

MediaRecorder准备工作、SurfaceView预览录像

由于MediaRecorder负责接收相机数据源。与拍照预览不同,要实现录像的实时预览,必须将SurfaceView绑定到MediaRecorder的数据流中,而不是绑定到相机。

所以实现SurfaceHolder.Callback接口函数surfaceCreated执行以后才可以创建MediaRecorder,并将其绑定到相机,设置视频源、音频源、画质,输出地址等参数,在设置SurfaceView为预览控件,并准备录制

public class VideoCameraActivity extends Activity implements SurfaceHolder.Callback {
 private Camera camera;
  private SurfaceHolder holder;
  private MediaRecorder mediaRecorder;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.videocamera);

    SurfaceView surface = (SurfaceView)findViewById(R.id.video_surfaceView);
    SurfaceHolder holder = surface.getHolder();
    holder.addCallback(this);
    holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    holder.setFixedSize(400, 300);
    }

//创建MediaRecorder绑定到相机,设置视频源、音频源、画质,输出地址等参数,并准备录制
  private void prepareVideoCamera() throws IllegalStateException, IOException {
    //创建一个新的MediaRecorder
    mediaRecorder = new MediaRecorder();

    //录制前的准备工作

    //解锁摄像头并将其分配给MediaRecorder
    camera.unlock();
    mediaRecorder.setCamera(camera);

    //指定用于录制的输入源
    mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
    mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);

    //设置配置文件,或者定义输出格式,音频视频编码器,帧速以及输出尺寸
    CamcorderProfile profile = null;

    if (CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_1080P))
      profile = CamcorderProfile.get(CamcorderProfile.QUALITY_1080P);
    else if (CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_720P))
      profile = CamcorderProfile.get(CamcorderProfile.QUALITY_720P);
    else if (CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_480P))
      profile = CamcorderProfile.get(CamcorderProfile.QUALITY_480P);
    else if (CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_HIGH))
      profile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH);

    if (profile != null)
      mediaRecorder.setProfile(profile); 

    //指定一个输出文件
    mediaRecorder.setOutputFile("/sdcard/myvideorecording.mp4");

    //预览视频流,在指定了录制源和输出文件后,在prepare前设置
    mediaRecorder.setPreviewDisplay(holder.getSurface());

    //准备录制
    mediaRecorder.prepare();
  }



  //接口函数
  public void surfaceCreated(SurfaceHolder holder) { 
    this.holder = holder;
    try {
      prepareVideoCamera();   //准备好MediaRecorder接收相机视频音频源
      Log.v("相机录像", "启动录制");
    } catch (IllegalStateException e) {
      Log.e("相机录像", "Illegal State Exception", e);
    } catch (IOException e) {
      Log.e("相机录像", "I/O Exception", e);
    }
  }

  //接口函数
  public void surfaceDestroyed(SurfaceHolder holder) {
    this.holder = null;
  }
  //接口函数
  public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
  }

}

mediaRecorder开始录制

准备工作做好以后,开始录制就非常简单了。


  //开始录制
  private void startRecording() {
    try {
      mediaRecorder.start();
      Log.v("相机录像", "开始录制");
    } catch (IllegalStateException e) {
      mediaRecorder.release();
      camera.lock();
      Log.d("相机录像", "Illegal State Exception", e);
    }
  }

mediaRecorder停止录制

  //停止录像
  private void stopRecording() {
    mediaRecorder.stop();    
    //重置和释放 mediaRecorder
    mediaRecorder.reset();
    mediaRecorder.release();
    camera.lock();
    Log.v("相机录像", "停止录制,文件已保存");
  }
作者:luanpeng825485697 发表于2017/11/15 20:47:57 原文链接
阅读:142 评论:0 查看评论

android调用dialog.hide()引起的输入事件派发错误问题追踪

$
0
0

image

问题描述:

某个界面启动后,上面的actionbar的item点击不起作用

问题调研:

00

在activity的启动过程中,创建了一个Fragment.java,在Fragment.java的createView回调中,调用了一个线程,线程中使用postUI调用dialog.show(),然后加载图片,如果没有图片,会postUi调用dialog.hide()隐藏,之后activity上面的actionbar Item点击没响应。

初步怀疑,是由于Fragment.java的写法有误,导致没有调用onCreateOptionsMenu,引起onOptionsItemSelected没有响应。但是通过断点跟踪,发现不是,这里的onCreateOptionsMenu调用了。按照网上的说法是加入setHasOptionsMenu( true );,查看代码是有此逻辑,因此可以确定,这块添加的代码是没有问题的。

于是上断点,调试DecorView.java的dispatchTouchEvent方法,为什么调试的是DecorView.java呢?因为我们activity在使用setContentView将一个布局加载起来时候,实际挂在DecorView的目录树里,因此这里便是事件的分派地方,当然,如果要说activity和inputmanager的消息传递位置,会在ViewRootImpl.java的onInputEvent方法里面。

image

我们在DecorView.java的dispatchTouchEvent方法打上断点,然后点击actionbar的item,然后发现这里的信息

image

发现这里的cb是个ProcessDialog,于是得出结论,这个当前屏幕上虽然看不到对话框(使用hide()隐藏掉),但是inputmanager那边,却还是将此事件传递给了它,所以初步结论,focus window出现错误,导致事件派发错误,引出问题。

那么,我们继续深究,从inputmanager这里,先进行一个初步判断
电脑连上手机,使用 adb shell dumpsys >~/1.txt 将dump信息存储下来,然后打开1.txt
搜索

Input Dispatcher State:

image

这里可以找到input可以传递的一个窗口列表
这里关键的几个信息:

FocusedApplication :

当前焦点app

FocusedWindow: name=’Window{f8c1e72 u0 com.codegg.fba/com.codegg.fba.activity.romListActivity}’

当前focus的窗口信息

后面紧跟着一堆窗口列表:

image

列表的一些信息:

name=

‘Window{1781b28 u0 com.codegg.fba/com.codegg.fba.activity.romListActivity}’

窗口名字,以及内存地址,title

displayId=0 

显示在哪个屏幕id上,默认为0,可以是其他,比如我们投屏到电视,或者模拟虚拟的屏幕上。

hasFocus=false

是否获取焦点

visible=true 

是否可见

canReceiveKeys=false

是否处理按键消息

layer=21025

当前在绘制里面的层大小,这个值越大,代表z序列越高,屏幕显示是按照z排序进行绘制,从低向高,如果高的layer是个全屏,则会将低值的那些界面全部覆盖。

frame=[27,780][1053,1068]

此窗口在屏幕上的布局大小

touchableRegion=[0,0][1080,1920]

此窗口的可点击区域

然后我们查找代码,去看下输入服务那边,是如何判断发送给谁的呢?

image

我们找到

InputDispatcher.cpp

findTouchedWindowAtLocked

,可以看到,这里关键的信息是:

windowInfo->visible

,由于我们排列顺序是从前往后,因此第一个遍历到对话框窗口的时候,发现

windowInfo->visible=True

,因此系统会将触摸消息,发送给这个窗口,也就是对话框。然而,实际上对话框在apk这边,已经是隐藏状态,同时自身也不消耗触摸事件,因此导致事件一直发给一个隐藏的窗口,引出问题。

01

到这里,就完了?那你还是比较年轻。虽然最终的解决方案是使用dismiss替换掉了hide,但是我们不能停留在这个表象,继续深挖下此问题。问题最终的解决,只是规避了出现此问题,但是最根本的原因,我们还需要继续寻找。

我们知道了这里有个mWindowHandles列表存储了当前的窗口,并且已经排序,那么我们找下,这个值是谁给的,因此我们在本文件查找,发现了关键方法setInputWindows,image

这里会将窗口赋值进来。然后我们全局搜索

setInputWindows

,最终在

InputMonitor.java

updateInputWindowsLw

方法里面,锁定了关键逻辑。

updateInputWindowsLw

里面,我们发现了一段很关键的代码

image

这里有个方法

isVisible = child.isVisibleLw();

会去更新显示状态,我们之前看到,就是这个变量是Ture,导致系统认为我们的对话框是可见,引出的问题。

于是我们的重心,转移到了这里,我们看下代码:

image

我们主要关心

!mAnimatingExit && !mDestroying

这两个值(

其他本身也是要关注,但是因为已经跟过,知道他们不变,所以去掉了那些无关的变量

02

当前窗口的信息,这些变量如何得知的呢?我们来看个推演过程,我们之前使用adb shell dumpsys的文档,打开,

我们通过

Input Dispatcher State

,找到了当前focus的是romListActivity,但是显示的有两个,一个是activity的主窗口,一个是对话框的窗口,对话框的layer比activity的layer高,因此它优先得到了触摸响应。
具体对话框的信息如下:

image

我们使用这里的

name=’Window{1781b28 的1781b28

,在文本中搜索,可以找到window的详细信息:

image

mHasSurface=true
mPolicyVisibility =true
mAttachedHidden=false
mAnimatingExit=false
mDestroying=false
mIsWallpaper=false
mWallpaperVisible=xxx

关于这些值怎么算出来的,是通过这里的dump信息,我们找到windowState.java的dump,我们调用的dumpsys命令,会走到这里,

image

然后这里的dump方法有这段逻辑,通过查看,我们的dumpsys里面没有出现这些数据,因此它们的值就可以确定出来的。

03

当前情况,我们是没法知晓到底是哪个值引起的问题,然后如果我们直接去看代码,分析定位到底是哪个值引起,那你会崩溃掉的,系统里面,最不喜欢跟踪的就是显示隐藏,以及动画过程,太过杂乱,很多方法频繁调用,输出的log信息过多,逻辑错综复杂,很难把握,跟进这种问题,往往太耗精力。

我这里尝试使用demo来测试,写了如下代码:

image

也就是把出问题的那段逻辑,搬出来独立测试下,发现没有问题,这样子我们就可以进行对比了。然后通过

dumpsys

之后,发现了关键数据,在dump里面,出现了一些数据:

image

我们发现,这里的mDestroying=true,所以这时的dialog.hide ()之后,窗口就不会获取焦点,同时也不是显示状态,逻辑正常。

通过对比,我们发现线索,可以追踪

mDestroying

是何时进行更新,变成true的。

我们找了很多地方,同时在每个地方,进行添加log信息,然后抓取log。同时将Windowmanage的调试信息全部打开(将

WindowManagerDebugConfig.java

里面的所有变量为false全部置成true),然后编译mmm frameworks/base/services ,make snod打包,然后将system.img刷入手机,再次进行复现问题,同时抓取log,通过查阅log,可以得出结论,
系统在修改

mDestroying

的地方,最终锁定在

WindowStateAnimator.java

的finishExit方法中。

image

这条线追到这里,那么我们就在代码查找这个finishExit里面的 这段 finishExit in 信息,想从log信息中,找到一些蛛丝马迹。

image

搜索得到一些数据,我们可以使用后面 的

WindowStateAnimator{91b6679

这里的

91b6679

便是地址,那么我们从dumpsys里面,找到当前dialog窗口的动画地址,

91b6679

image

所以我们就可以锁定到我们 dialog窗口的动画是哪个log了。

我们继续查找,使用

91b6679

,发现了一段异常逻辑。

image

这里前面可以看到,对应的窗口已经在退出window{1781b28 u0 com.codegg.fba/com.codegg.fba.activity.romListActivity

EXITING

}

log中的

addInputWindowHandle

就是系统设置input信息的地方,可以确定这里这个对话框窗口已经在退出中

image

也就是

mAnimatingExit=true

,根据之前的

isVisibleUnchecked

逻辑可知,这里如果

mAnimatingExit=true

,那么

InputMonitor.java

里面的

updateInputWindowsLw

得到的

final boolean isVisible = child.isVisibleLw();

就是false了,也就是ok的了。
通过紧跟着的log继续去看,发现了出错地方:

Update reported visibility:
Win Window{f8c1e72 

这个窗口是activity的,问题点就在这里,这里会更新,让对应的

VIS AppWindowToken{2090d

显示出来,而我们的对话框,是在这个

VIS

AppWindowToken{2090d

里面的。因为它是activity的子窗口。

于是,紧跟着的log就出现了如下语句:
OPEN TRANSACTION handleAppTransitionReadyLocked()
performing show on: WindowStateAnimator{91b6679  我们的动画重新更新了,也就不退出来。

performShow on WindowStateAnimator{91b6679

performing show on: WindowStateAnimator{9e9f896

这里是我们的activity对应的动画。

performShow on WindowStateAnimator{9e9f896

出错就在这里。然后我们需要看下这个逻辑,是怎么出现的,通过定位代码,搜索关键字

handleAppTransitionReadyLocked

找到问题点。最终我们找到,代码在

WindowSurfacePlacer.java

的 

handleOpeningApps

方法里面。

image

同时我们在

handleAppTransitionReadyLocked

方法中,看到如下语句:

image

可以看到,这时我们的标志被清除掉了,引发了问题。

然后我们在

handleOpeningApps

里面,找到一段log文字

Now opening app

,通过检索log,对比正确与错误的log备份,发现了问题。

正确的:

9886 start u0
11790 relayout dialog viewVisibility=0
12828 relayout activity viewVisibility=0
14740 WindowSurfacePlacer: ** GOOD TO GO
14883 Now opening appAppWindowToken
14946 dialog handleOpeningApps
15133 activity handleOpeningApps
15691 realyout dialog viewVisibility=8

出问题的:

3018 start u0
9023 relayout dialog viewVisibility=0
11788 relayout activity viewVisibility=0
14912 relayout dialog viewVisibility=8
19169 WindowSurfacePlacer: ** GOOD TO GO
19337 Now opening app
19403  dialog  activity handleOpeningApps

出问题的时候,这个

handleOpeningApps

的调用时机,远远晚于了

dialog.hide

的过程,因此在后续更新activity的时候,意外的将其子窗口的动画进行了重置,引发此问题。

04

这里我们再进行扩展下:我们跟踪下dialog.hide()方法,可以看到这里只是简单的修改了根节点View的显示属性。

image

那么这个属性在哪里被检测到的呢?我们知道,每个activity对应一个ViewRootImpl,系统实时都会调用这里的

image

这里performTraversals里面有个方法,叫做        final int viewVisibility = getHostVisibility();会拿到刚才hide()设置的那个View的显示隐藏状态,如果发生改变,会调用这里的

image

然后这里的relayoutWindow实质的代码位置,在:

mWindowSession.relayout 

–>mService.relayoutWindow(Session.java)

–>relayoutWindow(WindowManagerService.java)

在这个方法里面,也输出来一段关键log,这里为Relayout …: viewVisibility= 我们可以使用: viewVisibility= 去搜索log,然后使用viewVisibility=8 进行过滤,因为8=View.GONE,从而可以得出,dialog.hide()真正被系统处理的时间。错误的时候,因为触发的时机过早,导致后续的activity还没open起来,子窗口却意外的要去隐藏,导致更新时错误,引发问题。

错误的时候
01-02 16:56:39.

790 

  982  2627 V WindowManager: Relayout Window{1781b28 u0 com.codegg.fba/com.codegg.fba.activity.romListActivity}:

viewVisibility=8

然后handleOpeningApps的时间
01-02 16:56:39.

956

   982  1270 I WindowManagerService:     at com.android.server.wm.WindowSurfacePlacer.

handleOpeningApps

(WindowSurfacePlacer.java:1246)
所以是在后面,导致dialog的hide被冲掉了。

正确的时候:(demo应用)
01-02 21:13:21.

580 

  982 11320 I WindowManagerService:     at com.android.server.wm.WindowSurfacePlacer

.handleOpeningApps

(WindowSurfacePlacer.java:1246)
然后才是隐藏:
01-02 21:13:26.

939

   982  7983 V WindowManager: Relayout Window{123729 u0 wwww}:

viewVisibility=8

req=1026x483 WM.LayoutParams{(0,0)(wrapxwrap) gr=#11 sim=#120 ty=2 fl=#1820002 fmt=-3 wanim=0x1030466 surfaceInsets=Rect(96, 96 - 96, 96) needsMenuKey=2}
这个就是正确的了,系统就会判断dialog的状态是销毁中,隐藏状态,未获取焦点,输入触摸事件,则会正确的传递给对应的activity。

此问题还没追踪结束,我们继续来看log,继续细化log,再次看下问题:

正确的:

9886 start u0

11040 WindowManager: handleMessage: entry what=2

就是 REPORT_FOCUS_CHANGE = 2

11790 relayout dialog viewVisibility=0

12828 relayout activity viewVisibility=0

14127  WindowManager: handleMessage: entry what=4  

就是  DO_TRAVERSAL = 4

这个4是关键

14740 WindowSurfacePlacer: ** GOOD TO GO

14883 Now opening appAppWindowToken

14946 dialog handleOpeningApps

15133 activity handleOpeningApps

15691 realyout dialog viewVisibility=8

出问题的:

3018 start u0

6627 WindowManager: handleMessage: entry what=2

就是 REPORT_FOCUS_CHANGE = 2聚焦到dialog

9023 relayout dialog viewVisibility=0

11788 relayout activity viewVisibility=0

12595 WindowManager: handleMessage: entry what=41

14912 relayout dialog viewVisibility=8

15576 WindowManager: handleMessage: entry what=2

就是 REPORT_FOCUS_CHANGE = 2切换到acitivty

18851 WindowManager: handleMessage: entry what=4  

就是  DO_TRAVERSAL = 4这个4是关键 ,同步更新

19169 WindowSurfacePlacer: ** GOOD TO GO

wtoken.clearAnimatingFlags();

将标识在这里清掉了,导致设置的隐藏状态消失。

19337 Now opening app

19403  dialog  activity handleOpeningApps

可以看到,同步的消息必须在隐藏前被调用一次,否则便会出错。这里的同步是在WindowSurfacePlacer.java代码里面

image

于是,我们又需要去检查,出错的时候,为什么

requestTraversal

方法,触发的时机慢了一些。或者说是hide()的处理时机,为什么超前了一些呢?

错误的:

72057 22:50:44.369 start u0

73349 01-03 22:50:44.646 24013 24050 I Thread xxx: run 0—-

277ms

75853 22:50:44.947 hide dialog  

586ms

01-03 22:50:44.947 24013 24013 I Thread xxx: run 1—-

76475 relayout dialog 隐藏

77317 22:50:45.078 finishDrawingWindow

709ms

正确的:

84501 22:55:47.726  start u0

87357 22:55:47.893 24439 24439 I Thread xxx: run 0—-

167ms

96824 ViewRootImpl[wwww]: FINISHED DRAWING: wwww

96843 22:55:48.427  finishDrawingWindow: Window{b8c0aef u0 wwww}  

701ms

98403 22:55:48.520 hide dialog 794ms 01-03 22:55:48.520 24439 24439 I Thread xxx: run 1—-

794ms

98841 handleOpeningApps  dialog

99776 relayout dialog 隐藏

从时间的log来看,我们发现绘制的时间是一致的 (

finishDrawingWindow

一个

701ms

一个

709ms)

,所以就可以得出了结论,确实是线程运行的时候,这个消息抛出的时间太早,引起这里的隐藏 在系统windowstate这里处理的出现了问题,引发故障。

05

总结:挖掘此问题,主要是要解决,到底我们输入出错后,该如何分析,主要抓住dumpsys信息,看焦点窗口到底在哪个上面,然后再去根据

handleOpeningApps

viewVisibility=  
finishExit in  
handleAppTransitionReadyLocked 

等一些关键log,去推断出逻辑,同时根据代码,去排查,最终锁定问题。

最终我们抽离出来错误代码:

image

这里差异就是,使用

MainActivity.this.runOnUiThread

和使用

view.post

的微小差别。
我们看下对应代码:

MainActivity.this.runOnUiThread

image

可以看到

Activity.runOnUiThread

里面,如果不在主线程,直接给主线程post一个消息action。
如果是在主线程,直接运行。我们这里不在主线程,是给主线程post了一个消息。

image

View.post

里面,可以看到如果attachInfo为空,就扔到一个队列里面,后续在

dispatchAttachedToWindow

回调中才取出来,所以就会将消息向后推迟一会,就是这一会,状态就OK的啦。

技术在于灵活使用,才能发挥巨大作用。

本文完。喜欢本文,分享给别人,喜欢代码GG,扫二维码,关注代码GG之家。

image

作者:a332324956 发表于2017/11/16 9:57:21 原文链接
阅读:203 评论:1 查看评论

Android 源码分析——阅读源码工具选用

$
0
0

此系列文章在 https://github.com/mzlogin/rtfsc-android 集中更新发布,欢迎感兴趣的童鞋们关注。

本篇原始链接:https://github.com/mzlogin/rtfsc-android/blob/master/0x001-tools.md

之前我写过一篇 搭建大型源码阅读环境——使用 OpenGrok,给大家介绍了一款开源的源码阅读工具的安装方法,实际到目前为止,OpenGrok 仍是我最喜爱的源码阅读工具之一。

本次 Android 源码阅读之旅,我也将继续使用 OpenGrok,有网络的时候使用在线的 http://androidxref.com/,离线时使用自己本地搭建的。

但同时在阅读 frameworks 源码时会首选使用 Chrome 插件 Insight.io for GitHub 来打开 GitHub 上的 frameworks base 镜像 android/platform_frameworks_base,它是一款强大的辅助工具,目录树、文件搜索、方法列表、定义跳转、引用查找等等都不在话下,要感受它的妙处可以安装起来体验,你一定会觉得值得拥有,先来几张图感受一下:

目录树、文件搜索:

file tree and search

方法列表、引用查找、弹出注释:

outline and reference

然后,找不到的定义去 OpenGrok,找不到的文件去 OpenGrok,双剑在手,天下我有……其实,也不是很爽,啥时候 Insight.io 有完整的 Android 源码就好了。

欢迎关注我的微信公众号,接收我的 Android 源码分析心得。

作者:mzlogin 发表于2017/11/16 13:53:13 原文链接
阅读:142 评论:0 查看评论

Android 应用内悬浮控件实践总结

$
0
0

在工作中遇到一个需求,需要在整个应用的上层悬浮显示控件,目标效果如下图:

这里写图片描述

首先想到的是申请悬浮窗权限,OK~ 打开搜索引擎,映入眼帘的并不是如何申请,而是“Android 悬浮窗权限各机型各系统适配大全、Android 绕过权限显示悬浮窗…”,为什么悬浮窗权限会有这么多坑呢?悬浮窗可以在桌面显示,被恶意软件用来偷偷弹广告怎么办?作为一个系统级别的特殊权限,这是它应有的高傲 - -

正确引导用户打开悬浮窗权限才是标准做法,若这就是定论的话这篇文章也没必要写了,我们绕过悬浮窗权限直接去显示,大多数是为了优化用户体验,并不是恶意的。有时我们只想在自己的应用内实现悬浮窗,然而 Andorid 并没有提供这样的方法,也只好退而求其此的去使用系统级别的悬浮窗权限。

OK ,既然可以绕过权限申请,再重新定义一下需求:

 尽量绕过申请权限,实现在 app 指定界面显示悬浮控件,控件的位置不需要改变

怎么绕过悬浮窗权限呢?网上大多数通过 WindowManager 添加一个 TYPE_TOAST 类型的控件,如下:

    WindowManager windowManager = (WindowManager) 
            applicationContext.getSystemService(Context.WINDOW_SERVICE);
    WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
    layoutParams.type = WindowManager.LayoutParams.TYPE_TOAST;
    windowManager.addView(view, layoutParams);

而系统在添加 TYPE_TOAST 类型控件时默认不需要权限,从而可以绕过悬浮窗权限。但是这种做法并不适配所有机型,比如我亲测过的小米(MIUI8) 和 Nexus 7.1.1 机型上就会报错 Permission Denial ,需要申请权限,之前这种方式或许可行,但现在肯定不行。

放弃 TYPE_TOAST 方案,不能往窗口里添加视图,那只能乖乖的申请权限了吗?这时你可能想到往所有 Activity 的固定位置添加视图,模拟“悬浮”效果,比如要实现文章开头的效果,只需要进入新 Activity 时初始化旋转的角度,让其在视觉上连续就行了。

但是要考虑一个问题,在切换 Activity 时旧 Activity 的悬浮控件是要销毁的,新 Activity 的悬浮控件是要生成的,也就是说在切换 Activity 时这个悬浮控件是会短暂的消失一下,那把 Activity 切换效果设置为淡入淡出可以吗,在视觉上是可以实现的,但是严格限制了 Activity 的切换效果,不可行。那还有什么方法可以实现切换 Activity 时控件在视觉上连续吗?如果你用过共享元素动画的话,便有答案了。

悬浮控件在哪里添加呢?可以在 BaseActivity 里,也可以为 Application 注册 Activity 生命周期回调,下面通过后者实现,在 Application 中为每个 Activity 添加悬浮控件:

public class BaseApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {

            @Override
            public void onActivityStarted(Activity activity) {
              if(findViewById(R.id.floating_view_id) != null) return;
              View view = LayoutInflater.from(activity).inflate(R.layout.floating_view, null);
              view.setId(R.id.floating_view_id);
              if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                  view.setTransitionName(activity.getString(R.string.transitionName));
              }
              WindowManager.LayoutParams params = new WindowManager.LayoutParams();
              params.gravity = Gravity.TOP | Gravity.LEFT;
              activity.addContentView(mPopView, mLayoutParams);
}

//省略...

切换 Activity 时启用共享元素动画:

   Intent intent = new Intent(this, Main2Activity.class);
   View view = findViewById(R.id.floating_view_id);
   if ( view != null) {
       ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(
               this,view, getString(R.string.transitionName));
       ContextCompat.startActivity(this, intent, options.toBundle());
   }else{
       startActivity(intent);
   }

这样就解决了切换 Activity 时悬浮控件短暂消失一下这个问题,然后在添加悬浮控件时,初始化旋转角度就可以实现文章开头的效果了。但是这种方式存在很大的缺陷,首先就是它不兼容 Andorid 5.0 以下,看看 4.4 那百分之十几的小伙伴,嗯~ 缺陷很大,其次还有一个致命缺陷,不管把悬浮控件设为 INVISIBLE 还是透明,只要已经添加了此控件,在切换时它都会先显示一下,这应该是共享元素动画本身的一个 BUG .

OK~ 放弃共享元素方案, 真的绕不过申请权限了吗? 再考虑一下 TYPE_TOAST 方案, 为什么它失效了呢? 应该是系统对此类型的控件加了限制, 对待 TYPE_TOAST 不再跳过检查权限步骤, 而是像 TYPE_PHONE 之类一视同仁, 那为什么我们的 toast 却可以跳过呢? toast 不就是 TYPE_TOAST 类型的视图吗? 不管如何, 反正 toast 是不需要权限的, 那就尝试从 toast 入手. OK~ ,现在的关键词是 自定义 toast .

查看 Toast 类源码, 有一个方法眼前一亮:

    /**
     * Set the view to show.
     * @see #getView
     */
    public void setView(View view) {
        mNextView = view;
    }

Toast 是可以自定义视图的, 这为自定义 toast 提供了可能性, 但是显示时长只能设置为 LENGTH_SHORT 或 LENGTH_LONG ,我们需要的是无限时长, 没有方法实现, 除非反射之类的怪招了~ 嗯~ 下面奉上通过反射实现无限时长 toast 的完整代码 :


/**
 * 自定义 toast , 无限时长
 * 可设置显示位置 尺寸
 */

class AlwaysShowToast  {


    private Toast toast;

    private Object mTN;
    private Method show;
    private Method hide;

    private int mWidth = WindowManager.LayoutParams.WRAP_CONTENT;
    private int mHeight = WindowManager.LayoutParams.WRAP_CONTENT;


    public FixedFloatToast(Context applicationContext) {
        toast = new Toast(applicationContext);
    }


    public void setView(View view, int width, int height) {
        mWidth = width;
        mHeight = height;
        setView(view);
    }


    public void setView(View view) {
        toast.setView(view);
        initTN();
    }


    public void setGravity(int gravity, int xOffset, int yOffset) {
        toast.setGravity(gravity, xOffset, yOffset);
    }


    public void show() {
        try {
            show.invoke(mTN);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    public void hide() {
        try {
            hide.invoke(mTN);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 利用反射设置 toast 参数
     */
    private void initTN() {
        try {
            Field tnField = toast.getClass().getDeclaredField("mTN");
            tnField.setAccessible(true);
            mTN = tnField.get(toast);
            show = mTN.getClass().getMethod("show");
            hide = mTN.getClass().getMethod("hide");

            Field tnParamsField = mTN.getClass().getDeclaredField("mParams");
            tnParamsField.setAccessible(true);
            WindowManager.LayoutParams params = (WindowManager.LayoutParams) tnParamsField.get(mTN);
            params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
            params.width = mWidth;
            params.height = mHeight;
            Field tnNextViewField = mTN.getClass().getDeclaredField("mNextView");
            tnNextViewField.setAccessible(true);
            tnNextViewField.set(mTN, toast.getView());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }


}

有了这个自定义 toast , 跳过权限显示悬浮窗就非常容易了, 理论上可以兼容任意版本,任意机型, 因为这只是一个普通的 toast , 系统没理由不允许一个 toast 显示的~ 然而… 亲测在 Nexus7.1.1 及以上不显示 , 在 Android 4.4 以下无法接受触摸事件, 在小米部分机型上无法改变位置.

OK~ 对比一下这些方案 :

方案1: 申请权限

   优点:实现简单,只要正确引导用户打开权限即可
   缺点:部分机型默认禁用; 需权限不友好

方案2: 每个界面添加,共享元素过渡

   优点:不需权限
   缺点:较复杂,只适用于5.0以上,且悬浮控件不可隐藏(共享元素会闪显控件)

方案3: TYPE_TOAST

   优点:实现简单
   缺点:小米(MIUI8)、7.1.1需要权限,4.4以下无法接受点击事件

方案4:自定义 toast

  优点:大部分机型不需权限,实现简单
  缺点:Nexus7.1.1及以上不显示,4.4以下无法接受点击事件,小米(MIUI8)及部分机型不可改变位置

结合我的需求, 我的悬浮控件并不需要改变位置, 所以最终选择方案为:

最终方案 : 7.0 以下采用自定义 toast, 7.1 及以上引导用户申请权限

如果你的需求也适合此方案的话, 告诉你个好消息, 我已经将此方案封装为可直接调用的库 : FixedFloatWindow , 即 fixed (位置固定的) float(悬浮) Window (窗), 可以很方便的使用 :

    FixedFloatWindow fixedFloatWindow = new FixedFloatWindow(getApplicationContext());
    fixedFloatWindow.setView(view);
    fixedFloatWindow.setGravity(Gravity.RIGHT | Gravity.TOP, 100, 150);
    fixedFloatWindow.show();
//   fixedFloatWindow.hide();

最后还有一个问题要解决, 我们要实现的是应用内悬浮控件 , 此方案应用退到后台后仍然可以在桌面显示 , 怎么控制呢? 我们可以记录当前 start 的 Activity 数量, 每当有 Activity stop 时, 便将此数量减 1 , 当此数量为 0 时表示应用退到后台 , 这时隐藏悬浮窗即可 , 类似于这样:

    @Override
    public void onActivityStarted(Activity activity) {
        mActivityNum++;
        if (isNeedShow(activity)) {
            show();
        }else{
            hide();
        }
    }

    @Override
    public void onActivityStopped(Activity activity) {
        mActivityNum--;
        if (mActivityNum == 0) {
            hide();
        }
    }

关于文章开头的实现效果就是用的这种方法, 将悬浮窗控制在应用内显示, 效果完整代码见 FixedFloatWindow 库 sample 示例 .

FixedFloatWindow 库地址: https://github.com/yhaolpz/FixedFloatWindow

参考文章:

Android应用内悬浮窗的实现方案

Android 悬浮窗权限各机型各系统适配大全

Android无需权限显示悬浮窗, 兼谈逆向分析app

突破小米悬浮窗权限控制–不需要权限的悬浮窗

作者:yhaolpz 发表于2017/11/16 19:33:11 原文链接
阅读:160 评论:1 查看评论

INSTALL_FAILED_TEST_ONLY 运行报错

$
0
0

INSTALL_FAILED_TEST_ONLY 运行报错

在使用Android Studio编译通过后,我们一般会运行程序来查看相应的效果。BUT,有的时候明明编译通过的程序却不能运行甚至是报错不能安装,比如下图所示:

这里写图片描述

这里报了一个错误:

INSTALL_FAILED_TEST_ONLY

按照字面意思:不允许我们调用者运行【测试版】的程序。

常规的原因以及解决办法有两个:

1,编译工具gradle使用了测试版本——XXX-alpha

buildscript {
    ...
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.0-alpha3'  
    }
    ...
}

很简单,我们只需要使用正式版的gradle编译运行程序即可。

如:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.0'
        ...
    }
}

2,AndroidMainfest有多余的语句

<appliaction
...
    android:testOnly="true"
...>
....
</application>

只要有【testOnly】属性,程序就不会被运行,无论是在真机还是模拟器。

解决的方法就是去掉这一句重新编译运行。

PS:
除了上述两个方法外,我们还有大招:

adb install -t *.apk

使用adb命令安装上述问题也能解决。
其实,这条命令的意思与方法二一样,也是:允许安装 AndroidManifest.xml 里 application 指定 android:testOnly=”true” 的应用

作者:xiaoyaozaimz 发表于2017/11/16 21:52:52 原文链接
阅读:172 评论:0 查看评论

Flutter插件一野狗云身份认证

$
0
0

Flutter插件一野狗云身份认证

pub package

使用野狗身份认证(Wilddog Auth)的Flutter插件。野狗云身份认证即Auth,用于帮助企业和开发者将野狗快速接入应用的身份认证系统,一次身份认证打通野狗所有产品。还可以用于增强已有帐户体系和简化新应用中账号系统的开发。

开发者使用Auth能让新应用避开从0开始的帐户系统开发,轻松搞定用户注册登录、用户信息存储。同时野狗采用行业标准的JWT格式对传输数据进行加密,有效提高帐号系统的安全性,防止用户信息泄漏。

注意:此插件还不是很完善,有些功能仍在开发中,如果你发现任何问题,请加入QQ群:271733776【Flutter程序员】,期待你的反馈。

安装与配置

打开野狗云官网,注册一个野狗云帐号,已有账号的直接登陆。

创建一个新Wilddog项目

在Flutter项目上配置Wilddog Sync的第一步是创建一个新的Wilddog项目,在浏览器中打开Wilddog控制台,选择“创建应用”,输入项目名称,然后单击“创建”。

这里写图片描述

Wilddog生成了一个App ID的字符串,这是Wilddog项目唯一ID,用于连接到刚创建的Wilddog服务。复制这个ID字符串值,下面在Android、iOS平台上配置Wilddog时需要用到这个值。

注意,新项目需要开启身份认证服务才能正常使用,不然会报无权限异常。

将插件添加到应用程序

将以下内容添加到的Flutter项目的pubspec.yaml文件中。

dependencies:
  wilddog_auth: "^0.0.2"

更新并保存此文件后,点击顶部的“Packages Get”,等待下载完成。打开main.dart文件,IntelliJ IDEA或其他编辑器可能会在上方显示一个提示,提醒我们重新加载pubspec.yaml文件,点击“Get dependencies”以检索其他软件包,并在Flutter项目中使用它们。

开发iOS必须使用macOS,而在macOS中,想要在Flutter应用程序中使用Flutter插件,需要安装homebrew,并打开终端运行以下命令来安装CocoaPods。

brew install cocoapods
pod setup

为Android配置Wilddog

启动Android Studio后选择项目的android文件夹,打开Flutter项目的Android部分,然后再打开“android/app/src/main/java/<项目名>”文件夹中的MainActivity.java文件,将Wilddog的初始化代码添加到文件中。

//...
import com.wilddog.wilddogcore.WilddogOptions;
import com.wilddog.wilddogcore.WilddogApp;

public class MainActivity extends FlutterActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    //...
    super.onCreate(savedInstanceState);
    WilddogOptions options = new WilddogOptions.Builder().setSyncUrl("https://<前面复制的AppID>.wilddogio.com/").build();
    WilddogApp.initializeApp(this, options);
    GeneratedPluginRegistrant.registerWith(this);
  }
}

注意,如果应用程序编译时出现文件重复导致的编译错误时,可以选择在android/app/build.gradle中添加“packagingOptions”。

android {
    ...
    packagingOptions {
        exclude 'META-INF/LICENSE'
        exclude 'META-INF/NOTICE'
    }
}

如果出现tools:replace=”android:label”的异常,在AndroidManifest.xml中添加下面的两行代码即可解决。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    +[xmlns:tools="http://schemas.android.com/tools"]
    package="com.hekaiyou.wilddogauthexample">

    <uses-permission android:name="android.permission.INTERNET"/>

    <application
        +[tools:replace="android:label"]
        android:name="io.flutter.app.FlutterApplication"

完成配置后,建议先在IntelliJ IDEA中执行一次项目,编译Android应用程序,以确保Flutter项目下载所有依赖文件。

为iOS配置Wilddog

在Flutter项目的ios目录下,使用Xcode打开“Runner.xcworkspace”文件。然后打开“ios/Runner”文件夹中的AppDelegate.m文件,将Wilddog的初始化代码添加到文件中。

//...
#import "Wilddog.h"
//...
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  //...
  WDGOptions *option = [[WDGOptions alloc] initWithSyncURL:@"https://<前面复制的AppID>.wilddogio.com/"];
  [WDGApp configureWithOptions:option];
  //...
}

完成配置后,建议先在IntelliJ IDEA中执行一次项目,编译iOS应用程序,以确保Flutter项目下载所有依赖文件。

使用与入门

要使用Flutter的平台插件,必须在Dart代码中导入对应包,使用以下代码导入wilddog_auth包。

import 'package:wilddog_auth/wilddog_auth.dart';

同时,要为应用程序提供默认的WilddogAuth类实例。

final WilddogAuth _auth = WilddogAuth.instance;

用户管理

用户生命周期包含以下三种状态:用户注册或登录成功、当前的Wilddog ID Token已刷新(重新进行身份认证)、退出登录。

匿名身份认证

实现匿名身份认证需要在“控制面板 身份认证—登录方式”中打开匿名登录方式。匿名登录的帐号数据将不会被保存,可以通过绑定邮箱认证或第三方认证方式将匿名帐号转成永久帐号。

final WilddogUser user = await _auth.signInAnonymously();

获取当前登录用户

Wilddog Auth中用户有一组基本属性:Wilddog ID、主邮箱地址、名称、照片地址,获取当前登录用户是管理用户的基础。

final WilddogUser user = await _auth.currentUser();

获取用户属性

通过返回的WilddogUser实例可以用于获取用户以下属性:

  • providerId:身份认证提供方ID(QQ、微信)
  • uid:Wilddog ID(用户唯一的标识)
  • displayName:用户名称
  • photoUrl:用户头像Url
  • email:电子邮箱地址
  • isAnonymous:是否匿名用户
  • isEmailVerified:电子邮箱是否已验证

更新用户属性

更新当前用户的昵称信息和头像URL。

await _auth.updateProfile(
  displayName: 'hekaiyou',
  photoURL: 'https://example.com/hekaiyou/profile.jpg'
);

更新用户认证密码

更新当前登录认证用户的密码信息。需要注意的是,要更新密码,该用户必须最近登录过(重新进行身份认证)。

await _auth.updatePassword('654321');

退出登录

登出当前用户,清除登录数据。

await _auth.signOut();

邮箱登录

实现邮箱登录需要在控制面板“身份认证—登录方式”中打开邮箱登录方式。

创建用户

用邮箱地址和密码创建一个新用户,创建一个新用户,创建成功后会自动登录。

final WilddogUser user = await _auth.createUserWithEmailAndPassword(
  email: 'hky2014@yeah.net',
  password: '123456',
);

登录用户

使用电子邮箱和密码登录。

final WilddogUser user = await _auth.signInWithEmailAndPassword(
  email: 'hky2014@yeah.net',
  password: '123456',
);

更新邮箱地址

更新帐号邮箱,如果更新成功,本地缓存也会刷新。如果这个邮箱已经创建过用户,则会更新失败。需要注意的是,要更新用户的邮箱地址,该用户必须最近登录过(重新进行身份认证)。

更新邮箱地址会向旧邮箱发送提醒邮件,在控制面板“身份认证—登录方式—邮箱登录—配置”中定制更新邮箱邮件模版。

await _auth.updateEmail('hky2014@yeah.net');

发送邮箱验证邮件

在控制面板“身份认证—登录方式—邮箱登录—配置”中定制邮箱验证邮件模版。

await _auth.sendEmailVerification();

发送重置密码邮件

在控制面板“身份认证—登录方式—邮箱登录—配置”中定制重置密码邮件模版。

await _auth.sendPasswordResetEmail('hky2014@yeah.net');

重新认证邮箱帐户

用户长时间未登录的情况下进行下列安全敏感操作会失败:删除帐户、设置主邮箱地址、更改密码。此时需要重新对用户进行身份认证。

await _auth.reauthenticateEmail(
  email: 'hky2014@yeah.net',
  password: '123456',
);

绑定邮箱认证方式

如果用户使用其他认证方式登录,可以将当前用户与给定的邮箱认证方式绑定,之后支持绑定的邮箱认证方式登录。(必须是未被使用的邮箱)

final WilddogUser user = await _auth.linkWithEmailAndPassword(
  email: 'hky2014@yeah.net',
  password: '123456',
);

手机登录

开发中

加入我们

如果你也想使用Flutter开发一个开源项目,欢迎加入Flutter开荒团,让我们一起开发Flutter的荒漠!(将你的GitHub账户名发到评论区或私信我,或者加入QQ群271733776联系我们,我会发送邀请邮件给你)

作者:hekaiyou 发表于2017/11/16 23:22:07 原文链接
阅读:698 评论:0 查看评论

阅读源码的意义与方法

$
0
0

此系列文章在 https://github.com/mzlogin/rtfsc-android 集中更新发布,欢迎感兴趣的童鞋们关注。

本篇原始链接 https://github.com/mzlogin/rtfsc-android/blob/master/0x002-methods-and-meaning.md

思索了这两个问题良久,也去知乎找了一些相关话题的问答,但并没有标准答案。所以,我这里也只是记录一些我对此的看法,也许会随着 RTFSC 阅历的丰富而发生变化,我会记录更新于 https://github.com/mzlogin/rtfsc-android

意义

在我看来,阅读源码的意义在于学习优秀的「套路」。

这里的「套路」所指范围很广,大到架构设计,小到可取的命名风格,还有设计模式、实现某类功能使用到的数据结构和算法等等。所谓高手,其实就是能比大部分人更早更快地掌握套路并熟练运用之人。

埋头在自己的天地里耕芸固然也能逐渐进步和成长,但总会有时候会遇到一些场景,你苦思良久也无法做出良好的设计,总会有一些时候,纠结如何为一个变量命名让你停下飞速敲击的手指。这些令你为难的场景,先贤们也许早就遇到过,并且给出了优雅的解决方案。看优秀的源码的时候,将这样的场景与对应的方案收入囊中,或者仅仅在脑中留下一个印象也好,以便在需要的时候,你的武器库里总能掏出一把称手的家伙来。

一些方法

不应该这样

不应该漫无目的地随手拿起一分源码,试图去通读。这一方面会过目即忘无所收获,另一方面会枯燥得让你迅速从着手到放弃。学习的方式有很多种,阅读源码并不一定是最适合你当前的情况的。

应该这样

  1. 精心挑选要阅读的源码项目。

    这最好是与你的编程语言、你的工作内容、你的兴趣所在相关的,这样才能更切实地感受到阅读源码给你带来的益处,更有动力继续。

  2. 如果你想学习的知识点有官方文档,先看文档再看源码。

    直接从源码着手,搞清楚原理固然是好,但是源码有可能是难啃的,先熟悉官方提供给所有人看的文档,能较为平滑地对这方面的知识先有个大概的了解,然后再结合源码去深入。

  3. 提出具体的问题,然后带着问题到源码中找答案。

    比如在使用 Toast 的过程中,你可能会想到一些问题:Toast.makeText(...).show() 时发生了什么?Toast 能不能在非 UI 线程调用?能不能自定义 Toast 布局?诸如此类。在源码中探寻完你想要的答案,你的目的也就达到了。

  4. 从一些共性层面入手。

    大部分的程序里都会使用到的东西,比如线程模型、UI 组织结构、任务调度方式等等。针对某一个方面去了解,比漫无目的要有效率得多。

  5. 最好能够编译运行起来。

    如果一份代码你只能看不能跑,那可能读到一些地方你只能猜这个地方的数据值和跳转结构是怎么样的,而很有可能你猜的是错的。但如果你能编译运行,那在需要的时候你可以修改,加日志等等来更好地观察和验证你的想法,得到正确的理解。

  6. 做一些笔记。

    一方面是将你的学习成果保留下来,方便随时查阅,毕竟只凭脑子记忆是不靠谱的;另一方面在学习的过程中,也能帮助理解。

一些小技巧

这部分是在撰写本系列的过程中,发现和收集的一些个人觉得比较有用的小技巧,不断更新中……

逻辑追踪篇

  1. 查看符号、关键字符串被引用和出现的地方;

  2. 根据方法调用一层一层追踪;

  3. 使用 git blame 查看关键代码行的提交日志,里面也许包含有用的信息;

  4. 形如 IXxxManager 这类接口的实现类要么是 XxxManagerService,要么是 XxxManagerImpl;

欢迎关注我的微信公众号,接收我的 Android 源码分析心得。

作者:mzlogin 发表于2017/11/17 8:13:12 原文链接
阅读:108 评论:0 查看评论

Unity3D - Shader - 实现漫反射光照模型

$
0
0

计算公式

基本光照模型中漫反射部分的计算公式:

  • C(diffuse) = C(light) * M(diffuse) * max (0, n’ * l’)
  • C(light) = 入射光线的颜色和强度
  • M(diffuse) = 材质的漫反射系数
  • n’ = 表面法线
  • l’ = 光源方向

为防止点积结果为负值,需要使用max操作,保证结果在(0,1)内。

逐顶点实现

以下是漫反射部分计算的Shader(逐顶点实现):

Shader "Unity Shaders Book/Chapter 6/Diffuse Vertex-Level" {
    Properties {
        // 用于控制材质的漫反射颜色 初始值为白色
        _Diffuse ("Diffuse", Color) = (1,1,1,1)
    }

    SubShader {
        Pass {
            // 指明该Pass的光照模式
            // 只有定义了正确的LightMode才能在Unity中得到一些内置光照变量
            // 例如_LightColor()
            Tags {"LightMode" = "ForwardBase"}

            CGPROGRAM

            // 告诉Unity顶点着色器与片元着色器的名字
            #pragma vertex vert
            #pragma fragment frag

            // Unity内置文件
            #include "Lighting.cginc"

            // 为了使用Properties语义块中声明的属性,需要定义
            // 一个和该属性类型相匹配的变量
            // 材质的漫反射属性
            fixed4 _Diffuse;

            // 顶点着色器的输入结构体
            struct a2v {
                // 顶点位置
                float4 vertex : POSITION;
                // 顶点法线
                float3 normal : NORMAL;
            };

            // 顶点周色漆的输出结构体
            struct v2f {
                float4 pos : SV_POSITION;
                fixed3 color : COLOR;
            };

            // 顶点着色器
            // 实现一个逐顶点的漫反射光照
            v2f vert(a2v v) {
                v2f o;

                // Transform the vertex from object space to projection space
                // mul(UNITY_MATRIX_MVP, v.vertex)
                // 把顶点位置从模型空间转换到裁剪空间
                o.pos = UnityObjectToClipPos(v.vertex);

                // Get ambient term
                // 通过Unity内置变量得到环境光
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                // Transform the normal frame object space to world space
                // _World2Object = unity_WorldToObject
                // v.normal是模型空间下的,需要把法线转换到世界空间中。
                // 需要使用原变换矩阵的逆转置矩阵来变换法线就可以得到正确的世界空间结果
                // 模型空间到世界空间的变换矩阵的逆矩阵 = _WorldToObject
                // 调换mul函数中的位置得到和转置矩阵相同的乘法
                fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));

                // Get the light direction in world space
                // 规范化光源方向
                fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);

                // Compute diffuse term
                // 计算漫反射:漫反射 = (入射光线的颜色和强度 * 材质的漫反射颜色) * max(表面法线 * 光源方向)
                // 材质漫反射颜色 = _Diffuse.rgb
                // 光源颜色和强度信息 = _LightColor0.rgb
                // 光源方向 = _WorldSpaceLightPos0.xyz
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));

                // 环境光添加到输出颜色上
                o.color = ambient + diffuse;

                return o;
            }

            fixed4 frag(v2f i) : SV_Target {
                return fixed4(i.color, 1.0);
            }

            ENDCG
        }
    }
    Fallback "Diffuse"
}

逐像素实现

逐像素实现可以得到更加平滑的光照效果。

逐像素实现:

Shader "Unity Shaders Book/Chapter 6/Diffuse Pixel-Level" {
    Properties {
        _Diffuse ("Diffuse", Color) = (1,1,1,1)
    }

    SubShader {
        Pass {
            Tags {"LightMode" = "ForwardBase"}

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Diffuse;

            struct a2v {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                fixed3 worldNormal : TEXCOORD0;
            };

            // 顶点着色器
            // 实现一个逐顶点的漫反射光照
            v2f vert(a2v v) {
                v2f o;

                // Transform the vertex from object space to projection space
                // mul(UNITY_MATRIX_MVP, v.vertex)
                o.pos = UnityObjectToClipPos(v.vertex);

                // Transform the normal fram object space to world space
                o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);

                return o;
            }

            fixed4 frag(v2f i) : SV_Target {

                // Get ambient term
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                // Get the normal in the world space
                fixed3 worldNormal = normalize(i.worldNormal);

                // Get the light direction in world space
                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

                // compute diffuse term
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

                fixed3 color = ambient + diffuse;

                return fixed4(color, 1.0);
            }

            ENDCG
        }
    }
    Fallback "Diffuse"
}

此外还有个问题,在光无法达到的区域,模型外观通常是全黑的,没有任何明暗变化,这会是背光区域就像平面一样,失去立体。
为了解决这个问题,半兰伯特光照模型被提出。

半兰伯特(Half Lambert)光照明模型

半兰伯特光照模型仅仅是一个视觉效果,没有任何物理依据。

公式为:

  • C(diffuse) = C(light) * M(diffuse) * (0.5 * (0, n’ * l’) + 0.5)

通过如上方式,可以把n’和l’的计算结果从[-1, 1]映射到[0,1]范围内,也就是说对于模型的背光面,原兰伯特光照模型中点积的结果都是同一个值,即0;而在半兰伯特模型中,背光面也可以有明暗变化,不同的点积结果会映射到不同的值上。


Shader "Unity Shaders Book/Chapter 6/HalfLambert" {
    Properties {
        _Diffuse ("Diffuse", Color) = (1,1,1,1)
    }

    SubShader {
        Pass {
            Tags {"LightMode" = "ForwardBase"}

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Diffuse;

            struct a2v {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                fixed3 worldNormal : TEXCOORD0;
            };

            // 顶点着色器
            // 实现一个逐顶点的漫反射光照
            v2f vert(a2v v) {
                v2f o;

                // Transform the vertex from object space to projection space
                // mul(UNITY_MATRIX_MVP, v.vertex)
                o.pos = UnityObjectToClipPos(v.vertex);

                // Transform the normal fram object space to world space
                o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);

                return o;
            }

            fixed4 frag(v2f i) : SV_Target {

                // Get ambient term
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                // Get the normal in the world space
                fixed3 worldNormal = normalize(i.worldNormal);

                // Get the light direction in world space
                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

                // compute diffuse term
                fixed halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5;
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;

                fixed3 color = ambient + diffuse;

                return fixed4(color, 1.0);
            }

            ENDCG
        }
    }
    Fallback "Diffuse"
}
作者:biezhihua 发表于2017/11/15 22:56:09 原文链接
阅读:12 评论:0 查看评论

android 使用Robotium自动化测试

$
0
0

Robotium是一款国外的Android自动化测试框架,主要针对Android平台的应用进行黑盒自动化测试,它提供了模拟各种手势操作(点击、长按、滑动等)、查找和断言机制的API,能够对各种控件进行操作。Robotium结合Android官方提供的测试框架达到对应用程序进行自动化的测试。另外,Robotium 4.0版本已经支持对WebView的操作。Robotium 对Activity,Dialog,Toast,Menu 都是支持的。

我这里介绍是eclipse进行黑盒测试。

1、重签名apk

把apk的后缀名改成zip,然后删掉里面META-INF这个文件夹,重新把后缀名改回apk。

复制eclipse下面默认的签名证书:



使用cmd命令进入需要签名的目录,apk和签名证书放在一起。

重签名的命令:

jarsigner -verbose -keystore debug.keystore -storepass android -signedjar OsmdroidSample_news.apk -digestalg SHA1 -sigalg MD5withRSA OsmdroidSample.apk androiddebugkey

eclipse默认调试签名证书是debug.keystore,密码是android,别名androiddebugkey,别名的密码android

如果出现找不到证书链,那就是签名的别名不对


OsmdroidSample_news.apk成功的apk。其实重签名就是删除之前的签名,改成默认eclipse默认的调试的签名证书。

重签名之后apk,就装在真机上。

2、查看apk里面id的自带工具

在F:\adt-bundle-windows-x86_64-20140321\sdk\tools目录下有个文件uiautomatorviewer.bat ,双击这个文件

点击刚刚装的apk

在点击下图


相关的界面出来,在右侧能找到id。对后面写测试点击id就有用了。

切换界面了还是要点击红色箭头那个。

3、eclipse创建项目写代码了


下一步,填写名称,再下一步选第一个,第二个是白盒测试


最后就是finish了。

创建完项目之后就是配置




选择JUint


下一步,选择JUint4 finish就行了。

新建一个libs文件夹,把jar复制进行。



配置好了。下面可以开始写代码了。


4、写代码

manifest的配置

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="android.support.v7.appcompat.test"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="19" />

    <instrumentation
        android:name="android.test.InstrumentationTestRunner"
        android:targetPackage="com.osmdroid.sample" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <uses-library android:name="android.test.runner" />
    </application>

</manifest>

其中android:targetPackage="com.osmdroid.sample"这个就是你要测试应用程序的包名。

部分示例代码:

package com.osmdroid.test;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.robotium.solo.Solo;

import android.test.ActivityInstrumentationTestCase2;
import android.widget.Button;
import android.widget.TextView;

public class OsmdroidTest extends ActivityInstrumentationTestCase2{
	private static final String cls = "com.osmdroid.sample.MainActivity";
	private static Class<?> target;
	private Solo solo;
	static {
		try {
			target = Class.forName(cls);
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}

	public OsmdroidTest() {
		super(target);
	}

	@Before
	protected void setUp() throws Exception {
		solo = new Solo(getInstrumentation(), getActivity());
	}

	@After
	protected void tearDown() throws Exception {
		solo.finishOpenedActivities();
	}

	@Test
	public void test() {
		solo.sleep(1000);
		solo.clickOnView((Button) solo.getView("com.osmdroid.sample:id/"
				+ "button"));
		solo.sleep(500);
		solo.goBack();
		
		solo.sleep(1000);
		solo.clickOnView((Button) solo.getView("com.osmdroid.sample:id/"
				+ "button1"));
		solo.sleep(500);
		solo.goBack();
		
		solo.sleep(1000);
		solo.clickOnView((Button) solo.getView("com.osmdroid.sample:id/"
				+ "button3"));
		solo.sleep(500);
		solo.goBack();
		
		solo.sleep(1000);
		solo.clickOnView((Button) solo.getView("com.osmdroid.sample:id/"
				+ "button4"));
		solo.sleep(500);
		solo.goBack();
		
		solo.sleep(1000);
		solo.clickOnView((Button) solo.getView("com.osmdroid.sample:id/"
				+ "button5"));
		solo.sleep(500);
		solo.goBack();
		
		solo.sleep(1000);
		solo.clickOnView((Button) solo.getView("com.osmdroid.sample:id/"
				+ "button6"));
		solo.sleep(500);
		solo.goBack();
		
		solo.sleep(1000);
		solo.clickOnView((Button) solo.getView("com.osmdroid.sample:id/"
				+ "button7"));
		solo.sleep(500);
		solo.goBack();
		
		solo.sleep(1000);
		solo.clickOnView((Button) solo.getView("com.osmdroid.sample:id/"
				+ "button2"));
		solo.sleep(500);
		solo.goBack();
		
		
		
//		EditText earthP = (EditText) solo
//				.getView("com.osmdroid.sample:id/EditTextValue_1");
//		solo.clearEditText(earthP); 
//		solo.enterText(earthP, "804671.2996");
//		
//		EditText x = (EditText) solo.getView("com.osmdroid.sample:id/EditTextOutputX");
//		String xValue = x.getText().toString();
//		assertEquals("计算不正确", "2636657.570", xValue);
//		solo.goBack();
	}

}

@Before 被这个注解的方法会阻塞 @Test 注解方法,在 @Test 注解方法前执行,如果存在多个 @Before 方法,执行顺序随机。这个注解的方法主要用来执行一些测试的设置。

@After 被这个注解标志的方法,会在所有的 @Test 方法之后执行,主要用来释放资源的操作

@Test 被这个注解标志的方法,就是你的测试方法,一个单元测试类可以有多个测试方法。

@Test(timeout=) 带参数的 @Test 注解标签,如果方法在参数时间内没有成功完成,则返回失败。

@BeforeClass 被这个标签标注的方法,会在Class第一次加载调用,所以你可以在里面执行一个 static 方法,比如一些 static 方法是很费资源的,例如 JDBC 操作。

@AfterClass 对应着 

@BeforeClass ,你可以进行一些 static 操作之后的资源释放。

具体的Solo 的更多用法请参考javadoc 文档。

右键Run As 选择Android Junit  Test 就可以跑起来了。

5、注意事项

就是如果你在这个电脑上重签名,可能发给人家进行测试编译时候回出现:

Test run failed: Permission Denial: starting instrumentation ComponentInfo{com.osmdroid.test/android.test.InstrumentationTestRunner} from pid=8661, uid=8661 not allowed because package com.osmdroid.test does not have a signature matching the target com.osmdroid.sample

那就是签名证书不对了

还是让他重新签名,安装吧。

有些时候运行失败,重启一下eclipse吧。


这就是黑盒测试,比较简单,成本还好。如果是白盒测试,那就成本要高了。


作者:qq_16064871 发表于2017/11/17 15:44:13 原文链接
阅读:24 评论:0 查看评论

Cordova App 打包全揭秘

$
0
0

这里写图片描述


本文作者:大师兄(高武军)

现就职于某公司移动端架构师兼产品开发。
主要开发产品:mdn(适配app和微信的移动端解决方案),pageui(移动端ui组件库),formBuilder(可以让前端建表和操作表的表单设计器)。

课程介绍 点击查看原文

Cordova 是一个开源的移动开发框架。允许你用标准的 Web 技术——HTML5,CSS3 和 JavaScript 做跨平台开发。应用在每个平台的具体执行被封装了起来,并依靠符合标准的 API 绑定去访问每个设备的功能。

本课程是一个系列基础教程,目标是带领读者上手实战,你可以掌握和 Cordova 相关环境搭建,创建项目,添加插件等核心技能。随心随意打包自己的 App 应用。


导读:混合开发 APP

近年来随着移动设备类型与操作系统的增多,用户需求也日益增长,因此在项目启动前,大家都会考虑到它的需求、成本、需要投入的时间,自己的团队成员与技术成熟度等一系列因素。在这种情况下,App 的开发方案也尽显多元。

曾经 Html5 有一段小浪潮,很多人看到或参与过这样一个讨论:是选择原生开发、混合开发,还是 Web 开发?究竟哪个才是最佳实践,笔者认为只有实践过的人才知道,尤其是在当今这个充满变数的移动互联网时代。

混合移动应用程序开发的流行

当涉及到移动应用程序开发时,目前还没有一种可以适合所有场景的方式。你可以选择开发本地应用程序、基于网络的移动应用程序或是混合移动应用程序,而在以上选项中,混合移动应用程序开发应该是最具优势的。

那么什么是混合应用程序开发呢?

App 混合开发,顾名思义是一个开发模式,它指的是开发 App 时一部分功能用 native 构建,另一部分功能用 Html5 构建。介于 web-app、native-app 这两者之间的 App,英文名叫:Hybrid App。

其实,早在几年前就已经出现了 App 混合开发模式,到2014年末才开始流行,2016年已经被广泛使用,而2017年甚至达到鼎盛期。

混合开发模式的流行离不开它自身的优势,因为 Hybrid App 总体特性更接近 Native App,但是和 Web App 区别较大,同时使用了网页语言编码,所以开发成本和难度比 Native App 要小很多。因此说,Hybrid App(混合模式移动应用)兼具“Native App 良好用户交互体验的优势”和“Web App 跨平台开发的优势”。目前有名的混合开发框架有 Cordova,React-Native,Weex,Ionic 等。

Cordova

Cordova 是 Apache 的一个开源项目。

Cordova 的体系架构从上往下分为,前端(html,js),Html 渲染引擎(各平台的浏览器组件),cordova plugin。Cordova Plugin 为 Cordova 重最核心的部分,扩展了 js 访问手机硬件和原生 API 的接口。

开发者可自定义 Cordova Plugin 扩展自己的原生接口,添加到 Cordova。总体来说 Cordova 即可以简单的部署到多平台,也支持单平台的深层定制。

ReactNative

Facebook 推出的跨平台框架。 使用 JS 和 React 就可以开发 App,React 是 Facebook 自己的 JS 库。用 react-native 开发的界面会完全转化为 native 界面。部署的时候不用重新编译,可以热加载而且可以植入原生代码。

react-native 是通过 JavaScript 将 native 中的方法映射到 JS 中,没有使用浏览器组件(除低版本 iOS),严格意义上来说开发出来的并不是 Hybrid App。所以,开发时比较依赖 react-native 暴露出来的接口,除此之外,初次学习成本比较高,需要学习 react 的使用(一套前端不能复用到 PC、手机浏览器、微信公众号网页)。

Weex

阿里推出的跨平台框架,Weex 的理念是write once run anywhere 也就是三端体验一致,让大家专注于业务,写一份代码 iOS、Android、H5 都能够适用。当然也有为企业节省开发成本的考虑。所以 Weex 技术方案的关键在于要拥有一个强大的解析渲染引擎。

Weex 所采用的技术架构方案:JS 引擎用 V8,JS 开发框架基于 vue.js。可以看出学习的成本也是非常高的。

Ionic

Ionic 是一个专注于用 Web 开发技术,基于 Html5 创建类似于手机平台原生应用的一个开发框架。绑定了 AngularJS 和 Sass。这个框架的目的是从 Web 的角度开发手机应用,基于 Cordova 的编译平台,可以实现编译成各个平台的应用程序。

Ionic 所采用的 JS 开发框架基于 AngularJS,并且还提供了很多 UI 组件,封装了 Cordova 常用的 Native API,但是看出学习的成本也是非常高的。

Cordova 的优势在于你熟悉了 Cordova 开发方式,熟悉了一套框架,把 Html5 开发手机 App 的坑大部分都踩过一遍之后,开发第二个 App 会极大的提高你的开发速度!

所以那些会 Html5 技术想更进一步掌握跨平台,高性能 App 开发技术,增加职场竞争力的前端人员。优秀的 Cordova 将是你的第一选择。

Cordova——混合型框架中的佼佼者

Cordova 主张一站式开发,简而言之,就是一个纯 Html5 的开发人员也可以开发和发布 Android 和 iOS 的 App了,而不需要专门的 Android 和 iOS 开发人员。

而在众多混合型框架中,Apache 作为幕后老大,背景强大,Cordova 可以说是佼佼者。Cordova 基于标准的 Web 技术——HTML、JavaScript 和 CSS,用 JavaScript 包装平台的 API 供开发者调用,具备强大的编译工具为不同平台生成应用,同时拥有丰富的第三方资源和产业链。

Cordova 的特点有以下:

  • 插件丰富

Cordova 的插件是其生态系统的重要组成部分,能帮助你快速地抵达移动设备的原生 API 上面。它提供了Cordova 和原生组件相互通信的接口,将其绑定到了标准的设备 API 上。这使你能够通过 JavaScript 调用原生代码。

Apache Cordova 项目维护着一组插件叫做核心插件。这些核心插件可以让你的应用程序访问设备功能,比如:电源、相机、联系人、摄像头、麦克风、传感器、数据、网络状态等。除了核心插件,Cordova 还有一些第三方插件提供若干个附加功能。

  • 可移植性

基于浏览器的移动 Web 可移植性和跨平台最突出表现为混合 App 也能节省跨平台的时间与成本,开发者只需编写一次核心代码就可部署到多个平台,而原生 App 的跨平台性能最差。

目前已有部分开发商采取了只编写一次应用程序,就可以在6个主要的移动平台和应用程序商店(App Store)里进行发布的方式,这些移动平台和应用程序商店包括:iOS、Android、BlackBerry、webOS、Bada 以及 Symbian 等。

当然这种方式也存在缺点,笔者认为其最主要的缺点有两个:一是性能不够好,二是兼容性较差。

但随着 Android 5.0+ 的普及以及 iOS 9.0+ 的普及,性能缺陷和兼容性问题都在减少,也就是说,如果未来某天 Android 最低支持版本从5.0开始,iOS 最低支持版本从9.0开始了,那么混合开发 App 的缺点就变得不那么明显了。很明显现在这都不是问题。

随着移动互联网的发展,现在基本是 App 满天飞,如果我去下载一个 App,那么基本都能看到有两种选择,一种是 Android 版本,一种是 iOS 版本。不管我的手机是哪种操作系统,安装完一个 App 之后,后续如果有新的版本发布的时候,我还必须去更新,才能享用新版本里的功能。比如我装了“京东”这个 App,前几天正好碰到“618”活动,那么之前一个月 App Store 就提醒我要去更新最新的 App 版本,以免错过“618”活动中新的功能使用。相对来说 iOS 系统更新 App 比起 Android 系统用户体验会好一点,但是还是稍显麻烦。

那么有没有一种方式,我只需要开发一个 App 版本,就能去适配通用的操作系统呢,不仅可以适配 Android、iOS,还可以适配其他系统,比如 Windows Phone、 Palm WebOS、Blackberry 等等。有,Cordova 就能提供这种能力,代码写一次,就能到处运行,跟我们日常开发网站效果一样,基于写 Web App,根据输出平台要求不同,就能提供不同类型的安装包。Cordova 其设计初衷是希望用户群体能够通过跨平台开发的方法降低原生开发的成本,为此,开发人员需要安装原生开发环境,配置工程,使用 HTML5、CSS3、JS 和原生 SDK 生成应用。

使用 Apache Cordova 的人群

  • 移动应用开发者,想扩展一个应用的使用平台,而不通过每个平台的语言和工具集重新实现。
  • Web 开发者,想包装部署自己的 Web App 将其分发到各个应用商店门户。
  • 移动应用开发者,有兴趣混合原生应用组建和一个 WebView(一个特别的浏览器窗口)可以接触设备 A 级 PI,或者你想开发一个原生和 WebView 组件之间的插件接口。

或许在未来几年,Android 和 iOS 开发人员需求量将会逐渐减少,而 Html5 开发人员需求量会与日俱增,那么,从 Android 或 iOS 转型为 Html5 的开发人员会更加地走俏。


下一篇

更多课程内容

导读:混合开发 APP

第01课:一起搭建 Cordova 开发环境

第02课:Cordova 生命周期

第03课:Android 与 IOS 打包

作者撰写中…

第04课:JS 是如何和本地 API 进行通信

作者撰写中…

第05课:利用 Hooks 预处理打包流程

作者撰写中…

第06课:Cordova Plugin 的使用和自定义

作者撰写中…

第07课:手机按键与 Cordova

作者撰写中…

第08课:App 自动更新设计

作者撰写中…

第09课:Cordova 实战开发技巧(一)

作者撰写中…

第10课:Cordova 实战开发技巧(二)

作者撰写中…

第11课:Cordova 实战开发技巧(三)

作者撰写中…

作者:GitChat 发表于2017/11/17 12:49:17 原文链接
阅读:63 评论:0 查看评论

Chromium网页输入事件捕捉和手势检测过程分析

$
0
0

       连续的输入事件可能会产生一定的手势操作,例如滑动手势和捏合手势。在Chromium中,网页的输入事件是在Browser进程中捕捉的。Browser进程捕获输入事件之后,会进行手势操作检测。检测出来的手势操作将会发送给Render进程处理,因为它们需要应用在网页之上。与此同时,Browser进程也会将原始的输入事件发送给Render进程处理。本文接下来就分析Browser进程处理网页输入事件的过程。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

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

       接下来我们将以Chromium自带的Content Shell APK为例,说明Chromium的Browser进程捕获网页输入事件以及检测手势操作的过程,如图1所示:


图1 Browser进程处理网页输入事件的过程

       从前面Chromium网页输入事件处理机制简要介绍和学习计划一文可以知道,Content Shell APK将网页渲染在一个SurfaceView控件上。这个SurfaceView又是嵌入在一个ContentView控件里面的。当用户在网页上触发了一个输入事件时,例如触发一个Touch事件时,这个Touch事件就会被系统分发给上述ContentView控件处理,表现为该ContentView控件的成员函数onTouchEvent被调用。

       ContentView控件得到Touch事件之后,会将它传递到Chromium的C++层去处理。Java层的每一个ContentView控件在C++层都对应一个ContentViewCore对象。C++层的ContentViewCore对象得到Touch事件之后,就会通过一个Gesture Dector和一个Scale Gesture Detector进行滑动(Scroll)和捏合(Pinch)手势检测。检测出来的滑动和捏合手势将会统一保存在一个Gestrue Packet中。这个Gestrue Packet接下来会被一个Input Router封装在一个类型为InputMsg_HandleInputEvent的IPC消息中,发送给Render进程处理。

       注意,Touch事件经过手势检测之后,它本身也会被上述Input Router通过另外一个InputMsg_HandleInputEvent消息发送给Render进程处理。这意味着在这种情况下,Render进程将收到两个InputMsg_HandleInputEvent消息。

       接下来,我们就从ContentView类的成员函数onTouchEvent开始,分析Browser进程处理网页输入事件的过程,如下所示:

public class ContentView extends FrameLayout
        implements ContentViewCore.InternalAccessDelegate, SmartClipProvider {
    ......

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return mContentViewCore.onTouchEvent(event);
    }

    ......
}

       这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentView.java中。

       参数event指向的MotionEvent对象描述的就是当前发生的Touch事件。ContentView类的成员变量mContentViewCore指向的是一个ContentViewCore对象,ContentView类的成员函数onTouchEvent调用这个ContentViewCore对象的成员函数onTouchEvent处理参数event所描述的Touch事件。

       ContentViewCore类的成员函数onTouchEvent的实现如下所示:

public class ContentViewCore
        implements NavigationClient, AccessibilityStateChangeListener, ScreenOrientationObserver {
    ......

    public boolean onTouchEvent(MotionEvent event) {
        TraceEvent.begin("onTouchEvent");
        try {
            ......

            final int pointerCount = event.getPointerCount();
            final boolean consumed = nativeOnTouchEvent(mNativeContentViewCore, event,
                    event.getEventTime(), eventAction,
                    pointerCount, event.getHistorySize(), event.getActionIndex(),
                    event.getX(), event.getY(),
                    pointerCount > 1 ? event.getX(1) : 0,
                    pointerCount > 1 ? event.getY(1) : 0,
                    event.getPointerId(0), pointerCount > 1 ? event.getPointerId(1) : -1,
                    event.getTouchMajor(), pointerCount > 1 ? event.getTouchMajor(1) : 0,
                    event.getRawX(), event.getRawY(),
                    event.getToolType(0),
                    pointerCount > 1 ? event.getToolType(1) : MotionEvent.TOOL_TYPE_UNKNOWN,
                    event.getButtonState());

            ......
            return consumed;
        } finally {
            TraceEvent.end("onTouchEvent");
        }
    }

    ......
}

      这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java中。

      ContentViewCore类的成员函数onTouchEvent主要是调用另外一个成员函数nativeOnTouchEvent处理参数event描述的Touch事件。

      ContentViewCore类的成员函数nativeOnTouchEvent是一个JNI函数,它由C++层的函数Java_com_android_org_chromium_content_browser_ContentViewCore_nativeOnTouchEvent实现,如下所示:

__attribute__((visibility("default")))
jboolean
    Java_com_android_org_chromium_content_browser_ContentViewCore_nativeOnTouchEvent(JNIEnv*
    env,
    jobject jcaller,
    jlong nativeContentViewCoreImpl,
    jobject event,
    jlong timeMs,
    jint action,
    jint pointerCount,
    jint historySize,
    jint actionIndex,
    jfloat x0,
    jfloat y0,
    jfloat x1,
    jfloat y1,
    jint pointerId0,
    jint pointerId1,
    jfloat touchMajor0,
    jfloat touchMajor1,
    jfloat rawX,
    jfloat rawY,
    jint androidToolType0,
    jint androidToolType1,
    jint androidButtonState) {
  ContentViewCoreImpl* native =
      reinterpret_cast<ContentViewCoreImpl*>(nativeContentViewCoreImpl);
  CHECK_NATIVE_PTR(env, jcaller, native, "OnTouchEvent", false);
  return native->OnTouchEvent(env, jcaller, event, timeMs, action, pointerCount,
      historySize, actionIndex, x0, y0, x1, y1, pointerId0, pointerId1,
      touchMajor0, touchMajor1, rawX, rawY, androidToolType0, androidToolType1,
      androidButtonState);
}
       这个函数定义在文件out/target/product/generic/obj/GYP/shared_intermediates/content/jni/ContentViewCore_jni.h中。

       参数nativeContentViewCoreImpl描述的是C++层的一个ContentViewCoreImpl对象,函数Java_com_android_org_chromium_content_browser_ContentViewCore_nativeOnTouchEvent调用这个ContentViewCoreImpl对象的成员函数OnTouchEvent处理其它参数所描述的Touch事件。

       ContentViewCoreImpl类的成员函数OnTouchEvent的实现如下所示:

jboolean ContentViewCoreImpl::OnTouchEvent(JNIEnv* env,
                                           jobject obj,
                                           jobject motion_event,
                                           jlong time_ms,
                                           jint android_action,
                                           jint pointer_count,
                                           jint history_size,
                                           jint action_index,
                                           jfloat pos_x_0,
                                           jfloat pos_y_0,
                                           jfloat pos_x_1,
                                           jfloat pos_y_1,
                                           jint pointer_id_0,
                                           jint pointer_id_1,
                                           jfloat touch_major_0,
                                           jfloat touch_major_1,
                                           jfloat raw_pos_x,
                                           jfloat raw_pos_y,
                                           jint android_tool_type_0,
                                           jint android_tool_type_1,
                                           jint android_button_state) {
  RenderWidgetHostViewAndroid* rwhv = GetRenderWidgetHostViewAndroid();
  ......

  MotionEventAndroid event(1.f / dpi_scale(),
                           env,
                           motion_event,
                           time_ms,
                           android_action,
                           pointer_count,
                           history_size,
                           action_index,
                           pos_x_0,
                           pos_y_0,
                           pos_x_1,
                           pos_y_1,
                           pointer_id_0,
                           pointer_id_1,
                           touch_major_0,
                           touch_major_1,
                           raw_pos_x,
                           raw_pos_y,
                           android_tool_type_0,
                           android_tool_type_1,
                           android_button_state);

  return rwhv->OnTouchEvent(event);
}
       这个函数定义在文件external/chromium_org/content/browser/android/content_view_core_impl.cc中。

       ContentViewCoreImpl类的成员函数OnTouchEvent首先调用成员函数GetRenderWidgetHostViewAndroid获得一个RenderWidgetHostViewAndroid对象。这个RenderWidgetHostViewAndroid对象用来在C++层描述加载网页的控件,它的创建过程可以参考前面Chromium硬件加速渲染的OpenGL上下文绘图表面创建过程分析一文。

       ContentViewCoreImpl类的成员函数OnTouchEvent接下来又将参数描述的Touch事件封装在一个MotionEventAndroid对象中,然后将该MotionEventAndroid对象传递给前面获得的RenderWidgetHostViewAndroid对象的成员函数OnTouchEvent处理。

       RenderWidgetHostViewAndroid对象的成员函数OnTouchEvent的实现如下所示:

bool RenderWidgetHostViewAndroid::OnTouchEvent(
    const ui::MotionEvent& event) {
  ......

  if (!gesture_provider_.OnTouchEvent(event))
    return false;

  ......

  // Short-circuit touch forwarding if no touch handlers exist.
  if (!host_->ShouldForwardTouchEvent()) {
    const bool event_consumed = false;
    gesture_provider_.OnTouchEventAck(event_consumed);
    return true;
  }

  SendTouchEvent(CreateWebTouchEventFromMotionEvent(event));
  return true;
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_widget_host_view_android.cc中。

       RenderWidgetHostViewAndroid类的成员变量gesture_provider_描述的是一个FilteredGestureProvider对象。RenderWidgetHostViewAndroid类的成员函数OnTouchEvent首先调用这个FilteredGestureProvider对象的成员函数OnTouchEvent检测参数event描述的Touch事件是否产生了手势操作。如果有发生,那么就会将它们发送给Render进程处理。

       RenderWidgetHostViewAndroid类的成员变量host_指向的是一个RenderWidgetHostImpl对象。这个RenderWidgetHostImpl对象也是用来在C++层描述加载网页的控件的,它的创建过程可以参考前面Chromium硬件加速渲染的OpenGL上下文绘图表面创建过程分析一文。RenderWidgetHostViewAndroid类的成员函数OnTouchEvent接下来调用这个RenderWidgetHostImpl对象的成员函数ShouldForwardTouchEvent检查Render进程是否注册了处理Touch事件的Handler。如果没有注册的话,那么就不需要将参数event描述的Touch事件发送给它处理了。

       我们假设Render进程注册了处理Touch事件的Handler。在这种情况下,RenderWidgetHostViewAndroid类的成员函数OnTouchEvent就会调用函数CreateWebTouchEventFromMotionEvent将参数event描述的Touch事件封装成一个blink::WebTouchEvent对象,并且调用另外一个成员函数SendTouchEvent将该blink::WebTouchEvent对象发送给Render进程处理。注意,这个blink::WebTouchEvent对象描述的是原始的Touch事件,它不是一个手势操作。

       接下来,我们先分析FilteredGestureProvider类的成员函数OnTouchEvent检测手势操作的过程,接着再分析函数CreateWebTouchEventFromMotionEvent创建blink::WebTouchEvent对象的过程,以及RenderWidgetHostViewAndroid类的成员函数SendTouchEvent向Render进程发送Touch事件的过程。

       FilteredGestureProvider类的成员函数OnTouchEvent的实现如下所示:

bool FilteredGestureProvider::OnTouchEvent(const MotionEvent& event) {
  DCHECK(!handling_event_);
  base::AutoReset<bool> handling_event(&handling_event_, true);

  pending_gesture_packet_ = GestureEventDataPacket::FromTouch(event);

  if (!gesture_provider_.OnTouchEvent(event))
    return false;

  TouchDispositionGestureFilter::PacketResult result =
      gesture_filter_.OnGesturePacket(pending_gesture_packet_);
  if (result != TouchDispositionGestureFilter::SUCCESS) {
    NOTREACHED() << "Invalid touch gesture sequence detected.";
    return false;
  }

  return true;
}
       这个函数定义在文件external/chromium_org/ui/events/gesture_detection/filtered_gesture_provider.cc中。

       FilteredGestureProvider类的成员函数OnTouchEvent首先将成员变量handling_event_的值设置为true,表示当前正处于收集手势操作的过程中,不要将正在收集的手势操作发送给Render进程处理,而是等到全部收集完毕再一起发送给Render进程处理。注意,当FilteredGestureProvider类的成员函数OnTouchEvent的调用结束后,FilteredGestureProvider类的成员变量handling_event的值将自动恢复为false。

      FilteredGestureProvider类的成员函数OnTouchEvent接下来调用GestureEventDataPacket类的静态成员函数FromTouch创建一个用来保存手势操作的Gesture Event Data Packet,如下所示:

GestureEventDataPacket GestureEventDataPacket::FromTouch(
    const ui::MotionEvent& touch) {
  return GestureEventDataPacket(touch.GetEventTime(),
                                ToGestureSource(touch),
                                gfx::PointF(touch.GetX(), touch.GetY()),
                                gfx::PointF(touch.GetRawX(), touch.GetRawY()));
}
      这个函数定义在文件external/chromium_org/ui/events/gesture_detection/gesture_event_data_packet.cc中。

      GestureEventDataPacket类的静态成员函数FromTouch首先调用函数ToGestureSource获得接下来要创建的Gesture Event Data Packet的类型,接着创建一个该类型的Gesture Event Data Packet返回给调用者。

      函数ToGestureSource的实现如下所示:

GestureEventDataPacket::GestureSource ToGestureSource(
    const ui::MotionEvent& event) {
  switch (event.GetAction()) {
    case ui::MotionEvent::ACTION_DOWN:
      return GestureEventDataPacket::TOUCH_SEQUENCE_START;
    case ui::MotionEvent::ACTION_UP:
      return GestureEventDataPacket::TOUCH_SEQUENCE_END;
    case ui::MotionEvent::ACTION_MOVE:
      return GestureEventDataPacket::TOUCH_MOVE;
    case ui::MotionEvent::ACTION_CANCEL:
      return GestureEventDataPacket::TOUCH_SEQUENCE_CANCEL;
    case ui::MotionEvent::ACTION_POINTER_DOWN:
      return GestureEventDataPacket::TOUCH_START;
    case ui::MotionEvent::ACTION_POINTER_UP:
      return GestureEventDataPacket::TOUCH_END;
  };
  NOTREACHED() << "Invalid ui::MotionEvent action: " << event.GetAction();
  return GestureEventDataPacket::INVALID;
}
       这个函数定义在文件external/chromium_org/ui/events/gesture_detection/gesture_event_data_packet.cc中。

       函数ToGestureSource返回的Gesture Event Data Packet的类型与参数evnet描述的Touch事件的类型有关。例如,假设event描述的是一个ACTION_MOVE类型的Touch事件,那么函数ToGestureSource返回的Gesture Event Data Packet的类型就为GestureEventDataPacket::TOUCH_MOVE。

       在接下来的分析中,我们就假设当前要处理的是一个ACTION_MOVE类型的Touch事件,这意味着FilteredGestureProvider类的成员函数OnTouchEvent调用GestureEventDataPacket类的静态成员函数FromTouch获得的是一个类型为GestureEventDataPacket::TOUCH_MOVE的Gesture Event Data Packet。这个Gesture Event Data Packet保存在FilteredGestureProvider类的成员变量pending_gesture_packet_中。 

       回到FilteredGestureProvider类的成员函数OnTouchEvent中,它接下来调用成员变量gesture_provider_描述的一个GestureProvider对象的成员函数OnTouchEvent检查参数event描述的Touch事件是否产生了手势操作。如果产生了,那么就会将它们保存在成员变量pending_gesture_packet_描述的Gesture Event Data Packet中。

       FilteredGestureProvider类的成员变量gesture_filter_描述的是一个TouchDispositionGestureFilter对象,FilteredGestureProvider类的成员函数OnTouchEvent最后调用这个TouchDispositionGestureFilter对象的成员函数OnGesturePacket将成员变量pending_gesture_packet_描述的Gesture Event Data Packet发送给Render进程处理,也就是将前面检测到的手势操作发送给Render进程处理。

       接下来,我们先分析GestureProvider对象的成员函数OnTouchEvent检测手势操作的过程,接下来再分析TouchDispositionGestureFilter类的成员函数OnGesturePacket发送手势操作给Render进程的过程。

       GestureProvider类的成员函数OnTouchEvent的实现如下所示:

bool GestureProvider::OnTouchEvent(const MotionEvent& event) {
  ......

  gesture_listener_->OnTouchEvent(event, in_scale_gesture);
  scale_gesture_listener_->OnTouchEvent(event);

  ......

  return true;
}
       这个函数定义在文件external/chromium_org/ui/events/gesture_detection/gesture_provider.cc中。

       GestureProvider类的成员变量gesture_listener_指向的是一个GestureListenerImpl对象。这个GestureListenerImpl对象负责检测参数event描述的Touch事件是否产生滑动手势操作。这是通过调用它的成员函数OnTouchEvent实现的。

       GestureProvider类的成员变量scale_gesture_listener_指向的是一个ScaleGestureListenerImpl对象。这个ScaleGestureListenerImpl对象负责检测参数event描述的Touch事件是否产生捏合手势操作。这是通过调用它的成员函数OnTouchEvent实现的。

       接下来,我们就分别分析GestureListenerImpl类和ScaleGestureListenerImpl类的成员函数OnTouchEvent的实现,以便了解滑动和捏合手势操作的检测过程。

       GestureListenerImpl类的成员函数OnTouchEvent的实现如下所示:

class GestureProvider::GestureListenerImpl
    : public GestureDetector::GestureListener,
      public GestureDetector::DoubleTapListener {
 public:
  ......

  bool OnTouchEvent(const MotionEvent& e,
                    bool is_scale_gesture_detection_in_progress) {
    ......

    return gesture_detector_.OnTouchEvent(e);
  }

 private:
  ......

  GestureDetector gesture_detector_;
  
  ......
};
       这个函数定义在文件external/chromium_org/ui/events/gesture_detection/gesture_provider.cc中。

       GestureListenerImpl类的成员函数OnTouchEvent通过调用成员变量gesture_detector_描述的一个GestureDetector对象的成员函数OnTouchEvent检测测参数e描述的Touch事件是否产生了滑动手势操作。

       GestureDetector类的成员函数OnTouchEvent的实现如下所示:

bool GestureDetector::OnTouchEvent(const MotionEvent& ev) {
  const MotionEvent::Action action = ev.GetAction();

  ......

  const bool pointer_up = action == MotionEvent::ACTION_POINTER_UP;
  const int skip_index = pointer_up ? ev.GetActionIndex() : -1;

  // Determine focal point.
  float sum_x = 0, sum_y = 0;
  const int count = static_cast<int>(ev.GetPointerCount());
  for (int i = 0; i < count; i++) {
    if (skip_index == i)
      continue;
    sum_x += ev.GetX(i);
    sum_y += ev.GetY(i);
  }
  const int div = pointer_up ? count - 1 : count;
  const float focus_x = sum_x / div;
  const float focus_y = sum_y / div;

  bool handled = false;

  switch (action) {
    ......

    case MotionEvent::ACTION_MOVE:
      {
        const float scroll_x = last_focus_x_ - focus_x;
        const float scroll_y = last_focus_y_ - focus_y;
        if (is_double_tapping_) {
          // Give the move events of the double-tap.
          DCHECK(double_tap_listener_);
          handled |= double_tap_listener_->OnDoubleTapEvent(ev);
        } else if (always_in_tap_region_) {
          const float delta_x = focus_x - down_focus_x_;
          const float delta_y = focus_y - down_focus_y_;
          const float distance_square = delta_x * delta_x + delta_y * delta_y;
          if (distance_square > touch_slop_square_) {
            handled = listener_->OnScroll(
                *current_down_event_, ev, scroll_x, scroll_y);
            last_focus_x_ = focus_x;
            last_focus_y_ = focus_y;
            always_in_tap_region_ = false;
            ......
          }
          ......
        } else if (std::abs(scroll_x) > kScrollEpsilon ||
                   std::abs(scroll_y) > kScrollEpsilon) {
          handled =
              listener_->OnScroll(*current_down_event_, ev, scroll_x, scroll_y);
          last_focus_x_ = focus_x;
          last_focus_y_ = focus_y;
        }

      ......

      break;

    ......
  }

  return handled;
}
       这个函数定义在文件external/chromium_org/ui/events/gesture_detection/gesture_detector.cc中。

       前面我们假设参数ev描述的是一个ACTION_MOVE类型的Touch事件。GestureDetector类的成员函数OnTouchEvent首先会计算这个Touch事件的位置(focus_x, focus_y)。注意,这个Touch事件可能包含了多个触摸点,因此在计算它的位置时,通过将所有的触摸点进行算术平均得到。

       GestureDetector类的成员变量last_focus_x_和last_focus_y_记录的是上一个类型为ACTION_MOVE的Touch事件的位置(last_focus_x_, last_focus_y_)。GestureDetector类的成员函数OnTouchEvent通过比较(last_focus_x_, last_focus_y_)和(focus_x, focus_y)的值,得到连续两个类型为ACTION_MOVE的Touch事件在网页的X轴和Y轴上所产生的滑动量scroll_x和scroll_y。

       GestureDetector类的成员变量is_double_tapping_是一个布尔变量。当它的值等于true的时候,表示用户在规定的时间和空间内连续点击了两次网页。这种情况称为Double Tap,这时候GestureDetector类的成员函数OnTouchEvent会将参数ev描述的类型为ACTION_MOVE的Touch事件交给成员变量double_tap_listener_指向的一个DoubleTapListener对象的成员函数OnDoubleTapEvent处理。也就是说,Double Tap之后的类型为ACTION_MOVE的Touch事件将不会产生滑动手势操作。

       GestureDetector类的成员变量always_in_tap_region_也是一个布尔变量。当它的值等于true的时候,表示用户之前触发了一个类型为ACTION_DOWN的Touch事件。在这种情况下,GestureDetector类的成员函数OnTouchEvent需要计算当前发生的类型为ACTION_MOVE的Touch事件与之前触发的类型为ACTION_DOWN的Touch事件的位置距离。当这个距离大于预设的值之时,GestureDetector类的成员变量always_in_tap_region_会被重置为false,表示后面触发类型为ACTION_UP的Touch事件时,不要产生一个Single Tap事件。与此同时,需要产生一个滑动手势操作。这个滑动手势操作通过调用GestureDetector类的成员变量listener_描述的一个GestureListener对象的成员函数OnScroll进行处理。

      从前面的分析我们可以看出,Single Tap事件与滑动手势操作是互斥的。一个Single Tap事件指的是指在规定时间和空间内先后发生了一个类型为ACTION_DOWN的Touch事件和一个类型为ACTION_UP的Touch事件。在这两个Touch事件之间发生的类型为ACTION_MOVE的Touch事件将不会产生手势操作。

      当GestureDetector类的成员变量is_double_tapping_和always_in_tap_region_ 的值都等于false的时候,GestureDetector类的成员函数OnTouchEvent检查连续两个类型为ACTION_MOVE的Touch事件在网页的X轴和Y轴上所产生的滑动量scroll_x和scroll_y是否超过了预设的阀值。如果超过了,那么就认为产生了一个滑动手势操作。这个滑动手势操作也是通过调用GestureDetector类的成员变量listener_描述的一个GestureListener对象的成员函数OnScroll进行处理。

       接下来我们主要关注滑动手势操作的处理过程。GestureDetector类的成员变量listener_指向的实际上是一个GestureListenerImpl对象。这个GestureListenerImpl对象就是前面提到的GestureProvider类的成员变量gesture_listener_所指向的GestureListenerImpl对象。这意味着GestureDetector类的成员函数OnTouchEvent检测到的滑动手势操作将由这个GestureListenerImpl对象的成员函数OnScroll进行处理。

       GestureListenerImpl类的成员函数OnScroll的实现如下所示:

class GestureProvider::GestureListenerImpl
    : public GestureDetector::GestureListener,
      public GestureDetector::DoubleTapListener {
 public:
  ......

  virtual bool OnScroll(const MotionEvent& e1,
                        const MotionEvent& e2,
                        float raw_distance_x,
                        float raw_distance_y) OVERRIDE {
    float distance_x = raw_distance_x;
    float distance_y = raw_distance_y;
    ......

    if (!provider_->IsScrollInProgress()) {
      // Note that scroll start hints are in distance traveled, where
      // scroll deltas are in the opposite direction.
      GestureEventDetails scroll_details(
          ET_GESTURE_SCROLL_BEGIN, -raw_distance_x, -raw_distance_y);

      // Use the co-ordinates from the touch down, as these co-ordinates are
      // used to determine which layer the scroll should affect.
      provider_->Send(CreateGesture(scroll_details,
                                    e2.GetId(),
                                    e2.GetEventTime(),
                                    e1.GetX(),
                                    e1.GetY(),
                                    e1.GetRawX(),
                                    e1.GetRawY(),
                                    e2.GetPointerCount(),
                                    GetBoundingBox(e2)));
    }


    if (distance_x || distance_y) {
      const gfx::RectF bounding_box = GetBoundingBox(e2);
      const gfx::PointF center = bounding_box.CenterPoint();
      const gfx::PointF raw_center =
          center + gfx::Vector2dF(e2.GetRawOffsetX(), e2.GetRawOffsetY());
      GestureEventDetails scroll_details(
          ET_GESTURE_SCROLL_UPDATE, -distance_x, -distance_y);
      provider_->Send(CreateGesture(scroll_details,
                                    e2.GetId(),
                                    e2.GetEventTime(),
                                    center.x(),
                                    center.y(),
                                    raw_center.x(),
                                    raw_center.y(),
                                    e2.GetPointerCount(),
                                    bounding_box));
    }

    return true;
  }

  ......
};

       这个函数定义在文件external/chromium_org/ui/events/gesture_detection/gesture_provider.cc中。

       GestureListenerImpl类的成员函数OnScroll首先调用成员变量provider_指向的一个GestureProvider对象的成员函数IsScrollInProgress判断网页当前是否正在滑动过程中。如果不是的话,那么就说明现在要开始对网页进行滑动。这时候Browser进程会先发送一个类型为ET_GESTURE_SCROLL_BEGIN的手势操作给Render进程。

       在网页有滑动的情况下,也就是网页至少在X轴和Y轴之一有偏移时,GestureListenerImpl类的成员函数OnScroll接下来还会向Render进程发送一个类型为ET_GESTURE_SCROLL_UPDATE的手势操作。

       不管是类型为ET_GESTURE_SCROLL_BEGIN的手势操作,还是类型为ET_GESTURE_SCROLL_UPDATE的手势操作,它们都会通过函数CreateGesture封装为一个GestureEventData对象。这两个GestureEventData对象都是通过调用GestureListenerImpl类的成员变量provider_指向的GestureProvider对象的成员函数Send发送给Render进程的,如下所示:

void GestureProvider::Send(GestureEventData gesture) {
  ......

  client_->OnGestureEvent(gesture);
}
       这个函数定义在文件external/chromium_org/ui/events/gesture_detection/gesture_provider.cc中。

       GestureProvider类的成员变量client_指向的是一个FilteredGestureProvider对象。这个FilteredGestureProvider对象就是前面分析的RenderWidgetHostViewAndroid类的成员变量gesture_provider_所指向的FilteredGestureProvider对象。

       GestureProvider类的成员函数Send主要是调用上述FilteredGestureProvider对象的成员函数OnGestureEvent将参数gesture描述的手势操作发送给Render进程处理,如下所示:

void FilteredGestureProvider::OnGestureEvent(const GestureEventData& event) {
  if (handling_event_) {
    pending_gesture_packet_.Push(event);
    return;
  }

  gesture_filter_.OnGesturePacket(
      GestureEventDataPacket::FromTouchTimeout(event));
}
       这个函数定义在文件external/chromium_org/ui/events/gesture_detection/filtered_gesture_provider.cc中。

       从前面的分析可以知道,当前正在处理的FilteredGestureProvider对象的成员变量handling_event_已经被设置为true,表示此时仅仅收集参数event描述的手势操作,而不要将它发送给Render进程。参数event描述的手势操作被收集在FilteredGestureProvider类的成员变量pending_gesture_packet_描述的一个Gesture Event Data Packet中。从前面的分析可以知道,这个Gesture Event Data Packet的类型为GestureEventDataPacket::TOUCH_MOVE。

       如果当前正在处理的FilteredGestureProvider对象的成员变量handling_event_的值不等于true,那么FilteredGestureProvider类的成员函数OnGestureEvent将会直接将参数event描述的手势操作发送给Render进程,这是通过调用另外一个成员变量gesture_filter_描述的一个TouchDispositionGestureFilter对象的成员函数OnGesturePacket实现的。后面我们再分析这个发送过程。

       这一步执行完成后,Browser进程就对当前发生的Touch事件进行了滑动手势检测,并且检测到的滑动手势操作已经保存在一个Gesture Event Data Packet中。回到前面分析的GestureProvider类的成员函数OnTouchEvent中,接下来它会继续调用另外一个成员变量scale_gesture_listener_指向的是ScaleGestureListenerImpl对象的成员函数OnTouchEvent检测当前发生的Touch事件是否产生了捏合手势操作。如果产生了,那么同样将它收集在上述的Gesture Event Data Packet中。

       接下来我们就继续分析捏合手势操作的检测过程,也就是ScaleGestureListenerImpl类的成员函数OnTouchEvent的实现,如下所示:

class GestureProvider::ScaleGestureListenerImpl
    : public ScaleGestureDetector::ScaleGestureListener {
 public:
  ......

  bool OnTouchEvent(const MotionEvent& event) {
    ......
    bool handled = scale_gesture_detector_.OnTouchEvent(event);
    ......
    return handled;
  }

  ......

 private:
  ......

  ScaleGestureDetector scale_gesture_detector_;
 
  ......
};
       这个函数定义在文件external/chromium_org/ui/events/gesture_detection/gesture_provider.cc中。

       ScaleGestureListenerImpl类的成员函数OnTouchEvent主要是调用成员变量scale_gesture_detector_描述的一个ScaleGestureDetector对象的成员函数OnTouchEvent检测参数event描述的Touch事件是否产生捏合手势操作。

       ScaleGestureDetector类的成员函数OnTouchEvent的实现如下所示:

bool ScaleGestureDetector::OnTouchEvent(const MotionEvent& event) {
  ......

  const int action = event.GetAction();

  ......

  // Span is the average distance between touch points through the focal point;
  // i.e. the diameter of the circle with a radius of the average deviation from
  // the focal point.
  const float span_x = dev_x * 2;
  const float span_y = dev_y * 2;
  float span;
  if (InDoubleTapMode()) {
    span = span_y;
  } else {
    span = std::sqrt(span_x * span_x + span_y * span_y);
  }
  ......

  const float min_span = InDoubleTapMode() ? span_slop_ : min_span_;
  if (!in_progress_ && span >= min_span && (InDoubleTapMode() || count > 1) &&
      (was_in_progress || std::abs(span - initial_span_) > span_slop_)) {
    ......
    in_progress_ = listener_->OnScaleBegin(*this, event);
  }

  // Handle motion; focal point and span/scale factor are changing.
  if (action == MotionEvent::ACTION_MOVE) {
    ......

    if (in_progress_) {
      update_prev = listener_->OnScale(*this, event);
    }

    ......
  }

  return true;
}

       这个函数定义在文件external/chromium_org/ui/events/gesture_detection/scale_gesture_detector.cc中。

       ScaleGestureDetector类的成员函数OnTouchEvent首先计算网页被捏合的大小span。这是根据网页在X轴和Y轴上的捏合大小span_x和span_y计算得到的。

       ScaleGestureDetector类的成员函数OnTouchEvent接下来判断网页被捏合的大小span是否大于等于预设的阀值。如果大于等于,并且网页是刚开始被捏合,那么就会调用成员变量listener_指向的一个ScaleGestureListenerImpl对象的成员函数OnScaleBegin,用来询问是否允许产生一个捏合手势操作。如果允许的话,ScaleGestureDetector类的成员变量in_progress_就会被设置为true。

       上述ScaleGestureListenerImpl对象就是前面分析的GestureProvider类的成员变量scale_gesture_listener_所指向的ScaleGestureListenerImpl对象,它的成员函数OnScaleBegin的实现如下所示:

class GestureProvider::ScaleGestureListenerImpl
    : public ScaleGestureDetector::ScaleGestureListener {
 public:
  ......

  virtual bool OnScaleBegin(const ScaleGestureDetector& detector,
                            const MotionEvent& e) OVERRIDE {
    if (ignore_multitouch_events_ && !detector.InDoubleTapMode())
      return false;
    ......
    return true;
  }

  ......
};
       这个函数定义在文件external/chromium_org/ui/events/gesture_detection/gesture_provider.cc中。

       ScaleGestureListenerImpl类的成员函数OnScaleBegin在两种情况下的返回值为true。第一种情况是当它的成员变量ignore_multitouch_events_的值等于false时。这表示当网页被多点触摸时,有可能需要对网页进行缩放,也就是要产生一个捏合手势操作。第二种情况是网页被Double Tap时。

       回到ScaleGestureDetector类的成员函数OnTouchEvent中。综合起来,我们就可以知道,当网页被多点触摸移动或者Double Tap后移动,并且移动的距离或者两次Tap的距离大于等于预设值时,那么就会产生捏合手势操作。这个捏合手势操作将会交给ScaleGestureDetector类的成员变量listener_指向的ScaleGestureListenerImpl对象的成员函数OnScale处理,如下所示:

class GestureProvider::ScaleGestureListenerImpl
    : public ScaleGestureDetector::ScaleGestureListener {
 public:
  ......

  virtual bool OnScale(const ScaleGestureDetector& detector,
                       const MotionEvent& e) OVERRIDE {
    ......

    if (!pinch_event_sent_) {
      pinch_event_sent_ = true;
      provider_->Send(CreateGesture(ET_GESTURE_PINCH_BEGIN,
                                    e.GetId(),
                                    detector.GetEventTime(),
                                    detector.GetFocusX(),
                                    detector.GetFocusY(),
                                    detector.GetFocusX() + e.GetRawOffsetX(),
                                    detector.GetFocusY() + e.GetRawOffsetY(),
                                    e.GetPointerCount(),
                                    GetBoundingBox(e)));
    }

    ......

    float scale = detector.GetScaleFactor();
    ......

    GestureEventDetails pinch_details(ET_GESTURE_PINCH_UPDATE, scale, 0);
    provider_->Send(CreateGesture(pinch_details,
                                  e.GetId(),
                                  detector.GetEventTime(),
                                  detector.GetFocusX(),
                                  detector.GetFocusY(),
                                  detector.GetFocusX() + e.GetRawOffsetX(),
                                  detector.GetFocusY() + e.GetRawOffsetY(),
                                  e.GetPointerCount(),
                                  GetBoundingBox(e)));
    return true;
  }

  ......
};
       这个函数定义在文件external/chromium_org/ui/events/gesture_detection/gesture_provider.cc中。

       ScaleGestureListenerImpl类的成员函数OnScale首先检查网页是否刚刚开始被捏合。如果是的话,ScaleGestureListenerImpl类的成员变量pinch_event_sent_的值就会等于false。在这种情况下,Browser进程会先发送一个类型为ET_GESTURE_PINCH_BEGIN的手势操作给Render进程。

       ScaleGestureListenerImpl类的成员函数OnScale接下来又通过调用参数detector描述的ScaleGestureDetector对象的成员函数GetScaleFactor得到捏合手势操作所产生的缩放因子,然后将这个缩放因子封装在一个类型为ET_GESTURE_PINCH_UPDATE的手势操作中发送给Render进程。

       与前面提到的类型为ET_GESTURE_SCROLL_BEGIN和ET_GESTURE_SCROLL_UPDATE的手势操作一样,类型为ET_GESTURE_PINCH_BEGIN和ET_GESTURE_PINCH_UPDATE的手势操作也是通过GestureProvider类的成员函数Send发送给Render进程的。但是在我们这个情景中,GestureProvider类的成员函数Send并没有将这些手势操作发送给Render进程,而仅仅是将它们收集在一个Gesture Event Data Packet中。

       这一步执行完成之后,Browser进程就对当前发生的Touch事件进行了滑动手势和捏合手势检测,并且检测出来的手势操作(ET_GESTURE_SCROLL_BEGIN、ET_GESTURE_SCROLL_UPDATE、ET_GESTURE_PINCH_BEGIN和ET_GESTURE_PINCH_UPDATE)都保存了FilteredGestureProvider类的成员变量pending_gesture_packet_描述的一个类型为GestureEventDataPacket::TOUCH_MOVE的Gesture Event Data Packet。

       回到FilteredGestureProvider类的成员函数OnTouchEvent中,它接下来要做的工作就将保存在成员变量pending_gesture_packet_描述的Gesture Event Data Packet中的手势操作发送给Render进程处理,这是通过调用另外一个成员变量gesture_filter_描述的一个TouchDispositionGestureFilter对象的成员函数OnGesturePacket实现的,如下所示:

TouchDispositionGestureFilter::PacketResult
TouchDispositionGestureFilter::OnGesturePacket(
    const GestureEventDataPacket& packet) {
  ......

  if (packet.gesture_source() == GestureEventDataPacket::TOUCH_TIMEOUT &&
      Tail().empty()) {
    // Handle the timeout packet immediately if the packet preceding the timeout
    // has already been dispatched.
    FilterAndSendPacket(packet);
    return SUCCESS;
  }

  Tail().push(packet);
  return SUCCESS;
}

       这个函数定义在文件external/chromium_org/ui/events/gesture_detection/touch_disposition_gesture_filter.cc中。

       TouchDispositionGestureFilter类的成员函数OnGesturePacket首先判断参数packet描述的Gesture Event Data Packet的类型是否等于GestureEventDataPacket::TOUCH_TIMEOUT。如果等于,并且当前的Gesture Event Data Packet队列为空,那么参数packet描述的Gesture Event Data Packet就会马上被发送给Render进程。这个发送过程是通过调用TouchDispositionGestureFilter类的成员函数FilterAndSendPacket进行的。

       从前面的分析可以知道,参数packet描述的Gesture Event Data Packet的类型为GestureEventDataPacket::TOUCH_MOVE,因此它将不会马上被发送给Render进程,而是被保存在一个Gesture Event Data Packet队列中。那么,这个队列中的Gesture Event Data Packet什么会被发送给Render进程呢?当Render进程处理完成Browser进程上一次发送给它的Gesture Event Data Packet之后,它就会给Browser进程发送一个ACK。Browser进程接收到这个ACK之后,就会从队列中取出下一个Gesture Event Data Packet发送给Render进程处理。这个发送过程同样也是通过调用TouchDispositionGestureFilter类的成员函数FilterAndSendPacket进行的。因此,接下来我们就继续分析TouchDispositionGestureFilter类的成员函数FilterAndSendPacket的实现,如下所示:

void TouchDispositionGestureFilter::FilterAndSendPacket(
    const GestureEventDataPacket& packet) {
  ......

  for (size_t i = 0; i < packet.gesture_count(); ++i) {
    const GestureEventData& gesture = packet.gesture(i);
    ......

    SendGesture(gesture, packet);
  }

  ......
}
      这个函数定义在文件external/chromium_org/ui/events/gesture_detection/touch_disposition_gesture_filter.cc中。

      TouchDispositionGestureFilter类的成员函数FilterAndSendPacket遍历保存在参数packet描述的Gesture Event Data Packet中的每一个手势操作,并且调用另外一个成员函数SendGesture分别将这些手势操作发送给Render进程,如下所示:

void TouchDispositionGestureFilter::SendGesture(
    const GestureEventData& event,
    const GestureEventDataPacket& packet_being_sent) {
  ......

  client_->ForwardGestureEvent(event);
}
       这个函数定义在文件external/chromium_org/ui/events/gesture_detection/touch_disposition_gesture_filter.cc中。

       TouchDispositionGestureFilter类的成员变量client_指向的是一个FilteredGestureProvider对象。这个FilteredGestureProvider对象就是前面分析的RenderWidgetHostViewAndroid类的成员变量gesture_provider_所描述的FilteredGestureProvider对象。TouchDispositionGestureFilter类的成员函数SendGesture通过调用这个FilteredGestureProvider对象的成员函数ForwardGestureEvent将参数event描述的手势操作发送给Render进程。

       FilteredGestureProvider类的成员函数ForwardGestureEvent的实现如下所示:

void FilteredGestureProvider::ForwardGestureEvent(
    const GestureEventData& event) {
  client_->OnGestureEvent(event);
}
       这个函数定义在文件external/chromium_org/ui/events/gesture_detection/filtered_gesture_provider.cc中。

       FilteredGestureProvider类的成员变量client_指向的是一个RenderWidgetHostViewAndroid对象。这个RenderWidgetHostViewAndroid对象就前面描述的在Browser进程中用来加载网页的控件。FilteredGestureProvider类的成员函数ForwardGestureEvent通过调用这个RenderWidgetHostViewAndroid对象的成员函数OnGestureEvent将参数event描述的手势操作发送给Render进程。

       RenderWidgetHostViewAndroid类的成员函数OnGestureEvent的实现如下所示:

void RenderWidgetHostViewAndroid::OnGestureEvent(
    const ui::GestureEventData& gesture) {
  ......

  SendGestureEvent(CreateWebGestureEventFromGestureEventData(gesture));
}
      这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_widget_host_view_android.cc中。

      RenderWidgetHostViewAndroid类的成员函数OnGestureEvent首先调用函数CreateWebGestureEventFromGestureEventData将参数gesture描述的手势操作封装在一个WebGestureEvent对象中,如下所示:

WebGestureEvent CreateWebGestureEventFromGestureEventData(
    const ui::GestureEventData& data) {
  WebGestureEvent gesture;
  gesture.x = data.x;
  gesture.y = data.y;
  gesture.globalX = data.raw_x;
  gesture.globalY = data.raw_y;
  gesture.timeStampSeconds = (data.time - base::TimeTicks()).InSecondsF();
  gesture.sourceDevice = blink::WebGestureDeviceTouchscreen;

  switch (data.type()) {
    ......

    case ui::ET_GESTURE_SCROLL_UPDATE:
      gesture.type = WebInputEvent::GestureScrollUpdate;
      gesture.data.scrollUpdate.deltaX = data.details.scroll_x();
      gesture.data.scrollUpdate.deltaY = data.details.scroll_y();
      break;

    ......
  
    case ui::ET_GESTURE_PINCH_UPDATE:
      gesture.type = WebInputEvent::GesturePinchUpdate;
      gesture.data.pinchUpdate.scale = data.details.scale();
      break;

    ......
  }

  return gesture;
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/input/web_input_event_util.cc中。

       函数CreateWebGestureEventFromGestureEventData会将不同类型的手势操作封装在不同类型的WebGestureEvent对象中。例如,ui::ET_GESTURE_SCROLL_UPDATE类的手势操作,即滑动手势操作,会封装在一个类型为WebInputEvent::GestureScrollUpdate的WebGestureEvent对象中;又如,ui::ET_GESTURE_PINCH_UPDATE类型的手势操作,即捏合手势操作,会封装在一个类型为WebInputEvent::GesturePinchUpdate的WebGestureEvent对象中。

       回到RenderWidgetHostViewAndroid类的成员函数OnGestureEvent中,它将手势操作封装在一个WebGestureEvent对象之后,再调用另外一个成员函数SendGestureEvent将这个WebGestureEvent对象发送给Render进程,如下所示:

void RenderWidgetHostViewAndroid::SendGestureEvent(
    const blink::WebGestureEvent& event) {
  ......

  if (host_)
    host_->ForwardGestureEventWithLatencyInfo(event, CreateLatencyInfo(event));
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_widget_host_view_android.cc中。

       RenderWidgetHostViewAndroid类的成员变量host_指向的是一个RenderWidgetHostImpl对象。这个RenderWidgetHostImpl对象描述的是加载当前正在发生输入事件的网页的Render进程。RenderWidgetHostViewAndroid类的成员函数SendGestureEvent调用这个RenderWidgetHostImpl对象的成员函数ForwardGestureEventWithLatencyInfo将参数event描述的手势操作发送给它所描述的Render进程。

       RenderWidgetHostImpl类的成员函数ForwardGestureEventWithLatencyInfo的实现如下所示:

void RenderWidgetHostImpl::ForwardGestureEventWithLatencyInfo(
    const blink::WebGestureEvent& gesture_event,
    const ui::LatencyInfo& ui_latency) {
  ......

  GestureEventWithLatencyInfo gesture_with_latency(gesture_event, latency_info);
  input_router_->SendGestureEvent(gesture_with_latency);
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_widget_host_impl.cc中。

       RenderWidgetHostImpl类的成员函数ForwardGestureEventWithLatencyInfo首先将参数gesture_event描述的手势操作封装在一个GestureEventWithLatencyInfo对象中。

       RenderWidgetHostImpl类的成员变量input_router_指向的是一个InputRouterImpl对象。这个InputRouterImpl负责将输入事件发送给Render进程。因此,RenderWidgetHostImpl类的成员函数SendGestureEvent就通过调用这个InputRouterImpl对象的成员函数SendGestureEvent将上述封装了手势操作的GestureEventWithLatencyInfo对象发送给Render进程。

       InputRouterImpl类的成员函数SendGestureEvent的实现如下所示:

void InputRouterImpl::SendGestureEvent(
    const GestureEventWithLatencyInfo& original_gesture_event) {
  ......

  GestureEventWithLatencyInfo gesture_event(original_gesture_event);

  ......

  SendGestureEventImmediately(gesture_event);
}
      这个函数定义在文件external/chromium_org/content/browser/renderer_host/input/input_router_impl.cc中。

      InputRouterImpl类的成员函数SendGestureEvent主要是调用另外一个成员函数SendGestureEventImmediately将参数original_gesture_event描述的手势操作发送给Render进程,如下所示:

void InputRouterImpl::SendGestureEventImmediately(
    const GestureEventWithLatencyInfo& gesture_event) {
  ......

  FilterAndSendWebInputEvent(gesture_event.event, gesture_event.latency, false);
}
      这个函数定义在文件external/chromium_org/content/browser/renderer_host/input/input_router_impl.cc中。

      InputRouterImpl类的成员函数SendGestureEventImmediately又主要是调用另外一个成员函数FilterAndSendWebInputEvent将参数gesture_event描述的手势操作发送给Render进程,如下所示:

void InputRouterImpl::FilterAndSendWebInputEvent(
    const WebInputEvent& input_event,
    const ui::LatencyInfo& latency_info,
    bool is_keyboard_shortcut) {
  ......

  OfferToHandlers(input_event, latency_info, is_keyboard_shortcut);
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/input/input_router_impl.cc中。

       InputRouterImpl类的成员函数FilterAndSendWebInputEvent又主要是调用另外一个成员函数OfferToHandlers将参数input_event描述的手势操作发送给Render进程,如下所示:       

void InputRouterImpl::OfferToHandlers(const WebInputEvent& input_event,
                                      const ui::LatencyInfo& latency_info,
                                      bool is_keyboard_shortcut) {
  ......

  if (OfferToClient(input_event, latency_info))
    return;

  OfferToRenderer(input_event, latency_info, is_keyboard_shortcut);

  ......
}
      这个函数定义在文件external/chromium_org/content/browser/renderer_host/input/input_router_impl.cc中。

      InputRouterImpl类的成员函数OfferToHandlers首先调用成员函数OfferToClient询问Browser进程是否要过滤参数input_event描述的手势操作。如果过滤的话,那么InputRouterImpl类的成员函数OfferToHandlers就不会将它发送给Render进程。否则的话,就会调用另外一个成员函数OfferToRenderer进行发送。

      我们假设Browser进程不过滤参数input_event描述的手势操作,因此接下来这个手势就会通过InputRouterImpl类的成员函数OfferToRenderer发送给Render进程,如下所示:

bool InputRouterImpl::OfferToRenderer(const WebInputEvent& input_event,
                                      const ui::LatencyInfo& latency_info,
                                      bool is_keyboard_shortcut) {
  if (Send(new InputMsg_HandleInputEvent(
          routing_id(), &input_event, latency_info, is_keyboard_shortcut))) {
    ......
    return true;
  }
  return false;
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/input/input_router_impl.cc中。

       从这里就可以看到,InputRouterImpl类的成员函数OfferToRenderer会将参数input_event描述的手势操作封装在一个类型为InputMsg_HandleInputEvent的IPC消息中,然后再将这个消息发送给Render进程处理。这个处理过程我们在接下来的一篇文章中再详细分析。

       这一步执行完成后,Browser进程就将检测到的手势操作发送给Render进程了。回到前面分析的RenderWidgetHostViewAndroid类的成员函数OnTouchEvent中,它接下来再调用函数CreateWebTouchEventFromMotionEvent将原始的Touch事件封装在一个blink::WebTouchEvent对象中,如下所示:

blink::WebTouchEvent CreateWebTouchEventFromMotionEvent(
    const ui::MotionEvent& event) {
  blink::WebTouchEvent result;

  WebTouchEventTraits::ResetType(
      ToWebInputEventType(event.GetAction()),
      (event.GetEventTime() - base::TimeTicks()).InSecondsF(),
      &result);

  result.touchesLength =
      std::min(event.GetPointerCount(),
               static_cast<size_t>(WebTouchEvent::touchesLengthCap));
  DCHECK_GT(result.touchesLength, 0U);

  for (size_t i = 0; i < result.touchesLength; ++i)
    result.touches[i] = CreateWebTouchPoint(event, i);

  return result;
}
      这个函数定义在文件external/chromium_org/content/browser/renderer_host/input/web_input_event_util.cc中。

      函数CreateWebTouchEventFromMotionEvent首先调用函数ToWebInputEventType获得接下来要创建的blink::WebTouchEvent对象的类型,如下所示:

WebInputEvent::Type ToWebInputEventType(MotionEvent::Action action) {
  switch (action) {
    case MotionEvent::ACTION_DOWN:
      return WebInputEvent::TouchStart;
    case MotionEvent::ACTION_MOVE:
      return WebInputEvent::TouchMove;
    case MotionEvent::ACTION_UP:
      return WebInputEvent::TouchEnd;
    case MotionEvent::ACTION_CANCEL:
      return WebInputEvent::TouchCancel;
    case MotionEvent::ACTION_POINTER_DOWN:
      return WebInputEvent::TouchStart;
    case MotionEvent::ACTION_POINTER_UP:
      return WebInputEvent::TouchEnd;
  }
  NOTREACHED() << "Invalid MotionEvent::Action.";
  return WebInputEvent::Undefined;
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/input/web_input_event_util.cc中。

       参数action表示要封装的Touch事件的类型。函数ToWebInputEventType会根据不同的Touch事件类型返回不同的blink::WebTouchEvent对象类型。例如,对于类型为MotionEvent::ACTION_MOVE的Touch事件,函数ToWebInputEventType返回的blink::WebTouchEvent对象类型为WebInputEvent::TouchMove。

       回到函数CreateWebTouchEventFromMotionEvent中,它获得了接下来要创建的blink::WebTouchEvent对象的类型之后,就会创建这个blink::WebTouchEvent对象,并且会将event描述的Touch事件的所有信息,例如触摸点位置,保存在创建出来的blink::WebTouchEvent对象中。

       这一步执行完成之后,再回到前面分析的RenderWidgetHostViewAndroid类的成员函数OnTouchEvent中,它接下来就会将前面创建的blink::WebTouchEvent对象发送给Render进程处理,这是通过调用另外一个成员函数SendTouchEvent实现的,如下所示:

void RenderWidgetHostViewAndroid::SendTouchEvent(
    const blink::WebTouchEvent& event) {
  if (host_)
    host_->ForwardTouchEventWithLatencyInfo(event, CreateLatencyInfo(event));

  ......
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_widget_host_view_android.cc中。

       前面提到,RenderWidgetHostViewAndroid类的成员变量host_指向的是一个RenderWidgetHostImpl对象,RenderWidgetHostViewAndroid类的成员函数SendTouchEvent调用这个RenderWidgetHostImpl对象的成员函数ForwardTouchEventWithLatencyInfo将参数event描述的Touch事件发送给Render进程。

       RenderWidgetHostImpl类的成员函数ForwardTouchEventWithLatencyInfo的实现如下所示:

void RenderWidgetHostImpl::ForwardTouchEventWithLatencyInfo(
      const blink::WebTouchEvent& touch_event,
      const ui::LatencyInfo& ui_latency) {
  ......

  ui::LatencyInfo latency_info =
      CreateRWHLatencyInfoIfNotExist(&ui_latency, touch_event.type);
  TouchEventWithLatencyInfo touch_with_latency(touch_event, latency_info);
  input_router_->SendTouchEvent(touch_with_latency);
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_widget_host_impl.cc中。

       RenderWidgetHostImpl类的成员函数ForwardTouchEventWithLatencyInfo首先将参数touch_event描述的Touch事件封装在一个TouchEventWithLatencyInfo对象中,然后再调用成员变量input_router_指向的一个InputRouterImpl对象的成员函数SendTouchEvent将这个TouchEventWithLatencyInfo对象发送给Render进程。

       InputRouterImpl类的成员函数SendTouchEvent的实现如下所示:

void InputRouterImpl::SendTouchEvent(
    const TouchEventWithLatencyInfo& touch_event) {
  ......
  touch_event_queue_.QueueEvent(touch_event);
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/input/input_router_impl.cc中。

       InputRouterImpl类的成员变量touch_event_queue_描述的是一个TouchEventQueue对象,InputRouterImpl类的成员函数SendTouchEvent调用这个TouchEventQueue对象的成员函数QueueEvent将参数touch_event描述的Touch事件发送给Render进程。

       TouchEventQueue类的成员函数QueueEvent的实现如下所示:

void TouchEventQueue::QueueEvent(const TouchEventWithLatencyInfo& event) {
  ......

  // If the queueing of |event| was triggered by an ack dispatch, defer
  // processing the event until the dispatch has finished.
  if (touch_queue_.empty() && !dispatching_touch_ack_) {
    ......

    // There is no touch event in the queue. Forward it to the renderer
    // immediately.
    touch_queue_.push_back(new CoalescedWebTouchEvent(event, false));
    ForwardNextEventToRenderer();
    return;
  }

  // If the last queued touch-event was a touch-move, and the current event is
  // also a touch-move, then the events can be coalesced into a single event.
  if (touch_queue_.size() > 1) {
    CoalescedWebTouchEvent* last_event = touch_queue_.back();
    if (last_event->CoalesceEventIfPossible(event))
      return;
  }
  touch_queue_.push_back(new CoalescedWebTouchEvent(event, false));
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/input/touch_event_queue.cc中。

       TouchEventQueue类的成员变量touch_queue_描述的是一个Touch事件队列。这个队列用来暂存即将要发送给Render进程的Touch事件。一个即将要发送的Touch事件在两种情况下需要暂存在队列中:

       1. 在它之前的Touch事件还未发送给Render进程,即Touch事件队列不为空。

       2. Render进程正在发送一个ACK事件给Browser进程,而Browser进程正在分发这个ACK事件。这个ACK事件分发完成之后,Browser进程才可以将下一个Touch事件发送给Render进程处理。这时候TouchEventQueue类的成员变量dispatching_touch_ack_的值就不等于NULL,它指向正在分发的ACK事件。

       TouchEventQueue类的成员函数QueueEvent所做的事情就是判断参数event描述的Touch事件是否能够马上发送。如果能马上发送,那么就会将它保存在Touch事件队列中,然后再调用另外一个成员函数ForwardNextEventToRenderer将它从Touch事件队列读取出来,并且发送给Render进程。如果不能马上发送,那么同样会将它保存在Touch事件队列中,不过要等到上一个发送给Render进程的Touch事件被ACK之后,才能继续将它发送给Render进程。这同样是通过调用TouchEventQueue类的成员函数ForwardNextEventToRenderer进行发送的。

       我们注意到,在将参数event描述的Touch事件保存在Touch事件队列之前,如果队列不为空,那么TouchEventQueue类的成员函数QueueEvent会判断参数event描述的Touch事件与队列中最后一个Touch事件是否是相同的,也就是它们所包含的触摸点都是一样的。如果相同,那么就可以合并为一个Touch事件发送给Render进程。合并后的Touch事件使用一个CoalescedWebTouchEvent对象描述。这样可以避免重复向Render进程发送相同的Touch事件。

       我们假设参数event描述的Touch事件可以马上发送给Render进程,因此接下来我们就继续分析TouchEventQueue类的成员函数ForwardNextEventToRenderer的实现,如下所示:

void TouchEventQueue::ForwardNextEventToRenderer() {
  ......

  TouchEventWithLatencyInfo touch = touch_queue_.front()->coalesced_event();
  ......

  // A synchronous ack will reset |dispatching_touch_|, in which case
  // the touch timeout should not be started.
  base::AutoReset<bool> dispatching_touch(&dispatching_touch_, true);
  SendTouchEventImmediately(touch);
  
  ......
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/input/touch_event_queue.cc中。

       TouchEventQueue类的成员函数ForwardNextEventToRenderer首先从Touch事件队列中取出第一个Touch事件,然后调用另外一个成员函数SendTouchEventImmediately将该Touch事件发送给Render进程。在发送的过程中,TouchEventQueue类的成员变量dispatching_touch_会被设置为true,并且会在发送结束后(也就是TouchEventQueue类的成员函数ForwardNextEventToRenderer调用结束),恢复为false。

       TouchEventQueue类的成员函数SendTouchEventImmediately的实现如下所示:

void TouchEventQueue::SendTouchEventImmediately(
    const TouchEventWithLatencyInfo& touch) {
  ......

  client_->SendTouchEventImmediately(touch);
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/input/touch_event_queue.cc中。

       TouchEventQueue类的成员变量client_指向的是一个InputRouterImpl对象。这个InputRouterImpl对象就前面分析的RenderWidgetHostImpl类的成员变量input_router_所指向的InputRouterImpl对象。TouchEventQueue类的成员函数SendTouchEventImmediately调用这个InputRouterImpl对象的成员函数SendTouchEventImmediately将参数touch描述的Touch事件发送给Render进程。

       InputRouterImpl类的成员函数SendTouchEventImmediately的实现如下所示:

void InputRouterImpl::SendTouchEventImmediately(
    const TouchEventWithLatencyInfo& touch_event) {
  ......

  FilterAndSendWebInputEvent(touch_event.event, touch_event.latency, false);
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/input/input_router_impl.cc中。

       从这里可以看到,InputRouterImpl类的成员函数SendTouchEventImmediately是调用我们前面已经分析过的另外一个成员函数FilterAndSendWebInputEvent将参数touch_event描述的Touch事件发送给Render进程的。从前面的分析可以知道,这个Touch事件封装在一个类型为WebInputEvent::TouchMove的WebInputEvent对象中,它的发送过程与前面分析的滑动手势操作和捏合手势操作的发送过程是一样的,只不过后两者分别封装在类型为WebInputEvent::GestureScrollUpdate和WebInputEvent::GesturePinchUpdate的WebInputEvent对象中。

       至此,我们就以Touch事件为例,分析完成了Browser进程捕捉网页输入事件,以及从中检测手势操作的过程。这些网页输入事件和手势操作都是通过类型为InputMsg_HandleInputEvent的IPC消息发送给Render进程处理的。在接下来的两篇文章中,我们就详细分析Render进程处理网页输入事件和手势操作的过程,也就是Render进程处理类型为InputMsg_HandleInputEvent的IPC消息的过程,敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/7/4 1:01:09 原文链接
阅读:15333 评论:4 查看评论

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

$
0
0

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

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

《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类的另外一个成员变量target_loop_。以后Input Event Filter就会通过这个消息循环代理对象将滑动和捏合手势分发给Compositor线程处理。

       InputEventFilter类的构造函数还是调用base::MessageLoopProxy类的静态成员函数current获得当前线程(Main线程)的消息循环代理对象,并且将这个消息循环代理对象保存在InputEventFilter类的成员变量main_loop_。以后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类还有另外一个成员变量scroll_delta_,用来表示当前正在处理的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_scale_factor_。

       2. 最小缩放因子min_page_scale_factor_。

       3. 最大缩放因子max_page_scale_factor_。

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

       在我们这个情景中,前面3个缩放因子是保持不变的。发生变化的是第四个缩放因子。当前缩放因子page_scale_factor_乘以新的未同步到CC Layer Tree去的缩放因子page_scale_delta,将会得到CC Active Layer Tree新的缩放因子。这个缩放因子不能小于最小缩放因子min_page_scale_factor_,以及不能大于最大缩放因子max_page_scale_factor_。因此,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

作者:Luoshengyang 发表于2016/7/11 0:59:35 原文链接
阅读:15050 评论:6 查看评论

Chromium分发输入事件给WebKit处理的过程分析

$
0
0

       Chromium的Render进程接收到Browser进程分发过来的输入事件之后,会在Compoistor线程中处理掉滑动和捏合手势这两种特殊的输入事件,其它类型的输入事件则交给Main线程处理。Main线程又会进一步将输入事件分发给WebKit处理。WebKit则根据输入事件发生的位置在网页中找到对应的HTML元素进行处理。本文接下来详细分析Chromium分发输入事件给WebKit处理的过程。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

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

       以Touch事件为例。WebKit接收到Chromium分发给它的Touch事件通知后,所做的第一件事情是做Hit Test,也就是检测Touch事件发生在哪一个HTML元素上,然后再将接收到的Touch事件分发给该它处理,如图1所示:


图1 WebKit处理Touch事件的过程

       HTML元素对应于网页DOM Tree中的Node。要从DOM Tree中找到输入事件的目标Node,必须要知道所有Node的Z轴位置大小,然后按照从上到下的顺序进行Hit Test。从前面Chromium网页Graphics Layer Tree创建过程分析一文可以知道,DOM Tree中的Node在Z轴上的实际位置,除了跟它们本身的Z-Index属性值有关之外,还与它们所处理的Stacking Context的Z-Index值有关。网页的Render Layer Tree记录了所有的Stacking Context,因此,WebKit要从网页的Render Layer Tree入手,才能在DOM Tree中找到正确的目标Node处理当前发生的输入事件。

       接下来,我们就从Compoistor线程将输入事件分发给Main线程开始,分析WebKit接收和处理输入事件的过程。从前面Chromium网页滑动和捏合手势处理过程分析一文可以知道,Compoistor线程是在InputEventFilter类的成员函数ForwardToHandler中将它不处理的输入事件发分给Main线程处理的,如下所示:

void InputEventFilter::ForwardToHandler(const IPC::Message& message) {  
  ......  
  
  int routing_id = message.routing_id();  
  InputMsg_HandleInputEvent::Param params;  
  if (!InputMsg_HandleInputEvent::Read(&message, &params))  
    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;  
  }  
  
  ......   
}
       这个函数定义在文件external/chromium_org/content/renderer/input/input_event_filter.cc中。

       InputEventFilter类的成员函数ForwardToHandler的详细分析可以参考前面Chromium网页滑动和捏合手势处理过程分析一文。对于Compositor线程不处理的输入事件,InputEventFilter类的成员函数ForwardToHandler会将它重新封装在一个InputMsg_HandleInputEvent消息中。这个InputMsg_HandleInputEvent消息又会进一步封装在一个Task中,并且发送给Main线程的消息队列。这个Task绑定了InputEventFilter类的成员函数ForwardToMainListener。

       这意味着接下来InputEventFilter类的成员函数ForwardToMainListener就会在Main线程中被调用,用来处理Compositor线程分发过来的输入事件,如下所示:

void InputEventFilter::ForwardToMainListener(const IPC::Message& message) {
  main_listener_->OnMessageReceived(message);
}
       这个函数定义在文件external/chromium_org/content/renderer/input/input_event_filter.cc中。

       从前面Chromium网页滑动和捏合手势处理过程分析一文可以知道,InputEventFilter类的成员变量main_listener_指向的是一个RenderThreadImpl对象。这个RenderThreadImpl对象描述的就是Render进程中的Render Thread,也就是Main Thread。InputEventFilter类的成员函数ForwardToMainListener调用这个RenderThreadImpl对象的成员函数OnMessageReceived处理参数message描述的输入事件。

       RenderThreadImpl类的成员函数OnMessageReceived是从父类ChildThread继承下来的,它的实现如下所示:

bool ChildThread::OnMessageReceived(const IPC::Message& msg) {
  .....

  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(ChildThread, msg)
    ......
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()

  if (handled)
    return true;

  ......

  return router_.OnMessageReceived(msg);
}
       这个函数定义在文件external/chromium_org/content/child/child_thread.cc 中。

       ChildThread类的成员函数OnMessageReceived首先检查参数msg描述的消息是否要由自己处理。如果不处理,那么就再分发给注册在它里面的Route进行处理。

       从前面Chromium网页Frame Tree创建过程分析一文可以知道,Render进程会为每一个网页创建一个RenderViewImpl对象,用来代表网页所加载在的控件。这个RenderViewImpl对象在初始化的过程中,会通过父类RenderWidget的成员函数DoInit将自己注册为Render Thread中的一个Route,如下所示:

bool RenderWidget::DoInit(int32 opener_id,
                          WebWidget* web_widget,
                          IPC::SyncMessage* create_widget_message) {
  ......

  bool result = RenderThread::Get()->Send(create_widget_message);
  if (result) {
    RenderThread::Get()->AddRoute(routing_id_, this);
    .......
    return true;
  } 

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

      这意味着前面分析的RenderThreadImpl类的成员函数OnMessageReceived会将接收到的、自己又不处理的消息分发给RenderWidget类处理。RenderWidget类通过成员函数OnMessageReceived接收RenderThreadImpl类分发过来的消息,并且判断分发过来的消息是否与输入事件有关,也就是判断是否为一个类型为InputMsg_HandleInputEvent的消息。如果是的话,那么就会进行处理。如下所示:

bool RenderWidget::OnMessageReceived(const IPC::Message& message) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(RenderWidget, message)
    IPC_MESSAGE_HANDLER(InputMsg_HandleInputEvent, OnHandleInputEvent)
    ......
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}
       这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。

       RenderWidget类通过成员函数OnMessageReceived会将类型为InputMsg_HandleInputEvent的消息分发给另外一个成员函数OnHandleInputEvent处理,如下所示:

void RenderWidget::OnHandleInputEvent(const blink::WebInputEvent* input_event,
                                      const ui::LatencyInfo& latency_info,
                                      bool is_keyboard_shortcut) {
  ......

  base::TimeTicks start_time;
  if (base::TimeTicks::IsHighResNowFastAndReliable())
    start_time = base::TimeTicks::HighResNow();
  ......

  bool prevent_default = false;
  if (WebInputEvent::isMouseEventType(input_event->type)) {
    const WebMouseEvent& mouse_event =
        *static_cast<const WebMouseEvent*>(input_event);
    ......
    prevent_default = WillHandleMouseEvent(mouse_event);
  }

  if (WebInputEvent::isKeyboardEventType(input_event->type)) {
    ......
#if defined(OS_ANDROID)
    // The DPAD_CENTER key on Android has a dual semantic: (1) in the general
    // case it should behave like a select key (i.e. causing a click if a button
    // is focused). However, if a text field is focused (2), its intended
    // behavior is to just show the IME and don't propagate the key.
    // A typical use case is a web form: the DPAD_CENTER should bring up the IME
    // when clicked on an input text field and cause the form submit if clicked
    // when the submit button is focused, but not vice-versa.
    // The UI layer takes care of translating DPAD_CENTER into a RETURN key,
    // but at this point we have to swallow the event for the scenario (2).
    const WebKeyboardEvent& key_event =
        *static_cast<const WebKeyboardEvent*>(input_event);
    if (key_event.nativeKeyCode == AKEYCODE_DPAD_CENTER &&
        GetTextInputType() != ui::TEXT_INPUT_TYPE_NONE) {
      OnShowImeIfNeeded();
      prevent_default = true;
    }
#endif
  }

  if (WebInputEvent::isGestureEventType(input_event->type)) {
    const WebGestureEvent& gesture_event =
        *static_cast<const WebGestureEvent*>(input_event);
    ......
    prevent_default = prevent_default || WillHandleGestureEvent(gesture_event);
  }

  bool processed = prevent_default;
  if (input_event->type != WebInputEvent::Char || !suppress_next_char_events_) {
    suppress_next_char_events_ = false;
    if (!processed && webwidget_)
      processed = webwidget_->handleInputEvent(*input_event);
  }

  // If this RawKeyDown event corresponds to a browser keyboard shortcut and
  // it's not processed by webkit, then we need to suppress the upcoming Char
  // events.
  if (!processed && is_keyboard_shortcut)
    suppress_next_char_events_ = true;

  InputEventAckState ack_result = processed ?
      INPUT_EVENT_ACK_STATE_CONSUMED : INPUT_EVENT_ACK_STATE_NOT_CONSUMED;  
  ......

  // dispatch compositor-handled scroll gestures.
  bool event_type_can_be_rate_limited =
      input_event->type == WebInputEvent::MouseMove ||
      input_event->type == WebInputEvent::MouseWheel ||
      (input_event->type == WebInputEvent::TouchMove &&
       ack_result == INPUT_EVENT_ACK_STATE_CONSUMED);

  bool frame_pending = compositor_ && compositor_->BeginMainFrameRequested();

  // If we don't have a fast and accurate HighResNow, we assume the input
  // handlers are heavy and rate limit them.
  bool rate_limiting_wanted = true;
  if (base::TimeTicks::IsHighResNowFastAndReliable()) {
      base::TimeTicks end_time = base::TimeTicks::HighResNow();
      total_input_handling_time_this_frame_ += (end_time - start_time);
      rate_limiting_wanted =
          total_input_handling_time_this_frame_.InMicroseconds() >
          kInputHandlingTimeThrottlingThresholdMicroseconds;
  }

  // Note that we can't use handling_event_type_ here since it will be overriden
  // by reentrant calls for events after the paused one.
  bool no_ack = ignore_ack_for_mouse_move_from_debugger_ &&
      input_event->type == WebInputEvent::MouseMove;
  if (!WebInputEventTraits::IgnoresAckDisposition(*input_event) && !no_ack) {
    InputHostMsg_HandleInputEvent_ACK_Params ack;
    ack.type = input_event->type;
    ack.state = ack_result;
    ack.latency = swap_latency_info;
    scoped_ptr<IPC::Message> response(
        new InputHostMsg_HandleInputEvent_ACK(routing_id_, ack));
    if (rate_limiting_wanted && event_type_can_be_rate_limited &&
        frame_pending && !is_hidden_) {
      // We want to rate limit the input events in this case, so we'll wait for
      // painting to finish before ACKing this message.
      ......
      if (pending_input_event_ack_) {
        // As two different kinds of events could cause us to postpone an ack
        // we send it now, if we have one pending. The Browser should never
        // send us the same kind of event we are delaying the ack for.
        Send(pending_input_event_ack_.release());
      }
      pending_input_event_ack_ = response.Pass();
      ......
    } else {
      Send(response.release());
    }
  }

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

       RenderWidget类的成员函数OnHandleInputEvent将参数input_event描述的输入事件分发给WebKit之前,会做以下三件事情:

       1. 检查参数input_event描述的输入事件是否是一个鼠标事件。如果是的话,那么就会调用另外一个成员函数WillHandleMouseEvent询问RenderWidget类是否要对它进行处理。如果处理,那么本地变量prevent_default的值就会被设置为true。

       2. 检查参数input_event描述的输入事件是否是一个DPAD键盘事件。如果是的话,并且当前获得焦点的是一个Input Text控件,那么就调用另外一个成员函数OnShowImeIfNeeded弹出输入法,以便用户可以输入文本给Input Text控件去。注意,这个检查是针对Android平台的,因为Android平台才会存在DPAD键。

       3. 检查参数input_event描述的输入事件是否是一个手势操作。如果是的话,那么就会调用另外一个成员函数WillHandleGestureEvent询问RenderWidget类是否要对它进行处理。如果处理,那么本地变量prevent_default的值也会被设置为true。

       RenderWidget类的成员函数WillHandleMouseEvent和WillHandleGestureEvent的返回值均为false,这表明RenderWidget类不会处理鼠标和手势操作这两种输入事件。

       RenderWidget类的成员函数OnHandleInputEvent接下来继续判断参数input_event描述的输入事件是否为键盘字符输入事件。如果不是,并且RenderWidget类不对它进行处理,那么RenderWidget类的成员函数OnHandleInputEvent就会将它分发给WebKit处理。这表明在RenderWidget类不处理的情况下,非键盘字符输入事件一定会分发给WebKit处理。

       另一方面,对于键盘字符输入事件,它也会分发给WebKit处理。不过,分发给WebKit之后,如果WebKit决定不对它进行处理,并且它被设置为浏览器的快捷键,即参数is_keyboard_shortcut的值等于true,那么下一个键盘字符输入事件将不会再分发给WebKit处理。因为下一个键盘字符输入事件将会作为浏览器快捷键的另外一部分。在这种情况下,RenderWidget类的成员变量suppress_next_char_events_的值会被设置为true。

       将参数input_event描述的输入事件分发给WebKit之后,RenderWidget类的成员函数OnHandleInputEvent考虑是否需要给Browser进程发送一个ACK。从前面Chromium网页输入事件捕捉和手势检测过程分析一文可以知道,Browser进程收到Render进程对上一个输入事件的ACK之后,才会给它分发下一个输入事件。因此,Render进程是否给Browser进程发送ACK,可以用来控制Browser进程分发输入事件给Render进程的节奏。

       有三种类型的输入事件是需要控制分发节奏的:鼠标移动、鼠标中键滚动和触摸事件。这三类输入事件都有一个特点,它们会连续大量地输入。每一次输入Render进程都需要对它们作出响应,也就是对网页进行处理。如果它们发生得太过频繁,那么就会造成Render进程的负担很重。因此,对于这三类输入事件,RenderWidget类的成员函数OnHandleInputEvent并不会都马上对它们进行ACK。这里有一点需要注意,对于触摸事件,只有WebKit没有对它进行处理,那么RenderWidget类的成员函数OnHandleInputEvent会马上对它进行ACK。这是因为这个触摸事件和接下来发生的触摸事件,可能会触发手势操作,例如滑动手势和捏合手势。这些手势操作必须要迅速进行处理,否则的话,用户就会觉得浏览器没有反应了。

       什么情况下不会马上进行ACK呢?如果网页的当前帧请求执行了一次Commit操作,也就是请求了重新绘制网页的CC Layer Tree,但是网页的CC Layer Tree还没有绘制完成,并且也没有同步到CC Pending Layer Tree,那么就不会马上进行ACK。这个ACK会被缓存在RenderWidget类的成员变量pending_input_event_ack_中。等到网页的CC Layer Tree重新绘制完成并且同步到CC Pending Layer Tree之后,被缓存的ACK才会发送给Browser进程。之所以要这样做,是因为Main线程在重新绘制网页的CC Layer Tree的时候,任务是相当重的,这时候不宜再分发新的输入事件给它处理。

        此外,如果平台实现了高精度时钟,那么RenderWidget类的成员函数OnHandleInputEvent也不一定要等到网页的CC Layer Tree重新绘制完成并且同步到CC Pending Layer Tree之后,才将缓存的ACK才会发送给Browser进程。如果已经过去了一段时间,网页的CC Layer Tree还没有绘制完成,也没有同步到CC Pending Layer Tree,那么RenderWidget类的成员函数OnHandleInputEvent就会马上给Browser进程发送一个ACK。这个时间被设置为kInputHandlingTimeThrottlingThresholdMicroseconds(4166)微秒,大约等于1/4帧时间(假设一帧时间为16毫秒)。

        还有一种特殊情况,造成RenderWidget类的成员函数OnHandleInputEvent不会缓存输入事件的ACK,那就是网页当前不可见。对于不可见的网页,Main线程不需要对它们进行处理,因为处理了也是白处理(用户看不到)。因此,可以认为此时分发给网页的任何输入都在瞬间完成,于是就可以安全地将它们ACK给Browser进程,不用担心Main线程的负载问题。

       最后,我们还看到,如果浏览器连上了Debugger,并且Debugger希望不要对鼠标移动事件进行ACK,那么鼠标移动事件在任何情况下,都不会进行ACK。Browser进程是通过类型为InputHostMsg_HandleInputEvent的消息向Render进程分发输入事件的。相应地,Render进程通过向Browser进程发送类型为InputHostMsg_HandleInputEvent_ACK的消息对输入事件进行ACK。

       接下来,我们就重点分析Chromium分发输入事件给WebKit处理的过程。从前面Chromium网页Frame Tree创建过程分析一文可以知道, RenderWidget类的成员变量webwidget_指向的是一个WebViewImpl对象。RenderWidget类的成员函数OnHandleInputEvent就是通过调用这个WebViewImpl对象的成员函数handleInputEvent将参数input_event描述的输入事件分发给WebKit处理的。前面我们已经假设这是一个Touch事件。

       WebViewImpl类的成员函数handleInputEvent的实现如下所示:

bool WebViewImpl::handleInputEvent(const WebInputEvent& inputEvent)
{
    ......

    return PageWidgetDelegate::handleInputEvent(m_page.get(), *this, inputEvent);
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebViewImpl.cpp中。

       对于Touch事件,WebViewImpl类的成员函数handleInputEvent将会调用PageWidgetDelegate类的静态成员函数handleInputEvent对它进行处理,如下所示:

bool PageWidgetDelegate::handleInputEvent(Page* page, PageWidgetEventHandler& handler, const WebInputEvent& event)
{
    LocalFrame* frame = page && page->mainFrame()->isLocalFrame() ? page->deprecatedLocalMainFrame() : 0;
    switch (event.type) {
    ......

    case WebInputEvent::TouchStart:
    case WebInputEvent::TouchMove:
    case WebInputEvent::TouchEnd:
    case WebInputEvent::TouchCancel:
        if (!frame || !frame->view())
            return false;
        return handler.handleTouchEvent(*frame, *static_cast<const WebTouchEvent*>(&event));

    ......

    default:
        return false;
    }
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/PageWidgetDelegate.cpp中。

       从前面的调用过程可以知道,参数page是从WebViewImpl类的成员变量m_page传递过来的,它指向的是一个Page对象。PageWidgetDelegate类的静态成员函数handleInputEvent通过这个Page对象可以获得一个LocalFrame对象。这个LocalFrame对象用来在WebKit层描述在当前进程中加载的网页,它的创建过程可以参考前面Chromium网页Frame Tree创建过程分析一文。

       另外一个参数handler描述的是一个WebViewImpl对象。当第三个参数event描述的是一个Touch相关的事件时,PageWidgetDelegate类的静态成员函数handleInputEvent就会调用参数handler描述的WebViewImpl对象的成员函数handleTouchEvent对它进行处理。

       WebViewImpl类的成员函数handleTouchEvent是从父类PageWidgetEventHandler继承下来的,它的实现如下所示:

bool PageWidgetEventHandler::handleTouchEvent(LocalFrame& mainFrame, const WebTouchEvent& event)
{
    return mainFrame.eventHandler().handleTouchEvent(PlatformTouchEventBuilder(mainFrame.view(), event));
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/PageWidgetDelegate.cpp中。

       PageWidgetEventHandler类的成员函数handleTouchEvent首先调用参数mainFrame描述的一个LocalFrame对象的成员函数eventHandler获得一个EventHandler对象,接着再调用这个EventHandler对象的成员函数handleTouchEvent将处参数event描述的Touch事件,如下所示:

bool EventHandler::handleTouchEvent(const PlatformTouchEvent& event)
{
    ......

    const Vector<PlatformTouchPoint>& points = event.touchPoints();  

    unsigned i;
    ......
    bool allTouchReleased = true;
    for (i = 0; i < points.size(); ++i) {
        const PlatformTouchPoint& point = points[i];
        ......
        if (point.state() != PlatformTouchPoint::TouchReleased && point.state() != PlatformTouchPoint::TouchCancelled)
            allTouchReleased = false;
    }

    ......

    // First do hit tests for any new touch points.
    for (i = 0; i < points.size(); ++i) {
        const PlatformTouchPoint& point = points[i];
        ......
        
        if (point.state() == PlatformTouchPoint::TouchPressed) {
            HitTestRequest::HitTestRequestType hitType = HitTestRequest::TouchEvent | HitTestRequest::ReadOnly | HitTestRequest::Active;
            LayoutPoint pagePoint = roundedLayoutPoint(m_frame->view()->windowToContents(point.pos()));
            HitTestResult result;
            if (!m_touchSequenceDocument) {
                result = hitTestResultAtPoint(pagePoint, hitType);
            } else if (m_touchSequenceDocument->frame()) {
                LayoutPoint framePoint = roundedLayoutPoint(m_touchSequenceDocument->frame()->view()->windowToContents(point.pos()));
                result = hitTestResultInFrame(m_touchSequenceDocument->frame(), framePoint, hitType);
            } else
                continue;

            Node* node = result.innerNode();
            ......

            if (!m_touchSequenceDocument) {
                // Keep track of which document should receive all touch events
                // in the active sequence. This must be a single document to
                // ensure we don't leak Nodes between documents.
                m_touchSequenceDocument = &(result.innerNode()->document());
                ......
            }

            ......

            m_targetForTouchID.set(point.id(), node);

            ......
        }
    }

    ......

    // Holds the complete set of touches on the screen.
    RefPtrWillBeRawPtr<TouchList> touches = TouchList::create();

    // A different view on the 'touches' list above, filtered and grouped by
    // event target. Used for the 'targetTouches' list in the JS event.
    typedef WillBeHeapHashMap<EventTarget*, RefPtrWillBeMember<TouchList> > TargetTouchesHeapMap;
    TargetTouchesHeapMap touchesByTarget;

    // Array of touches per state, used to assemble the 'changedTouches' list.
    typedef WillBeHeapHashSet<RefPtrWillBeMember<EventTarget> > EventTargetSet;
    struct {
        // The touches corresponding to the particular change state this struct
        // instance represents.
        RefPtrWillBeMember<TouchList> m_touches;
        // Set of targets involved in m_touches.
        EventTargetSet m_targets;
    } changedTouches[PlatformTouchPoint::TouchStateEnd];

    for (i = 0; i < points.size(); ++i) {
        const PlatformTouchPoint& point = points[i];
        PlatformTouchPoint::State pointState = point.state();
        RefPtrWillBeRawPtr<EventTarget> touchTarget = nullptr;

        if (pointState == PlatformTouchPoint::TouchReleased || pointState == PlatformTouchPoint::TouchCancelled) {
            // The target should be the original target for this touch, so get
            // it from the hashmap. As it's a release or cancel we also remove
            // it from the map.
            touchTarget = m_targetForTouchID.take(point.id());
        } else {
            // No hittest is performed on move or stationary, since the target
            // is not allowed to change anyway.
            touchTarget = m_targetForTouchID.get(point.id());
        }

        LocalFrame* targetFrame = 0;
        bool knownTarget = false;
        if (touchTarget) {
            Document& doc = touchTarget->toNode()->document();
            // If the target node has moved to a new document while it was being touched,
            // we can't send events to the new document because that could leak nodes
            // from one document to another. See http://crbug.com/394339.
            if (&doc == m_touchSequenceDocument.get()) {
                targetFrame = doc.frame();
                knownTarget = true;
            }
        }

        ......

        RefPtrWillBeRawPtr<Touch> touch = Touch::create(
            targetFrame, touchTarget.get(), point.id(), point.screenPos(), adjustedPagePoint, adjustedRadius, point.rotationAngle(), point.force());

        ......

        // Ensure this target's touch list exists, even if it ends up empty, so
        // it can always be passed to TouchEvent::Create below.
        TargetTouchesHeapMap::iterator targetTouchesIterator = touchesByTarget.find(touchTarget.get());
        if (targetTouchesIterator == touchesByTarget.end()) {
            touchesByTarget.set(touchTarget.get(), TouchList::create());
            targetTouchesIterator = touchesByTarget.find(touchTarget.get());
        }

        // touches and targetTouches should only contain information about
        // touches still on the screen, so if this point is released or
        // cancelled it will only appear in the changedTouches list.
        if (pointState != PlatformTouchPoint::TouchReleased && pointState != PlatformTouchPoint::TouchCancelled) {
            touches->append(touch);
            targetTouchesIterator->value->append(touch);
        }

        // Now build up the correct list for changedTouches.
        // Note that  any touches that are in the TouchStationary state (e.g. if
        // the user had several points touched but did not move them all) should
        // never be in the changedTouches list so we do not handle them
        // explicitly here. See https://bugs.webkit.org/show_bug.cgi?id=37609
        // for further discussion about the TouchStationary state.
        if (pointState != PlatformTouchPoint::TouchStationary && knownTarget) {
            ......
            if (!changedTouches[pointState].m_touches)
                changedTouches[pointState].m_touches = TouchList::create();
            changedTouches[pointState].m_touches->append(touch);
            changedTouches[pointState].m_targets.add(touchTarget);
        }
    }
    if (allTouchReleased) {
        m_touchSequenceDocument.clear();
        ......
    }

    ......

    // Now iterate the changedTouches list and m_targets within it, sending
    // events to the targets as required.
    bool swallowedEvent = false;
    for (unsigned state = 0; state != PlatformTouchPoint::TouchStateEnd; ++state) {
        ......

        const AtomicString& stateName(eventNameForTouchPointState(static_cast<PlatformTouchPoint::State>(state)));
        const EventTargetSet& targetsForState = changedTouches[state].m_targets;
        for (EventTargetSet::const_iterator it = targetsForState.begin(); it != targetsForState.end(); ++it) {
            EventTarget* touchEventTarget = it->get();
            RefPtrWillBeRawPtr<TouchEvent> touchEvent = TouchEvent::create(
                touches.get(), touchesByTarget.get(touchEventTarget), changedTouches[state].m_touches.get(),
                stateName, touchEventTarget->toNode()->document().domWindow(),
                event.ctrlKey(), event.altKey(), event.shiftKey(), event.metaKey(), event.cancelable());
            touchEventTarget->toNode()->dispatchTouchEvent(touchEvent.get());
            swallowedEvent = swallowedEvent || touchEvent->defaultPrevented() || touchEvent->defaultHandled();
        }
    }

    return swallowedEvent;
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/page/EventHandler.cpp中。

       EventHandler类的成员函数handleTouchEvent主要是做三件事情:

       1. 对当前发生的Touch事件的每一个Touch Point进行Hit Test,分别找到它们的Target Node。

       2. 对Touch Point进行分类。还与屏幕接触是一类,不再与屏幕接触是另一类。与屏幕接触的一类又划分为两个子类。一个子类是静止不动的,另一个子类是正在移动的。另外,目标Node相同的Touch Point也会被组织在同一个Touch List中。

       3. 将Touch事件分发给Target Node处理。

       接下来,我们就将EventHandler类的成员函数handleTouchEvent划分为三段进行分析,每一段对应于上述的一个事件。

       第一段代码如下所示:

bool EventHandler::handleTouchEvent(const PlatformTouchEvent& event)
{
    ......

    const Vector<PlatformTouchPoint>& points = event.touchPoints();  

    unsigned i;
    ......
    bool allTouchReleased = true;
    for (i = 0; i < points.size(); ++i) {
        const PlatformTouchPoint& point = points[i];
        ......
        if (point.state() != PlatformTouchPoint::TouchReleased && point.state() != PlatformTouchPoint::TouchCancelled)
            allTouchReleased = false;
    }

    ......

    // First do hit tests for any new touch points.
    for (i = 0; i < points.size(); ++i) {
        const PlatformTouchPoint& point = points[i];
        ......
        
        if (point.state() == PlatformTouchPoint::TouchPressed) {
            HitTestRequest::HitTestRequestType hitType = HitTestRequest::TouchEvent | HitTestRequest::ReadOnly | HitTestRequest::Active;
            LayoutPoint pagePoint = roundedLayoutPoint(m_frame->view()->windowToContents(point.pos()));
            HitTestResult result;
            if (!m_touchSequenceDocument) {
                result = hitTestResultAtPoint(pagePoint, hitType);
            } else if (m_touchSequenceDocument->frame()) {
                LayoutPoint framePoint = roundedLayoutPoint(m_touchSequenceDocument->frame()->view()->windowToContents(point.pos()));
                result = hitTestResultInFrame(m_touchSequenceDocument->frame(), framePoint, hitType);
            } else
                continue;

            Node* node = result.innerNode();
            ......

            if (!m_touchSequenceDocument) {
                // Keep track of which document should receive all touch events
                // in the active sequence. This must be a single document to
                // ensure we don't leak Nodes between documents.
                m_touchSequenceDocument = &(result.innerNode()->document());
                ......
            }

            ......

            m_targetForTouchID.set(point.id(), node);

            ......
        }
    }

       这段代码首先获得参数event描述的Touch事件关联的所有Touch Point,保存在本地变量points描述的一个Vector中。

       每一个Touch Point都用一个PlatformTouchPoint对象描述,它有五种状态,如下所示:

class PlatformTouchPoint {
public:
    enum State {
        TouchReleased,
        TouchPressed,
        TouchMoved,
        TouchStationary,
        TouchCancelled,
        TouchStateEnd // Placeholder: must remain the last item.
    };

    ......
};
       这些状态定义在文件external/chromium_org/third_party/WebKit/Source/platform/PlatformTouchPoint.h中。

       一个Touch Point的一般状态变化过程为:TouchPressed->TouchMoved/TouchStationary->TouchReleased/TouchCancelled。

       回到上面第一段代码中,它主要做的事情就是对那些状态为TouchPressed的Touch Point进行Hit Test,目的是找到它们的Target Node,并且将这些Target Node保存在EventHandler类的成员变量m_targetForTouchID描述的一个Hash Map中,键值为Touch Point对应的ID。有了这个Hash Map,当一个Touch Point从TouchPressed状态变为其它状态时,就可以轻松地知道它的Target Node,避免做重复的Hit Test。

       一系列连续的Touch Event只能发生在一个Document上。如果两个Touch Event有两个或者两个以上的Touch Point具有相同的ID,那么它们就是连续的Touch Event。它们所发生在的Document由第一个连续的Touch Event的第一个处于TouchPressed状态的Touch Point确定,也就是这个Touch Point的Target Node所在的Document。这个Document一旦确定,就会维护在EventHandler类的成员变量m_touchSequenceDocument中。

       当一个Touch Event的所有Touch Point的状态都处于TouchReleased或者TouchCancelled时,它就结束一个连续的Touch Event系列。这时候EventHandler类的成员变量m_touchSequenceDocument就会设置为NULL,表示接下来发生的Touch Event属于另外一个连续的系列。

       当一个Touch Event所在的Document所未确定时,EventHandler类的成员函数handleTouchEvent调用成员函数hitTestResultAtPoint对第一个Touch Point做Hit Test;当一个Touch Event所在的Document确定时,则调用另外一个成员函数hitTestResultInFrame做Hit Test。后者会将Hit Test的范围限制在指定的Document中。后面我们将以EventHandler类的成员函数hitTestResultAtPoint为例,分析Hit Test的执行过程。

       第二段代码如下所示:

    // Holds the complete set of touches on the screen.
    RefPtrWillBeRawPtr<TouchList> touches = TouchList::create();

    // A different view on the 'touches' list above, filtered and grouped by
    // event target. Used for the 'targetTouches' list in the JS event.
    typedef WillBeHeapHashMap<EventTarget*, RefPtrWillBeMember<TouchList> > TargetTouchesHeapMap;
    TargetTouchesHeapMap touchesByTarget;

    // Array of touches per state, used to assemble the 'changedTouches' list.
    typedef WillBeHeapHashSet<RefPtrWillBeMember<EventTarget> > EventTargetSet;
    struct {
        // The touches corresponding to the particular change state this struct
        // instance represents.
        RefPtrWillBeMember<TouchList> m_touches;
        // Set of targets involved in m_touches.
        EventTargetSet m_targets;
    } changedTouches[PlatformTouchPoint::TouchStateEnd];

    for (i = 0; i < points.size(); ++i) {
        const PlatformTouchPoint& point = points[i];
        PlatformTouchPoint::State pointState = point.state();
        RefPtrWillBeRawPtr<EventTarget> touchTarget = nullptr;

        if (pointState == PlatformTouchPoint::TouchReleased || pointState == PlatformTouchPoint::TouchCancelled) {
            // The target should be the original target for this touch, so get
            // it from the hashmap. As it's a release or cancel we also remove
            // it from the map.
            touchTarget = m_targetForTouchID.take(point.id());
        } else {
            // No hittest is performed on move or stationary, since the target
            // is not allowed to change anyway.
            touchTarget = m_targetForTouchID.get(point.id());
        }

        LocalFrame* targetFrame = 0;
        bool knownTarget = false;
        if (touchTarget) {
            Document& doc = touchTarget->toNode()->document();
            // If the target node has moved to a new document while it was being touched,
            // we can't send events to the new document because that could leak nodes
            // from one document to another. See http://crbug.com/394339.
            if (&doc == m_touchSequenceDocument.get()) {
                targetFrame = doc.frame();
                knownTarget = true;
            }
        }

        ......

        RefPtrWillBeRawPtr<Touch> touch = Touch::create(
            targetFrame, touchTarget.get(), point.id(), point.screenPos(), adjustedPagePoint, adjustedRadius, point.rotationAngle(), point.force());

        ......

        // Ensure this target's touch list exists, even if it ends up empty, so
        // it can always be passed to TouchEvent::Create below.
        TargetTouchesHeapMap::iterator targetTouchesIterator = touchesByTarget.find(touchTarget.get());
        if (targetTouchesIterator == touchesByTarget.end()) {
            touchesByTarget.set(touchTarget.get(), TouchList::create());
            targetTouchesIterator = touchesByTarget.find(touchTarget.get());
        }

        // touches and targetTouches should only contain information about
        // touches still on the screen, so if this point is released or
        // cancelled it will only appear in the changedTouches list.
        if (pointState != PlatformTouchPoint::TouchReleased && pointState != PlatformTouchPoint::TouchCancelled) {
            touches->append(touch);
            targetTouchesIterator->value->append(touch);
        }

        // Now build up the correct list for changedTouches.
        // Note that  any touches that are in the TouchStationary state (e.g. if
        // the user had several points touched but did not move them all) should
        // never be in the changedTouches list so we do not handle them
        // explicitly here. See https://bugs.webkit.org/show_bug.cgi?id=37609
        // for further discussion about the TouchStationary state.
        if (pointState != PlatformTouchPoint::TouchStationary && knownTarget) {
            ......
            if (!changedTouches[pointState].m_touches)
                changedTouches[pointState].m_touches = TouchList::create();
            changedTouches[pointState].m_touches->append(touch);
            changedTouches[pointState].m_targets.add(touchTarget);
        }
    }
    if (allTouchReleased) {
        m_touchSequenceDocument.clear();
        ......
    }
       这段代码主要是对Touch Point进行分门别类。

       首先,那些还与屏幕有接触的Touch Point将会保存在本地变量touches描述的一个Touch List中。那些状态不等于TouchReleased和TouchCancelled的Touch Point即为还与屏幕有接触的Touch Point。

       其次,具有相同Target Node的Touch Point又会保存在相同的Touch List中。这些Touch List它们关联的Target Node为键值,保存在本地变量touchesByTarget描述的一个Hash Map中。

       第三,那些位置或者状态发生变化,并且有Target Node的Touch Point会按照状态保存在本地变量changedTouches描述的一个数组中。相同状态的Touch Point保存在同一个Touch List中,它们的Target Node也会保存在同一个Hash Set中。位置或者状态发生变化的Touch Point,即为那些状态不等于TouchStationary的Touch Point。另外,如果一个Touch Point的Target Node所在的Document与当前Touch Event所发生在的Document不一致,那么该Touch Point会被认为是没有Target Node。

       这段代码还会做另外两件事情:

       1. 如果一个Touch Point的状态变为TouchReleased或者TouchCancelled,那么它就会从EventHandler类的成员变量m_targetForTouchID描述的一个Hash Map中移除。结合前面对第一段代码的分析,我们就可以知道,一个连续的Touch Event系列,它关联的Touch Point是会动态增加和移除的。

       2. 如果当前发生的Touch Event的所有Touch Point的状态均为TouchReleased或者TouchCancelled,那么当前连续的Touch Event系列就会结束。这时候EventHandler类的成员变量m_touchSequenceDocument将被设置为NULL。

       第三段代码如下所示:

   // Now iterate the changedTouches list and m_targets within it, sending
    // events to the targets as required.
    bool swallowedEvent = false;
    for (unsigned state = 0; state != PlatformTouchPoint::TouchStateEnd; ++state) {
        ......

        const AtomicString& stateName(eventNameForTouchPointState(static_cast<PlatformTouchPoint::State>(state)));
        const EventTargetSet& targetsForState = changedTouches[state].m_targets;
        for (EventTargetSet::const_iterator it = targetsForState.begin(); it != targetsForState.end(); ++it) {
            EventTarget* touchEventTarget = it->get();
            RefPtrWillBeRawPtr<TouchEvent> touchEvent = TouchEvent::create(
                touches.get(), touchesByTarget.get(touchEventTarget), changedTouches[state].m_touches.get(),
                stateName, touchEventTarget->toNode()->document().domWindow(),
                event.ctrlKey(), event.altKey(), event.shiftKey(), event.metaKey(), event.cancelable());
            touchEventTarget->toNode()->dispatchTouchEvent(touchEvent.get());
            swallowedEvent = swallowedEvent || touchEvent->defaultPrevented() || touchEvent->defaultHandled();
        }
    }

    return swallowedEvent;
}
       这段代码将Touch Event分发给Target Node处理。注意,并不是所有的Target Node都会被分发Touch Event。只有那些Touch Point位置或者状态发生了变化的Target Node才会获得Touch Event。

       这段代码按照Touch Point的状态分发Touch Event给Target Node处理,顺序为TouchReleased->TouchPressed->TouchMoved->TouchCancelled。如果具有相同状态的Touch Point关联了不同的Target Node,那么每一个Target Node都会获得一个Touch Event。

       每一个Target Node获得的Touch Event是不同的TouchEvent对象,每一个TouchEvent对象包含了以下三种信息:

       1. 所有与屏幕接触的Touch Point。这些Touch Point有的位于Target Node的范围内,有的可能位于Target Node的范围外。

       2. 位于Target Node的范围内的Touch Point。

       3. 具有相同状态的Touch Point。

       注意,这些Touch Point限定在当前发生的Touch Event关联的Touch Point中,也就是限定在参数event描述的Touch Event关联的Touch Point中。

       EventHandler类的成员函数是通过Target Node的成员函数handleTouchEvent给它们分发Touch Event,也就是通过调用Node类的成员函数handleTouchEvent将Touch Event分发给Target Node处理。

       接下来,我们就继续分析EventHandler类的成员函数hitTestResultAtPoint和Node类的成员函数handleTouchEvent的实现,以便了解WebKit根据Touch Point找到Target Node和将Touch Event分发给Target Node处理的过程。

       EventHandler类的成员函数hitTestResultAtPoint的实现如下所示:

HitTestResult EventHandler::hitTestResultAtPoint(const LayoutPoint& point, HitTestRequest::HitTestRequestType hitType, const LayoutSize& padding)
{
    ......

    // We always send hitTestResultAtPoint to the main frame if we have one,
    // otherwise we might hit areas that are obscured by higher frames.
    if (Page* page = m_frame->page()) {
        LocalFrame* mainFrame = page->mainFrame()->isLocalFrame() ? page->deprecatedLocalMainFrame() : 0;
        if (mainFrame && m_frame != mainFrame) {
            FrameView* frameView = m_frame->view();
            FrameView* mainView = mainFrame->view();
            if (frameView && mainView) {
                IntPoint mainFramePoint = mainView->rootViewToContents(frameView->contentsToRootView(roundedIntPoint(point)));
                return mainFrame->eventHandler().hitTestResultAtPoint(mainFramePoint, hitType, padding);
            }
        }
    }

    HitTestResult result(point, padding.height(), padding.width(), padding.height(), padding.width());
    ......

    // hitTestResultAtPoint is specifically used to hitTest into all frames, thus it always allows child frame content.
    HitTestRequest request(hitType | HitTestRequest::AllowChildFrameContent);
    m_frame->contentRenderer()->hitTest(request, result);
    ......

    return result;
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/page/EventHandler.cpp中。

       EventHandler类的成员函数hitTestResultAtPoint首先会检查当前正在处理的网页的Main Frame是否是一个Local Frame。如果是的话,那么就会在它上面做Hit Test。否则的话,就在当前正在处理的网页所加载在的Frame上做Hit Test,也就是EventHandler类的成员变量m_frame描述的Frame。这个Frame是一个Local Frame。

       前面Chromium网页加载过程简要介绍和学习计划一文提到,在WebKit中,一个Frame有可能是Local的,也可能是Remote的。Local Frame就在当前进程加载一个网页,而Remote Frame在另外一个进程加载一个网页。Frame与Frame之间会形成一个Frame Tree。Frame Tree的根节点就是一个Main Frame。结合前面分析的EventHandler类的成员函数handleTouchEvent,我们就可以知道,如果当前正在处理的网页不是加载在一个Main Frame上,并且这个网页的Main Frame是一个Local Frame,那么发生在这个网页上的Touch Event将会在它的Main Frame上做Hit Test。在其余情况下,发生在当前正在处理的网页上的Touch Event,将会在该网页所加载在的Frame上做Hit Test。

       一旦确定在哪个Frame上做Hit Test之后,EventHandler类的成员函数hitTestResultAtPoint就会调用这个Frame的成员函数contentRenderer获得一个RenderView对象。从前面Chromium网页Render Object Tree创建过程分析一文可以知道,这个RenderView对象描述的就是当前正在处理的网页的Render Object Tree的根节点。有了这个RenderView对象之后,EventHandler类的成员函数hitTestResultAtPoint就会调用它的成员函数hitTest对参数point描述的Touch Point进行Hit Test。

       RenderView类的成员函数hitTest的实现如下所示:

bool RenderView::hitTest(const HitTestRequest& request, HitTestResult& result)
{
    return hitTest(request, result.hitTestLocation(), result);
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderView.cpp中。

       RenderView类的成员函数hitTest调用另外一个重载版本的成员函数hitTest对参数result描述的Touch Point进行Hit Test,如下所示:

bool RenderView::hitTest(const HitTestRequest& request, const HitTestLocation& location, HitTestResult& result)
{
    ......

    return layer()->hitTest(request, location, result);
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderView.cpp中。

       RenderView类重载版本的成员函数hitTest首先会调用另外一个成员函数layer获得一个RenderLayer对象。从前面Chromium网页Render Layer Tree创建过程分析一文可以知道,这个RenderLayer对象描述的就是网页的Render Layer Tree的根节点。有了这个RenderLayer对象之后,RenderView类重载版本的成员函数hitTest就会调用它的成员函数hitTest对象参数result描述的Touch Point进行Hit Test,如下所示:

bool RenderLayer::hitTest(const HitTestRequest& request, const HitTestLocation& hitTestLocation, HitTestResult& result)
{
    ......

    LayoutRect hitTestArea = renderer()->view()->documentRect();
    ......

    RenderLayer* insideLayer = hitTestLayer(this, 0, request, result, hitTestArea, hitTestLocation, false);
    ......

    return insideLayer;
}

      这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderLayer.cpp中。

      RenderLayer类的成员函数hitTest首先是获得网页的Document对象占据的区域hitTestArea,然后再调用另外一个成员函数hitTestLayer在该区域上对参数hitTestLocation描述的Touch Point进行Hit Test,如下所示:

RenderLayer* RenderLayer::hitTestLayer(RenderLayer* rootLayer, RenderLayer* containerLayer, const HitTestRequest& request, HitTestResult& result,
                                       const LayoutRect& hitTestRect, const HitTestLocation& hitTestLocation, bool appliedTransform,
                                       const HitTestingTransformState* transformState, double* zOffset)
{
    ......

    // Ensure our lists and 3d status are up-to-date.
    m_stackingNode->updateLayerListsIfNeeded();
    update3DTransformedDescendantStatus();
    ......

    // The following are used for keeping track of the z-depth of the hit point of 3d-transformed
    // descendants.
    double localZOffset = -numeric_limits<double>::infinity();
    double* zOffsetForDescendantsPtr = 0;
    double* zOffsetForContentsPtr = 0;

    bool depthSortDescendants = false;
    if (preserves3D()) {
        depthSortDescendants = true;
        // Our layers can depth-test with our container, so share the z depth pointer with the container, if it passed one down.
        zOffsetForDescendantsPtr = zOffset ? zOffset : &localZOffset;
        zOffsetForContentsPtr = zOffset ? zOffset : &localZOffset;
    } else if (m_has3DTransformedDescendant) {
        // Flattening layer with 3d children; use a local zOffset pointer to depth-test children and foreground.
        depthSortDescendants = true;
        zOffsetForDescendantsPtr = zOffset ? zOffset : &localZOffset;
        zOffsetForContentsPtr = zOffset ? zOffset : &localZOffset;
    } else if (zOffset) {
        zOffsetForDescendantsPtr = 0;
        // Container needs us to give back a z offset for the hit layer.
        zOffsetForContentsPtr = zOffset;
    }

    ......

    // This variable tracks which layer the mouse ends up being inside.
    RenderLayer* candidateLayer = 0;

    // Begin by walking our list of positive layers from highest z-index down to the lowest z-index.
    RenderLayer* hitLayer = hitTestChildren(PositiveZOrderChildren, rootLayer, request, result, hitTestRect, hitTestLocation,
                                        localTransformState.get(), zOffsetForDescendantsPtr, zOffset, unflattenedTransformState.get(), depthSortDescendants);
    if (hitLayer) {
        if (!depthSortDescendants)
            return hitLayer;
        candidateLayer = hitLayer;
    }

    // Now check our overflow objects.
    hitLayer = hitTestChildren(NormalFlowChildren, rootLayer, request, result, hitTestRect, hitTestLocation,
                           localTransformState.get(), zOffsetForDescendantsPtr, zOffset, unflattenedTransformState.get(), depthSortDescendants);
    if (hitLayer) {
        if (!depthSortDescendants)
            return hitLayer;
        candidateLayer = hitLayer;
    }

    ......

    // Next we want to see if the mouse pos is inside the child RenderObjects of the layer. Check
    // every fragment in reverse order.
    if (isSelfPaintingLayer()) {
        // Hit test with a temporary HitTestResult, because we only want to commit to 'result' if we know we're frontmost.
        HitTestResult tempResult(result.hitTestLocation());
        bool insideFragmentForegroundRect = false;
        if (hitTestContentsForFragments(layerFragments, request, tempResult, hitTestLocation, HitTestDescendants, insideFragmentForegroundRect)
            && isHitCandidate(this, false, zOffsetForContentsPtr, unflattenedTransformState.get())) {
            ......
            if (!depthSortDescendants)
                return this;
            // Foreground can depth-sort with descendant layers, so keep this as a candidate.
            candidateLayer = this;
        } 
    }

    // Now check our negative z-index children.
    hitLayer = hitTestChildren(NegativeZOrderChildren, rootLayer, request, result, hitTestRect, hitTestLocation,
        localTransformState.get(), zOffsetForDescendantsPtr, zOffset, unflattenedTransformState.get(), depthSortDescendants);
    if (hitLayer) {
        if (!depthSortDescendants)
            return hitLayer;
        candidateLayer = hitLayer;
    }

    ......

    // If we found a layer, return. Child layers, and foreground always render in front of background.
    if (candidateLayer)
        return candidateLayer;

    if (isSelfPaintingLayer()) {
        HitTestResult tempResult(result.hitTestLocation());
        bool insideFragmentBackgroundRect = false;
        if (hitTestContentsForFragments(layerFragments, request, tempResult, hitTestLocation, HitTestSelf, insideFragmentBackgroundRect)
            && isHitCandidate(this, false, zOffsetForContentsPtr, unflattenedTransformState.get())) {
            ......
            return this;
        }
        ......
    }

    return 0;
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderLayer.cpp中。

       RenderLayer类的成员变量m_stackingNode指向的是一个RenderLayerStackingNode对象。这个RenderLayerStackingNode对象描述了当前正在处理的Render Layer所在的Stacking Context。RenderLayer类的成员函数hitTestLayer首先调用这个RenderLayerStackingNode对象的成员函数updateLayerListsIfNeeded更新它所描述的Stacking Context所包含的Render Layer的层次关系,以便接下来可以按照它们的Z轴位置进行Hit Test。

       RenderLayer类的成员函数hitTestLayer同时还会调用另外一个成员函数update3DTransformedDescendantStatus检查当前正在处理的Render Layer的子Render Layer是否设置了3D。如果设置了,那么RenderLayer类的成员变量m_has3DTransformedDescendant就会被设置为true。3D变换使得Hit Test不能简单地按照原来Z-Index的大小进行Hit Test。

       RenderLayer类的成员函数hitTestLayer接下来根据两种不同的情况,采取两种不同的Hit Test方法:

       1. 当前正在处理的Render Layer将CSS属性tranform-type设置为“preserve-3d”,或者它的子Render Layer设置了3D变换。在这种情况下,本地变量depthSortDescendants的值会被设置为true,并且另外两个本地变量zOffsetForDescendantsPtr和zOffsetForContentsPtr指向了一个类型为double的地址。这个地址包含的double值描述的是上一个被Hit的Render Layer在Touch Point处的Z轴位置。其中,本地变量zOffsetForContentsPtr描述的Z轴位置是给当前正在处理的Render Layer使用的,而本地变量zOffsetForDescendantsPtr描述的Z轴位置是给当前正在处理的Render Layer的子Render Layer使用的。在一个设置了3D变换的环境中,Z-Index值大的Render Layer不一定位于Z-Index值小的Render Layer的上面,需要进一步结合它们的3D变换情况进行判断。因此,就需要将上一个被Hit的Render Layer在Touch Point处的Z轴位置保存下来,用来与下一个也被Hit的Render Layer进行比较,以便得出正确的被Hit的Render Layer。

       2. 当前正在处理的Render Layer没有将CSS属性tranform-type设置为“preserve-3d”,以及它的子Render Layer也没有设置3D变换。在这种情况下,本地变量depthSortDescendants的值会被设置为false。另外两个本地变量zOffsetForDescendantsPtr和zOffsetForContentsPtr,前者被设置为NULL,后者设置为参数zOffset的值。将本地变量zOffsetForDescendantsPtr设置为NULL,是因为当前正在处理的Render Layer的子Render Layer在做Hit Test时,不需要与其它的子Render Layer在Touch Point处进行Z轴位置。将zOffsetForContentsPtr的值指定为参数zOffset的值,是因为调用者可能会指定一个Z轴位置,要求当前正在处理的Render Layer在Touch Point处与其进行比较。

       对第二种情况的处理比较简单,流程如下所示: 

       1. 按照Z-Index从大到小的顺序对Z-Index值大于等于0的子Render Layer进行Hit Test。如果发生了Hit,那么停止Hit Test流程。

       2. 对当前正在处理的Render Layer的Foreground层进行Hit Test。如果发生了Hit,那么停止Hit Test流程。

       3. 按照Z-Index从大到小的顺序对Z-Index值小于0的子Render Layer进行Hit Test。如果发生了Hit,那么停止Hit Test流程。

       4. 对当前正在处理的Render Layer关联的Render Object的Background层进行Hit Test。

       对第一种情况的处理相对就会复杂一些,如下所示:

       1. 对当前正在处理的Render Layer的所有子Render Layer,以及当前正在处理的Render Layer的Foreground层,都会一一进行Hit Test。在这个Hit Test过程中,所有被Hit的Render Layer,都会根据它们3D变换情况,检查它们在Touch Point处的Z轴位置。Z轴位置最大的Render Layer或者Render Object,将会选择用来接收Touch Event。

       2. 如果所有子Render Layer和当前正在处理的Render Layer的Foreground都没有发生Hit,那么就会再对当前正在处理的Render Layer的Background层进行Hit Test。

       注意,对于每一个子Render Layer,RenderLayer类的成员函数hitTestLayer就会调用另外一个成员函数hitTestChildren对分别对它们进行Hit Test。RenderLayer类的成员函数hitTestChildren又会调用hitTestLayer对每一个子Render Layer执行具体的Hit Test。

       这意味着,RenderLayer类的成员函数hitTestLayer会被递归调用来对Render Layer Tree中的每一个Render Layer进行Hit Test。当前正在处理的Render Layer是否被Hit,RenderLayer类的成员函数hitTestLayer是通过调用两次成员函数hitTestContentsForFragments进行检查。第一次调用是确定当前正在处理的Render Layer的Foreground层是否发生了Hit Test。第二次调用是确定当前正在处理的Render Layer的Background层是否发生了Hit Test。

        一旦检查当前正在处理的Render Layer发生了Hit,那么RenderLayer类的成员函数hitTestLayer还需要调用另外一个成员函数isHitCandidate将它在Touch Point处的Z轴位置与本地变量zOffsetForContentsPtr描述的Z轴位置(上一个被Hit的Render Layer在Touch Point的Z轴位置)进行比较。比较后如果发现当前正在处理的Render Layer在Touch Point处的Z轴位置较大,那么才会认为它是被Hit的Render Layer。

       接下来我们就继续分析RenderLayer类的成员函数hitTestContentsForFragments的实现,以便可以了解一个Render Layer在什么情况会被认为是发生了Hit,如下所示:

bool RenderLayer::hitTestContentsForFragments(const LayerFragments& layerFragments, const HitTestRequest& request, HitTestResult& result,
    const HitTestLocation& hitTestLocation, HitTestFilter hitTestFilter, bool& insideClipRect) const
{
    ......

    for (int i = layerFragments.size() - 1; i >= 0; --i) {
        const LayerFragment& fragment = layerFragments.at(i);
        ......
        if (hitTestContents(request, result, fragment.layerBounds, hitTestLocation, hitTestFilter))
            return true;
    }

    return false;
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderLayer.cpp中。

       前面Chromium网页Layer Tree绘制过程分析一文提到,Render Layer是按Fragment进行划分的。因此,RenderLayer类的成员函数hitTestContentsForFragments分别调用另外一个成员函数hitTestContents对每一个Fragment进行Hit Test。只要其中一个Fragment发生了Hit,那么就会认为它所在的Render Layer发生了Hit。

       RenderLayer类的成员函数hitTestContents的实现如下所示:

bool RenderLayer::hitTestContents(const HitTestRequest& request, HitTestResult& result, const LayoutRect& layerBounds, const HitTestLocation& hitTestLocation, HitTestFilter hitTestFilter) const
{
    ......

    if (!renderer()->hitTest(request, result, hitTestLocation, toLayoutPoint(layerBounds.location() - renderBoxLocation()), hitTestFilter)) {
        ......
        return false;
    }

    ......

    return true;
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderLayer.cpp中。

       RenderLayer类的成员函数hitTestContents首先调用成员函数renderer获得一个Render Object。这个Render Object是当前正在处理的Render Layer的宿主Render Object。也就是说,我们在为网页创建Render Layer Tree时,为上述Render Object创建了一个Render Layer。该Render Object的子Render Object如果没有自己的Render Layer,那么就会与该Render Object共享同一个Render Layer。这一点可以参考前面Chromium网页Render Layer Tree创建过程分析一文。 

       获得了当前正在处理的Render Layer的宿主Render Object之后,RenderLayer类的成员函数hitTestContents就调用它的成员函数hitTest检查它在参数layerBounds描述的区域内是否发生了Hit。如果发生了Hit,那么RenderLayer类的成员函数hitTestContents就直接返回一个true值给调用者。否则的话,就会返回一个false值给调用者。

       这一步执行完成之后,就从Render Layer Tree转移到Render Object Tree进行Hit Test,也就是调用RenderObject类的成员函数hitTest进行Hit Test,如下所示:

bool RenderObject::hitTest(const HitTestRequest& request, HitTestResult& result, const HitTestLocation& locationInContainer, const LayoutPoint& accumulatedOffset, HitTestFilter hitTestFilter)
{
    bool inside = false;
    if (hitTestFilter != HitTestSelf) {
        // First test the foreground layer (lines and inlines).
        inside = nodeAtPoint(request, result, locationInContainer, accumulatedOffset, HitTestForeground);

        // Test floats next.
        if (!inside)
            inside = nodeAtPoint(request, result, locationInContainer, accumulatedOffset, HitTestFloat);

        // Finally test to see if the mouse is in the background (within a child block's background).
        if (!inside)
            inside = nodeAtPoint(request, result, locationInContainer, accumulatedOffset, HitTestChildBlockBackgrounds);
    }

    // See if the mouse is inside us but not any of our descendants
    if (hitTestFilter != HitTestDescendants && !inside)
        inside = nodeAtPoint(request, result, locationInContainer, accumulatedOffset, HitTestBlockBackground);

    return inside;
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderObject.cpp中。

       从前面分析的RenderLayer类的成员函数hitTestLayer可以知道,一个Reder Layer关联的Render Object会进行两次Hit Test。第一次是针对该Render Object的Foreground进行Hit Test,这时候参数hitTestFilter的值等于HitTestDescendants。第二次是针对该Render Object的Background层进行Hit Test,这时候参数hitTestFilter的值等于HitTestSelf。

       无论是Foreground层,还是Background层,RenderObject类的成员函数hitTest都是通过调用另外一个成员函数nodeAtPoint进行Hit Test的。RenderObject类的成员函数nodeAtPoint是从父类RenderBox继承下来的,它的实现如下所示:

bool RenderBox::nodeAtPoint(const HitTestRequest& request, HitTestResult& result, const HitTestLocation& locationInContainer, const LayoutPoint& accumulatedOffset, HitTestAction action)
{
    LayoutPoint adjustedLocation = accumulatedOffset + location();

    // Check kids first.
    for (RenderObject* child = slowLastChild(); child; child = child->previousSibling()) {
        if ((!child->hasLayer() || !toRenderLayerModelObject(child)->layer()->isSelfPaintingLayer()) && child->nodeAtPoint(request, result, locationInContainer, adjustedLocation, action)) {
            updateHitTestResult(result, locationInContainer.point() - toLayoutSize(adjustedLocation));
            return true;
        }
    }

    // Check our bounds next. For this purpose always assume that we can only be hit in the
    // foreground phase (which is true for replaced elements like images).
    LayoutRect boundsRect = borderBoxRect();
    boundsRect.moveBy(adjustedLocation);
    if (visibleToHitTestRequest(request) && action == HitTestForeground && locationInContainer.intersects(boundsRect)) {
        updateHitTestResult(result, locationInContainer.point() - toLayoutSize(adjustedLocation));
        if (!result.addNodeToRectBasedTestResult(node(), request, locationInContainer, boundsRect))
            return true;
    }

    return false;
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderBox.cpp中。

       RenderBox类的成员函数nodeAtPoint首先对当前正在处理的Render Object的子Render Object进行Hit Test。这是通过递归调用RenderBox类的成员函数nodeAtPoint实现的。如果子Render Object没有被Hit,那么RenderBox类的成员函数才会判断当前正在处理的Render Object是否发生Hit,也就是判断参数locationInContainer描述的Touch Point是否落在当前正在处理的Render Object的区域boundsRect内。

       这里有一点需要注意,当前正在处理的Render Object的每一个子Render Objec并不是都会被递归Hit Test。只有那些没有创建Render Layer的子Render Object才会进行递归Hit Test。这些没有创建自己的Render Layer的子Render Object将会与当前正在处理的Render Object共享同一个Render Layer。对于那些有自己的Render Layer的子Render Object,它们的Hit Test将由前面分析的RenderLayer类的成员函数hitTestLayer发起。

       当一个Render Object发生Hit时,RenderBox类的成员函数nodeAtPoint就会调用另外一个成员函数updateHitTestResult将Hit信息记录在参数result描述的一个HitTestResult对象中。RenderBox类的成员函数updateHitTestResult由子类RenderObject实现,如下所示:

void RenderObject::updateHitTestResult(HitTestResult& result, const LayoutPoint& point)
{
    ......

    Node* node = this->node();

    // If we hit the anonymous renderers inside generated content we should
    // actually hit the generated content so walk up to the PseudoElement.
    if (!node && parent() && parent()->isBeforeOrAfterContent()) {
        for (RenderObject* renderer = parent(); renderer && !node; renderer = renderer->parent())
            node = renderer->node();
    }

    if (node) {
        result.setInnerNode(node);
        ......
    }
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderObject.cpp中。

       RenderObject类的成员函数updateHitTestResult首先调用成员函数node获得与当前正在处理的Render Object关联的HTML元素,也就是位于网页的DOM Tree中的一个Node,作为当前发生的Touch Event的Target Node。

       如果当前正在处理的Render Object没有关联一个HTML元素,那么就说明当前正在处理的Render Object是一个匿名的Render Object。这时候需要在网页的Render Object Tree中找到一个负责生成它的、非匿名的父Render Object,然后再获得与这个父Render Object关联的HTML元素,作为当前发生的Touch Event的Target Node。

       一旦找到了Target Node,RenderObject类的成员函数updateHitTestResult就会将它保存在参数result描述的一个HitTestResult对象中。这是通过调用HitTestResult类的成员函数setInnerNode实现的,如下所示:

void HitTestResult::setInnerNode(Node* n)
{
    ......
    m_innerNode = n;
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/HitTestResult.cpp中。

       HitTestResult类的成员函数setInnerNode主要是将参数n描述的一个HTML元素保存在成员变量m_innerNode中。这个HTML元素可以通过调用HitTestResult类的成员函数innerNode获得,如下所示:

class HitTestResult {
public:
    ......

    Node* innerNode() const { return m_innerNode.get(); }

    ......
};
      这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/HitTestResult.h中。

      这一步执行完成后,WebKit就通过网页的Render Layer Tree和Render Object Tree,最终在DOM Tree中找到了Target Node。这个Target Node负责接收和处理当前发生的Touch Event。

      回到前面分析的EventHandler类的成员函数handleTouchEvent中,接下来它就会将当前发生的Touch Event分发给前面找到的Target Node处理,这是通过调用它的成员函数dispatchTouchEvent实现的,如下所示:

bool Node::dispatchTouchEvent(PassRefPtrWillBeRawPtr<TouchEvent> event)
{
    return EventDispatcher::dispatchEvent(this, TouchEventDispatchMediator::create(event));
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/dom/Node.cpp中。

       Node类的成员函数dispatchTouchEvent首先调用TouchEventDispatchMediator类的静态成员函数create将参数描述的Touch Event封装在一个TouchEventDispatchMediator对象中,然的再调用EventDispatcher类的静态成员函数dispatchEvent对该Touch Event进行处理,如下所示:

bool EventDispatcher::dispatchEvent(Node* node, PassRefPtrWillBeRawPtr<EventDispatchMediator> mediator)
{
    ......
    EventDispatcher dispatcher(node, mediator->event());
    return mediator->dispatchEvent(&dispatcher);
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/events/EventDispatcher.cpp中。

       EventDispatcher类的静态成员函数dispatchEvent首先从参数mediator描述的TouchEventDispatchMediator对象中取出它所封装的Touch Event,并且将该Touch Event封装在一个EventDispatcher对象中,最后调用参数mediator描述的TouchEventDispatchMediator对象的成员函数dispatchEvent对上述Touch Event进行分发处理,如下所示:

bool TouchEventDispatchMediator::dispatchEvent(EventDispatcher* dispatcher) const
{
    event()->eventPath().adjustForTouchEvent(dispatcher->node(), *event());
    return dispatcher->dispatch();
}

       这个函数定义在文件/external/chromium_org/third_party/WebKit/Source/core/events/TouchEvent.cpp中。

       TouchEventDispatchMediator类的成员函数dispatchEvent首先调用成员函数event获得一个TouchEvent对象。这个TouchEvent描述的就是当前发生的Touch Event。调用这个TouchEvent对象的成员函数eventPath可以获得一个EventPath对象。这个EventPath对象描述的是当前发生的Touch Event的分发路径。关于WebKit中的Event分发路径,下面我们再描述。有了这个EventPath对象之后,TouchEventDispatchMediator类的成员函数dispatchEvent调用它的成员函数adjustForTouchEvent调整Touch Event分发路径中的Shadow DOM的Touch List。关于Shadow DOM的更详细描述,可以参考What the Heck is Shadow DOM这篇文章。

        TouchEventDispatchMediator类的成员函数dispatchEvent最后调用参数dispatcher描述的EventDispatcher对象的成员函数dispatch处理它所封装的Touch Event,如下所示:

bool EventDispatcher::dispatch()
{
    .......

    void* preDispatchEventHandlerResult;
    if (dispatchEventPreProcess(preDispatchEventHandlerResult) == ContinueDispatching)
        if (dispatchEventAtCapturing(windowEventContext) == ContinueDispatching)
            if (dispatchEventAtTarget() == ContinueDispatching)
                dispatchEventAtBubbling(windowEventContext);
    dispatchEventPostProcess(preDispatchEventHandlerResult);

    ......

    return !m_event->defaultPrevented();
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/events/EventDispatcher.cpp中。

       在网页发生的一个Event,分为五个阶段进行分发处理:

       1. Pre Process

       2. Capturing

       3. Target

       4. Bubbling

       5. Post Process

       我们通过一个例子说明这五个阶段的处理过程,如下所示:

<html>
    <body>
        <div id="div1"> 
            <div id="div2">
                <div id="div3">
                </div>
            </div>
        </div>
    </body>
</html>
       假设在div3上发生了一个Touch Event。

       在Pre Process阶段,WebKit会将Touch Event分发给div3的Pre Dispatch Event Handler处理,让div3有机会在DOM Event Handler处理Touch Event之前做一些事情,用来实现自己的行为。

       在Capturing阶段,WebKit会将Touch Event依次分发给html -> body -> div1 -> div2的DOM Event Handler处理。

       在Target阶段,WebKit会将Touch Event依次分发给div3的DOM Event Handler处理。。

       在Bubbling阶段, WebKit会将Touch Event依次分发给div2 -> div1 -> body -> html的DOM Event Handler处理。

       在Post Process阶段,WebKit会将Touch Event分发给div3的Post Dispatch Event Handler处理,让div3有机会在DOM Event Handler处理Touch Event之后做一些事情,与它的Pre Dispatch Event Handler相呼应。

       此外,如果在前面4个阶段,Touch Event的preventDefault函数没有被调用,那么WebKit会将它依次分发给div3 -> div2 -> div1 -> body -> html的Default Event Handler处理。在这个过程中,如果某一个Node的Default Event Handler处理了该Touch Event,那么该Touch Event的分发过程就会中止。

       其中,Pre Process和Post Process这两个阶段是一定会执行的。在Capturing、Target和Bubbling这三个阶段,如果某一个Node的DOM Event Handler调用了Touch Event的stopPropagation函数,那么它就会提前中止,后面的阶段也不会被执行。

       Pre Dispatch Event Handler、Post Dispatch Event Handler和Default Event Handler是由WebKit实现的,DOM Event Handler可以通过JavaScript进行注册。在注册的时候,可以指定DOM Event Handler在Capturing阶段还是Bubbling阶段接收Event,但是不能同时在这两个阶段都接收。此外,注册Target Node上的DOM Event Handler没有Capturing阶段还是Bubbling阶段之分,如果Event在Capturing阶段没有被中止,那么它将在Target阶段接收。

       明白了WebKit中的Event处理流程之后,接下来我们主要分析Target Node在Target阶段处理Touch Event的过程,也就是EventDispatcher类的成员函数dispatchEventAtTarget的实现,如下所示:

inline EventDispatchContinuation EventDispatcher::dispatchEventAtTarget()
{
    m_event->setEventPhase(Event::AT_TARGET);
    m_event->eventPath()[0].handleLocalEvents(m_event.get());
    return m_event->propagationStopped() ? DoneDispatching : ContinueDispatching;
}

      这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/events/EventDispatcher.cpp中。

      EventDispatcher类的成员变量m_event描述的就是当前发生的Touch Event。这个Touch Event的Event Path是一个Node Event Context列表。列表中的第一个Node Event Context描述的就是当前发生的Touch Event的Target Node的上下文信息。有了这个Node Event Context之后,就可以调用它的成员函数handleLocalEvents将当前发生的Touch Event分发给Target Node的DOM Event Handler处理,如下所示:

void NodeEventContext::handleLocalEvents(Event* event) const
{
    ......

    event->setTarget(target());
    event->setCurrentTarget(m_currentTarget.get());
    m_node->handleLocalEvents(event);
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/events/NodeEventContext.cpp中。

       这时候,NodeEventContext类的成员变量m_node描述的就是Touch Event的Target Node。NodeEventContext类的成员函数handleLocalEvents调用这个Target Node的成员函数handleLocalEvents,用来将当前发生的Touch Event分发给它的DOM Event Handler处理,如下所示:

void Node::handleLocalEvents(Event* event)
{
    ......

    fireEventListeners(event);
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/dom/Node.cpp中。

       Node类的成员函数handleLocalEvents主要是调用另外一个成员函数fireEventListeners将当前发生的Touch Event分发给Target Node的DOM Event Handler处理。

       Node类的成员函数fireEventListeners是从父类EventTarget继承下来的,它的实现如下所示:

bool EventTarget::fireEventListeners(Event* event)
{
    ......

    EventTargetData* d = eventTargetData();
    ......

    EventListenerVector* legacyListenersVector = 0;
    AtomicString legacyTypeName = legacyType(event);
    if (!legacyTypeName.isEmpty())
        legacyListenersVector = d->eventListenerMap.find(legacyTypeName);

    EventListenerVector* listenersVector = d->eventListenerMap.find(event->type());
    ......

    if (listenersVector) {
        fireEventListeners(event, d, *listenersVector);
    } else if (legacyListenersVector) {
        ......
        fireEventListeners(event, d, *legacyListenersVector);
        ......
    }

    ......
    return !event->defaultPrevented();
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/events/EventTarget.cpp中。

       Node类的成员函数fireEventListeners主要根据Touch Event的类型在Target Node注册的DOM Event Handler中找到对应的DOM Event Handler。例如,如果当前发生的是类型为MOVE的Touch Event,那么Node类的成员函数fireEventListeners就会找到注册在Target Node上的类型为TouchMove的DOM Event Handler。

       找到了对应的DOM Event Handler之后,Node类的成员函数fireEventListeners再调用另外一个重载版本的成员函数fireEventListeners将当前发生的Touch Event分发给它们处理,如下所示:

void EventTarget::fireEventListeners(Event* event, EventTargetData* d, EventListenerVector& entry)
{
    ......

    size_t i = 0;
    size_t size = entry.size();
    ......

    for ( ; i < size; ++i) {
        RegisteredEventListener& registeredListener = entry[i];
        if (event->eventPhase() == Event::CAPTURING_PHASE && !registeredListener.useCapture)
            continue;
        if (event->eventPhase() == Event::BUBBLING_PHASE && registeredListener.useCapture)
            continue;

        // If stopImmediatePropagation has been called, we just break out immediately, without
        // handling any more events on this target.
        if (event->immediatePropagationStopped())
            break;

        ExecutionContext* context = executionContext();
        if (!context)
            break;

        ......
        registeredListener.listener->handleEvent(context, event);
        ......
    }
    ......
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/events/EventTarget.cpp中。

       在Capturing和Bubbling阶段,WebKit也是通过Node类的成员函数fireEventListeners将当前发生的Event分发给Event Path上的Node处理。从这里我们就可以看到,在Capturing阶段,如果一个Node在注册了一个在Capturing阶段接收Event的DOM Event Handler,那么此时该DOM Event Handler就会获得当前发生的Event。同样,在Bubbling阶段,如果一个Node在注册了一个在Bubbling阶段接收Event的DOM Event Handler,那么此时该DOM Event Handler就会获得当前发生的Event。对于Target Node来说,它注册的DOM Event Handler则会在Target阶段获得当前发生的Event。

       这些DOM Event Handler在注册的时候,会被JavaScript引擎V8封装在一个V8AbstractEventListener对象中。Node类的成员函数fireEventListeners通过调用这些V8AbstractEventListener对象的成员函数handleEvent将当前发生的Event分发给它们所封装的DOM Event Handler处理。这就会进入到JavaScript引擎V8里面去执行了。以后我们分析JavaScript引擎V8时,再回过头来看它对Event的处理流程。

       至此,我们就分析完成Chromium分发输入事件给WebKit,以及WebKit进行处理的过程了。这个过程是在Render进程的Main线程中执行的。回忆前面Chromium网页滑动和捏合手势处理过程分析一文,滑动和捏合手势这两种特殊的输入事件,是在Render进程的Compositor线程中处理的。

       WebKit在处理输入事件的过程中,需要通过Render Layer Tree和Render Object Tree,在DOM Tree中找到输入事件的Target Node(Hit Test)。找到了输入事件的Target Node之后,分为Pre Process、Capturing、Target、Bubbling和Post Process五个阶段对输入事件进行处理。其中,Capturing、Target和Bubbling这三个阶段是将输入事件分发给Target Node的DOM Event Handler处理,也就是我们通过JavaScript注册的Event Handler。DOM Event Handler最终是在JavaScript引擎V8执行的。

       至此,Chromium的网页输入事件处理机制我们就全部分析完成了。重新学习可以参考前面Chromium网页输入事件处理机制简要介绍和学习计划这篇文章。更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/7/25 1:00:35 原文链接
阅读:39461 评论:7 查看评论

Chromium视频标签简要介绍和学习计划

$
0
0

       随着互联网的发展,在网页上观看视频变得越来越流行,尤其是泛娱乐(手机直播)大行其道的今天。在HTML5之前,在网页上播放视频需要插件支持,例如Flash插件。有了HTML5之后,标签<video>使得浏览器有了播放视频的功能。与插件相比,浏览器的视频播放功能不仅在产品上体验更好,在技术上也更加稳定。本文接下来就简要介绍Chromium是如何实现<video>标签的视频播放功能的,以及制定学习计划。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

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

       本文以及接下来的文章,我们主要关注Chromium在Android平台上是如何实现<video>标签的视频播放功能的。我们知道,Android平台提供播放视频的API接口,也就是MediaPlayer接口。这个接口不仅可以用来播放本地媒体文件,也就用来播放网络上的流媒体文件。这可以大大简化Chromium在Android平台上支持<video>标签的工作,因为前者可以直接利用后者提供的MediaPlayer接口实现视频播放功能,如图1所示:


图1 <video>标签的实现

       从前面Chromium多进程架构简要介绍和学习计划这个系列的文章可以知道,Chromium是一个多进程架构。其中,Render进程负责加载、解析和渲染网页,Browser进程负责将Render进程渲染出来的网页内容合成到屏幕上显示。Render进程又是通过WebKit来加载和解析网页内容的。

       WebKit在解析网页内容时,每当遇到<video>标签,就会在DOM Tree中创建一个类型为HTMLMediaElement的节点。这个HTMLMediaElement节点又会在内部创建一个WebMediaPlayerClientImpl对象。这个WebMediaPlayerClientImpl对象在WebKit内部就描述为一个播放器,用来为<video>标签提供视频播放功能。

       WebMediaPlayerClientImpl类是由WebKit提供的,它本身不实现视频播放功能,因为视频播放是一个平台相关的功能。我们知道,WebKit是平台无关的,所有平台相关的功能都需要由它的使用者实现。在Chromium中,WebKit的使用者即为运行在Render进程中的Content模块。Content模块提供了一个WebMediaPlayerAndroid类,用来向WebKit提供视频播放功能。

       WebKit层的每一个WebMediaPlayerClientImpl对象在Content层都有一个对应的WebMediaPlayerAndroid对象。这些WebMediaPlayerAndroid对象就相当于是在Render进程内部实现的播放器。每一个播放器都关联有一个ID,它们被另外一个称为RendererMediaPlayerManager的对象管理。通过这种方式,就可以在一个网页上同时支持多个<video>标签,也就是可以同时播放多个视频。

       我们知道,Render进程运行在一个沙箱中,也就是它是一个受限进程。播放网络上的视频需要访问网络,以及使用系统的解码器等资源。因此,Render进程也没有实现视频播放功能,而是通过Browser进程进行播放。因此,对于Render进程中的每一个WebMediaPlayerAndroid对象,在Browser进程中都会有一个对应的WebMediaPlayerBridge对象。这些WebMediaPlayerBridge对象就相当于在Browser进程内部实现的播放器。这些播放器被另外一个称为BrowserMediaPlayerManager的对象管理,使得Browser进程可以同时创建多个播放器,以支持在一个网页上同时播放多个视频。

       WebMediaPlayerBridge类本身也不实现播放器功能。在Android平台上,WebMediaPlayerBridge类将通过SDK提供的MediaPlayer接口来实现视频播放功能。SDK是在Java层提供MediaPlayer接口的,而WebMediaPlayerBridge类是实现在C++层的,因此后者在使用前者时,需要通过JNI使用。

       总结来说,在Android平台上,Chromium会通过SDK接口MediaPlayer为网页中的每一个<video>标签创建一个播放器。播放器负责从网络上下载视频内容,并且进行解码。解码后得到的视频画面需要作为网页的一部分进行显示。从前面Chromium网页渲染机制简要介绍和学习计划这个系列的文章可以知道,在Chromium中,网页是Render进程进行渲染的,并且当前需要渲染的内容来自于网页的CC Active Layer Tree。这意味着要将播放器解码出来的视频画面交给网页的CC Active Layer Tree处理。

       播放器解码出来的视频画面是通过SurfaceTexture接口交给网页的CC Active Layer Tree处理的,如图2所示:


图2 <video>标签的视频画面渲染方式

       在Android平台上,SurfaceTexture是一个完美的接口。一方面它支持跨进程传输数据,另一方面传输的数据可以作为纹理使用。在我们这个情景中,MediaPlayer运行在Browser进程中,CC Active Layer Tree运行在Render进程中,并且是通过OpenGL进行渲染的。因此,SurfaceTexture非常适合将前者的输出作为后者的输入,并且通过OpenGL以纹理的方式渲染出来。

       具体来说,就是CC Active Layer Tree会为每一个<video>标签创建一个类型为VideoLayerImpl的节点。这个节点的内容就来自于播放器解码出来的视频画面。这些视频画面最终又是通过纹理来描述的。从前面Chromium网页渲染机制简要介绍和学习计划这个系列的文章可以知道,CC Active Layer Tree其它节点的内容,最终也是描述为纹理进行渲染的。因此,Chromium可以轻松地将播放器解码出来的视频画面作为网页的一部分进行显示。

       在Android平台上,Chromium还为<video>标签提供了全屏播放功能。全屏播放与非全屏播放可以进行无缝切换,它是怎么实现的呢?我们通过图3来说明,如下所示:


图3 <video>标签的全屏播放功能实现

       在<video>标签全屏播放的情况下,我们是看不到网页的其它内容的。这使得Chromium可以使用一种简单的方式实现<video>标签的全屏播放功能。当<video>标签全屏播放的时候,Browser进程会在浏览器窗口上创建一个全屏的SurfaceView,然后将这个SurfaceView底层的Surface取出来,设置为MediaPlayer的解码输出。这样就可以将MediaPlayer的解码输出全屏显示在屏幕上了。这时候由于网页是不可见的,Render进程不需要对它进行渲染。

       接下来,我们结合源码,按照三个情景深入分析Chromium对<video>标签的支持:

       1. 为<video>标签创建播放器的过程

       2. 渲染<video>标签视频画面的过程

       3. 全屏播放<video>标签视频的过程

       学习了这三个情景之后 ,我们就会对HTML5中的<video>标签有更深刻的了解,敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/8/8 1:00:30 原文链接
阅读:38806 评论:6 查看评论

Chromium为视频标签全屏播放的过程分析

$
0
0

       在Chromium中,<video>标签有全屏和非全屏两种播放模式。在非全屏模式下,<video>标签播放的视频嵌入在网页中显示,也就是视频画面作为网页的一部分显示。在全屏模式下,我们是看不到网页其它内容的,因此<video>标签播放的视频可以在一个独立的全屏窗口中显示。这两种截然不同的播放模式,导致Chromium使用不同的方式渲染视频画面。本文接下来就详细分析<video>标签全屏播放的过程。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

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

       从前面Chromium为视频标签<video>渲染视频画面的过程分析一文可以知道,在Android平台上,<video>标签指定的视频是通过系统提供的MediaPlayer进行播放的。MediaPlayer提供了一个setSurface接口,用来给MediaPlayer设置一个Surface。Surface内部有一个GPU缓冲区队列,以后MediaPlayer会将解码出来的视频画面写入到这个队列中去。

       Surface有两种获取方式。第一种方式是通过SurfaceTexture构造一个新的Surface。第二种方式是从SurfaceView内部获得。在非全屏模式下,Chromium就是通过第一种方式构造一个Surface,然后设置给MediaPlayer的。在全屏模式下,Chromium将会直接创建一个全屏的SurfaceView,然后再从这个SurfaceView内部获得一个Surface,并且设置给MediaPlayer。

       在Android平台上,SurfaceView的本质是一个窗口。既然是窗口,那么它的UI就是由系统(SurfaceFlinger)合成在屏幕上显示的。它的UI就来源于它内部的Surface描述的GPU缓冲区队列。因此,当MediaPlayer将解码出来的视频画面写入到SurfaceView内部的Surface描述的GPU缓冲区队列去时,SurfaceFlinger就会从该GPU缓冲区队列中将新写入的视频画面提取出来,并且合成在屏幕上显示。关于SurfaceView的更多知识,可以参考前面Android视图SurfaceView的实现原理分析一文。

       Surface描述的GPU缓冲区队列,是一个生产者/消息者模型。在我们这个情景中,生产者便是MediaPlayer。如果Surface是通过SurfaceTexture构造的,那么SurfaceTexture的所有者,也就是Chromium,就是消费者。消费者有责任将视频画面从GPU缓冲区队列中提取出来,并且进行渲染。渲染完成后,再交给SurfaceFlinger合成显示在屏幕中。如果Surface是从SurfaceView内部获取的,那么SurfaceView就是消费者,然后再交给SurfaceFlinger合成显示在屏幕中。

       简单来说,在非全屏模式下,<video>标签的视频画面经过MediaPlayer->Chromium->SurfaceFlinger显示在屏幕中,而在全屏模式下,经过MediaPlayer->SurfaceView->SurfaceFlinger显示在屏幕中。

       Chromium支持<video>标签在全屏和非全屏模式之间无缝切换,也就是从一个模式切换到另外一个模式的时候,不需要重新创建MediaPlayer,只需要给原先使用的MediaPlayer设置一个新的Surface即可。图1描述的是<video>标签从非全屏模式切换为全屏模式的示意图,如下所示:


图1 <video>标签从非全屏模式切换为全屏模式

      当<video>标签从非全屏模式切换为全屏模式时,Chromium会为它创建一个全屏的SurfaceView,并且将这个SurfaceView内部的Surface设置给MediaPlayer。以后MediaPlayer就不会再将解码出来的视频画面通过原先设置的SurfaceTexture交给Chromium处理,而是通过后面设置的Surface交给SurfaceView处理。

      接下来,我们就结合源代码,从<video>标签进入全屏模式开始,分析<video>标签全屏播放视频的过程。从前面Chromium为视频标签<video>创建播放器的过程分析一文可以知道,在WebKit中,<video>标签是通过HTMLMediaElement类描述的。当<video>标签进入全屏模式时,HTMLMediaElement类的成员函数enterFullscreen就会被调用,它的实现如下所示:

void HTMLMediaElement::enterFullscreen()
{
    WTF_LOG(Media, "HTMLMediaElement::enterFullscreen");

    FullscreenElementStack::from(document()).requestFullScreenForElement(this, 0, FullscreenElementStack::ExemptIFrameAllowFullScreenRequirement);
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       在WebKit中,网页的每一个标签都可以进入全屏模式。每一个网页都对应有一个FullscreenElementStack对象。这个FullscreenElementStack对象内部有一个栈,用来记录它对应的网页有哪些标签进入了全屏模式。

       HTMLMediaElement类的成员函数enterFullscreen首先调用成员函数document获得当前正在处理的<video>标签所属的网页,然后再通过调用FullscreenElementStack类的静态成员函数from获得这个网页所对应的FullscreenElementStack对象。有了这个FullscreenElementStack对象之后,就可以调用它的成员函数requestFullScreenForElement请求将当前正在处理的<video>标签设置为全屏模式。

       FullscreenElementStack类的成员函数requestFullScreenForElement的实现如下所示:

void FullscreenElementStack::requestFullScreenForElement(Element* element, unsigned short flags, FullScreenCheckType checkType)
{
    ......

    // The Mozilla Full Screen API <https://wiki.mozilla.org/Gecko:FullScreenAPI> has different requirements
    // for full screen mode, and do not have the concept of a full screen element stack.
    bool inLegacyMozillaMode = (flags & Element::LEGACY_MOZILLA_REQUEST);

    do {
        ......

        // 1. If any of the following conditions are true, terminate these steps and queue a task to fire
        // an event named fullscreenerror with its bubbles attribute set to true on the context object's
        // node document:
        ......

        // The context object's node document fullscreen element stack is not empty and its top element
        // is not an ancestor of the context object. (NOTE: Ignore this requirement if the request was
        // made via the legacy Mozilla-style API.)
        if (!m_fullScreenElementStack.isEmpty() && !inLegacyMozillaMode) {
            Element* lastElementOnStack = m_fullScreenElementStack.last().get();
            if (lastElementOnStack == element || !lastElementOnStack->contains(element))
                break;
        }

        // A descendant browsing context's document has a non-empty fullscreen element stack.
        bool descendentHasNonEmptyStack = false;
        for (Frame* descendant = document()->frame() ? document()->frame()->tree().traverseNext() : 0; descendant; descendant = descendant->tree().traverseNext()) {
            ......
            if (fullscreenElementFrom(*toLocalFrame(descendant)->document())) {
                descendentHasNonEmptyStack = true;
                break;
            }
        }
        if (descendentHasNonEmptyStack && !inLegacyMozillaMode)
            break;

        ......


        // 2. Let doc be element's node document. (i.e. "this")
        Document* currentDoc = document();

        // 3. Let docs be all doc's ancestor browsing context's documents (if any) and doc.
        Deque<Document*> docs;

        do {
            docs.prepend(currentDoc);
            currentDoc = currentDoc->ownerElement() ? ¤tDoc->ownerElement()->document() : 0;
        } while (currentDoc);

        // 4. For each document in docs, run these substeps:
        Deque<Document*>::iterator current = docs.begin(), following = docs.begin();

        do {
            ++following;

            // 1. Let following document be the document after document in docs, or null if there is no
            // such document.
            Document* currentDoc = *current;
            Document* followingDoc = following != docs.end() ? *following : 0;

            // 2. If following document is null, push context object on document's fullscreen element
            // stack, and queue a task to fire an event named fullscreenchange with its bubbles attribute
            // set to true on the document.
            if (!followingDoc) {
                from(*currentDoc).pushFullscreenElementStack(element);
                addDocumentToFullScreenChangeEventQueue(currentDoc);
                continue;
            }

            // 3. Otherwise, if document's fullscreen element stack is either empty or its top element
            // is not following document's browsing context container,
            Element* topElement = fullscreenElementFrom(*currentDoc);
            if (!topElement || topElement != followingDoc->ownerElement()) {
                // ...push following document's browsing context container on document's fullscreen element
                // stack, and queue a task to fire an event named fullscreenchange with its bubbles attribute
                // set to true on document.
                from(*currentDoc).pushFullscreenElementStack(followingDoc->ownerElement());
                addDocumentToFullScreenChangeEventQueue(currentDoc);
                continue;
            }

            // 4. Otherwise, do nothing for this document. It stays the same.
        } while (++current != docs.end());

        // 5. Return, and run the remaining steps asynchronously.
        // 6. Optionally, perform some animation.
        ......
        document()->frameHost()->chrome().client().enterFullScreenForElement(element);

        // 7. Optionally, display a message indicating how the user can exit displaying the context object fullscreen.
        return;
    } while (0);

    ......
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/dom/FullscreenElementStack.cpp中。

       FullscreenElementStack类的成员函数requestFullScreenForElement主要是用来为网页中的每一个Document建立一个Stack。这个Stack记录了Document中所有请求设置为全屏模式的标签。我们通过图2所示的例子说明FullscreenElementStack类的成员函数requestFullScreenForElement的实现:


图2 Fullscreen Stack for Document

       图2所示的网页包含了两个Document:Doc1和Doc2。其中,Doc1通过<iframe>标签加载了Doc2,后者里面有一个<video>标签。当Doc2里面的<video>标签被设置为全屏模式时,Doc1的Stack会被压入一个<iframe>标签和一个<video>标签,其中,<iframe>标签代表的是Doc1,Doc2的Stack会被压入一个<video>标签。

       注释中提到,Mozilla定义的Fullscreen API没有Fullscreen Element Stack的概念。没有Fullscreen Element Stack,意味着网页的标签在任意情况下都可以设置为全屏模式。不过,非Mozilla定义的Fullscreen API是要求Fullscreen Element Stack的。Fullscreen Element Stack用来限制一个标签是否可以设置为全屏模式:

       1. 当Stack为空时,任意标签均可设置为全屏模式。

       2. 当Stack非空时,栈顶标签的子标签才可以设置为全屏模式。

       FullscreenElementStack类的成员函数requestFullScreenForElement就是根据上述逻辑判断参数element描述的标签是否可以设置为全屏模式的。如果可以,那么就会更新与它相关的Stack,并且在最后调用在WebKit Glue层创建一个WebViewImpl对象的成员函数enterFullScreenForElement,用来通知WebKit Glue层有一个标签要进入全屏模式。这个WebViewImpl对象的创建过程可以参考前面Chromium网页Frame Tree创建过程分析一文。WebKit Glue层的作用,可以参考前面Chromium网页加载过程简要介绍和学习计划一文。

       接下来,我们就继续分析WebViewImpl类的成员函数enterFullScreenForElement的实现,如下所示:

void WebViewImpl::enterFullScreenForElement(WebCore::Element* element)
{
    m_fullscreenController->enterFullScreenForElement(element);
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebViewImpl.cpp中。

       WebViewImpl类的成员变量m_fullscreenController指向的是一个FullscreenController对象。WebViewImpl类的成员函数enterFullScreenForElement调用这个FullscreenController对象的成员函数enterFullScreenForElement,用来通知它将参数element描述的标签设置为全屏模式。

       FullscreenController类的成员函数enterFullScreenForElement的实现如下所示:

void FullscreenController::enterFullScreenForElement(WebCore::Element* element)
{
    // We are already transitioning to fullscreen for a different element.
    if (m_provisionalFullScreenElement) {
        m_provisionalFullScreenElement = element;
        return;
    }

    // We are already in fullscreen mode.
    if (m_fullScreenFrame) {
        m_provisionalFullScreenElement = element;
        willEnterFullScreen();
        didEnterFullScreen();
        return;
    }

    // We need to transition to fullscreen mode.
    if (WebViewClient* client = m_webViewImpl->client()) {
        if (client->enterFullScreen())
            m_provisionalFullScreenElement = element;
    }
}
      这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/FullscreenController.cpp中。

      如果参数element描述的标签所在的网页已经有标签处于全屏模式,那么FullscreenController类的成员变量m_provisionalFullScreenElement就会指向该标签,并且FullscreenController类的另外一个成员变量m_fullScreenFrame会指向一个LocalFrame对象。这个LocalFrame对象描述的就是处于全屏模式的标签所在的网页。

      我们假设参数element描述的标签所在的网页还没有标签被设置为全屏模式。这时候FullscreenController类的成员函数enterFullScreenForElement会调用成员变量m_webViewImpl指向的一个WebViewImpl对象的成员函数client获得一个WebViewClient接口,然后再调用这个WebViewClient接口的成员函数enterFullScreen,用来通知它进入全屏模式。

      上述WebViewClient接口是由WebKit的使用者设置的。在我们这个情景中,WebKit的使用者即Render进程中的Content模块,它设置的WebViewClient接口指向的是一个RenderViewImpl对象。这个RenderViewImpl对象的创建过程可以参考前面Chromium网页Frame Tree创建过程分析一文,它描述的是当前正在处理的网页所加载在的一个RenderView控件。这一步实际上是通知Chromium的Content层进入全屏模式。

       FullscreenController类的成员函数enterFullScreenForElement通知了Content层进入全屏模式之后,会将引发Content层进入全屏模式的标签记录在成员变量m_provisionalFullScreenElement中。在我们这个情景中,这个标签即为一个<video>标签。

       接下来我们继续分析Chromium的Content层进入全屏模式的过程,也就是RenderViewImpl类的成员函数enterFullScreen的实现,如下所示:

bool RenderViewImpl::enterFullScreen() {
  Send(new ViewHostMsg_ToggleFullscreen(routing_id_, true));
  return true;
}
       这个函数定义在文件external/chromium_org/content/renderer/render_view_impl.cc。

       RenderViewImpl类的成员函数enterFullScreen向Browser进程发送一个类型为ViewHostMsg_ToggleFullscreen的IPC消息,用来通知它将Routing ID为routing_id_的网页所加载在的Tab设置为全屏模式。

       Browser进程通过RenderViewHostImpl类的成员函数OnMessageReceived接收类型为ViewHostMsg_ToggleFullscreen的IPC消息,如下所示:

bool RenderViewHostImpl::OnMessageReceived(const IPC::Message& msg) {
  ......

  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(RenderViewHostImpl, msg)
    ......
    IPC_MESSAGE_HANDLER(ViewHostMsg_ToggleFullscreen, OnToggleFullscreen)
    ......
    IPC_MESSAGE_UNHANDLED(
        handled = RenderWidgetHostImpl::OnMessageReceived(msg))
  IPC_END_MESSAGE_MAP()

  return handled;
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_view_host_impl.cc中。

       RenderViewHostImpl类的成员函数OnMessageReceived将类型为ViewHostMsg_ToggleFullscreen的IPC消息分发给另外一个成员函数OnToggleFullscreen处理,如下所示:

void RenderViewHostImpl::OnToggleFullscreen(bool enter_fullscreen) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  delegate_->ToggleFullscreenMode(enter_fullscreen);
  // We need to notify the contents that its fullscreen state has changed. This
  // is done as part of the resize message.
  WasResized();
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_view_host_impl.cc中。

       RenderViewHostImpl类的成员函数OnToggleFullscreen首先通过成员变量delegate_描述的一个RenderViewHostDelegate委托接口将浏览器窗口设置为全屏模式,然后再调用另外一个成员函数WasResized通知Render进程,浏览器窗口大小已经发生了变化,也就是它进入了全屏模式。

       RenderViewHostImpl类的成员变量delegate_描述的RenderViewHostDelegate委托接口指向的是一个WebContentsImpl对象。这个WebContentsImpl对象是Content层提供给外界的一个接口,用来描述当前正在加载的一个网页。外界通过这个接口就可以访问当前正在加载的网页。

       RenderViewHostImpl类的成员函数WasResized是从父类RenderWidgetHostImpl继承下来的,它的实现如下所示:

void RenderWidgetHostImpl::WasResized() {
  ......

  is_fullscreen_ = IsFullscreen();
  ......

  ViewMsg_Resize_Params params;
  ......
  params.is_fullscreen = is_fullscreen_;
  if (!Send(new ViewMsg_Resize(routing_id_, params))) {
    resize_ack_pending_ = false;
  } else {
    last_requested_size_ = new_size;
  }
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_widget_host_impl.cc中。

       RenderWidgetHostImpl类的成员函数WasResized主要是向Render进程发送一个类型为ViewMsg_Resize的IPC消息。这个ViewMsg_Resize的IPC消息。携带了一个is_fullscreen信息,用来告知Render进程当前正在处理的网页是否已经进入了全屏模式。

       Render进程通过RenderWidget类的成员函数OnMessageReceived接收类型为ViewMsg_Resize的IPC消息,如下所示:

bool RenderWidget::OnMessageReceived(const IPC::Message& message) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(RenderWidget, message)
    ......
    IPC_MESSAGE_HANDLER(ViewMsg_Resize, OnResize)
    ......
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}
      这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。

      RenderWidget类的成员函数OnMessageReceived将类型为ViewMsg_Resize的IPC消息分发给另外一个成员函数OnResize处理,如下所示:

void RenderWidget::OnResize(const ViewMsg_Resize_Params& params) {
  ......

  Resize(params.new_size, params.physical_backing_size,
         params.overdraw_bottom_height, params.visible_viewport_size,
         params.resizer_rect, params.is_fullscreen, SEND_RESIZE_ACK);

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

      RenderWidget类的成员函数OnResize又调用另外一个成员函数Resize处理类型为ViewMsg_Resize的IPC消息,如下所示:

void RenderWidget::Resize(const gfx::Size& new_size,
                          const gfx::Size& physical_backing_size,
                          float overdraw_bottom_height,
                          const gfx::Size& visible_viewport_size,
                          const gfx::Rect& resizer_rect,
                          bool is_fullscreen,
                          ResizeAck resize_ack) {
  ......

  if (compositor_) {
    compositor_->setViewportSize(new_size, physical_backing_size);
    ......
  }

  ......

  // NOTE: We may have entered fullscreen mode without changing our size.
  bool fullscreen_change = is_fullscreen_ != is_fullscreen;
  if (fullscreen_change)
    WillToggleFullscreen();
  is_fullscreen_ = is_fullscreen;

  if (size_ != new_size) {
    size_ = new_size;

    // When resizing, we want to wait to paint before ACK'ing the resize.  This
    // ensures that we only resize as fast as we can paint.  We only need to
    // send an ACK if we are resized to a non-empty rect.
    webwidget_->resize(new_size);
  } 

  ......

  if (fullscreen_change)
    DidToggleFullscreen();

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

       RenderWidget类的成员函数Resize一方面会通知网页的UI合成器,它负责渲染的网页的大小发生了变化,以便它修改网页UI的Viewport大小。这是通过调用成员变量compositor_指向的一个RenderWidgetCompositor对象的成员函数setViewportSize实现的。这个RenderWidgetCompositor对象的创建过程可以参考前面Chromium网页Layer Tree创建过程分析一文。

       另一方面,RenderWidget类的成员函数Resize又会通知WebKit,它当前正在加载网页的大小发生了变化。这是通过调用成员变量webwidget_指向的一个WebViewImpl对象的成员函数resize实现的。这个WebViewImpl对象的创建过程可以参考前面Chromium网页Frame Tree创建过程分析一文。

       此外,RenderWidget类的成员函数Resize还会判断网页是否从全屏模式退出,或者进入全屏模式。如果是的话,那么就在通知WebKit修改网页的大小前后,RenderWidget类的成员函数Resize还会分别调用另外两个成员函数WillToggleFullscreen和DidToggleFullscreen,用来通知WebKit网页进入或者退出了全屏模式。

       在我们这个情景中,网页是进入了全屏模式。接下来我们就先分析RenderWidget类的成员函数WillToggleFullscreen的实现,然后再分析另外一个成员函数DidToggleFullscreen的实现。

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

void RenderWidget::WillToggleFullscreen() {
  ......

  if (is_fullscreen_) {
    webwidget_->willExitFullScreen();
  } else {
    webwidget_->willEnterFullScreen();
  }
}
       这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。

       从前面的分析可以知道,RenderWidget类的成员函数WillToggleFullscreen是在通知WebKit进入全屏模式之前被调用的。这时候RenderWidget类的成员变量is_fullscreen_的值为false。因此,接下来RenderWidget类的成员函数WillToggleFullscreen会调用成员变量webwidget_指向的一个WebViewImpl对象的成员函数willEnterFullScreen,用来通知WebKit它即将要进入全屏模式。

       WebViewImpl类的成员函数willEnterFullScreen的实现如下所示:

void WebViewImpl::willEnterFullScreen()
{
    m_fullscreenController->willEnterFullScreen();
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebViewImpl.cpp中。

       WebViewImpl类的成员函数willEnterFullScreen调用成员变量m_fullscreenController指向的一个FullscreenController对象的成员函数willEnterFullScreen,用来通知WebKit即将要进入全屏模式,如下所示:

void FullscreenController::willEnterFullScreen()
{
    if (!m_provisionalFullScreenElement)
        return;

    // Ensure that this element's document is still attached.
    Document& doc = m_provisionalFullScreenElement->document();
    if (doc.frame()) {
        ......
        m_fullScreenFrame = doc.frame();
    }
    m_provisionalFullScreenElement.clear();
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/FullscreenController.cpp中。

       从前面的分析可以知道,此时FullscreenController类的成员变量m_provisionalFullScreenElement的值不等于NULL,它指向了一个<video>标签。FullscreenController类的成员函数willEnterFullScreen找到这个<video>标签所在的Document,并且获得与这个Document关联的一个LocalFrame对象,保存在成员变量m_fullScreenFrame中,表示当前处于全屏模式的网页。

       这一步执行完成后,回到前面分析的RenderWidget类的成员函数Resize,它在通知了WebKit修改当前正在加载的网页的大小之后,会调用另外一个成员函数DidToggleFullscreen,用来通知WebKit已经进入了全屏模式,如下所示:

void RenderWidget::DidToggleFullscreen() {
  ......

  if (is_fullscreen_) {
    webwidget_->didEnterFullScreen();
  } else {
    webwidget_->didExitFullScreen();
  }
}
      这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。

      从前面的分析可以知道,此时RenderWidget类的成员变量is_fullscreen_的值已经被设置为true。因此,RenderWidget类的成员函数DidToggleFullscreen接下来就会调用成员变量webwidget_指向的一个WebViewImpl对象的成员函数didEnterFullScreen,用来通知WebKit它已经进入全屏模式,如下所示:

void WebViewImpl::didEnterFullScreen()
{
    m_fullscreenController->didEnterFullScreen();
}

       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebViewImpl.cpp中。

       WebViewImpl类的成员函数didEnterFullScreen调用成员变量m_fullscreenController指向的一个FullscreenController对象的成员函数didEnterFullScreen,用来通知WebKit已经进入全屏模式,如下所示:

void FullscreenController::didEnterFullScreen()
{
    if (!m_fullScreenFrame)
        return;

    if (Document* doc = m_fullScreenFrame->document()) {
        if (FullscreenElementStack::isFullScreen(*doc)) {
            ......

            if (RuntimeEnabledFeatures::overlayFullscreenVideoEnabled()) {
                Element* element = FullscreenElementStack::currentFullScreenElementFrom(*doc);
                ASSERT(element);
                if (isHTMLMediaElement(*element)) {
                    HTMLMediaElement* mediaElement = toHTMLMediaElement(element);
                    if (mediaElement->webMediaPlayer() && mediaElement->webMediaPlayer()->canEnterFullscreen()
                        // FIXME: There is no embedder-side handling in layout test mode.
                        && !isRunningLayoutTest()) {
                        mediaElement->webMediaPlayer()->enterFullscreen();
                    }
                    .......
                }
            }
        }
    }
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/FullscreenController.cpp中。

       从前面的分析可以知道,FullscreenController类的成员变量m_fullScreenFrame的值不等于NULL。它指向了一个LocalFrame对象。这个LocalFrame对象描述的就是当前被设置为全屏模式的标签所在的网页。通过这个LocalFrame对象可以获得一个Document对象。

       有了这个Document对象之后,FullscreenController类的成员函数didEnterFullScreen就会检查它的Fullscreen Element Stack栈顶标签。如果这是一个<video>标签,并且已经为这个<video>标签创建过MediaPlayer接口,以及这个MediaPlayer接口允许进入全屏模式,那么FullscreenController类的成员函数didEnterFullScreen就会调用这个MediaPlayer接口的成员函数enterFullScreen,让其进入全屏模式。

       从前面Chromium为视频标签<video>创建播放器的过程分析一文可以知道,WebKit为<video>标签创建的MediaPlayer接口指向的是一个WebMediaPlayerAndroid对象。因此,FullscreenController类的成员函数didEnterFullScreen调用的是WebMediaPlayerAndroid类的成员函数enenterFullScreen让<video>标签的播放器进入全屏模式。

       WebMediaPlayerAndroid类的成员函数enenterFullScreen的实现如下所示:

void WebMediaPlayerAndroid::enterFullscreen() {
  if (player_manager_->CanEnterFullscreen(frame_)) {
    player_manager_->EnterFullscreen(player_id_, frame_);
    ......
  }
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       从前面Chromium为视频标签<video>创建播放器的过程分析一文可以知道,WebMediaPlayerAndroid类的成员变量player_manager_指向的是一个RendererMediaPlayerManager对象。这个RendererMediaPlayerManager对象负责管理为当前正在处理的网页中的所有<video>标签创建的播放器实例。

       WebMediaPlayerAndroid类的成员函数enenterFullScreen首先调用上述RendererMediaPlayerManager对象的成员函数CanEnterFullscreen检查与当前正在处理的播放器实例关联的<video>标签所在的网页是否已经进入了全屏模式。如果已经进入,那么就会继续调用RendererMediaPlayerManager对象的成员函数EnterFullscreen使得当前正在处理的播放器实例进入全屏模式。

       从前面的分析可以知道,与当前正在处理的播放器实例关联的<video>标签所在的网页已经进入了全屏模式。因此,接下来RendererMediaPlayerManager类的成员函数EnterFullscreen就会被调用,如下所示:

void RendererMediaPlayerManager::EnterFullscreen(int player_id,
                                                 blink::WebFrame* frame) {
  pending_fullscreen_frame_ = frame;
  Send(new MediaPlayerHostMsg_EnterFullscreen(routing_id(), player_id));
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/renderer_media_player_manager.cc中。

       RendererMediaPlayerManager类的成员函数EnterFullscreen主要是向Browser进程发送一个类型为MediaPlayerHostMsg_EnterFullscreen的IPC消息,通知它将ID为player_id的播放器设置为全屏模式。

       Browser进程通过MediaWebContentsObserver类的成员函数OnMediaPlayerMessageReceived接收类型为MediaPlayerHostMsg_EnterFullscreen的IPC消息,如下所示:

bool MediaWebContentsObserver::OnMediaPlayerMessageReceived(
    const IPC::Message& msg,
    RenderFrameHost* render_frame_host) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(MediaWebContentsObserver, msg)
    IPC_MESSAGE_FORWARD(MediaPlayerHostMsg_EnterFullscreen,
                        GetMediaPlayerManager(render_frame_host),
                        BrowserMediaPlayerManager::OnEnterFullscreen)
    ......
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}
       这个函数定义在文件external/chromium_org/content/browser/media/media_web_contents_observer.cc中。

       MediaWebContentsObserver类的成员函数OnMediaPlayerMessageReceived首先调用成员函数GetMediaPlayerManager获得一个BrowserMediaPlayerManager对象,然后调用这个BrowserMediaPlayerManager对象的成员函数OnEnterFullscreen处理类型为MediaPlayerHostMsg_EnterFullscreen的IPC消息,如下所示:

void BrowserMediaPlayerManager::OnEnterFullscreen(int player_id) {
  ......

  if (video_view_.get()) {
    fullscreen_player_id_ = player_id;
    video_view_->OpenVideo();
    return;
  } else if (!ContentVideoView::GetInstance()) {
    ......

    video_view_.reset(new ContentVideoView(this));
    base::android::ScopedJavaLocalRef<jobject> j_content_video_view =
        video_view_->GetJavaObject(base::android::AttachCurrentThread());
    if (!j_content_video_view.is_null()) {
      fullscreen_player_id_ = player_id;
      return;
    }
  }

  .....
}
       这个函数定义在文件external/chromium_org/content/browser/media/android/browser_media_player_manager.cc中。

       当BrowserMediaPlayerManager类的成员变量video_view_的值不等于NULL时,它指向一个ContentVideoView对象。这个ContentVideoView对象描述的是一个全屏播放器窗口。

       在Browser进程中,一个网页对应有一个BrowserMediaPlayerManager对象。同一个网页中的所有<video>标签在设置为全屏模式时,使用的是同一个全屏播放器窗口。不同的网页的<video>标签在设置为全屏模式时,使用不同的全屏播放器窗口。在同一时刻,Browser进程只能存在一个全屏播放器窗口。

       BrowserMediaPlayerManager类的成员函数OnEnterFullscreen的执行逻辑如下所示:

       1. 如果当前正在处理的BrowserMediaPlayerManager对象的成员变量video_view_的值不等于NULL,也就是它指向了一个ContentVideoView对象,那么就说明Browser进程已经为当前正在处理的BrowserMediaPlayerManager对象创建过全屏播放器窗口了。这时候就会将参数player_id的值保存在另外一个成员变量fullscreen_player_id_中,表示当前正在处理的BrowserMediaPlayerManager对象所管理的全屏播放器窗口正在被ID为player_id的播放器使用。同时,会调用上述ContentVideoView对象的成员函数OpenVideo,让其全屏播放当前被设置为全屏模式的<video>标签的视频。

       2. 如果当前正在处理的BrowserMediaPlayerManager对象的成员变量video_view_的值NULL,并且Browser进程当前没有为其它网页的<video>标签全屏播放视频(这时候调用ContentVideoView类的静态成员函数GetInstance获得的返回值为NULL),那么就会为当前正在处理的BrowserMediaPlayerManager对象创建一个全屏播放窗口,也就是创建一个ContentVideoView对象(这时候调用ContentVideoView类的静态成员函数GetInstance将会返回该ContentVideoView对象),并且保存在其成员变量video_view_中。

       C++层ContentVideoView对象在创建的过程中,又会在Java层创建一个对应的ContentVideoView对象。这个Java层ContentVideoView对象如果能成功创建,那么就可以通过调用其对应的C++层ContentVideoView对象的成员函数GetJavaObject获得。这时候说明Browser进程成功创建了一个全屏播放器窗口。这个全屏播放器窗口实际上就是一个SurfaceView对象。

       一个SurfaceView对象刚创建出来的时候,它描述的窗口没有创建出来。Browser进程需要等它描述的窗口创建出来之后,才使用它全屏播放当前被设置为全屏模式的<video>标签的视频。因此,BrowserMediaPlayerManager类的成员函数OnEnterFullscreen在创建了一个SurfaceView对象之后,只做了一件简单的事情,就是记录当前是哪一个播放器进入了全屏模式,即将参数player_id的值保存在当前正在处理的BrowserMediaPlayerManager对象的成员变量fullscreen_player_id_中。

       接下来,我们就继续分析全屏播放器窗口的创建过程,以及Browser进程使用它来全屏播放当前设置为全屏模式的<video>标签的视频的过程。

       全屏播放器窗口是在C++层ContentVideoView对象的创建过程当中创建出来的,因此,我们就从C++层ContentVideoView类的构造函数开始,分析全屏播放器窗口的创建过程,如下所示:

ContentVideoView::ContentVideoView(
    BrowserMediaPlayerManager* manager)
    : manager_(manager),
      weak_factory_(this) {
  DCHECK(!g_content_video_view);
  j_content_video_view_ = CreateJavaObject();
  g_content_video_view = this;
  ......
}
       这个函数定义在文件external/chromium_org/content/browser/android/content_video_view.cc中。

       C++层ContentVideoView类的构造函数会调用另外一个成员函数CreateJavaObject在Java层创建一个对应的ContentVideoView对象,并且保存在成员变量j_content_video_view_中。

       与此同时,C++层ContentVideoView类的构造函数会将当前正在创建的ContentVideoView对象保存一个静态成员变量g_content_video_view_中,表示Browser进程当前已经存在一个全屏窗口。根据我们前面的分析,这样就会阻止其它网页全屏播放它们的<video>标签的视频。这个静态成员变量g_content_video_view_的值可以通过调用前面提到的C++层ContentVideoView类的静态成员函数GetInstance获得,如下所示:

ContentVideoView* ContentVideoView::GetInstance() {
  return g_content_video_view;
}
       这个函数定义在文件external/chromium_org/content/browser/android/content_video_view.cc中。

       回到C++层的ContentVideoView类的构造函数中,我们主要关注Java层的ContentVideoView对象的创建过程,因此接下来我们继续分析C++层的ContentVideoView类的成员函数CreateJavaObject的实现,如下所示:

JavaObjectWeakGlobalRef ContentVideoView::CreateJavaObject() {
  ContentViewCoreImpl* content_view_core = manager_->GetContentViewCore();
  JNIEnv* env = AttachCurrentThread();
  bool legacyMode = CommandLine::ForCurrentProcess()->HasSwitch(
      switches::kDisableOverlayFullscreenVideoSubtitle);
  return JavaObjectWeakGlobalRef(
      env,
      Java_ContentVideoView_createContentVideoView(
          env,
          content_view_core->GetContext().obj(),
          reinterpret_cast<intptr_t>(this),
          content_view_core->GetContentVideoViewClient().obj(),
          legacyMode).obj());
}
       这个函数定义在文件external/chromium_org/content/browser/android/content_video_view.cc中。

       C++层的ContentVideoView类的成员函数CreateJavaObject主要是通过JNI接口Java_ContentVideoView_createContentVideoView调用Java层的ContentVideoView类的静态成员函数createContentVideoView创建一个Java层的ContentVideoView对象,如下所示:

public class ContentVideoView extends FrameLayout
        implements SurfaceHolder.Callback, ViewAndroidDelegate {
    ......

    @CalledByNative
    private static ContentVideoView createContentVideoView(
            Context context, long nativeContentVideoView, ContentVideoViewClient client,
            boolean legacy) {
        ......

        ContentVideoView videoView = null;
        if (legacy) {
            videoView = new ContentVideoViewLegacy(context, nativeContentVideoView, client);
        } else {
            videoView = new ContentVideoView(context, nativeContentVideoView, client);
        }

        if (videoView.getContentVideoViewClient().onShowCustomView(videoView)) {
            return videoView;
        }
        return null;
    }

    ......
}
       这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。

       从前面的调用过程可以知道,当Browser进程设置了kDisableOverlayFullscreenVideoSubtitle(disable-overlay-fullscreen-video-subtitle)启动选项时,参数legacy的值就会等于true时,表示禁止在全屏播放器窗口上面显示播放控制控件以及字幕。这时候全屏播放器窗口通过一个ContentVideoViewLegacy对象描述。另一方面,如果参数legacy的值等于false,那么全屏播放器窗口将会通过一个ContentVideoView对象描述。

       一个ContentVideoViewLegacy对象或者一个ContentVideoView对象描述的全屏播放器窗口实际上是一个SurfaceView。这个SurfaceView只有添加到浏览器窗口之后,才能显示在屏幕中。为了将该全屏播放器窗口显示出来,ContentVideoView类的静态成员函数createContentVideoView将会通过调用前面创建的ContentVideoViewLegacy对象或者ContentVideoView对象的成员函数getContentVideoViewClient获得一个ContentVideoViewClient接口。有了这个ContentVideoViewClient接口之后,就可以调用它的成员函数onShowCustomView将刚才创建出来的全屏播放器窗口显示出来了。

       我们假设参数legacy的值等于false。这意味着全屏播放器窗口是通过一个ContentVideoView对象描述的。接下来我们就继续分析这个ContentVideoView对象的创建过程,即ContentVideoView类的构造函数的实现,以便了解它内部的SurfaceView的创建过程,如下所示:

public class ContentVideoView extends FrameLayout
        implements SurfaceHolder.Callback, ViewAndroidDelegate {
    ......

    // This view will contain the video.
    private VideoSurfaceView mVideoSurfaceView;
    ......

    protected ContentVideoView(Context context, long nativeContentVideoView,
            ContentVideoViewClient client) {
        super(context);
        ......
        mVideoSurfaceView = new VideoSurfaceView(context);
        showContentVideoView();
        ......
    }

    ......
}
       这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。

       ContentVideoView类的构造函数会创建一个VideoSurfaceView对象,并且保存在成员变量mVideoSurfaceView中。

       VideoSurfaceView类是从SurfaceView类继承下来的,如下所示:

public class ContentVideoView extends FrameLayout
        implements SurfaceHolder.Callback, ViewAndroidDelegate {
    ......

    private class VideoSurfaceView extends SurfaceView {
        ......
    }

    ......
}
       这个类定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。

       ContentVideoView类的构造函数在创建了一个VideoSurfaceView对象,接下来会调用另外一个成员函数showContentVideoView将它作为当前正在创建的ContentVideoView对象的子View,如下所示:

public class ContentVideoView extends FrameLayout
        implements SurfaceHolder.Callback, ViewAndroidDelegate {
    ......

    protected void showContentVideoView() {
        mVideoSurfaceView.getHolder().addCallback(this);
        this.addView(mVideoSurfaceView, new FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT,
                Gravity.CENTER));

        ......
    }

    ......
}
       这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。

       ContentVideoView类的成员函数showContentVideoView将前面创建的VideoSurfaceView对象作为当前正在创建的ContentVideoView对象的子View之外,还会使用后者监听前者的surfaceCreated事件,也就是它描述的窗口创建完成事件。

       当前正在创建的ContentVideoView对象实现了SurfaceHolder.Callback接口,一旦前面创建的VideoSurfaceView对象发出surfaceCreated事件通知,那么前者的成员函数surfaceCreated就会被调用,如下所示:

public class ContentVideoView extends FrameLayout
        implements SurfaceHolder.Callback, ViewAndroidDelegate {
    ......

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mSurfaceHolder = holder;
        openVideo();
    }

    ......
}
       这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。

       ContentVideoView类的成员函数surfaceCreated会将参数holder描述的一个SurfaceHolder对象保存在成员变量mSurfaceHolder中。后面可以通过这个SurfaceHolder对象获得前面创建的VideoSurfaceView对象内部的一个Surface。这个Surface将会用来接收MediaPlayer的解码输出。

       接下来,ContentVideoView类的成员函数surfaceCreated就会调用另外一个成员函数openVideo将上述Surface设置为MediaPlayer的解码输出,如下所示:

public class ContentVideoView extends FrameLayout
        implements SurfaceHolder.Callback, ViewAndroidDelegate {
    ......

    @CalledByNative
    protected void openVideo() {
        if (mSurfaceHolder != null) {
            mCurrentState = STATE_IDLE;
            if (mNativeContentVideoView != 0) {
                nativeRequestMediaMetadata(mNativeContentVideoView);
                nativeSetSurface(mNativeContentVideoView,
                        mSurfaceHolder.getSurface());
            }
        }
    }

    ......
}
       这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。

       ContentVideoView类的成员变量mNativeContentVideoView描述的是前面在C++层创建的一个ContentVideoView对象。ContentVideoView类的成员函数openVideo主要是做两件事情:

       1. 获取要播放的视频的元数据,以便用来初始化全屏播放器窗口。

       2. 将前面创建的VideoSurfaceView对象内部维护的Surface设置为MediaPlayer的解码输出。

       这两件事情分别是通过调用成员函数nativeRequestMediaMetadata和nativeSetSurface完成的。当它们完成之后,<video>标签的视频可以开始全屏播放了。接下来我们继续分析它们的实现。

       ContentVideoView类的成员函数nativeRequestMediaMetadata是一个JNI方法,它由C++层的函数Java_com_android_org_chromium_content_browser_ContentVideoView_nativeRequestMediaMetadata实现,如下所示:

void
    Java_com_android_org_chromium_content_browser_ContentVideoView_nativeRequestMediaMetadata(JNIEnv*
    env,
    jobject jcaller,
    jlong nativeContentVideoView) {
  ContentVideoView* native =
      reinterpret_cast<ContentVideoView*>(nativeContentVideoView);
  CHECK_NATIVE_PTR(env, jcaller, native, "RequestMediaMetadata");
  return native->RequestMediaMetadata(env, jcaller);
}
       这个函数定义在文件out/target/product/generic/obj/GYP/shared_intermediates/content/jni/ContentVideoView_jni.h中。 

       从前面的调用过程可以知道,参数nativeContentVideoView描述的是一个C++层ContentVideoView对象,函数Java_com_android_org_chromium_content_browser_ContentVideoView_nativeRequestMediaMetadata调用它的成员函数RequestMediaMetadata获取要播放的视频的元数据,如下所示:

void ContentVideoView::RequestMediaMetadata(JNIEnv* env, jobject obj) {
  base::MessageLoop::current()->PostTask(
      FROM_HERE,
      base::Bind(&ContentVideoView::UpdateMediaMetadata,
                 weak_factory_.GetWeakPtr()));
}
       这个函数定义在文件external/chromium_org/content/browser/android/content_video_view.cc中。

       ContentVideoView类的成员函数RequestMediaMetadata向当前线程的消息队列发送一个Task。这个Task绑定了ContentVideoView类的另外一个成员函数UpdateMediaMetadata。这意味着接下来ContentVideoView类的成员函数UpdateMediaMetadata会在当前线程中调用,用来获取要播放的视频的元数据。

       ContentVideoView类的成员函数UpdateMediaMetadata的实现如下所示:

void ContentVideoView::UpdateMediaMetadata() {
  JNIEnv* env = AttachCurrentThread();
  ScopedJavaLocalRef<jobject> content_video_view = GetJavaObject(env);
  if (content_video_view.is_null())
    return;

  media::MediaPlayerAndroid* player = manager_->GetFullscreenPlayer();
  if (player && player->IsPlayerReady()) {
    Java_ContentVideoView_onUpdateMediaMetadata(
        env, content_video_view.obj(), player->GetVideoWidth(),
        player->GetVideoHeight(),
        static_cast<int>(player->GetDuration().InMilliseconds()),
        player->CanPause(),player->CanSeekForward(), player->CanSeekBackward());
  }
}
       这个函数定义在文件external/chromium_org/content/browser/android/content_video_view.cc中。

       ContentVideoView类的成员函数UpdateMediaMetadata首先调用另外一个成员函数GetJavaObject获得与当前正在处理的C++层ContentVideoView对象对应的Java层ContentVideoView对象。

       ContentVideoView类的成员函数UpdateMediaMetadata接下来又通过调用成员变量manager_指向的一个BrowserMediaPlayerManager对象的成员函数GetFullscreenPlayer获得当前处于全屏模式的播放器,如下所示:

MediaPlayerAndroid* BrowserMediaPlayerManager::GetFullscreenPlayer() {
  return GetPlayer(fullscreen_player_id_);
}
       这个函数定义在文件external/chromium_org/content/browser/media/android/browser_media_player_manager.cc中。

       从前面的分析可以知道,BrowserMediaPlayerManager类的成员变量fullscreen_player_id_记录了当前处于全屏模式的播放器的ID。有了这个ID之后,就可以调用另外一个成员函数GetPlayer获得一个对应的MediaPlayerBridge对象。这个MediaPlayerBridge对象描述的就是当前处于全屏模式的播放器。

       回到ContentVideoView类的成员函数UpdateMediaMetadata中,它获得的全屏播放器之后,实际上就是<video>标签进入全屏模式之前所使用的那个播放器。这意味着<video>标签在全屏和非全屏模式下,使用的是同一个播放器,区别只在于播放器在两种模式下使用的UI不一样。这个播放器之前已经获得了要播放的视频的元数据,并且保存在了内部,因此这里就不需要从网络上重新获取。

       有了要播放的视频的元数据之后,ContentVideoView类的成员函数UpdateMediaMetadata就通过JNI接口Java_ContentVideoView_onUpdateMediaMetadata调用前面获得的Java层ContentVideoView对象的成员函数onUpdateMediaMetadata,让其初始化全屏播放器窗口,如下所示:

public class ContentVideoView extends FrameLayout
        implements SurfaceHolder.Callback, ViewAndroidDelegate {
    ......

    @CalledByNative
    protected void onUpdateMediaMetadata(
            int videoWidth,
            int videoHeight,
            int duration,
            boolean canPause,
            boolean canSeekBack,
            boolean canSeekForward) {
        mDuration = duration;
        .....
        onVideoSizeChanged(videoWidth, videoHeight);
    }

    ......
}

      这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。 

      参数duration描述的就是要播放的视频的总时长,它会被记录在ContentVideoView类的成员变量mDuration中。

      另外两个参数videoWidth和videoHeight描述的是要播放的视频的宽和高,ContentVideoView类的成员函数onUpdateMediaMetadata将它们传递给另外一个成员函数onVideoSizeChanged,用来初始化全屏播放器窗口,如下所示:

public class ContentVideoView extends FrameLayout
        implements SurfaceHolder.Callback, ViewAndroidDelegate {
    ......

    @CalledByNative
    private void onVideoSizeChanged(int width, int height) {
        mVideoWidth = width;
        mVideoHeight = height;
        // This will trigger the SurfaceView.onMeasure() call.
        mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight);
    }

    ......
}
       这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。 

       ContentVideoView类的成员函数onVideoSizeChanged将视频的宽度和高度分别记录在成员变量mVideoWidth和mVideoHeight中,然后再将它们设置为用来显示视频画面的VideoSurfaceView的大小。

       这一步执行完成后,全屏播放器窗口就初始化完毕。回到前面分析的ContentVideoView类的成员函数openVideo中,它接下来将上述VideoSurfaceView内部使用的Surface设置为MediaPlayer的解码输出。这是通过调用ContentVideoView类的成员函数nativeSetSurface实现的。

       ContentVideoView类的成员函数nativeSetSurface是一个JNI方法,它由C++层的函数Java_com_android_org_chromium_content_browser_ContentVideoView_nativeSetSurface实现,如下所示:

void
    Java_com_android_org_chromium_content_browser_ContentVideoView_nativeSetSurface(JNIEnv*
    env,
    jobject jcaller,
    jlong nativeContentVideoView,
    jobject surface) {
  ContentVideoView* native =
      reinterpret_cast<ContentVideoView*>(nativeContentVideoView);
  CHECK_NATIVE_PTR(env, jcaller, native, "SetSurface");
  return native->SetSurface(env, jcaller, surface);
}
       这个函数定义在文件out/target/product/generic/obj/GYP/shared_intermediates/content/jni/ContentVideoView_jni.h中。

       参数nativeContentVideoView描述的是一个C++层ContentVideoView对象,另外一个参数surface描述的就是前面提到的用来显示视频画面的VideoSurfaceView内部使用的Surface。

       函数Java_com_android_org_chromium_content_browser_ContentVideoView_nativeSetSurface参数nativeContentVideoView描述的C++层ContentVideoView对象的成员函数SetSurface重新给当前处于全屏模式的播放器设置一个Surface,如下所示:

void ContentVideoView::SetSurface(JNIEnv* env, jobject obj,
                                  jobject surface) {
  manager_->SetVideoSurface(
      gfx::ScopedJavaSurface::AcquireExternalSurface(surface));
}
       这个函数定义在文件external/chromium_org/content/browser/android/content_video_view.cc中。

       ContentVideoView类的成员函数SetSurface调用成员变量manager_指向的一个BrowserMediaPlayerManager对象的成员函数SetVideoSurface重新给当前处于全屏模式的播放器设置一个Surface,如下所示:

void BrowserMediaPlayerManager::SetVideoSurface(
    gfx::ScopedJavaSurface surface) {
  MediaPlayerAndroid* player = GetFullscreenPlayer();
  ......

  player->SetVideoSurface(surface.Pass());
  
  ......
}
       这个函数定义在文件external/chromium_org/content/browser/media/android/browser_media_player_manager.cc中。

       BrowserMediaPlayerManager类的成员函数SetVideoSurface首先调用另外一个成员函数GetFullscreenPlayer获得一个MediaPlayerBridge对象。这个MediaPlayerBridge对象被一个MediaPlayerAndroid指针引用(MediaPlayerAndroid是MediaPlayerBridge的父类),它描述的便是当前处于全屏模式的播放器。

       获得了当前处于全屏模式的播放器之后,就可以调用它的成员函数SetVideoSurface将参数surface描述的一个Surface设置为它的解码输出。这个设置过程,也就是MediaPlayerBridge类的成员函数SetVideoSurface的实现,可以参考前面Chromium为视频标签<video>渲染视频画面的过程分析一文。它实际上就是给在Java层创建的一个MediaPlayer重新设置一个Surface作为解码输出。这个Surface是由一个VideoSurfaceView提供的。这个VideoSurfaceView实际上就是一个SurfaceView。SurfaceView会自己将MediaPlayer的解码输出交给系统渲染。因此就不再需要Chromium参与这个渲染过程。

       回忆<video>标签在进入全屏模式之前,Chromium会为它会创建一个纹理,然后用这个纹理创建一个SurfaceTexture。这个SurfaceTexture最终又会封装在一个Surface中。这个Surface就设置为MediaPlayer的解码输出。这时候MediaPlayer的解码输出需要由Chromium来处理,也就是渲染在浏览器窗口中。当浏览器窗口被系统渲染在屏幕上时,我们就可以看到MediaPlayer输出的视频画面了。这个过程可以参考前面Chromium为视频标签<video>渲染视频画面的过程分析一文。

       这样,我们就分析完成<video>标签全屏播放视频的过程了。从这个分析过程我们就可以知道,<video>标签在全屏模式和非全屏模式下使用的都是相同的MediaPlayer,区别只在于这个MediaPlayer将视频画面渲染在不同的Surface之上。因此,<video>标签可以在全屏模式和非全屏模式之间进行无缝的播放切换。

       至此,我们也分析完成了video标签在Chromium中的实现。视频在互联网中将扮演着越来越重要的角色。以前浏览器主要是通过Flash插件来支持视频播放。Flash插件有着臭名昭著的安全问题和Crash问题。因此,随着HTML5的出现,浏览器逐步转向使用<video>标签来支持视频播放。这不仅在产品上带来更好的体验(无需用户安装插件),而且在技术上也更加稳定。基于上述理由,理解<video>标签的实现原理就显得尤为重要。重新学习可以参考前面Chromium视频标签<video>简要介绍和学习计划一文。更多信息,可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/8/29 1:00:17 原文链接
阅读:38427 评论:15 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>