title author date CreateTime categories
WPF 渲染原理
lindexi
2019-9-8 10:40:0 +0800
2018-7-15 16:2:47 +0800
WPF 渲染

在 WPF 最主要的就是渲染,因为 WPF 是一个界面框架。想用一篇博客就能告诉大家完整的 WPF 渲染原理是不可能的。本文告诉大家 WPF 从开发者告诉如何画图像到在屏幕显示的过程。本文是从一个很高的地方来看渲染的过程,在本文之后会添加很多博客来告诉大家渲染的细节。

从 WPF 画图像到屏幕显示是比较复杂的,本渣也不敢说这就是 WPF 的做法,但是看了很多博客,好像都是这么说的,看了代码,好像 WPF 是这样写的。

本文将会分为三个不同的层来讲,第一层就是 WPF 的总体结构,第二层是消息循环相关,第三层是在 dx 渲染到屏幕。

WPF 组成

因为 WPF 是一个 UI 框架,作为一个框架最主要的是交互和显示。本文只告诉大家渲染的原理。但是本文不会告诉大家任何关于渲染的算法,只是告诉大家渲染的过程如何从 WPF 元素显示到屏幕。

下面的图片是从WPF Architecture 找到

WPF 有三个主要的模块 PresentationFramework、 PresentationCore 和基础层

在 WPF 最顶层,也就是给开发者使用的元素,元素的显示就是使用 DrawingContext 告诉 WPF 需要如何渲染。最简单的方法也就是继承 FrameworkElement 然后重写 OnRender 方法,通过 OnRender 方法画出最基础的界面。这就是在框架的顶层,在这的上面就不属于底层框架的。如在显示上面封装的 Image 等这些。

那么在调用 OnRender 画出内容,是不是 WPF 会立刻显示,如果大家有看 WPF 使用 Direct2D1 画图入门就会发现在渲染是调用 CompositionTarget.Rendering 才知道是什么时候渲染,因为 WPF 是分开渲染和交互,实际的 OnRender 画出的内容的代码是指导渲染,也就是告诉 WPF 如何渲染。那么 WPF 在什么时候渲染就是代码不知道的。为什么 WPF 需要这样做还需要从 WPF 的体系结构开始说起。

我在下面偷了一张图,图片是从Overview of Windows Presentation Foundation (WPF) Architecture找到,在 WPF 可以分为三层。第一层就是 WPF 的托管层,这一层的代码都是托管代码。第二层就是 WPF 的非托管层,包括刚才告诉大家的模块。最后一层就是系统核心元素层。下面简单介绍一下 WPF 的体系结构

如果觉得对 WPF 的体系结构已经足够了解,那么请跳到下一节。

托管层

在托管层最重要的就是 Presentation Framework、Presentation Core 和 Window Base ,很多小伙伴说的 WPF 只是包含这几个模块,因为其他的模块几乎都不会知道。也就是基本使用的类都在下面三个 dll 可以找到

  • PresentationFramework.dll 提供最顶层封装,包括应用的窗口、控制的 Panel 和 Styles 这些类都是在这可以找到,包括交互的控制,动画和几乎可以用到的界面的控件

  • PresentationCore.dll 提供底层的 WPF 功能,如2d、3d和 geometry 这些类,在 PresentationCore 是对 MIL 和托管中间封装,包括提供了 UI Element 和 Visual 这些类,在显示模块包含视觉树和显示指令,也就是刚才说的 OnRender 重写方法。

  • WindowsBase.dll 提供最底层的基础类,包括调度对象 Dispatcher objects 和依赖属性

非托管层

非托管层用来进行高性能的 DX 渲染和连接非托管层

  • milCore.dll 用来作为 WPF 的组合引擎,这时一个使用本地代码编译的库,包含最主要的媒体集成层 Media Integration Layer (MIL) 的基础支持,作用是封装 Dx 的接口支持 2D 和 3D 的渲染,使用本地代码编译是为了获得最好的性能,而且用在 WPF 上层和底层的 DirextX 和 User32 的接口之间。值的一说的是在 Windows Vista 的桌面窗口管理器(Desktop Windows Manager,DWM)就是使用milcore.dll渲染桌面的。

  • WindowsCodecs.dll 这时另一个底层的图片支持代码,用来支持 WPF 旋转、放大图片等,这是一个使用本地代码编译的,提供了很多图片的加密解密,可以让 WPF 把图片画在屏幕

