以前有个疑问,SurfaceView为什么可以开一个新的线程进行绘制,而其他的不行。我们知道View的布局绘制是在主线程执行的,通过ViewRootImpl的performTraversals方法开始驱动顶层DecorView和它的子View执行measure,layout, draw。同样是View类,那为什么SurfaceView的绘制操作却可以单独开新的线程去执行呢?

在这里先提前总结一下原理。View的绘制是由ViewRootImpl的Surface对象将绘制后的ui数据交给WindowManagerService,WindowManagerService会将多个Surface数据合并成一整屏的ui数据,交给SurfaceFlinger渲染对应的Layer。而SurfaceView内部维护着一块Surface用于ui数据的的绘制,同时在WindowManagerService端会创建一个新的绘图对象,对应着SurfaceFlinger的一个新的Layer,因此SurfaceView中绘制的数据就由新的Layer,而不是宿主DecorView的Layer,意思就是SurfaceView有和宿主DecorView对应的ViewRootImpl一样的一套绘制渲染模型,两者分别独立渲染。

现在简单讲讲SurfaceView的工作流程。

  • 1.在onAttachedToWindow中进行对SurfaceView的初始化和准备工作。完成透明区域的请求,获取WindowManagerService的本地代理对象mSession,和对绘制的监听addOnPreDrawListener

  • 2.在onWindowVisibilityChanged中将被显示时,调用updateWindow

  • 3.updateWindow,初始化MyWindow对象,用于WindowManagerService通知SurfaceView的状态变化,mSession.relayout以请求WindowManagerService对Surface的UI进行布局,这样SurfaceFlinger就会为其创建一个独立的Layer绘图对象。这里同时会回调SurfaceHolder.Callback的surfaceCreated和surfaceChanged等方法,通知当前的SurfaceView是否准备好了。

  • 4.然后我们就可以通过getHolder获取SurfaceHolder的对象,在新线程中,通过lockCanvas锁定画布,然后用Cavas对象进行绘制,内部是通过SurfaceView的Surface对象来取得Canvas对象,该Canvas通过JNI调用到底层的Surface的Canvas进行操作。

  • 5.在onDetachedFromWindow中进行对SurfaceView的清理工作。移除绘制监听,调用updateWindow通知回调SurfaceHolder.Callback的surfaceDestroyed,mSession移除mWindow以使WindowManagerService解除对SurfaceView的操作。

再次总结,因为普通View走的是ViewRootImpl的绘制流程,在里面有对线程进行检查,非主线程的话会抛异常,目的就是实现单线程绘制模型,同时又要接受输入事情等。而SurfaceView有独立的绘制机制,比如独立的客户端Surface,WindowManagerService中独立的绘图对象,SurfaceFlinger中独立的Layer渲染。因为它里面可以只负责绘制,所以效率要更高。当然,SurfaceView的其他处理比如输入事件还是继承使用了View的那一套。

好了,简述完了之后,觉得还是有必要贴出些源码,让描述显得更加有依据一些。

public SurfaceView(Context context) {super(context);init();
}public SurfaceView(Context context, AttributeSet attrs) {super(context, attrs);init();
}public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init();
}public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);init();
}private void init() {setWillNotDraw(true);
}

此处初始化,目的是不进行View的那一套绘制,专心做新线程的绘制。

@Override
protected void onAttachedToWindow() {super.onAttachedToWindow();mParent.requestTransparentRegion(this);mSession = getWindowSession();mLayout.token = getWindowToken();mLayout.setTitle("SurfaceView - " + getViewRootImpl().getTitle());mViewVisibility = getVisibility() == VISIBLE;if (!mGlobalListenersAdded) {ViewTreeObserver observer = getViewTreeObserver();observer.addOnScrollChangedListener(mScrollChangedListener);observer.addOnPreDrawListener(mDrawListener);mGlobalListenersAdded = true;}
}

当SurfaceView被添加到Window上即将显示时,调用mParent.requestTransparentRegion(this);请求顶层View给自己留出空白区域,这样我们的SurfaceView才能不会被其他View遮挡住。然后这里获取了WindowManager的代理对象,以后后面对WindowManager的请求操作。然后添加滚动监听和绘制监听

