码迷,mamicode.com
首页 > 移动开发 > 详细

Chromium on Android: Android L平台上WebView的变化及其对浏览器厂商的影响分析

时间:2014-11-05 00:27:02      阅读:362      评论:0      收藏:0      [点我收藏+]

标签:chromium webview   android l   浏览器   移动浏览器   

摘要:Android L平台在图形渲染方面有一项重要的改进,它引入了一个专门的线程用于执行渲染工作,UI线程负责生成的显示列表(DisplayList),渲染线程负责重放(playback)这个显示列表绘制最终的内容,因此Chromium WebView在图形栈的实现方面也作了相应的调整,以支持Android L系统上新的渲染线程模型。本文将深度分析Chromium WebView的渲染流水线是如何无缝整合到Android L系统的渲染模型中,以及对目前市场主流浏览器厂商将会产生什么样影响等问题。此外,本文对Chrome on Android浏览器的渲染模型和流水线工作也做了相应的介绍。

最新AndroidL平台的新特性 –Render线程

在Android L产品发布会上,Google向外界宣布了诸多AndroidL系统的新特性,引起了开发者的广泛关注。除了大力倡导的Material设计理念之外,还有一个非常值得关注的亮点就是,AndroidL这次有了独立的渲染线程。

长期以来,Android平台一直被人诟病的就是系统流畅度问题,用过Android 的人总感觉应用程序在来回切换或者执行动画时,总是感觉到有些卡顿或者在处理触摸事件时动画会停止,就是达不到iOS系统的流畅度。从直觉上来说,Davilk虚拟机似乎是问题的罪魁祸首,因为GC总是不可预测,随时都有可能发生,令人难以捉摸,而且一旦GC是以阻塞UI线程的方式运行的,但现实可能是Davilk的GC背了黑锅。问题的根源可能是阻塞式的渲染模型导致,UI线程实在太忙了,要做的事情太多了,例如,既要处理触摸事件,遍历View层次结构给每个需要更新的View逐一派发onDraw事件,还要与GPU设备打交道,执行GPU命令渲染。Android团队正在着手解决这个问题,提高系统UI的流畅度。

自Android 3.0开始就启用硬件加速渲染机制,绘制View分为更新/记录显示列表(DisplayList)和重放(playback)显示列表两个步骤,解决了软件绘制方式性能低下的问题,然而仍没有将界面的渲染工作从UI线程中解放出来。AndroidL系统为此引入专门的渲染线程,将重放显示列表的任务从UI线程中独立出来,例如动画可以以off-main-thread的方式运行,只要动画的时间线设置好了,渲染线程负责更新每一帧的动画,而UI线程可以腾出手去处理更加紧急的事情。

大道至简,大多设计原理念都是相通的。类似的渲染线程模型其实在Chromium图形栈中早就采用了, Chrome浏览器中,就有一个专门的GPU进程(或线程)和GPU设备进行交互,保证图形的渲染不会阻塞Blink线程,也不会阻塞浏览器线程,而且为了保证Web页面中对用户输入事件的及时响应,Chromium还会将用户输入事件首先派发给页面的Compositing线程,并由Compositing线程决定如何处理这个事件,例如当快速滑动页面时,Compositing线程会先于Blink线程将缓存好的Tile绘制出来,然后再请求Blink线程根据滑动距离去绘制新的曝光区域的Tile内容(注:为了与Android系统渲染线程做出区分,这里提到的Blink线程指的是ChromiumRenderer线程,在Web世界里,Blink线程类同于UI线程,负责页面的布局和JavaScript代码的执行,而Compositing线程负责将Web页面中多个层的合成,从而使页面的布局和层合成能够并行)。

Android L系统中UI线程根据View层次结构和更新区域,向需要重绘的View发送onDraw事件请求更新显示列表,然后再由渲染线程上重新绘制更新过的显示列表。如下图所示:

bubuko.com,布布扣

注:因为目前Android L只提供了开发者预览版的Image文件,尚无法从AOSP中获取完整的源代码,貌似还有些GPL许可证问题正在解决中。ThreadedRenderer应该是AndroidL平台引入新的线程化渲染器实现。

Chromium WebView的变化

