从源码角度解析View的绘制过程
来源:互联网 发布:淘宝客推广首页 编辑:程序博客网 时间:2024/06/02 06:26
View的绘制过程
下面是View绘制的函数流程
ActivityThread.java
1. private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) 2. final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) 3. public final ActivityClientRecord performResumeActivity(IBinder token, boolean clearHide, String reason)
WindowManagerImpl.java
4. public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params)
WindowManagerGlobal.java
5. public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow)
ViewRootImpl.java
6. public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) 7. public void requestLayout() 8. void scheduleTraversals()
Choreographer.java
9. public void postCallback(int callbackType, Runnable action, Object token)
ViewRootImpl.java
10. void doTraversal() 11. private void performTraversals() 12. private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) 13. private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility, boolean insetsPending) throws RemoteException
Session.java
14. public int relayout(IWindow window, int seq, WindowManager.LayoutParams attrs, int requestedWidth, int requestedHeight, int viewFlags, int flags, Rect outFrame, Rect outOverscanInsets, Rect outContentInsets, Rect outVisibleInsets, Rect outStableInsets, Rect outsets, Rect outBackdropFrame, Configuration outConfig, Surface outSurface)
WindowManagerService.java
15. private int relayoutVisibleWindow(Configuration outConfig, int result, WindowState win, WindowStateAnimator winAnimator, int attrChanges, int oldVisibility)
ViewRootImpl.java
16. private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec)
View
17. public final void measure(int widthMeasureSpec, int heightMeasureSpec)
ViewRootImpl.java
18. private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) 19. private void performDraw() 20. private void draw(boolean fullRedrawNeeded)
ViewTreeObserver.java
21. public final void dispatchOnDraw()
ViewRootImpl.java
22. private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty)
Surface.java
23. public Canvas lockCanvas(Rect inOutDirty) throws Surface.OutOfResourcesException, IllegalArgumentException 24. private static native long nativeLockCanvas(long nativeObject, Canvas canvas, Rect dirty) throws OutOfResourcesException
View.java
25. public void draw(Canvas canvas) 26. private void drawBackground(Canvas canvas) 27. protected void onDraw(Canvas canvas) 28. protected void dispatchDraw(Canvas canvas) 29. public void onDrawForeground(Canvas canvas) 30. private void onDrawScrollIndicators(Canvas c) 31. protected final void onDrawScrollBars(Canvas canvas)
以上便是关于view绘制流程的全部函数流程,下面我再通过解析具体步骤来说一说这个流程的相关操作。
在第1步中
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) { ··· Activity a = performLaunchActivity(r, customIntent);//通过intent所携带的类名实例化出activity的对象,并将其初始化然后调用完其生命周期中的onCreate()方法 if (a != null) { r.createdConfig = new Configuration(mConfiguration); reportSizeConfigurations(r); Bundle oldState = r.state; handleResumeActivity(r.token, false, r.isForward, !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);//由这里进入view的绘制过程 ··· }
在第2步中
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) { ··· r = performResumeActivity(token, clearHide, reason);//TAG1 if (r != null) { final Activity a = r.activity; ··· boolean willBeVisible = !a.mStartedActivity; if (!willBeVisible) { try { willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible( a.getActivityToken()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } if (r.window == null && !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); View decor = r.window.getDecorView();//这里这个view就是这个activity的根view在activity的onCreate()生命周期中调用setContentView()时初始化,这里不做详细介绍,想了解的同学可以自行查阅源码 decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager();//这个实例其实是一个WindowManagerImpl对象,WindowManager是一个继承自ViewManager的子接口,WindowManagerImpl是这个接口的实现类用于蹭加view到屏幕或者由屏幕移除view if (a.mVisibleFromClient && !a.mWindowAdded) { a.mWindowAdded = true; wm.addView(decor, l);//TAG2这里所传入的decor就是之前setContentView时所初始化的View } // If the window has already been added, but during resume // we started another activity, then don't yet make the // window visible. } else if (!willBeVisible) { if (localLOGV) Slog.v( TAG, "Launch " + r + " mStartedActivity set"); r.hideForNow = true; } }
在第3步中看一下上面的TAG1
public final ActivityClientRecord performResumeActivity(IBinder token, boolean clearHide, String reason) { ActivityClientRecord r = mActivities.get(token); ··· if (r != null && !r.activity.mFinished) { ··· try { ··· r.activity.performResume();//在这个方法里面就已经执行调用了activity生命周期中的onStart()和onResume()过程 ··· } catch (Exception e) { ··· } } return r; }
在第4步中看下上面的TAG2
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { applyDefaultToken(params); mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);//这里的mGlobal对象是处于WindowManagerImpl类中的一个单例成员,且WindowManagerImpl类中的业务逻辑大部分都是通过mGlobal对象的方法来实现的,可以将WindowManagerImpl类理解成为一个框架,mGlobal对象是其具体实现类 }
在第8步中
void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);//判断是否为立即执行,是的话执行,不是的话等待vsync的回调由队列中取出Runnable进行执行 if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); } }final class TraversalRunnable implements Runnable {//这个就是上面中那个Runnable中所实现的方法 @Override public void run() { doTraversal(); }}
在第11步中
private void performTraversals() { final View host = mView; ··· if (layoutRequested) { ··· windowSizeMayChange |= measureHierarchy(host, lp, res, desiredWindowWidth, desiredWindowHeight);//测量窗口的大小是否发生改变,函数内部会调用到根view的measure方法 } ··· final boolean isViewVisible = viewVisibility == View.VISIBLE; if (mFirst || windowShouldResize || insetsChanged || viewVisibilityChanged || params != null || mForceNextWindowRelayout) { mForceNextWindowRelayout = false; ··· relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);//TAG4 ··· if (mSurfaceHolder != null) { final ThreadedRenderer hardwareRenderer = mAttachInfo.mHardwareRenderer; if (hardwareRenderer != null && hardwareRenderer.isEnabled()) { if (hwInitialized || mWidth != hardwareRenderer.getWidth() || mHeight != hardwareRenderer.getHeight() || mNeedsHwRendererSetup) { hardwareRenderer.setup(mWidth, mHeight, mAttachInfo, mWindowAttributes.surfaceInsets); mNeedsHwRendererSetup = false; } } if (!mStopped || mReportNextDraw) { boolean focusChangedDueToTouchMode = ensureTouchModeLocally( (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0); if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight() || contentInsetsChanged || updatedConfiguration) { ··· performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//计算view的大小,会调到跟view的Measure()方法 ··· } } } else { ··· } ··· if (didLayout) { performLayout(lp, mWidth, mHeight);//确定view在界面中的位置 ···· } ··· if (!cancelDraw && !newSurface) { ··· performDraw();//TAG5 } else { ··· } ··· }
在第13步中接上面的TAG4这步里面涉及的类有点多
ViewRootImpl.javaprivate int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility, boolean insetsPending) throws RemoteException { ··· int relayoutResult = mWindowSession.relayout( mWindow, mSeq, params, (int) (mView.getMeasuredWidth() * appScale + 0.5f), (int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility, insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets, mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingConfiguration, mSurface);//首先,这个mWindowSession是一个IWindowSession对象,但是我们在源码中并不能搜索到IWindowSession这个类,所以要看mWindowSession.relayout()就得找到他的实现类,所以请往下看 ··· }public ViewRootImpl(Context context, Display display) { ··· mWindowSession = WindowManagerGlobal.getWindowSession();//在ViewRootImpl构造方法中我们可以看到mWindowSession其实是取自WindowManagerGlobal的所以我们继续往下看 ··· }WindowManagerGlobal.java public static IWindowSession getWindowSession() { synchronized (WindowManagerGlobal.class) { if (sWindowSession == null) { try { InputMethodManager imm = InputMethodManager.getInstance(); IWindowManager windowManager = getWindowManagerService(); sWindowSession = windowManager.openSession( new IWindowSessionCallback.Stub() { @Override public void onAnimatorScaleChanged(float scale) { ValueAnimator.setDurationScale(scale); } }, imm.getClient(), imm.getInputContext()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } return sWindowSession;//这个是sWindowSession对象是通过WindowManagerService的openSession对象得到的 } }WindowManagerService.javapublic IWindowSession openSession(IWindowSessionCallback callback, IInputMethodClient client, IInputContext inputContext) { if (client == null) throw new IllegalArgumentException("null client"); if (inputContext == null) throw new IllegalArgumentException("null inputContext"); Session session = new Session(this, callback, client, inputContext);//在这里我们是终于看到了上面那个mWindowSession的庐山真面目,他其实是一个Session对象,他是一个binder用来连接WindowManagerService与ViewRootImpl之间的通信 return session; }
在第14步中
public int relayout(IWindow window, int seq, WindowManager.LayoutParams attrs, int requestedWidth, int requestedHeight, int viewFlags, int flags, Rect outFrame, Rect outOverscanInsets, Rect outContentInsets, Rect outVisibleInsets, Rect outStableInsets, Rect outsets, Rect outBackdropFrame, Configuration outConfig, Surface outSurface) { if (false) Slog.d(TAG_WM, ">>>>>> ENTERED relayout from " + Binder.getCallingPid()); int res = mService.relayoutWindow(this, window, seq, attrs, requestedWidth, requestedHeight, viewFlags, flags, outFrame, outOverscanInsets, outContentInsets, outVisibleInsets, outStableInsets, outsets, outBackdropFrame, outConfig, outSurface);//这里调起WindowManagerService的relayoutWindow方法 if (false) Slog.d(TAG_WM, "<<<<<< EXITING relayout to " + Binder.getCallingPid()); return res; }
在第15步中
public int relayoutWindow(Session session, IWindow client, int seq, WindowManager.LayoutParams attrs, int requestedWidth, int requestedHeight, int viewVisibility, int flags, Rect outFrame, Rect outOverscanInsets, Rect outContentInsets, Rect outVisibleInsets, Rect outStableInsets, Rect outOutsets, Rect outBackdropFrame, Configuration outConfig, Surface outSurface) { ··· synchronized(mWindowMap) { WindowState win = windowForClientLocked(session, client, false); if (win == null) { return 0; } WindowStateAnimator winAnimator = win.mWinAnimator; if (viewVisibility != View.GONE) { win.setRequestedSize(requestedWidth, requestedHeight);//设置系统请求所要设置的高度和宽度 } ··· if (viewVisibility == View.VISIBLE && (win.mAppToken == null || !win.mAppToken.clientHidden)) { ··· try { result = createSurfaceControl(outSurface, result, win, winAnimator);//当窗口可见时为其创建一个Surface绘图表面 } catch (Exception e) { ··· } ··· } else { ··· } ··· //下面这些是返回一些经过计算之后的窗口参数 outFrame.set(win.mCompatFrame); outOverscanInsets.set(win.mOverscanInsets); outContentInsets.set(win.mContentInsets); outVisibleInsets.set(win.mVisibleInsets); outStableInsets.set(win.mStableInsets); outOutsets.set(win.mOutsets); outBackdropFrame.set(win.getBackdropFrame(win.mFrame)); ··· return result; }
在第20步中是接着之前的TAG5
private void draw(boolean fullRedrawNeeded) { Surface surface = mSurface; ··· mAttachInfo.mTreeObserver.dispatchOnDraw();//通知所有view的listener要开始绘制了 ··· if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {//先判断是否需要重新绘制 if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {//判端硬件加速是否开启 ··· mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);//硬件加速开启的绘制方法 } else { ··· if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {//没开启的绘制方法 return; } } } if (animating) { mFullRedrawNeeded = true; scheduleTraversals(); } }
在第22步中
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) { // Draw with software renderer. final Canvas canvas; try { canvas = mSurface.lockCanvas(dirty);//获取一片需要绘制区域的画布 canvas.setDensity(mDensity); } catch (Surface.OutOfResourcesException e) { ··· } catch (IllegalArgumentException e) { ··· } try { ··· if (!canvas.isOpaque() || yoff != 0 || xoff != 0) { canvas.drawColor(0, PorterDuff.Mode.CLEAR);//清除这个画布中的所有内容 } dirty.setEmpty(); mIsAnimating = false; mView.mPrivateFlags |= View.PFLAG_DRAWN; if (DEBUG_DRAW) { Context cxt = mView.getContext(); Log.i(mTag, "Drawing: package:" + cxt.getPackageName() + ", metrics=" + cxt.getResources().getDisplayMetrics() + ", compatibilityInfo=" + cxt.getResources().getCompatibilityInfo()); } try { canvas.translate(-xoff, -yoff); if (mTranslator != null) { mTranslator.translateCanvas(canvas); } canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0); attachInfo.mSetIgnoreDirtyState = false; mView.draw(canvas);//执行根View的draw方法 drawAccessibilityFocusedDrawableIfNeeded(canvas); } finally { ··· } finally { try { surface.unlockCanvasAndPost(canvas);//释放这块画布 } catch (IllegalArgumentException e) { ··· } ··· } return true; }
在第25步中,走了这么久终于走到了view的draw方法!!!
public void draw(Canvas canvas) { ··· /* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. Draw the background * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content * 4. Draw children * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance) */ // Step 1, draw the background, if needed int saveCount; if (!dirtyOpaque) { drawBackground(canvas);//先画出背景 } final int viewFlags = mViewFlags; boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; if (!verticalEdges && !horizontalEdges) {//判断是否能够横向或者纵向的滑动 // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas);//这是view自己的onDraw方法,用来画出一个view自己的样子 // Step 4, draw the children dispatchDraw(canvas);//向下分发,让他的子view也去执行draw方法,这样便能将一个根view中所有的view都全部遍历并且绘制完成 // Overlay is part of the content and draws beneath Foreground if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); } // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); // we're done... return; } /* * Here we do the full fledged routine... * (this is an uncommon case where speed matters less, * this is why we repeat some of the tests that have been * done above) */ boolean drawTop = false; boolean drawBottom = false; boolean drawLeft = false; boolean drawRight = false; float topFadeStrength = 0.0f; float bottomFadeStrength = 0.0f; float leftFadeStrength = 0.0f; float rightFadeStrength = 0.0f; // Step 2, save the canvas' layers int paddingLeft = mPaddingLeft; final boolean offsetRequired = isPaddingOffsetRequired(); if (offsetRequired) { paddingLeft += getLeftPaddingOffset(); } int left = mScrollX + paddingLeft; int right = left + mRight - mLeft - mPaddingRight - paddingLeft; int top = mScrollY + getFadeTop(offsetRequired); int bottom = top + getFadeHeight(offsetRequired); if (offsetRequired) { right += getRightPaddingOffset(); bottom += getBottomPaddingOffset(); } final ScrollabilityCache scrollabilityCache = mScrollCache; final float fadeHeight = scrollabilityCache.fadingEdgeLength; int length = (int) fadeHeight; // clip the fade length if top and bottom fades overlap // overlapping fades produce odd-looking artifacts if (verticalEdges && (top + length > bottom - length)) { length = (bottom - top) / 2; } // also clip horizontal fades if necessary if (horizontalEdges && (left + length > right - length)) { length = (right - left) / 2; } if (verticalEdges) { topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength())); drawTop = topFadeStrength * fadeHeight > 1.0f; bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength())); drawBottom = bottomFadeStrength * fadeHeight > 1.0f; } if (horizontalEdges) { leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength())); drawLeft = leftFadeStrength * fadeHeight > 1.0f; rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength())); drawRight = rightFadeStrength * fadeHeight > 1.0f; } saveCount = canvas.getSaveCount(); int solidColor = getSolidColor(); if (solidColor == 0) { final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG; if (drawTop) { canvas.saveLayer(left, top, right, top + length, null, flags); } if (drawBottom) { canvas.saveLayer(left, bottom - length, right, bottom, null, flags); } if (drawLeft) { canvas.saveLayer(left, top, left + length, bottom, null, flags); } if (drawRight) { canvas.saveLayer(right - length, top, right, bottom, null, flags); } } else { scrollabilityCache.setFadeColor(solidColor); } // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Step 5, draw the fade effect and restore layers final Paint p = scrollabilityCache.paint; final Matrix matrix = scrollabilityCache.matrix; final Shader fade = scrollabilityCache.shader; if (drawTop) { matrix.setScale(1, fadeHeight * topFadeStrength); matrix.postTranslate(left, top); fade.setLocalMatrix(matrix); p.setShader(fade); canvas.drawRect(left, top, right, top + length, p); } if (drawBottom) { matrix.setScale(1, fadeHeight * bottomFadeStrength); matrix.postRotate(180); matrix.postTranslate(left, bottom); fade.setLocalMatrix(matrix); p.setShader(fade); canvas.drawRect(left, bottom - length, right, bottom, p); } if (drawLeft) { matrix.setScale(1, fadeHeight * leftFadeStrength); matrix.postRotate(-90); matrix.postTranslate(left, top); fade.setLocalMatrix(matrix); p.setShader(fade); canvas.drawRect(left, top, left + length, bottom, p); } if (drawRight) { matrix.setScale(1, fadeHeight * rightFadeStrength); matrix.postRotate(90); matrix.postTranslate(right, top); fade.setLocalMatrix(matrix); p.setShader(fade); canvas.drawRect(right - length, top, right, bottom, p); } canvas.restoreToCount(saveCount); // Overlay is part of the content and draws beneath Foreground if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); } // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); }
在第29步中,如果这个view可以滑动的时候,才会起作用
public void onDrawForeground(Canvas canvas) { onDrawScrollIndicators(canvas);//画出滑动部分的指示器 onDrawScrollBars(canvas);//画出滑动部分的滑动条 final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null; if (foreground != null) { if (mForegroundInfo.mBoundsChanged) { mForegroundInfo.mBoundsChanged = false; final Rect selfBounds = mForegroundInfo.mSelfBounds; final Rect overlayBounds = mForegroundInfo.mOverlayBounds; if (mForegroundInfo.mInsidePadding) { selfBounds.set(0, 0, getWidth(), getHeight()); } else { selfBounds.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()); } final int ld = getLayoutDirection(); Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(), foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld); foreground.setBounds(overlayBounds); } foreground.draw(canvas); } }
到这里,一个view的绘制流程在framework层的工作就结束了,他从activity的启动到一个view的draw全部在上面了,view的从开始计算到绘制都是在activity的onRemuse生命周期之后进行工作的,而且view在绘制的时候是一层一层进行绘制,先绘制父view再在其上进行子view的绘制,Choreographer这个类可以监听到vsync的信号,vsync会每16毫秒发出一次通知,告诉系统该进行屏幕的更新,这样就形成了每秒60帧的刷新率。
阅读全文
0 0
- 从源码角度解析View的绘制过程
- 从源码角度看一个activity的绘制过程
- 从源码的角度解析View的事件分发
- 从源码的角度解析View的事件分发
- 从源码的角度解析View的事件分发
- 从源码角度分析view的layout过程
- 从源码角度分析view的draw过程
- 从源码角度分析Android View的绘制机制(一)
- 从源码角度看一个view和ViewGroup的测量过程
- 从零开始的自定义View(一)——View和ViewGroup绘制过程源码解析
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上,view)
- 从源码角度解析android APP启动过程中各类及其方法的调用
- Android View的绘制之 从源码了解measure的过程。
- Android从源码解析三:View绘制流程
- Android View绘制过程,基于Framework源码解析
- Android View 绘制流程 与invalidate 和postInvalidate 分析--从源码角度
- 从源码角度解析Handler
- View的事件分发机制,从源码角度分析一下
- Java对象的内存分配过程
- 17暑假多校联赛1.11 HDU 6043 KazaQ's Socks
- springboot+mybatis+gradle在idea和oracle使用
- Android获取路由ip
- CentOS配置日志集中管理
- 从源码角度解析View的绘制过程
- JVM垃圾收集算法
- Json字符串转excel表格文件
- 4.pandas基础使用
- Xamarin.Android入门
- cms启动流程
- VMWare虚拟机提供的桥接、NAT和Host-only的区别
- 三列布局-左右固定,中间自适应
- mysql数据库优化--(4)设计 存储引擎的选择