private final ViewTreeObserver.OnScrollChangedListener mScrollChangedListener= new ViewTreeObserver.OnScrollChangedListener() {@Overridepublic void onScrollChanged() {updateWindow(false, false);}};private final ViewTreeObserver.OnPreDrawListener mDrawListener =new ViewTreeObserver.OnPreDrawListener() {@Overridepublic boolean onPreDraw() {// reposition ourselves where the surface ismHaveFrame = getWidth() > 0 && getHeight() > 0;updateWindow(false, false);return true;}};

这样,即将要绘制的时候,就会执行updateWindow方法了。

@Override
protected void onWindowVisibilityChanged(int visibility) {super.onWindowVisibilityChanged(visibility);mWindowVisibility = visibility == VISIBLE;mRequestedVisible = mWindowVisibility && mViewVisibility;updateWindow(false, false);
}

SurfaceView依附的Window要显示或隐藏时,也同样调用updateWindow,那updateWindow是做什么操作的呢?

protected void updateWindow(boolean force, boolean redrawNeeded) {.....final boolean creating = mWindow == null;final boolean formatChanged = mFormat != mRequestedFormat;final boolean sizeChanged = mWindowSpaceWidth != myWidth || mWindowSpaceHeight != myHeight;final boolean visibleChanged = mVisible != mRequestedVisible;final boolean layoutSizeChanged = getWidth() != mLayout.width|| getHeight() != mLayout.height;//此处判断SurfaceView是否有发生变化,需要更新if (force || creating || formatChanged || sizeChanged || visibleChanged|| mUpdateWindowNeeded || mReportDrawNeeded || redrawNeeded) {//需要刷新SurfaceView,更新mLayout这个SurfaceView的LayoutParams的信息......if (mWindow == null) {//MyWindow是一个Binder类,用于WindowMangerService通知回调SurfaceView,这里创建MyWindow对象mWIndow,并通过mSession.addToDisplayWithoutInputChannel将这些参数传给WindowMangerService,通知它为SurfaceView创建一块不接收输入事件的Surface,以便后面绘图所用。Display display = getDisplay();mWindow = new MyWindow(this);mLayout.type = mWindowType;mLayout.gravity = Gravity.START|Gravity.TOP;mSession.addToDisplayWithoutInputChannel(mWindow, mWindow.mSeq, mLayout,mVisible ? VISIBLE : GONE, display.getDisplayId(), mContentInsets,mStableInsets);}boolean realSizeChanged;boolean reportDrawNeeded;int relayoutResult;//此处需要用锁锁住,防止其他线程同时修改mSurfaceLock.lock();try {mUpdateWindowNeeded = false;reportDrawNeeded = mReportDrawNeeded;mReportDrawNeeded = false;mDrawingStopped = !visible;//通过mSession这个binder对象请求WindowMangerService为SurfaceView的UI进行布局,然后WindowMangerService就会填充mNewSurface这个Surface对象,以便后面可以通过它获取画布Canvas,进行绘图操作。relayoutResult = mSession.relayout(mWindow, mWindow.mSeq, mLayout, mWindowSpaceWidth, mWindowSpaceHeight,visible ? VISIBLE : GONE,WindowManagerGlobal.RELAYOUT_DEFER_SURFACE_DESTROY,mWinFrame, mOverscanInsets, mContentInsets,mVisibleInsets, mStableInsets, mOutsets, mBackdropFrame,mConfiguration, mNewSurface);if ((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {reportDrawNeeded = true;}if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "+ "New surface: " + mNewSurface+ ", vis=" + visible + ", frame=" + mWinFrame);mSurfaceFrame.left = 0;mSurfaceFrame.top = 0;if (mTranslator == null) {mSurfaceFrame.right = mWinFrame.width();mSurfaceFrame.bottom = mWinFrame.height();} else {float appInvertedScale = mTranslator.applicationInvertedScale;mSurfaceFrame.right = (int) (mWinFrame.width() * appInvertedScale + 0.5f);mSurfaceFrame.bottom = (int) (mWinFrame.height() * appInvertedScale + 0.5f);}final int surfaceWidth = mSurfaceFrame.right;final int surfaceHeight = mSurfaceFrame.bottom;realSizeChanged = mLastSurfaceWidth != surfaceWidth|| mLastSurfaceHeight != surfaceHeight;mLastSurfaceWidth = surfaceWidth;mLastSurfaceHeight = surfaceHeight;} finally {//此处释放锁mSurfaceLock.unlock();}try {redrawNeeded |= creating | reportDrawNeeded;SurfaceHolder.Callback callbacks[] = null;final boolean surfaceChanged = (relayoutResult& WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED) != 0;if (mSurfaceCreated && (surfaceChanged || (!visible && visibleChanged))) {mSurfaceCreated = false;if (mSurface.isValid()) {if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "+ "visibleChanged -- surfaceDestroyed");callbacks = getSurfaceCallbacks();//这里判断了Surface被销毁了,回调实现了SurfaceHolder.Callback的对象,大部分情况就是我们继承实现的SurfaceView了for (SurfaceHolder.Callback c : callbacks) {c.surfaceDestroyed(mSurfaceHolder);}}}//这里将最新状态的mNewSuface对象的数据更新到当前的mSurface中mSurface.transferFrom(mNewSurface);if (visible && mSurface.isValid()) {if (!mSurfaceCreated && (surfaceChanged || visibleChanged)) {mSurfaceCreated = true;mIsCreating = true;if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "+ "visibleChanged -- surfaceCreated");if (callbacks == null) {callbacks = getSurfaceCallbacks();}//这里判断了Surface被创建了,回调实现了SurfaceHolder.Callback的对象,大部分情况就是我们继承实现的SurfaceView了for (SurfaceHolder.Callback c : callbacks) {c.surfaceCreated(mSurfaceHolder);}}if (creating || formatChanged || sizeChanged|| visibleChanged || realSizeChanged) {if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "+ "surfaceChanged -- format=" + mFormat+ " w=" + myWidth + " h=" + myHeight);if (callbacks == null) {callbacks = getSurfaceCallbacks();}//这里判断了Surface被更新了,回调实现了SurfaceHolder.Callback的对象,大部分情况就是我们继承实现的SurfaceView了for (SurfaceHolder.Callback c : callbacks) {c.surfaceChanged(mSurfaceHolder, mFormat, myWidth, myHeight);}}if (redrawNeeded) {if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "+ "surfaceRedrawNeeded");if (callbacks == null) {callbacks = getSurfaceCallbacks();}//这里判断了Surface需要被重绘,回调实现了SurfaceHolder.Callback的对象,大部分情况就是我们继承实现的SurfaceView了for (SurfaceHolder.Callback c : callbacks) {if (c instanceof SurfaceHolder.Callback2) {((SurfaceHolder.Callback2)c).surfaceRedrawNeeded(mSurfaceHolder);}}}}} finally {mIsCreating = false;if (redrawNeeded) {if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "+ "finishedDrawing");mSession.finishDrawing(mWindow);}mSession.performDeferredDestroy(mWindow);}} catch (RemoteException ex) {Log.e(TAG, "Exception from relayout", ex);}} else {//除了以上情况,如果是SufaceView的位置或大小发生改变,就进行UI线程的布局更新// Calculate the window position in case RT loses the window// and we need to fallback to a UI-thread driven position updategetLocationInWindow(mLocation);final boolean positionChanged = mWindowSpaceLeft != mLocation[0]|| mWindowSpaceTop != mLocation[1];if (positionChanged || layoutSizeChanged) { // Only the position has changedmWindowSpaceLeft = mLocation[0];mWindowSpaceTop = mLocation[1];// For our size changed check, we keep mLayout.width and mLayout.height// in view local space.mLocation[0] = mLayout.width = getWidth();mLocation[1] = mLayout.height = getHeight();transformFromViewToWindowSpace(mLocation);mWinFrame.set(mWindowSpaceLeft, mWindowSpaceTop,mLocation[0], mLocation[1]);if (mTranslator != null) {mTranslator.translateRectInAppWindowToScreen(mWinFrame);}if (!isHardwareAccelerated() || !mRtHandlingPositionUpdates) {try {if (DEBUG) Log.d(TAG, String.format("%d updateWindowPosition UI, " +"postion = [%d, %d, %d, %d]", System.identityHashCode(this),mWinFrame.left, mWinFrame.top,mWinFrame.right, mWinFrame.bottom));mSession.repositionChild(mWindow, mWinFrame.left, mWinFrame.top,mWinFrame.right, mWinFrame.bottom, -1, mWinFrame);} catch (RemoteException ex) {Log.e(TAG, "Exception from relayout", ex);}}}}
}

可见,updateWindow的主要作用是SurfaceView更新的处理,包括mWindow,通知WindowMangerService中Surface的创建,还有Surface的创建,更新,销毁的通知回调等。
我们在看看MyWindow这个Binder类

private static class MyWindow extends BaseIWindow {private final WeakReference<SurfaceView> mSurfaceView;public MyWindow(SurfaceView surfaceView) {//弱引用持有当前的SurfaceView,防止内存泄露mSurfaceView = new WeakReference<SurfaceView>(surfaceView);}@Overridepublic void resized(Rect frame, Rect overscanInsets, Rect contentInsets,Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw,Configuration newConfig, Rect backDropRect, boolean forceLayout,boolean alwaysConsumeNavBar) {//WindowMangerService中SurfaceView对应的Surface大小发生变化,发消息通知窗口布局发生了变化SurfaceView surfaceView = mSurfaceView.get();if (surfaceView != null) {if (DEBUG) Log.v(TAG, surfaceView + " got resized: w=" + frame.width()+ " h=" + frame.height() + ", cur w=" + mCurWidth + " h=" + mCurHeight);surfaceView.mSurfaceLock.lock();try {if (reportDraw) {surfaceView.mUpdateWindowNeeded = true;surfaceView.mReportDrawNeeded = true;surfaceView.mHandler.sendEmptyMessage(UPDATE_WINDOW_MSG);} else if (surfaceView.mWinFrame.width() != frame.width()|| surfaceView.mWinFrame.height() != frame.height()|| forceLayout) {surfaceView.mUpdateWindowNeeded = true;surfaceView.mHandler.sendEmptyMessage(UPDATE_WINDOW_MSG);}} finally {surfaceView.mSurfaceLock.unlock();}}}@Overridepublic void dispatchAppVisibility(boolean visible) {// The point of SurfaceView is to let the app control the surface.}@Overridepublic void dispatchGetNewSurface() {//通知对应的新的Surface创建了,这里消息处理会调用updateWindow,在其中会获得最新的Surface填充到mNewSurface中SurfaceView surfaceView = mSurfaceView.get();if (surfaceView != null) {Message msg = surfaceView.mHandler.obtainMessage(GET_NEW_SURFACE_MSG);surfaceView.mHandler.sendMessage(msg);}}@Overridepublic void windowFocusChanged(boolean hasFocus, boolean touchEnabled) {Log.w("SurfaceView", "Unexpected focus in surface: focus=" + hasFocus + ", touchEnabled=" + touchEnabled);}@Overridepublic void executeCommand(String command, String parameters, ParcelFileDescriptor out) {}int mCurWidth = -1;int mCurHeight = -1;
}

可见,MyWindow就是WindowMangerService用来通知SurfaceView的沟通工具,通知SurfaceView对应的Surface状态的变化。
最后就是mSurfaceHolder了,顾名思义就是Surface的持有者,通过它来操作Surface对象。

private final SurfaceHolder mSurfaceHolder = new SurfaceHolder() {private static final String LOG_TAG = "SurfaceHolder";@Overridepublic boolean isCreating() {return mIsCreating;}@Overridepublic void addCallback(Callback callback) {//添加callback回调synchronized (mCallbacks) {// This is a linear search, but in practice we'll// have only a couple callbacks, so it doesn't matter.if (mCallbacks.contains(callback) == false) {mCallbacks.add(callback);}}}@Overridepublic void removeCallback(Callback callback) {//移除callback回调synchronized (mCallbacks) {mCallbacks.remove(callback);}}@Overridepublic void setFixedSize(int width, int height) {if (mRequestedWidth != width || mRequestedHeight != height) {mRequestedWidth = width;mRequestedHeight = height;requestLayout();}}@Overridepublic void setSizeFromLayout() {if (mRequestedWidth != -1 || mRequestedHeight != -1) {mRequestedWidth = mRequestedHeight = -1;requestLayout();}}@Overridepublic void setFormat(int format) {// for backward compatibility reason, OPAQUE always// means 565 for SurfaceViewif (format == PixelFormat.OPAQUE)format = PixelFormat.RGB_565;mRequestedFormat = format;if (mWindow != null) {updateWindow(false, false);}}/*** @deprecated setType is now ignored.*/@Override@Deprecatedpublic void setType(int type) { }@Overridepublic void setKeepScreenOn(boolean screenOn) {Message msg = mHandler.obtainMessage(KEEP_SCREEN_ON_MSG);msg.arg1 = screenOn ? 1 : 0;mHandler.sendMessage(msg);}/*** Gets a {@link Canvas} for drawing into the SurfaceView's Surface** After drawing into the provided {@link Canvas}, the caller must* invoke {@link #unlockCanvasAndPost} to post the new contents to the surface.** The caller must redraw the entire surface.* @return A canvas for drawing into the surface.*/@Overridepublic Canvas lockCanvas() {return internalLockCanvas(null);}/*** Gets a {@link Canvas} for drawing into the SurfaceView's Surface** After drawing into the provided {@link Canvas}, the caller must* invoke {@link #unlockCanvasAndPost} to post the new contents to the surface.** @param inOutDirty A rectangle that represents the dirty region that the caller wants* to redraw.  This function may choose to expand the dirty rectangle if for example* the surface has been resized or if the previous contents of the surface were* not available.  The caller must redraw the entire dirty region as represented* by the contents of the inOutDirty rectangle upon return from this function.* The caller may also pass <code>null</code> instead, in the case where the* entire surface should be redrawn.* @return A canvas for drawing into the surface.*/@Overridepublic Canvas lockCanvas(Rect inOutDirty) {return internalLockCanvas(inOutDirty);}private final Canvas internalLockCanvas(Rect dirty) {//锁定并获取Canvas画布对象,以进行后面的绘画mSurfaceLock.lock();if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + "Locking canvas... stopped="+ mDrawingStopped + ", win=" + mWindow);Canvas c = null;if (!mDrawingStopped && mWindow != null) {try {c = mSurface.lockCanvas(dirty);} catch (Exception e) {Log.e(LOG_TAG, "Exception locking surface", e);}}if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + "Returned canvas: " + c);if (c != null) {mLastLockTime = SystemClock.uptimeMillis();return c;}// If the Surface is not ready to be drawn, then return null,// but throttle calls to this function so it isn't called more// than every 100ms.long now = SystemClock.uptimeMillis();long nextTime = mLastLockTime + 100;if (nextTime > now) {try {Thread.sleep(nextTime-now);} catch (InterruptedException e) {}now = SystemClock.uptimeMillis();}mLastLockTime = now;mSurfaceLock.unlock();return null;}/*** Posts the new contents of the {@link Canvas} to the surface and* releases the {@link Canvas}.** @param canvas The canvas previously obtained from {@link #lockCanvas}.*/@Overridepublic void unlockCanvasAndPost(Canvas canvas) {mSurface.unlockCanvasAndPost(canvas);mSurfaceLock.unlock();}@Overridepublic Surface getSurface() {return mSurface;}@Overridepublic Rect getSurfaceFrame() {return mSurfaceFrame;}
};

可见,SurfaceHolder就是SurfaceView对外提供访问Surface的接口,这里访问Surface有做同步,为空时重复请求的处理。

到这里的话,SurfaceView的大部分逻辑都在这了,就先到这里了。

SurfaceView原理简述相关推荐

  1. Java中CAS(Compare And Swap,比较和交换)算法的技术原理简述

    - title: Java中CAS(Compare And Swap,比较和交换)算法的技术原理简述 - date: 2021/8/14 文章目录 CAS全称 Compare And Swap,是一种 ...

  2. 电容三点式LC振荡器电路组成及工作原理简述

    电容三点式LC振荡器电路组成及工作原理简述 [复制链接]     husk2012 116 主题 21 好友 3550 积分 VIP会员 发消息 电梯直达 1#  发表于 2012-10-22 20: ...

  3. ELF PLT Hook 原理简述

    [无线平台]ELF PLT Hook 原理简述 简述 Android 是基于Linux的操作系统,因此在Android开发平台上,ELF是原生支持的可执行文件格式:ELF文件格式除了作为可执行文件,还 ...

  4. Excel阅读模式/聚光灯开发技术之二 超级逐步录入提示功能开发原理简述—— 隐鹤 / HelloWorld...

    Excel阅读模式/聚光灯开发技术之二 超级逐步录入提示功能开发原理简述---- 隐鹤  /  HelloWorld 1. 引言 自本人第一篇博文"Excel阅读模式/单元格行列指示/聚光灯 ...

  5. P型半导体,N型半导体,PN结原理简述

    前置知识: 半导体器件是构成各种电子电路的基础.而半导体的器件主要是由半导体材料制成的:如硅和锗. 为什么要使用半导体材料作为器件? 这就跟半导体的特性相关,需要一些初高中的化学基础:在元素表中,元素 ...

  6. Vue响应式原理简述

    Vue响应式原理简述 依赖技术 图解过程 依赖技术 问题:Vue如何监听data的改变? => 技术:Object.defineProperty函数.在其中的set方法中监听对象属性的改变. 问 ...

  7. 彩色图片转手绘线稿的原理简述与Python实现

    大家好,我是小小明,在学习 好友叶庭云 介绍的一门中国大学MOOC的课程中,学到手绘图像,下面我测试并总结一下. 课程链接是:https://www.icourse163.org/course/BIT ...

  8. android view的绘制原理,SurfaceView 原理

    Preview Q1: 啥是SurfaceView? [一个自带surface 画布的view] [能在子线程中做UI操作] Q2:在Activity 中 内嵌 SurfaceView的情况下 , U ...

  9. 陀螺仪加速度计 JY61(MPU6050) 原理简述及缺陷分析

    -- 该模块工作原理较为复杂,且涉及到数字信号处理,本文只是简单的描述其工作原理,并捎带上一些基础理论. JY61(MPU6050)简介 MPU6050由四部分组成:加速度计.陀螺仪.DMP.温度传感 ...

最新文章

  1. 第十五届全国大学生智能车竞赛浙江赛区隆重开幕
  2. 【十九】require和include的区别
  3. 阿里云高级技术专家白常明谈《边缘云的技术挑战和应用创新》
  4. 第六节: EF高级属性(二) 之延迟加载、立即加载、显示加载(含导航属性)
  5. 什么是补码-网上找到的,非原创
  6. Blockchain.com,Eden Block,DACM等知名公司加入Pocket生态
  7. 2015/8/26 Python基础(1):基本规则及赋值
  8. html基础知识补全
  9. Win8 开发者训练营第一天的过程,感受和收获
  10. 数据结构详解之向量vector
  11. Windows--IOmeter测试网络
  12. 树莓派所用到的软件工具及获取方法汇总
  13. Python数据库sqlite3详解
  14. Linux 复制文件命令
  15. 元器件 - TVS二极管
  16. 如何使用 Selenium 在 HTML 文本输入中模拟按 Enter 键?
  17. 【GITEE】解决 Push rejected
  18. 广域虚拟数据空间中边缘缓存系统的研究与实现
  19. Android开发 设置手机壁纸
  20. 频率选择性衰落和时间选择性衰落详解

热门文章

  1. 安卓关于不同屏幕大小的解决方案-支持多屏
  2. 解决Arcmap中遥感影像颜色显示异常问题
  3. c 语言private用法,举例分析private的作用(c/c++学习)
  4. 2022阿里云峰会·广东
  5. B站充电|单周充电超1500,银发UP主成B站新黑马?
  6. Linux tar压缩命令:打包与解打包命令
  7. 万恶的IIS+ASP
  8. 量化选股策略搭建(二)(数据更新)
  9. 【电脑自动重启,电脑自动重启的原因】
  10. 用python画lgx的图_【Python科学计算】matplotlib——python画图