AndroidL系统Chromium WebView开始支持WebGL,WebAudio以及WebRTC等新潮的HTML5特性,使Android系统上WebView组件更接近Chromium浏览器的HTML5能力,同时也表明,Android团队将会花大力气将WebView打造成为一个功能完善,具有相当竞争力的平台HTML5运行环境。实际上,iOS系统WebView已经走在Android系统前面,尤其是iOS8系统一系列关于WebView的增强,已经和Safari浏览器内核一致了,例如提供一个多进程版本的WKWebView,增加对WebGL支持, 以及使用Nitro JIT JavaScript引擎,无论从性能还是功能,都上了一个台阶了。AndroidWebView这方面还需要继续努力。

自从Android4.4 (KitKat)系统开始全面采用基于Chromium内核的WebView实现(基于Chromium M30)以来,Chromium WebView在实现上也一直跟随着Android系统在不断演进,Android L系统的WebView实现至少带来了两个值得特别关注的变化。 

变化一:页面渲染的工作流程和线程模型

Android 4.4系统上的WebView在功能和性能上都不完善,例如不支持WebGL和WebRTC以及一些轻量级的HTML5API,甚至连Canvas2D的硬件加速都不支持,但作为第一个基于Chromium内核的WebView,它需要重点解决的问题是与旧版本的WebView的兼容性问题,尤其是需要同时软件渲染模式和硬件渲染模式。

WebView的软件渲染模式就是将页面的内容渲染到Bitmap中,以供客户端代码所用,而且软件渲染模式要求必须支持的,甚至在硬件渲染模式下,软件渲染方式也必须随时随地的能够被使用,以往有很多基于WebView的应用会调用WebView.draw操作实现网页“快照”操作。关于ChromiumWebView是如何实现软件渲染模式,请参阅Chromium onAndroid: 分析ChromiumWebView的软件渲染方式一文。

硬件渲染模式中,最关键的问题是如何实现Chromium系统和Android系统两者的硬件加速机制的无缝整合,将Chromium图形栈的操作串到Android系统绘制View的渲染路径上。本文接下来的内容主要探讨硬件渲染模式。

Chrome on Android的渲染流水线

为了以比较的方式说明WebView特定的渲染模型,先来看看Chrome on Android是如何渲染WebGL网页的。对于移动Chrome浏览器来说,它是多进程架构的,这个对渲染模型也存在一定的影响,但不是最重要,页面内容的更新是由系统VSYNC信号触发,一旦Browser进程收到系统的VSYNC信号,整个Chromium图形栈的流水线工作立即会被启动,首先它会向Renderer进程发送IPC消息请求开始生成新的一帧,Renderer进程收到消息之后,主线程上开始BeginMainFrame的逻辑,执行requestAnimationFrame更新WebGL的渲染层,如果其他渲染层因DOM树变化或者CSS动画发生了改变,那么主线程还会执行必要的重新排版和渲染层的绘制任务(注:Chromium启用了ImplPainting特性后主线程上绘制任务指的是通过记录绘制命令生成SkPicture,再由Rasterization线程运行这些绘制命令生成最终的图像,最后上传到GPU的texture中),更新每个渲染层的绘制信息(如坐标,clip区域,变换矩阵等),当所有渲染层完成信息更新后,主线程再会将这些层的信息同步给Renderer进程中Compositing线程(注:Chromium采用了线程化的合成技术,即页面渲染层的合成操作在一个专门的线程中执行,而不是Renderer进程的主线程)。Compositing线程根据由主线程提交的状态信息将所有渲染层按照z-order次序依次叠加,绘制新的一帧(DrawFrame),将其打包到一个CompositorFrame结构体中,再通过IPC发送给Browser进程,请求Browser进程端的合成器将CompositorFrame内容与其他浏览器元素(如果有)进行最终内容的合成(Composition),最后GPU线程执行SwapBuffer操作将内容显示到SurfaceView上。至此,新的一帧内容渲染完毕,Browser进程收到VSYNC信号后又将开始下一帧内容。

bubuko.com,布布扣

Chromium WebView渲染流水线的演进