核心系统层

这一层就是系统的核心,如 User32、GDI、Device Drivers,显卡等,这些组合在程序里是最底层的接口

  • User32 提供内存和进程分割,这是一个通用的 API 不止是 WPF 使用,这个库决定一个元素可以在屏幕的哪里显示,也就是窗口显示的最底层的代码就在这。但是这个代码只提供让窗口在哪里显示,如何显示就需要下面的代码

  • DirectX 这就是 WPF 渲染的最底层的库,可以渲染 WPF 的几乎所有控件,需要注意 WPF 使用的是 Dx9 或 Dx12 fl9 没有充分使用 Dx 进行画出现代的窗口。

  • GDI 这个代码依赖显卡,是进行 CPU 渲染的接口,提供了绘制原语和提高质量

  • CLR 因为 WPF 现在还运行在 dotnet framework 所以需要使用运行时,提供了普通的类和方法,用来方便开发者,大概有 30 万 API 可以使用

  • Device Drivers 设备驱动程序是在系统特殊的用来驱动一些硬件

在下面使用到的代码实际都是从最上层拿到的,只有最上层的代码,微软才会开放。而关键的 milCore 代码我还拿不到,只能通过 WinDbg 拿到调用的堆栈。现在还没有完全知道 milCore 的过程,所以也不会在本文告诉大家。

本文的顺序是从消息调度到开发者使用 OnRender 方法给绘制原语,再到如何把绘制原语给渲染线程的过程。从渲染线程调用 milCore ,在通过 milCore 调用 DirectX 的过程就先简单说过。从 DirectX 绘制完成到屏幕显示的过程也是简单告诉大家。

消息循环

在 WPF 中也是使用消息循环,因为在之前的很多程序都是需要自己写消息循环才可以收到用户的交互,这里消息循环就是 Windows 会向 WPF 发送一些消息,而且 WPF 也可以给自己发消息,通过消息就可以判断当前软件需要做哪些