而对于Chromium WebView来说,首先它是单进程模型的,至少在使用CommandBuffer时可以省去不必要的跨进程IPC开销,尽管IPC开销并没有想象中那么大(根据性能评测,大概占了不超过5%的开销)。更为重要的是,WebView必须是Android窗口中一个普通的View,与窗口其他View共享同一个Android Surface绘制表面,因此,

  1. WebView在UI线程上响应onDraw事件,启动页面渲染的流水线工作,将新的帧内容通过onDraw绘制到Canvas上;
  2. WebView使用同步合成器(SynchronousCompositor)来合成页面内容,“同步”指的是运行在UI线程上,合成操作是由UI线程的onDraw事件触发的,而不是VSYNC触发的。从线程化合成器的角度看,UI线程实际上也是Compositing线程;
  3. WebView提供了一个硬件加速版本的回调函数DrawGL(即AwContents::DrawGL),将Chromium的渲染流水线工作串到AndroidView的渲染路径上。在使用硬件加速的情况下,WebView.onDraw接受的参数Canvas是一个对AndroidSDK不可见的android.view.HardwareCanvas对象,HardwareCanvas允许客户端提供自己的GL回调函数,Android  View系统在重放(playback)显示列表绘制View层次时会调用这个回调函数。(注:在AndroidFramework内部实现中,当开始绘制HardwareCanvas时,当前GL上下文会绑定HardwareCanvas的framebuffer对象,所以GL回调函数中所有的GL命令实际上在操作HardwareCanvas的framebuffer对象)。

Android 4.4系统中,在绘制View层次时,显示列表的更新记录以及重放都发生在UI线程上,相应地,WebView.onDraw事件的处理中,页面的层合成,以及通过DrawGL函数将合成的内容绘制到HardwareCanvas,都是在UI线程执行的。当WebView收到invalidate消息后,ViewRoot会遍历整个View树决定向哪些View派送onDraw事件,WebView响应onDraw事件时,会直接请求HardwareCanvas执行DrawGL回调函数,DrawGL再请求同步合成器以硬件加速方式合成新的一帧内容,并将这个新的内容通过GL命令绘制到HardwareCanvas上。这里没有特别提到Blink线程,原因是Blink线程相对来说还是比较独立的,在Blink线程启动后,会逐步执行DOM解析和排版,JS代码的执行,以及渲染层的状态信息计算等工作,这些工作运行完毕之后,Blink线程会通知UI线程(即Compositing线程)将WebView置为失效(invalidate)状态,继而触发onDraw事件。

还需要进一步说明的是,Android 4.4系统的WebView是没有单独的GPU线程执行GL命令的,所有的一切都发生在UI线程上,导致很难再提供WebGL的支持了,就算是花些力气支持了WebGL,估计也是吃力不讨好,因为性能是个大问题,在UI线程上执行WebGL还可能会导致UI根本不会有响应。

Chromium WebView的演进速度还是相当的快。从最新发布的Android L开发者预览版来看,在页面渲染的流水线和线程模型都有不少改进:

第一,为了吻合Android L引入的渲染线程(注:为了和Android系统的渲染线程作区分,Chromium的渲染线程称为Blink线程),将Chromium渲染流程无缝整合到AndroidL系统中,WebView新的架构中引入了两级合成器

  • Child Compositor:页面内容的合成器,就是前面提到的同步合成器,运行在UI线程上,是一个线程化的合成器,其中Blink线程负责生成渲染层的状态信息,而UI线程负责合成渲染层。每收到onDraw事件后,WebView会使用ChildCompositor合成新的一帧,并将内容打包到CompositorFrame结构体中,稍后Android渲染线程会访问这个结构体。ChildCompositor由android_webview::BrowserViewRenderer创建;
  • Parent Compositor:类似于Chrome on Android中Browser进程端的合成器,是一个单线程版本的合成器,其作用是在Android系统的渲染线程,注意不是UI线程,通过DrawGL回调函数将已经由ChildCompositor合成好的CompositorFrame绘制到HardwareCanvas上。ParentCompositor由android_webview::HardwareRender创建;

与Android 4.4系统不同的是,在Android L系统上WebView处理onDraw事件逻辑上可以分解为两个步骤:

  • 第一步,在UI线程上调用Child Compositor合成操作生成新的CompositorFrame,再向Android系统请求执行DrawGL回调函数,但此时DrawGL并不会立即执行。UI线程负责为发生invalidate操作的View生成显示列表,再将这个显示列表同步给ThreadedRenderer;
  • 第二步,Android View系统在渲染线程上将显示列表提交给GPU驱动,绘制View层次的内容,在绘制显示列表的过程中,DrawGL函数将会被调用,通过ParentCompositor在非UI线程上将合成的页面内容绘制到HardwareCanvas上。

注:关于DrawGL回调函数是如何分派到Android渲染线程上执行的,由于AndroidL的源代码尚未完全公开,还不能100%确定上述描述是否完全正确。从已有掌握的信息分析来看,UI线程会向HardwareCanvas发起DrawGL调用请求,此时可能不会立即执行DrawGL,而是作为drawOp先保存在HardwareCanvas中,当渲染线程开始重放HardwareCanvas显示列表时,再调用DrawGL。

上述两个步骤与Android L采用的渲染线程模型完全吻合,这也是要引入两级合成器的原因。然而,WebView这个新型的渲染模型与Android4.4系统是不兼容的,也就是从AndroidL编译出来的libwebviewchromium.so是不能够在Android 4.4系统运行的。

变化二:WebGL和硬件加速Canvas 2D的支持

以WebGL为例,WebView为渲染WebGL还创建 了专门的GPU线程,WebGL程序中所有的GL命令都发生在这个线程上。渲染WebGL的egl上下文就是这个线程上创建的,然而,这个egl上下文是一个独立的上下文,不与WebView进程内任何其他上下文共享资源,即与AndroidView系统的上下文不在同一个sharegroup。关于Chromium中上下文和sharegroup相关技术细节,可参阅ChromiumGraphics: 3D上下文及其虚拟化一文。Android View系统的上下文指的是调用DrawGL绘制HardwareCanvas时的上下文,因此,要将WebGL的内容绘制到HardwareCanvas上,需要解决将WebGL上下文和AndroidView系统上下文之间的texture共享和同步问题。首先,这两个上下文都属于同一进程,因此可以利用EGLImage进行跨线程的texture共享,再次,Chromium图形栈提供的信箱texture(mailboxtexture)机制有助于解决texture共享。Mailboxtexture机制为每个生成的texture生成一个64字节的唯一名字,并建立名字到textureid的全局映射表,所以不管在哪个上下文创建的texture,都可以通过mailbox的名字检索到对应的textureid,mailbox机制的好处就是在一个支持多上下文的环境提供了textureid的全局别名系统,换句话说,有了这个别名系统,所有上下文看起来都属于“同一个sharegroup”。

那么,渲染WebGL是如何使用EGLImage和mailboxtexture机制实现资源共享的呢?Chromium中WebGL的内容是通过离屏的framebuffer对象渲染到texture中的,在GPU线程的WebGL上下文创建这个texture时将其绑定一个唯一的mailbox名字,还会调用eglCreateImageKHR为这个textureid创建一个可以被其他上下文共享的EGLImage,当AndroidView系统的渲染线程在另一个上下文中执行DrawGL函数,通过mailbox名字可确定GPU线程中的EGLImage,将其绑定到该上下文的一个texture便可以直接使用WebGL上下文的texture了,至此解决了texture共享问题。这里还有一个问题需要交代一个,在多个上下文中共享texture时,需要处理好生产者和消费者之间的同步问题,即执行DrawGL函数绘制新的一帧时,要确保WebGL上下文的所有GL命令都已经执行完毕。Chromium同样提供了一套同步点(syncpoint)机制,来保证多个上下文(Chromium里也称CommandBuffer)之间GL命令的执行次序,在上下文A中插入一个同步点,然后在上下文B中等待这个同步点,可以保证上下文B中的GL命令必须等待上下文同步点A之前所有的GL命令全部执行完毕之后,才能开始执行。所以,当WebGL将新的一帧内容生成完毕后,会插入一个同步点,而DrawGL时AndroidView系统上下文会等待这个同步点,只有当前WebGL所有GL命令都提交给GPU驱动之后,才开始使用WebGL的内容,从而保证了WebGL内容和DrawGL的同步。关于同步点机制的细节,请参阅ChromiumGraphics: GPU客户端之间同步机制的原理和实现分析一文。

变化三:AndroidWebViewShell已经可以支持硬件渲染模式

WebViewShell构建在核心类AwContents之上,AwContents在功能基本上可以等同于android.webkit.WebView类。ChromiumM30的代码库中只能编译出仅支持软件渲染模式的WebViewShell,从ChromiumM38开始,WebViewShell也支持硬件加速渲染了,主要改进的地方是,使用GLSurfaceView作为最后的渲染目的地,如上所述,DrawGL回调函数可以在非UI线程上调用,而GLSurfaceView正好也支持在单独的线程中被渲染。