在处理消息的最主要的类是 HwndSubclass ,在创建应用就会执行 Attach 函数,这个函数请看代码

    internal IntPtr Attach(IntPtr hwnd){// 忽略代码return this.CriticalAttach(hwnd);}

从上面代码可以看到主要的是 CriticalAttach 函数,请看代码

    internal IntPtr CriticalAttach(IntPtr hwnd){// 忽略代码NativeMethods.WndProc newWndProc = new NativeMethods.WndProc(this.SubclassWndProc); // 创建处理消息IntPtr windowLongPtr = UnsafeNativeMethods.GetWindowLongPtr(new HandleRef((object) this, hwnd), -4);this.HookWindowProc(hwnd, newWndProc, windowLongPtr);return (IntPtr) this._gcHandle;}

这里 SubclassWndProc 就是主要处理消息,但是在上面代码是 HookWindowProc 注册。在 GetWindowLongPtr 大家也许都很熟悉,拿到这个函数就是拿到窗口,下面来看一下 HookWindowProc 代码

      private void HookWindowProc(IntPtr hwnd, NativeMethods.WndProc newWndProc, IntPtr oldWndProc){_hwndAttached = hwnd;_hwndHandleRef = new HandleRef(null,_hwndAttached);_bond = Bond.Attached;_attachedWndProc = newWndProc;_oldWndProc = oldWndProc;// 这有下面的代码才是获得消息IntPtr oldWndProc2 = (IntPtr)UnsafeNativeMethods.CriticalSetWindowLong(_hwndHandleRef, NativeMethods.GWL_WNDPROC, _attachedWndProc);// 跟踪这个窗口,可以在 CLR 关闭,撤掉管理的窗口代码ManagedWndProcTracker.TrackHwndSubclass(this, _hwndAttached);}

这里 NativeMethods.GWL_WNDPROC 的值就是 -4 在 CriticalSetWindowLong 是这样写

       internal static IntPtr CriticalSetWindowLong(HandleRef hWnd, int nIndex, NativeMethods.WndProc dwNewLong){int errorCode;IntPtr retVal;retVal = NativeMethodsSetLastError.SetWindowLongPtrWndProc(hWnd, nIndex, dwNewLong);errorCode = Marshal.GetLastWin32Error();return retVal;}

所以核心就是 SetWindowLongPtrWndProc 调用 SetWindowLongWrapper 设置获得消息

那么消息是如何作为事件?

       internal IntPtr SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam){// 忽略的代码// 获得当前线程的 dispatcher 也就是不是主线程也可以处理Dispatcher dispatcher = Dispatcher.FromThread(Thread.CurrentThread);// 忽略的代码DispatcherOperationCallbackParameter param = _paramDispatcherCallbackOperation;_paramDispatcherCallbackOperation = null;param.hwnd = hwnd;param.msg = msg;param.wParam = wParam;param.lParam = lParam;// 插入处理的消息object result = dispatcher.Invoke(DispatcherPriority.Send,_dispatcherOperationCallback,param);var retval = param.retVal;return retval;}

也就是调用的 SubclassWndProc 实际上就是封装消息,然后调用 dispatcher 执行。那么 _dispatcherOperationCallback 有是如何做的?实际上 这个也是调用这个弱引用委托,请看代码,下面的代码是去掉判断参数

      private object DispatcherCallbackOperation(object o){DispatcherOperationCallbackParameter param = (DispatcherOperationCallbackParameter)o;param.handled = false;param.retVal = IntPtr.Zero; // 把返回值设置为 空,把是否处理设置为 false ,在下面调用函数,如果可以处理就设置 param.handled 为 trueHwndWrapperHook hook = _hook.Target as HwndWrapperHook;param.retVal = hook(param.hwnd, param.msg, param.wParam, param.lParam, ref param.handled);return param;}

那么这个 Handle 有什么用?这个属性就是在为 false 会在 SubclassWndProc 使用下面代码

            // 如果窗口无法处理这个消息,把他发到下一个链,因为 Windows 消息是可以传到下一个窗口if(!handled){retval = CallOldWindowProc(oldWndProc, hwnd, message, wParam, lParam);}

那么这个处理 _hook 是怎么传过来的,这个_hook 是一个弱引用,具体的传送过来需要从 Dispatcher 开始说,请看下面

Dispatcher 调度

关于调度需要从 PushFrame 开始说,请看深入了解 WPF Dispatcher 的工作原理(PushFrame 部分) - walterlv,walterlv大佬已经告诉大家底层的原理我就可以不再这里详细告诉大家

在 Dispatcher 的构造可以看到下面代码

         MessageOnlyHwndWrapper window = new MessageOnlyHwndWrapper();

创建 MessageOnlyHwndWrapper 可以收到消息,并且把消息放在 Dispatcher 用来处理,那么 MessageOnlyHwndWrapper 为什么可以收到消息? 可以看到 MessageOnlyHwndWrapper 的代码很简单

    internal class MessageOnlyHwndWrapper : HwndWrapper{public MessageOnlyHwndWrapper() : base(0, 0, 0, 0, 0, 0, 0, "", NativeMethods.HWND_MESSAGE, null){}}

就需要看一下 HwndWrapper 类的代码,这个类的构造函数的关键代码

        public HwndWrapper()//太多参数,这里就不写了{_wndProc = new SecurityCriticalData<HwndWrapperHook>(new HwndWrapperHook(WndProc));// 还记得刚才说的消息,就是在这里创建 HwndSubclass 可以直接拿到消息HwndSubclass hwndSubclass = new HwndSubclass(_wndProc.Value);}

所以在上面说的 _hook 弱引用就是 传入的_wndProc ,那么这个 _wndProc 的关键代码就是WndProc,在 HwndWrapper 的函数

       private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled){IntPtr result = IntPtr.Zero;WindowMessage message = (WindowMessage)msg;// 遍历所有的 _hooks 处理消息foreach(HwndWrapperHook hook in _hooks.Value){result = hook(hwnd, msg, wParam, lParam, ref handled);if(handled){break;}}return result;}

所以这个方法也就是没有做具体处理,是通过AddHook函数添加的处理

       public void AddHook(HwndWrapperHook hook){//VerifyAccess();if(_hooks == null){_hooks = new SecurityCriticalDataClass<WeakReferenceList>(new WeakReferenceList());}_hooks.Value.Insert(0, hook);}

因为垃圾微软的代码,在 Dispatcher 的构造函数才调用 AddHook ,也就是在构造函数创建了 MessageOnlyHwndWrapper 在这个类初始化,具体处理是在初始化之后才加上,所以可以看到这个类有很多没有用的代码,因为初始化创建的值都是空,在创建之后才加上

 private Dispatcher(){MessageOnlyHwndWrapper window = new MessageOnlyHwndWrapper();_hook = new HwndWrapperHook(WndProcHook);window.AddHook(_hook);}

最核心的处理消息就是 Dispatcher 的 WndProcHook 这个方法实际上只是调用 ProcessQueue 方法。

在 SubclassWndProc 调用的dispatcher.Invoke( DispatcherPriority.Send,_dispatcherOperationCallback,param)就是调用WndProcHook 函数传入参数

如果收到了画窗口的消息,就会把这个消息发送给DWM,通过DWM把窗口画的内容画到屏幕。

RenderContent

在使用绘制原语之后,最底层的函数是 UIElement.RenderContent ,请看代码

  internal override void RenderContent(RenderContext ctx, bool isOnChannel){DUCE.Channel channel = ctx.Channel;DUCE.IResource drawingContent = (DUCE.IResource) this._drawingContent;drawingContent.AddRefOnChannel(channel);DUCE.CompositionNode.SetContent(this._proxy.GetHandle(channel), drawingContent.GetHandle(channel), channel);this.SetFlags(channel, true, VisualProxyFlags.IsContentConnected);}

通过 DUCE.CompositionNode.SetContent 调用 channel.SendCommand 就可以发送到渲染线程

      internal static unsafe void SetContent(DUCE.ResourceHandle hCompositionNode, DUCE.ResourceHandle hContent, DUCE.Channel channel){DUCE.MILCMD_VISUAL_SETCONTENT visualSetcontent;visualSetcontent.Type = MILCMD.MilCmdVisualSetContent;visualSetcontent.Handle = hCompositionNode;visualSetcontent.hContent = hContent;// 通过下面代码发送到另一个线程channel.SendCommand((byte*) &visualSetcontent, sizeof (DUCE.MILCMD_VISUAL_SETCONTENT));}

那么这个渲染是如何触发,实际上在Dispatcher.ProcessQueue调用,在上面已经有告诉大家 ProcessQueue 是在 WndProcHook 触发的

       private IntPtr WndProcHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled){if(message == _msgProcessQueue){ProcessQueue();}}

这里 _msgProcessQueue 就是在 Dispatcher 静态构造函数的自定义消息

      static Dispatcher(){_msgProcessQueue = UnsafeNativeMethods.RegisterWindowMessage("DispatcherProcessQueue");// 下面还有很多代码}

那么这个消息是怎么发送,在UIElement.InvalidateVisual函数会调用MediaContext.PostRender这里就发送自定义消息,于是就可以开始渲染

在消息循环是会不断获取消息,这里说的渲染是包括两个方面,一个是 WPF 把内容画到窗口,也就是上面说的自定义消息,还有另一个就是把窗口内容画在屏幕。这两个都是依靠 Windows 消息,只是第一个消息是 WPF 自己发给自己,也就是自己玩的。从 Dispatcher 拿到自定义的消息,就开始执行视觉树的对象,调用对应的绘制,这里是收集到绘制原语,也就是告诉显卡可以怎么画。在底层是通过 System.Windows.Media.Composition.DUCE 的 Channel 把数据发送到渲染线程,渲染线程就是使用 Dx 进行绘制。在 Dx 画是使用 MilCore 从渲染线程连接到 Dx 画出来的

在渲染线程收集到的都是绘制原语,绘制原语就是在 Visual 底层调用的DrawingContext 传入的方法

这一部分没有完全跟源代码,如果有哪些地方和实际不相同,请告诉我

渲染线程拿到了绘制原语就可以进行绘制,绘制的过程需要进行处理图片和一些基础形状,大概的过程请看下面

这时到了 Dx 才会使用显卡进行渲染,并且绘制的是窗口指针。也就是窗口绘制完成在屏幕还是无法看到的。

在绘制的时候需要使用 MIL 解码一些图片和一些形状才可以用到 dx 进行渲染

在窗口画完之后,会通过 WM_PAINT 告诉 DWM 可以画出窗口。但是现代的应用是不需要在窗口刷新的过程通过 windows 消息发送到 DWM 才进行窗口刷新。只有在窗口存在部分不可见,如窗口有一部分在屏幕之外,从屏幕外移到屏幕内,或者窗口最小化到显示才需要通过 windows 告诉 DWM 刷新。

那么这里 DWM 是什么?请看下面

桌面窗口管理

在 Windows 系统,很重要的就是 DWM(Desktop Window Manager)可以把窗口画到屏幕,并且支持窗口做半透明和其他的对其他窗口的视觉处理。

需要知道,发送 WM_PAINT 消息只能通过系统发送而不能通过应用发送,也就是上面说的通过 WM_PAINT 告诉 DWM 可以画出窗口,不是软件主动告诉系统,而是系统会不断刷新。

对于不可见的窗口,在 DWM 是不会发送 WM_PAINT 到这个窗口。

详细的 WM_PAINT 请看 WMPAINT详解和WMERASEBKGND - CSDN博客

如果系统没有发送 WM_PAINT 到应用,屏幕怎么知道窗口需要刷新?实际通过Drawing Without the WM_PAINT Message 的方法就可以做到。

在 Windows 8 之后就无法手动设置关闭 DWM 的合成,只有在 windows 7 和以前的系统才可以设置关闭合成。通过 DWM 合成技术可以将每个绘制的窗口认为是一个位图,通过对位图处理添加阴影等,做出好看界面。

更多请看 Desktop Window Manager

从控件画出来到屏幕显示

虽然上面写了很多,但是好多小伙伴都不会仔细去看,所以本渣就在这里重新把过程说一遍。需要知道,这里说的省略很多细节,上面的也是有很多细节没有告诉大家。

在渲染的时候,是需要通过多个方式把渲染的任务放在 Dispather 里,在 WPF 应用内是可以通过 InvalidateVisual 的方法通知,而系统也在不断发送消息告诉一个应用开始渲染。

在 Dispatcher 收到消息之后就可以把渲染任务放在队列,按照优先级一个个出队

这时在 Dispatcher 内部通过渲染的调用就会通过 DispatcherOperation 执行对应的任务

渲染的任务是通过mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state)触发,从 mscorlib 再次调进 WindowBase ,再从 WindowBase 的 WindowsBase.dll!System.Windows.Threading.ExceptionWrapper.InternalRealCall(System.Delegate callback, object args, int numArgs) 调用 PresentationFramework.dll!System.Windows.Window.ShowHelper(object booleanBox) 也就是显示的最近的代码

总的渲染结构请看下图

渲染需要经过消息循环和 Dispatcher 循环,也就是渲染的方法不是直接通过控件调用渲染。控件是通过发送消息经过消息循环再调用到控件的 OnRender 方法。再 OnRender 方法里,经过 Drawing 方法输出绘制原语到渲染线程。渲染线程经过 MIL 和 Dx 渲染界面到窗口。屏幕管理更新窗口让用户在屏幕可以看到、

关于渲染性能请看 WPF Drawing Performance

课件 WPF 渲染原理

更多 WPF 渲染请看 渲染相关

参见:

一站式WPF--Window(一) - 周永恒 - 博客园

WPF起步(上) --- WPF是如何把图像画到屏幕上 - CSDN博客

深入了解 WPF Dispatcher 的工作原理(PushFrame 部分) - walterlv

WPF 使用 Direct2D1 画图入门

Overview of Windows Presentation Foundation (WPF) Architecture

WPF Architecture

WPF Drawing Performance

WM_PAINT详解和WM_ERASEBKGND - CSDN博客

Sharing Message Loops Between Win32 and WPF

1.3 WPF体系结构 - 51CTO.COM

WM_PAINT message

Custom Window Frame Using DWM

Performance Considerations and Best Practices

Drawing Without the WM_PAINT Message

Win32知识之窗口绘制窗口第一讲

win32程序之窗口程序以及消息机制

2019-9-8-WPF-渲染原理相关推荐

  1. 高性能Web动画和渲染原理系列(2)——渲染管线和CPU渲染

    [摘要] 介绍浏览器渲染原理及CPU渲染流程 示例代码托管在:http://www.github.com/dashnowords/blogs 博客园地址:<大史住在大前端>原创博文目录 一 ...

  2. 移动端APP渲染原理

    渲染原理 我们看到的App界面,都是由CPU和GPU共同计算处理的. CPU内部流水线结构拥有并行计算能力,一般用于显示内容的计算.而GPU的并行计算能力更强,能够通过计算将图形结果显示在屏幕像素中. ...

  3. 【Unity】 Spine渲染原理解析与源码解读

    Spine渲染原理解析与源码解读 安装环境 从Spine编辑器导出 将资源导入Unity 基础概念 其他相关概念 Spine架构 Spine运行时的各个模块 有状态(Stateful) 和 无状态(S ...

  4. CSS 渲染原理以及优化策略

    大厂技术  高级前端  Node进阶 点击上方 程序员成长指北,关注公众号 回复1,加入高级Node交流群 推荐理由:由浅入深,鞭辟入里. 提起 CSS 很多童鞋都很不屑,尤其是看到 RedMonk ...

  5. 单文件浏览器_图文并茂深度解析浏览器渲染原理,包看懂超值得收藏

    在我们面试过程中,面试官经常会问到这么一个问题,那就是从在浏览器地址栏中输入URL到页面显示,浏览器到底发生了什么?这个问题看起来是老生常谈,但是这个问题回答的好坏,确实可以很好的反映出面试者知识的广 ...

  6. 浏览器渲染原理及可能出现的bug

    Web测试中必不可少的天天打交道的一个应用就是浏览器,它是由哪些部分组成的呢? · 用户接口 – 包括地址栏,前进后退,书签菜单等窗口上除了网页显示区域以外的部分. · 浏览器引擎 – 查询与操作渲染 ...

  7. 深入浅出浏览器渲染原理

    前言 浏览器的内核是指支持浏览器运行的最核心的程序,分为两个部分的,一是渲染引擎,另一个是JS引擎.渲染引擎在不同的浏览器中也不是都相同的.比如在 Firefox 中叫做 Gecko,在 Chrome ...

  8. 浏览器渲染机制面试_浏览器渲染原理

    本文目录结构 问题 浏览器渲染原理 渲染过程 1. 浏览器接收到 HTML ⽂件并转换为 DOM 树 当我们打开⼀个⽹⻚时,浏览器都会去请求对应的 HTML ⽂件.虽然平时我 们写代码时都会分为 JS ...

  9. 浏览器渲染机制面试_面试官不讲码德,问我Chrome浏览器的渲染原理(6000字长文)...

    前言 对于HTML,css和JavaScript是如何变成页面的,这个问题你了解过吗?浏览器究竟在背后都做了些什么事情呢?让我们去了解浏览器的渲染原理,是通往更深层次的开发必不可少的事情,能让我们更深 ...

  10. 浏览器内存不足导致页面崩溃_深度精读:浏览器渲染原理 [8000字图文并茂]

    原文地址:https://segmentfault.com/a/1190000022633988作者:_杨溜溜 在我们面试过程中,面试官经常会问到这么一个问题,那就是从在浏览器地址栏中输入URL到页面 ...

最新文章

  1. UC伯克利开源照片“隐写术”StegaStamp,打印照片能当二维码用!| 技术头条
  2. Mac OS上用item2连接CentOS7
  3. C#LeetCode刷题-数学
  4. mysql教程排序_MySQL中的排序函数field()实例详解
  5. MYSQL锁表的用法,防止并发情况下的重复数据
  6. 第五十二篇 仿真电路简单说明multisim12 函数发生器 示波器
  7. 为什么要从 Microsoft Store 下载 Visual Studio/VS Code?
  8. iptables中DNAT、SNAT和MASQUERADE
  9. 手机版会员中心html,会员中心.html
  10. Shader玉石效果
  11. 回锅肉飘香,《Pokémon GO》再度成为最卖座的iPhone游戏
  12. 误删分区怎么办?必备数据恢复工具!
  13. IAR集成开发环境---ewdewpeww文件介绍
  14. sysbench 压测 安装
  15. 英雄联盟s10信息详解
  16. 新视野大学英语第三版读写教程(四)Unite 4 Midterm test answer
  17. 还在为多个平台发布文章发愁吗?知道一文多发平台吗?
  18. 720s ideapad 黑苹果_现在不买就后悔了 首款完美支持黑苹果的超极本?
  19. 闲谈寻址-两种寻址方法
  20. JSON格式化插件 Chrome

热门文章

  1. Adobe XD CC 2020中文版
  2. LCD1602的引脚定义
  3. 为西安地铁“让路” 47条公交线路有调整
  4. aria2 bt下载做种问题
  5. 给定年月,打印当月的月历表。
  6. DataReader使用
  7. SAP部分支付和剩余支付清账的区别
  8. 用老电脑和老U盘Diy硬件防火墙
  9. 工作流Camunda入门demo
  10. linux makefile 框架分析2