对浏览器厂商的影响分析

ChromiumWebView仍在继续完善和演进之中,那么国内移动浏览器厂商对待Chromium内核将会是什么态度?对目前浏览器的结构有哪些影响呢?

目前国内排名前三的浏览器应用,QQ浏览器,UC浏览器和百度浏览器,其内核都是由AndroidWebKit发展而来,在网页加速速度,首屏渲染,减少网络流量和云端加速等方面做了不少工作。然而,自从Android4.4之后,AndroidWebView切换成Chromium内核,这就意味着AndroidWebKit将不再维护了,而且在HTML5特性支持和性能方面,AndroidWebKit本来就与Chromium内核存在不少差距,随着HTML5标准的日臻成熟,这个短板将显得越来越突出。

移动浏览器使用Chromium内核的两种方式

方式一:直接基于ContentAPI层,和Chromeon Android的做法相似

通过实现Content API层所需的一些Client或者Delegate接口,可将浏览器自定义的逻辑代码嵌入到Chromium内核的启动,网络请求,事件处理以及页面加载和渲染等流程中,浏览器UI部分可使用Android系统的UIToolkit完成。这种方式对于目前基于AndroidWebView兼容接口实现的浏览器来时,已有代码的迁移是个不小的问题,因此ContentAPI层与WebView接口完全是两套不一样的东西。另外一个问题是,这种方式完全是走硬件渲染模式,而且使用SurfaceView来显示页面内容的,SurfaceView是一个比较特殊的View部件,与其他View部件不同的是,SurfaceView有自己独立的后端Surface,不与Android窗口内其他View部件共享同一个Surface,也就是为什么SurfaceView能够在非UI线程渲染的原因,但是也正因为如此,客户端代码不能对SurfaceView执行动画,比如alpha值变化,缩放等,也能对两个或以上的SurfaceView进行层叠,否则会出现黑屏现象。

SurfaceView这个局限性带来一个难以回避的问题,如何实现对网页进行快照功能?这个功能在切换Tab标签页时特别有用,需要显示Tab标签页的缩略图。一种可能的方法就是通过glReadPixels将网页内容从GPU内存中读取出来,然而这种方法实际上是不可行,原因就是glReadPixels操作是个非常耗时的操作。Chrome解决这个问题比较彻底,Chrome浏览器整个UI,包括网页显示区域,都是通过GPU线程绘制在SurfaceView上的,它使用了ubercompositor技术将页面内容、浏览器UI元素都是Compositor中一个渲染层,ubercompositor合成任务就是遍历渲染层次,将所有层都绘制出来,特别是Chrome的Tab切换,非常流畅,这是因为每个Tab显示的内容在Compositor看来都是一个渲染层,GPU会每个层分配一个texture,切换tab时,只要通过GL命令直接对这个texture进行矩阵变换,就可以达到想要的动画效果,这个操作是非常快。但实现起来要花费不少力气,Chrome也曾考虑使用TextureView替代SurfaceView,但由于TextureView需要消耗更多GPU资源,最后还是选择了SurfaceView。

这种方法的好处就是能够获得更好的性能提升, 在GPU线程上操作SurfaceView避免了因阻塞UI线程而带来的用户响应延迟问题。

方式二:直接使用Chromium WebView 即在AwContents之上封装一层与WebView兼容的接口

这种方式更直接,现有主要的框架性代码也不存在迁移问题,基本上可以直接用了。因为ChromiuimWebView设计之初就考虑到网页快照此类的功能需求,所以软件渲染模式是必须支持,即使当前使用的硬件渲染模式,WebView也能随时随地通过WebView.onDraw方法将页面内容渲染到一个BitmapCanvas上。当然,这种方法主要是性能上逊色不少,因为渲染操作是发生在UI线程上的,也就是意味着如果UI线程上有频繁地用户输入,结果很可能用户的事件输入得到实时的反馈,伤害了用户体验。当然,WebView在渲染基本的页面,快速滑动和缩放时,性能方面已经难以出现明显的用户可感知的区别,所以从这个角度看,是否需要获得更高的性能提升取决于浏览器产品自身定位和实际需求,片面追求FPS的提高有可能得不偿失。如上提到,最新ChromiumWebView已经使用了线程化的合成技术,目前已成型,当然还有不少优化空间。如果采用这种方式,渲染优化方面可以挖掘的地方还是不少的。

升级到Chromium内核的考虑

尽管Chromium内核具有更好的HTML5特性和性能表现,技术上也可行,然而事实的情况是,目前三大浏览器厂商都没有切换到Chromium内核架构上,究其原因,我认为有三点:

第一,Binary大小问题。Chromium内核实际上要把整个Blink和Content层的代码都编译在一块,最后生成的WebViewShellAPK文件大小有19M,这就意味着一旦使用Chromium内核,浏览器单款应用的size将会超过20M,甚至达到30M,而目前大概就在10M左右。对于一款浏览器产品,应用程序的大小是用户直接可见,apk文件大意味着要花更多的时间去下载,安装后会占用更多的内部存储空间,贸然增加多达十几兆的apk,在激烈的市场竞争中不得不说,市场有风险,升级需谨慎。估计三大浏览器厂商谁也不愿意做第一个吃螃蟹的人,否则弄得不好,辛辛苦苦赢来的市场占有率可能会拱手让给竞争对手了。

第二,资源消耗问题。目前对Chromium内核一个普遍的观点是,内存资源消耗较大。ChromiumUpstream也非常重视这个问题,已有不少这方面的优化正在进行中。ChromiumWebView也增加对内存资源消耗的控制了,包括GPU资源,通过调整内存管理策略,做到能少用则少用,能不用则不用,能回收尽量回收,另外,如果发现Android系统运行在低内存的情形下,WebView会主动清理一些出于性能考虑而缓存的资源,尽可能地给系统释放更多的资源。这个问题比起用户直接可见的apk文件大小,显得并不是那么关键了。

第三,Android设备支持度。大多国内浏览器厂商的产品定位是通过浏览器这个平台尽可能将自己的云服务推出去,例如百度的搜索,腾讯的QQ空间等。所以,考虑到必须适配各种Android设备,而Chromium内核明确表示支持Android4.0之后的Android设备,尽管有数据显示目前Android4.0设备市场占有率已经达到80%以上了,但剩下的20%仍然是不可小觑的份额。

然而,即使Chromium内核存在上述问题,冒然升级到Chromium内核都也可能会给带来比较大的风险,但几乎每个浏览器厂商对Chromium技术给予了高度的关注和实践,甚至会从Chromium代码块中抽取某个模块单元为己所用,比如net模块,或者借鉴Chromium多线程的渲染模型,提高网页的渲染性能,以及一些HTML5API的实现等。目前市面上小米MIUI中的浏览器和猎豹浏览器是目前市场上为数不多的基于Chromium内核的。

Chromium内核确实在很多方面保持业界领先水平,自2013年4月份宣布从WebKit分支出Blink项目以来,明显感觉到Blink在Web技术改进和创新这方面加快了节奏,除了持续的性能优化工作之外,特别是针对移动设备的优化,还引入了不少新的Web技术,比如制定和实现ServiceWorker规范以更好的解决离线Web应用的问题,Blink-in-JavaScript允许用JavaScript开发新的DOMAPI,还有Oilpan自动垃圾回收框架试图从架构上解决DOM中难以定位的内存泄露问题,以及PromiseAPI的实现等等。

所以,从这个角度看,Chromium内核是浏览器内核升级的一个方向,但如果浏览器产品只是定位在满足大多数用户最基本的使用场景,例如看看小说,上上网等,确实没有必要将一个庞然大物 内嵌到产品中,但如果存有将浏览器打造成为Android系统上Web应用的运行平台这样的野心,升级到Chromium内核只是一个时间问题,再配合目前已有的云端加速能力和视频支持等,完全可以使Chromium内核更加完善。QQ浏览器,百度浏览器和UC浏览器,谁将会是第一吃螃蟹的人,拭目以待吧!

(全文完)

原创文章,转载请注明来自http://blog.csdn.net/hongbomin/article/details/40799167,谢谢!


Chromium on Android: Android L平台上WebView的变化及其对浏览器厂商的影响分析

标签:chromium webview   android l   浏览器   移动浏览器   

原文地址:http://blog.csdn.net/hongbomin/article/details/40799167

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!