当前位置: 首页 > news >正文

Android Framework(五)WMS-窗口显示流程——窗口布局与绘制显示

文章目录

  • relayoutWindow流程概览
  • 应用端处理——ViewRootImpl::setView -> relayoutWindow
    • ViewRootImpl::setView
    • ViewRootImpl::performTraversals
    • ViewRootImpl::relayoutWindow
  • Surface的创建
    • WindowManagerService::relayoutWindow
    • 了解容器类型和Buff类型的Surface
    • Buff类型Surface的创建与挂载
      • 设置窗口状态——DRAW_PENDIND
      • 创建与挂载“Buff”类型Surface
    • 创建Surface小结
    • WindowState “容器”概念拓展
  • 计算窗口大小与Surface放置
    • WindowSurfacePlacer::performSurfacePlacement
    • layout操作--RootWindowContainer::performSurfacePlacement
    • RootWindowContainer::applySurfaceChangesTransaction
    • layout——计算窗口大小
    • mPerformLayout 和 mPerformLayoutAttached

relayoutWindow流程概览

addWindow 流程在上一篇已经分析完了,现在 WindowManagerService 中已经有一个 WindowState 了并且也挂载到层级树中了。
但是一个窗口想要有 UI 内容需要底下的 View 树完成绘制,而 View 的绘制必须要有一个 Surface ,并且要进行绘制还需要自己的窗口在屏幕上的位置和宽高等信息。
这就是第二步 relayoutWindow 流程要做的2件事:

  • 1、为窗口申请Surface并返回给应用端
  • 2、计算返回窗口的大小,位置信息并返回给应用端。

整体流程框图如下:
在这里插入图片描述

  • ViewRootImpl 下有3个成员变量

    • mSurfaceControl 是应用端控制 Surface 的类
    • mTmpFrames 是应用端临时保存最新窗口尺寸信息的类
    • mWinFrame 是应用端真正保存窗口尺寸信息的类
  • 在触发 relayoutWindow 流程时,mSurfaceControl 和 mTmpFrames 会以出参的形式传递,在 system_service 端进行赋值

  • WindowManagerService 会与 SurfaceFlinger 通信创建 Surface 并返回给应用端

  • WindowManagerService 还会执行一次 layout 流程来重新计算所有窗口的位置和大小,并将当前这个窗口的大小位置信息返回给应用端,并设置给 mWinFrame

  • relayoutWindow 流程处理的2件事将分为2篇进行分析,本篇分析第一个处理:Surface 的创建,以及应用端的处理。

应用端处理——ViewRootImpl::setView -> relayoutWindow

回顾下应用的调用链: 窗口显示的三部曲的触发点都是在 ResumeActivityItem 事务执行到 ViewRootImpl::setView 方法触发的,调用链如下:

ViewRootImpl::setViewViewRootImpl::requestLayoutViewRootImpl::scheduleTraversals             ViewRootImpl.TraversalRunnable::run          -- Vsync相关--scheduleTraversalsViewRootImpl::doTraversalViewRootImpl::performTraversals ViewRootImpl::relayoutWindow        -- 第二步:relayoutWindowSession::relayout                -- 跨进程执行 relayoutWindow流程ViewRootImpl::updateBlastSurfaceIfNeededSurface::transferFrom         -- 应用端Surface赋值ViewRootImpl::setFrame           -- 应用端窗口大小赋值ViewRootImpl::performMeasure        -- View绘制三部曲ViewRootImpl::performLayoutViewRootImpl::performDraw        ViewRootImpl::createSyncIfNeeded    --- 第三步:绘制完成 finishDrawingWindowSession.addToDisplayAsUser                     --- 第一步:addWindow

应用端的逻辑还是从 ViewRootImpl::setView 方法开始看。

ViewRootImpl::setView

# ViewRootImpl// 重点* 1. 应用端这个View树的 Surfacepublic final Surface mSurface = new Surface();// 对应的SurfaceControlprivate final SurfaceControl mSurfaceControl = new SurfaceControl();// 临时保存最新的窗口信息private final ClientWindowFrames mTmpFrames = new ClientWindowFrames();// 当前窗口大小final Rect mWinFrame; // frame given by window manager. final IWindowSession mWindowSession;public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,int userId) {synchronized (this) {// 当前第一次执行肯定为nullif (mView == null) {mView = view;......int res; // 定义稍后跨进程add返回的结果// 重点* 3. 第二步:会触发relayoutWindowrequestLayout();  InputChannel inputChannel = null; // input事件相关if ((mWindowAttributes.inputFeatures& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {inputChannel = new InputChannel();}......try {......// 重点* 2. 第一步:addWindow流程res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,getHostVisibility(), mDisplay.getDisplayId(), userId,mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,mTempControls);......}// 后续流程与addWindow主流程无关,但是也非常重要......// 计算window的尺寸......if (res < WindowManagerGlobal.ADD_OKAY) {......// 对WMS调用后的结果判断是什么错误}......// DecorView::getParent 返回的是 ViewRootImpl 的原因view.assignParent(this);......}}}
  • 1、首先看到 ViewRootImpl 下面有2个和Surface相关的变量 mSurface,mSurfaceControl。 但是点击去会发现都没什么东西,这是因为真正的 Suface 创建是在 system_service 端触发
  • 2、调用 addToDisplayAsUser 方法触发了addWindow 流程
  • 3、本篇重点,触发 relayoutWindow

requestLayout 这个方法写App的同学可能比较熟悉,布局刷新的使用调用 View::requestLayout 虽然不是当前 ViewRootImpl 下的这个方法,但是最终也会触发 ViewRootImpl::requestLayout 的执行。

看看 ViewRootImpl::requestLayout 的代码:

# ViewRootImplboolean mLayoutRequested;@Overridepublic void requestLayout() {if (!mHandlingLayoutInLayoutRequest) {......// 只有主线程才能更新UIcheckThread();// 正确请求layoutmLayoutRequested = true;scheduleTraversals();}}void checkThread() {if (mThread != Thread.currentThread()) {throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views.");}}

这个方法主要是做了2件事:

  • 线程检查,可以看到 checkThread() 方法的报错很多写App的同学就很熟悉: 不能在子线程更新UI。
  • 执行 scheduleTraversals()
# ViewRootImplfinal TraversalRunnable mTraversalRunnable = new TraversalRunnable();//  是否在执行scheduleTraversalspublic boolean mTraversalScheduled;void scheduleTraversals() {// 如果遍历操作尚未被调度 if (!mTraversalScheduled) {// 将调度标志设置为true,表示遍历操作已被调度mTraversalScheduled = true;// 设置同步屏障 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();// 重点 * 执行mTraversalRunnablemChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);// 通知渲染器有一个新的帧即将开始处理notifyRendererOfFramePending();// 根据需要戳一下绘制锁pokeDrawLockIfNeeded();}}

这个方法虽然代码不多,但是还是有不少知识点的,比如: 同步屏障和Vsync,感兴趣的自行了解,当前不做拓展。
当前只要知道当下一个 VSync-app 到来的时候,会执行 TraversalRunnable 这个 Runnable 就好,所以重点看看这个 TraversalRunnable 做了什么。

前面看 ViewRootImpl::setView 方法的时候看到在代码顺序上是先执行 requestLayout 再执行 addToDisplayAsUser,就是因为 requestLayout 方法内部需要等待 Vsync 的到来,并且还是异步执行 Runable ,所以 addToDisplayAsUser 触发的 addWindow 流程是先于 relayoutWindow 流程执行的。

# ViewRootImplfinal class TraversalRunnable implements Runnable {@Overridepublic void run() {doTraversal();}}void doTraversal() {if (mTraversalScheduled) {// 正在执行或已经执行完毕 mTraversalScheduled = false;// 移除同步屏障mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);.....performTraversals();......}}

这里移除了同步屏障,那么 mHandler 就可以正常处理后面的消息了, 主要流程还是在 performTraversals() 中,这个方法非常重要。

ViewRootImpl::performTraversals

我手上 android 14 的源码中这个方法有 1890 行。 所以我省略了很多代码,保留了个人认为和当前学习相关的一些逻辑,本篇重点看注释的第2步 relayoutWindow 。

# ViewRootImplprivate SurfaceSyncGroup mActiveSurfaceSyncGroup;private void performTraversals() {......// mWinFrame保存的是当前窗口的尺寸Rect frame = mWinFrame;----1.1 硬绘相关----// 硬件加速是否初始化boolean hwInitialized = false;......----2. relayoutWindow流程----// 内部会将经过WMS计算后的窗口尺寸给mWinFramerelayoutResult = relayoutWindow(params, viewVisibility, insetsPending);......// 1.2 初始化硬件加速,将Surface与硬件加速绑定hwInitialized = mAttachInfo.mThreadedRenderer.initialize(mSurface);......----3. View绘制三部曲----performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);......performLayout(lp, mWidth, mHeight);......----4. finishDrawing流程----createSyncIfNeeded();......   mActiveSurfaceSyncGroup.markSyncReady();......}
  • 1、后续需要介绍软绘硬绘的流程,所以可以看到硬绘的初始化逻辑也在这个方法

  • 2、relayoutWindow 相关,也是当前分析重点

  • 3、经过第第二步 relayoutWindow 后就 View 就可以绘制了,也是需要分析的重点流程,后面会陆续写博客

  • 4、绘制完成后就要通知 SurfaceFlinger 进行合作了,finishDrawing 流程也很重要。

上面的分析有个印象就好,当前不关注其他,只看 relayoutWindow 流程,关心的太多没有重点分析对象就很容易跑偏。

ViewRootImpl::relayoutWindow

ViewRootImpl::relayoutWindow 方法如下:

# ViewRootImplpublic final Surface mSurface = new Surface();private final SurfaceControl mSurfaceControl = new SurfaceControl();// 临时保存最新的窗口信息private final ClientWindowFrames mTmpFrames = new ClientWindowFrames();// 当前窗口大小final Rect mWinFrame; // frame given by window manager. private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,boolean insetsPending) throws RemoteException {......int relayoutResult = 0;if (relayoutAsync) {// U 新增,暂时忽略mWindowSession.relayoutAsync(....);} else {// 重点* 1. 调用WMS的 relayoutWindow流程relayoutResult = mWindowSession.relayout(mWindow, ...,mTmpFrames, ..., mSurfaceControl,...);}......if (mSurfaceControl.isValid()) {if (!useBLAST()) {mSurface.copyFrom(mSurfaceControl);} else {// 重点* 2. 给mSurface赋值updateBlastSurfaceIfNeeded(); // 目前版本都走这}if (mAttachInfo.mThreadedRenderer != null) {// 注意* 置硬件加速渲染器的 SurfaceControl 和 BlastBufferQueuemAttachInfo.mThreadedRenderer.setSurfaceControl(mSurfaceControl, mBlastBufferQueue);}} else ............// 重点* 3.将WMS计算的窗口大小设置到当前setFrame(mTmpFrames.frame, true /* withinRelayout */);return relayoutResult;}
  • 1、跨进程通信触发 relayoutWindow 流程,注意这里将 mTmpFrames 和 mSurfaceControl 作为参数传递了过去。执行这个方法前 mSurfaceControl 只是一个没有实际内容的对象,但是经过 WMS::relayoutWindow 流程处理后,mSurfaceControl 就会真正持有一个 native 层的 Surface 句柄,有个这个 native 的 Surface 句柄,View 就可以把图像数据保存到Surface 中了。
  • 2、将 mSurfaceControl 下的 Surface 赋值给当前的变量 mSurface
  • 3、relayoutWindow 流程后 mTmpFrames 就有最新的尺寸信息了,需要赋值给真正保存窗口尺寸的变量 mWinFrame

在看主流程之前先看一下 ViewRootImpl::updateBlastSurfaceIfNeeded 方法:

# ViewRootImplprivate BLASTBufferQueue mBlastBufferQueue;void updateBlastSurfaceIfNeeded() {// 经过system_service处理后的mSurfaceControl有值if (!mSurfaceControl.isValid()) {return;}......// 创建对象mBlastBufferQueue = new BLASTBufferQueue(mTag, mSurfaceControl,mSurfaceSize.x, mSurfaceSize.y, mWindowAttributes.format);mBlastBufferQueue.setTransactionHangCallback(sTransactionHangCallback);Surface blastSurface = mBlastBufferQueue.createSurface();// Only call transferFrom if the surface has changed to prevent inc the generation ID and// causing EGL resources to be recreated.// 给当前mSurface赋值mSurface.transferFrom(blastSurface);}

现在知道了 relayoutWindow 流程执行后拿应用端拿到到 Surface 的和尺寸信息一些处理,需要回头正式看一下 relayoutWindow 流程到底做了什么。

Surface的创建

在【WindowContainer窗口层级-Surface树】中提过 SurfaceFlinger 层也映射了一个 Surface 树,还知道了“容器”类型和“Buff”类型 Surface 的区别。

只有“Buff”类型 Surface 才可以显示 UI 内容,relayoutWindow 流程的目的就是为创建创建一个“Buff”类型 Layer 。

addWindow 后 SurfaceFlinger 层也是创建的 WindowState 对应的 Layer ,但是实际上 WindowState 下面还有一个“Buff”类型 Layer ,这一步就是 relayoutWindow 流程创建的。

在这里插入图片描述
下面完整介绍 relayoutWindow 流程是如何创建“Buff”类型 Layer 的。

WindowManagerService::relayoutWindow

应用端端通过 Session 与 system_service 端通信。

# Session@Overridepublic int relayout(IWindow window, ...ClientWindowFrames outFrames,...SurfaceControl outSurfaceControl,...) {......int res = mService.relayoutWindow(this, window, attrs,requestedWidth, requestedHeight, viewFlags, flags,outFrames, mergedConfiguration, outSurfaceControl, outInsetsState,outActiveControls, outSyncSeqIdBundle);......return res;}

主要就是调用到通过 Session 调用到 WindowManagerService::relayoutWindow 方法,上面看到 ViewRootImpl 的 mSurface 和mSurfaceControl 对象都是直接创建的,然后将mSurfaceControl 专递到了 WMS ,这里注意在 Session::relayout 方法的参数中应用端传过来的 mSurfaceControl 变成了:outSurfaceControl,说明这是个出参会在 WindowManagerService::relayoutWindow 方法对其进行真正的赋值。

WindowManagerService::relayoutWindow代码如下:

# WindowManagerServicepublic int relayoutWindow(Session session, IWindow client, LayoutParams attrs,int requestedWidth, int requestedHeight, int viewVisibility, int flags, int seq,int lastSyncSeqId, ClientWindowFrames outFrames,MergedConfiguration outMergedConfiguration, SurfaceControl outSurfaceControl,InsetsState outInsetsState, InsetsSourceControl.Array outActiveControls,Bundle outSyncIdBundle) {......synchronized (mGlobalLock) {// 重点* 从mWindowMap中获取WindowStatefinal WindowState win = windowForClientLocked(session, client, false);if (win == null) {return 0;}......if (viewVisibility != View.GONE) {// 把应用端请求的大小,保存到WindowState下win.setRequestedSize(requestedWidth, requestedHeight);}......if (attrs != null) {// 调整窗口属性和类型displayPolicy.adjustWindowParamsLw(win, attrs);......}.......// 设置窗口可见 viewVisibility = VISIBLEwin.setViewVisibility(viewVisibility);// 打印Proto日志ProtoLog.i(WM_DEBUG_SCREEN_ON,"Relayout %s: oldVis=%d newVis=%d. %s", win, oldVisibility,viewVisibility, new RuntimeException().fillInStackTrace());......if (shouldRelayout && outSurfaceControl != null) {try {// 重点* 1. 创建SurfaceControlresult = createSurfaceControl(outSurfaceControl, result, win, winAnimator);} catch (Exception e) {......return 0;}}// 重点* 2. 计算窗口大小 (极其重要的方法)mWindowPlacerLocked.performSurfacePlacement(true /* force */);......if (focusMayChange) {// 更新焦点if (updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL, true /*updateInputWindows*/)) {imMayMove = false;}}......// 重点* 3. 填充WMS计算好后的数据,返回应用端win.fillClientWindowFramesAndConfiguration(outFrames, outMergedConfiguration,false /* useLatestConfig */, shouldRelayout);......}Binder.restoreCallingIdentity(origId);return result;}
  • 1、WindowManagerService::windowForClientLocked 方法是从 mWindowMap 去获取 WindowState ,这就体现出 addWindow 流程中首次看到 mWindowMap 的重要性了。

然后 setViewVisibility 设置可见性了,这里的参数是传过来的,根据打印的 ProtoLog:

09-25 14:10:36.963 10280 16547 I WindowManager: Relayout Window{2fa12a u0 com.example.myapplication/com.example.myapplication.MainActivity2}: oldVis=4 newVis=0. java.lang.RuntimeException

值为0,也就是 VISIBLE 。

这个方法在 WMS 中是个核心方法,注释都在代码中了,当前分析的 relayoutWindow 流程,所以主要跟踪下面3个执行逻辑:

  • 1、createSurfaceControl : 创建“Buff”类型的Surface
  • 2、performSurfacePlacement :窗口的摆放 (View一般有变化也要执行 layout,WMS在管理窗口这边肯定也要执行layout)
  • 3、fillClientWindowFramesAndConfiguration :将计算好的窗口尺寸返回给应用端

了解容器类型和Buff类型的Surface

看调用栈一般除了debug外,还可以在关键点加上堆栈,比如在SurfaceControl的构造方法加堆栈,只要有触发创建SurfaceControl的地方必然会打印,然后发现有以下2个输出(模拟的场景是在MainActivity点击按钮启动MainActivity2)
addWindow触发的堆栈:

09-25 19:42:46.028 13422 14723 E biubiubiu: SurfaceControl mName: 4e72d78 com.example.myapplication/com.example.myapplication.MainActivity2  mCallsiteWindowContainer.setInitialSurfaceControlProperties
09-25 19:42:46.028 13422 14723 E biubiubiu: java.lang.Exception
09-25 19:42:46.028 13422 14723 E biubiubiu: 	at android.view.SurfaceControl.<init>(SurfaceControl.java:1580)
09-25 19:42:46.028 13422 14723 E biubiubiu: 	at android.view.SurfaceControl.<init>(Unknown Source:0)
09-25 19:42:46.028 13422 14723 E biubiubiu: 	at android.view.SurfaceControl$Builder.build(SurfaceControl.java:1240)
09-25 19:42:46.028 13422 14723 E biubiubiu: 	at com.android.server.wm.WindowContainer.setInitialSurfaceControlProperties(WindowContainer.java:630)
09-25 19:42:46.028 13422 14723 E biubiubiu: 	at com.android.server.wm.WindowContainer.createSurfaceControl(WindowContainer.java:626)
09-25 19:42:46.028 13422 14723 E biubiubiu: 	at com.android.server.wm.WindowContainer.onParentChanged(WindowContainer.java:607)
09-25 19:42:46.028 13422 14723 E biubiubiu: 	at com.android.server.wm.WindowContainer.onParentChanged(WindowContainer.java:594)
09-25 19:42:46.028 13422 14723 E biubiubiu: 	at com.android.server.wm.WindowState.onParentChanged(WindowState.java:1341)
09-25 19:42:46.028 13422 14723 E biubiubiu: 	at com.android.server.wm.WindowContainer.setParent(WindowContainer.java:584)
09-25 19:42:46.028 13422 14723 E biubiubiu: 	at com.android.server.wm.WindowContainer.addChild(WindowContainer.java:730)
09-25 19:42:46.028 13422 14723 E biubiubiu: 	at com.android.server.wm.WindowToken.addWindow(WindowToken.java:302)
09-25 19:42:46.028 13422 14723 E biubiubiu: 	at com.android.server.wm.ActivityRecord.addWindow(ActivityRecord.java:4248)
09-25 19:42:46.028 13422 14723 E biubiubiu: 	at com.android.server.wm.WindowManagerService.addWindow(WindowManagerService.java:1814)
09-25 19:42:46.028 13422 14723 E biubiubiu: 	at com.android.server.wm.Session.addToDisplayAsUser(Session.java:215)

relayoutWindow触发的堆栈:

09-25 19:42:46.036 13422 14723 E biubiubiu: SurfaceControl mName: com.example.myapplication/com.example.myapplication.MainActivity2  mCallsiteWindowSurfaceController
09-25 19:42:46.036 13422 14723 E biubiubiu: java.lang.Exception
09-25 19:42:46.036 13422 14723 E biubiubiu: 	at android.view.SurfaceControl.<init>(SurfaceControl.java:1580)
09-25 19:42:46.036 13422 14723 E biubiubiu: 	at android.view.SurfaceControl.<init>(Unknown Source:0)
09-25 19:42:46.036 13422 14723 E biubiubiu: 	at android.view.SurfaceControl$Builder.build(SurfaceControl.java:1240)
09-25 19:42:46.036 13422 14723 E biubiubiu: 	at com.android.server.wm.WindowSurfaceController.<init>(WindowSurfaceController.java:109)
09-25 19:42:46.036 13422 14723 E biubiubiu: 	at com.android.server.wm.WindowStateAnimator.createSurfaceLocked(WindowStateAnimator.java:335)
09-25 19:42:46.036 13422 14723 E biubiubiu: 	at com.android.server.wm.WindowManagerService.createSurfaceControl(WindowManagerService.java:2686)
09-25 19:42:46.036 13422 14723 E biubiubiu: 	at com.android.server.wm.WindowManagerService.relayoutWindow(WindowManagerService.java:2449)
09-25 19:42:46.036 13422 14723 E biubiubiu: 	at com.android.server.wm.Session.relayout(Session.java:267)

发现2个地方创建了 SurfaceControl ,而且看名字都是为 MainActivity2 创建的,区别就是调用栈不同,和一个是带 "4e72d78 "这种对象名的,这让我很好奇,然后我立马想到这种类型之前在窗口层级树中见过。于是 dump 了层级树的信息。

在这里插入图片描述
果然就是以WindowState的名字取的,看调用栈在addWindow的时候将这个WindowState添加到层级树的时候就创建了。后面的“mCallsiteWindowContainer.setInitialSurfaceControlProperties”2个调用栈输出的也不同,代表的是调用的地方。
这就很奇怪了,在 addWindow 的时候就创建好了 SurfaceControl 为什么执行 relayoutWindow 的时候又创建一个?那到底是用的哪个呢?
我用 Winscope 看了 trace 后发现原来是下面这个结构:
在这里插入图片描述
原来下面创建的才是真正可见的,而带 "4e72d78 "的则是作为 parent ,dump 一下 SurfaceFlinger 看一下:
在这里插入图片描述
发现带"4e72d78 " 的是 ContainerLayer 类型,而下面的是 BufferStateLayer 类型,也是作为其孩子的存在,我们知道 BufferStateLayer 类型的才是真正绘制显示数据的 Surface 。

原来在 addWindow 流程中,将 WindowState 挂在到层级树中就创建了一个容器类型的 SurfaceControl ,而后在执行 WindowManagerService::relayoutWindow 又创建了一个BufferStateLayer 类型的 SurfaceControl 用来做真正的显示数据。

Buff类型Surface的创建与挂载

relayoutWindow的调用链如下:

WindowManagerService::relayoutWindowWindowManagerService::createSurfaceControlWindowStateAnimator::createSurfaceLocked -- 创建“Buff” 类型LayerWindowStateAnimator::resetDrawState   -- DRAW_PENDINGWindowSurfaceController::initSurfaceControl.Builder::buildSurfaceControl::initWindowSurfaceController::getSurfaceControl  -- 给应用端Surface赋值

在这里插入图片描述
开始撸代码,WindowManagerService::relayoutWindow 下调用 createSurfaceControl 方法有4个参数。

# WindowManagerService public int relayoutWindow(Session session, IWindow client, LayoutParams attrs,int requestedWidth, int requestedHeight, int viewVisibility, int flags,ClientWindowFrames outFrames, MergedConfiguration mergedConfiguration,SurfaceControl outSurfaceControl, InsetsState outInsetsState,InsetsSourceControl[] outActiveControls, Bundle outSyncIdBundle) {......result = createSurfaceControl(outSurfaceControl, result, win, winAnimator);......}

createSurfaceControl 方法有4个参数:

  • outSurfaceControl: WMS 创建好一个 Surface 后,还需要返回给应用端用于 View 的绘制,就是通过这个参数,由参数命名也可以知道这是一个“出参”。
  • result: 方法执行结果
  • win: 当前窗口对应的WindowState,稍后创建Surface会挂载到这个WindowState节点之下
  • winAnimator:WindowStateAnimator对象,管理窗口状态和动画,稍后通过其内部方法创建Surface
# WindowManagerServiceprivate int createSurfaceControl(SurfaceControl outSurfaceControl, int result,WindowState win, WindowStateAnimator winAnimator) {if (!win.mHasSurface) {result |= RELAYOUT_RES_SURFACE_CHANGED;}// 1. 创建WindowSurfaceController对象WindowSurfaceController surfaceController;try {// 2. 创建“Buff”类型SurfaceTrace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "createSurfaceControl");surfaceController = winAnimator.createSurfaceLocked();} finally {Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);}if (surfaceController != null) {// 3. 出参给应用端surfaceController.getSurfaceControl(outSurfaceControl);// 打印日志,outSurfaceControl复制到了framework的值ProtoLog.i(WM_SHOW_TRANSACTIONS, "OUT SURFACE %s: copied", outSurfaceControl);}......return result;}

这个方法主要有三步,都是围绕着 WindowSurfaceController 来的:

  • 1、先创建出一个WindowSurfaceController 对象 surfaceController
  • 2、通过 WindowStateAnimator::createSurfaceLocked 对 surfaceController 赋值,根据方法名猜测是创建了一个 Surface
  • 3、通过 WindowSurfaceController::getSurfaceControl,给应用端 Surface 赋值

这么看来重点是在第二步 WindowStateAnimator::createSurfaceLocked 是如何创建 Surface 的。

# WindowStateAnimatorWindowSurfaceController mSurfaceController;// WindowState的状态int mDrawState;WindowSurfaceController createSurfaceLocked() {final WindowState w = mWin;if (mSurfaceController != null) {return mSurfaceController;}w.setHasSurface(false);// 打印窗口状态ProtoLog.i(WM_DEBUG_ANIM, "createSurface %s: mDrawState=DRAW_PENDING", this);// 重点* 1. 重置窗口状态  -- DRAW_PENDINGresetDrawState();......// 重点* 2. 创建WindowSurfaceControllermSurfaceController = new WindowSurfaceController(attrs.getTitle().toString(), format,flags, this, attrs.type);......return mSurfaceController;}

这里有2个重点:

  • 1、设置窗口状态为 DRAW_PENDING
  • 2、创建Surface

设置窗口状态——DRAW_PENDIND

# WindowStateAnimatorvoid resetDrawState() {// 设置windowState状态为DRAW_PENDINGmDrawState = DRAW_PENDING;if (mWin.mActivityRecord == null) {return;}if (!mWin.mActivityRecord.isAnimating(TRANSITION)) {mWin.mActivityRecord.clearAllDrawn();}}

WindowState有很多状态,以后会单独说,这里需要注意:

  • 1、WindowState 状态是保存在 WindowStateAnimator 中
  • 2、WindowStateAnimator::createSurfaceLocked 方法会将 WindowState 状态设置为 DRAW_PENDING 表示等待绘制

创建与挂载“Buff”类型Surface

# WindowSurfaceControllerSurfaceControl mSurfaceControl;WindowSurfaceController(String name, int format, int flags, WindowStateAnimator animator,int windowType) {mAnimator = animator;// 1. 也会作为Surface的nametitle = name;mService = animator.mService;// 2. 拿到WindowStatefinal WindowState win = animator.mWin;mWindowType = windowType;mWindowSession = win.mSession;Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "new SurfaceControl");// 3. 重点* 构建Surface(也是通过makeSurface 方法)final SurfaceControl.Builder b = win.makeSurface().setParent(win.getSurfaceControl()) // 设置为父节点.setName(name) //设置name.setFormat(format).setFlags(flags).setMetadata(METADATA_WINDOW_TYPE, windowType).setMetadata(METADATA_OWNER_UID, mWindowSession.mUid).setMetadata(METADATA_OWNER_PID, mWindowSession.mPid).setCallsite("WindowSurfaceController");final boolean useBLAST = mService.mUseBLAST && ((win.getAttrs().privateFlags& WindowManager.LayoutParams.PRIVATE_FLAG_USE_BLAST) != 0);// 高版本都为BLASTif (useBLAST) {// 4. 重点* 设置为“Buff”图层b.setBLASTLayer();}// 触发buildmSurfaceControl = b.build();Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);}

分我4步:

  • 1、第一个参数传递的字符串最终也会作为Surface的name
  • 2、获取到 WindowState 对象,后面会设置为创建 Surface 的父节点
  • 3、构建出一个 Surface 对象, 注意 name 和父节点的设置。 另外可以知道也是通过 makeSurface() 方法构建的, 这个方法会构建出一个“容器”类型的 Surface。
  • 4、将 Surface 设置为“Buff”类型,这个非常重要,因为上一步默认还是“容器”类型,所以需要设置成“Buff”类型,再后面就是 build 出一个 Surface 了。

那么到这里 Surface 的创建就完成了,这里可能有的人如果对 Surface 知识不太清楚的话会比较迷糊,WindowSurfaceController,SurfaceController,Surface 到底是什么关系,这个不在当前流程的重点,暂且理解为同级吧,在 java 层这些都是空格,都是靠内部的 native 指针或者句柄持有底层对象。
在这里插入图片描述

  • 1、WindowSurfaceController 和 SurfaceController 在java层是持有关系。
  • 2、SurfaceController 创建的时候,会触发 native 层创建一个 SurfaceController 并返回句柄给 java 层,同时还会触发一个 Layer 的创建
  • 3、BLASTBufferQueue 的构建依赖一个 SurfaceController
  • 4、BLASTBufferQueue::createSurface 方法会创建一个 Surface 并返回指针给上层
  • 5、java 层的 Surface 靠指针找到 native 层的 Surface

最后再来看一下 WMS 这边创建好后的 Surface 是如何设置给应用端的,也就是如何设置给 relayoutWindow 的参数 outSurfaceControl 。
这一步在 WindowManagerService::createSurfaceControl 放中执行 WindowSurfaceController::getSurfaceControl 时完成。

# WindowSurfaceControllervoid getSurfaceControl(SurfaceControl outSurfaceControl) {// 将framework层的SurfaceControl copy给应用层传递过来的outSurfaceControloutSurfaceControl.copyFrom(mSurfaceControl, "WindowSurfaceController.getSurfaceControl");}

这样一来应用端就有了可以保持绘制数据的 Surface ,然后就可以执行 View 树的绘制了。

创建Surface小结

对于Surface的知识是一个复杂的模块,是需要单独详细讲解的,目前可以知道的是原以为给 WindowState 创建图层就是一个,但是实际上发现创建了2个。

1、WindowState 本身对应的是“容器”类型的 Surface ,在“addWindow流程”就创建了,而 relayoutWindow 创建的是一个“BufferStateLayer”类型的 Surface 这个也是被 copy 到应用层的Surface ,说明应用层的数据是被绘制在这个 Surface上 的

2、“BufferStateLayer”类型 Surface 的创建会先创建一个 WindowSurfaceController 对象,然后内部会创建 SurfaceController 。从 WindowSurfaceController 这个类名也能看出来是针对 Window显 示的

3、不仅仅 Framework 层的层级树有容器概念,SurfaceFlinger 里的 Layer 树也有容器概念

4、我们在执行adb shell dumpsys activity containers 看到层级结构树,最底层的 WindowState 其实也是个容器,不是真正显示的地方。这个点从 “containers”也能理解,毕竟是容器树。

WindowState “容器”概念拓展

WindowState是容器这个是肯定的,也是WindowContainer子类,然后他的孩子也是WindowState定义如下:

# WindowStatepublic class WindowState extends WindowContainer<WindowState> implementsWindowManagerPolicy.WindowState, InsetsControlTarget, InputTarget {}

那么什么场景下 WindowState 下还有孩子呢?答案是子窗口,子窗口的定义在 Window 类型里,具体的不在当前讨论,之前我一直有个误区,一直以为弹出的 Dialog 是子窗口,但是实际上并不是,我目前找到了一个比较常见的子窗口是 PopupWindow。
以在google电话应用打开一个菜单为例
对应的dump 为:
在这里插入图片描述
看的到子窗口PopupWindow的WindowState是被挂载到Activity的WindowState下 对应的winscope trace为:
在这里插入图片描述
这里能看到 PopupWindow 也有一个容器图层和显示图层,容器图层挂载在 Activity 窗口容器图层下,和 Activity 下的窗口显示图层同级。

计算窗口大小与Surface放置

上篇说过 relayoutWindow 流程主要做了两件事:

  • 1、通过 createSurfaceControl 创建SurfaceControl
  • 2、通过 WindowSurfacePlacer::performSurfacePlacement 计算窗口大小和摆放Surface

这一节主要分析核心方法:WindowSurfacePlacer::performSurfacePlacement

根据上篇的分析,relayoutWindow 流程会触发 WindowSurfacePlacer::performSurfacePlacement 的执行,需要注意的是会触发执行这个方法的逻辑非常多,为什么呢?

比如写 App 的时候界面有变化了,或者我们手动执行“View::requestLayout”就会触发界面重绘,也就是执行 View 绘制三部曲,其中第二步就是 layout。那为什么要执行 layout 呢?
因为界面上有 View 的添加移除,或者横竖屏切换等情况,那其他 View 的位置很可能就会受影响,所以为了保证界面上 View 所在位置的正确,就会触发一次 layout 重新计算每个 View 所在的位置。
触发 layout 是 ViewRootImpl 触发的,他只需要对整个 View 树的 RootView(DecorView)触发layout就行,然后 RootView(DecorView)内部会递归触发整个 View 树的 layout逻辑,从而保证整个 View 树的每一个 View 都出在正确的位置。

这里提取2个 View 层 layout 逻辑的信息:

  • 1、目的是确保界面View树中的各个View处在正确的位置。
  • 2、触发逻辑是从RootView(DecorView) 开始递归执行

其实 WMS 对窗口的管理也是和 View 管理的一样的,窗口有添加移除,或者屏幕旋转等场景,为了确保手机屏幕上各个窗口处在正确的位置,显示正确的大小。所以会触发执行 WindowSurfacePlacer::performSurfacePlacement 方法。

作为 WMS 的核心方法之一,WindowSurfacePlacer::performSurfacePlacement 方法做的事情其实也远不仅这些,但是当前是分析 relayoutWindow 流程进入了这个方法做的事,所以还是只关心 relayoutWindow 流程触发需要做哪些事情就好。

然后对这个方法有个印象,因为以后会经常看这个方法,毕竟界面上有点风吹操作都要触发 WindowSurfacePlacer::performSurfacePlacement 方法的执行。

本篇的调用链和时序图如下:

WindowSurfacePlacer::performSurfacePlacementWindowSurfacePlacer::performSurfacePlacementLoopRootWindowContainer::performSurfacePlacementRootWindowContainer::performSurfacePlacementNoTraceRootWindowContainer::applySurfaceChangesTransactionDisplayContent::applySurfaceChangesTransactionDisplayContent::performLayout DisplayContent::performLayoutNoTraceDisplayContent::mPerformLayout DisplayPolicy::layoutWindowLw WindowLayout::computeFrames   -- 计算窗口大小,保存在 sTmpClientFrames中WindowState::setFrames        -- 将计算结果 sTmpClientFrames 的数据设置给窗口

在这里插入图片描述

WindowSurfacePlacer::performSurfacePlacement

# WindowSurfacePlacer// 控制是否需要继续执行 performSurfacePlacementLoop方法private boolean mTraversalScheduled;// 延迟layout就会+1private int mDeferDepth = 0;final void performSurfacePlacement(boolean force) {if (mDeferDepth > 0 && !force) {mDeferredRequests++;return;}// 最大次数循环为6次int loopCount = 6;do {// 设置为falsemTraversalScheduled = false;// 重点方法performSurfacePlacementLoop();// 移除Handler的处理mService.mAnimationHandler.removeCallbacks(mPerformSurfacePlacement);loopCount--;// 结束条件为 mTraversalScheduled 不为false 和 loopCount大于0 ,也就最多6次} while (mTraversalScheduled && loopCount > 0);mService.mRoot.mWallpaperActionPending = false;}

relayoutWindow 方法调用的是传递的参数是 true ,那第一个if是走不进去的,主要看后面的逻辑控制。

里面的 performSurfacePlacementLoop() 方法是重点,在分析这个方法之前,先确认下停止循环的2个条件:

  • 1、loopCount > 0 这个条件比较简单,这个循环最多执行6次,为什么是6咱也不知道,查了一下说是google工程师根据经验设置,如果执行6次循环还没处理好Surface那肯定是出现问题了。(debug发现一般就执行1次,最多看到执行2次的情况)
  • 2、mTraversalScheduled 这个变量但是执行循环的时候就设置为 false ,说明正常情况下执行一次就不需要再执行了。

这里也其实要注意,bool 类型默认是 false ,所以找到在是哪里将 mTraversalScheduled 设置为 true,其实就是找到了什么情况下需要执行 performSurfacePlacementLoop 方法。

继续下一步 performSurfacePlacementLoop 方法的内容。

# WindowSurfacePlacerprivate void performSurfacePlacementLoop() {......// 重点*1. 对所有窗口执行布局操作mService.mRoot.performSurfacePlacement();// 布局完成mInLayout = false;// 若需要布局,(Root检查每个DC是否需要)if (mService.mRoot.isLayoutNeeded()) {if (++mLayoutRepeatCount < 6) {// 重点*2. 布局次数小于6次,则需要再次请求布局requestTraversal();} else {Slog.e(TAG, "Performed 6 layouts in a row. Skipping");mLayoutRepeatCount = 0;}} else {mLayoutRepeatCount = 0;}   }

1、执行 RootWindowContainer::performSurfacePlacement 这个代表对屏幕进行一次 layout ,后续的分析都在这
2、如果“mService.mRoot.isLayoutNeeded()”满足就执行 requestTraversal ,这个方法会将 mTraversalScheduled 变量设置为 true
3、“mService.mRoot.isLayoutNeeded()” 的返回其实受 DisplayContent 下的 mLayoutNeeded 变量控制,有任意一个屏幕为 true 就说明还需要一次 layout

主流程等会再看,先看一下 WindowSurfacePlacer::requestTraversal 的逻辑。

# WindowSurfacePlacervoid requestTraversal() {if (mTraversalScheduled) {return;}// Set as scheduled even the request will be deferred because mDeferredRequests is also// increased, then the end of deferring will perform the request.// 还需要一次mTraversalScheduled = true;if (mDeferDepth > 0) {mDeferredRequests++;if (DEBUG) Slog.i(TAG, "Defer requestTraversal " + Debug.getCallers(3));return;}// 通过Handler触发mService.mAnimationHandler.post(mPerformSurfacePlacement);}

当前不必纠结与执行几次的逻辑,这里的东西还挺复杂的,还会涉及到延迟 layout 等待,当前这块以简单的场景看就好了。

给个不是很精确但是覆盖百分之95以上场景的结论:
performSurfacePlacement 就是一次窗口的 layout ,是不是还有再执行的条件控制在 DisplayContent 下的 mLayoutNeeded 变量控制。如果这个变量为 false 则说明这次 layout 还有事情没完成,还要再来一次。

具体几次6次8次的没必要过于纠结,但是需要注意 “mService.mRoot.isLayoutNeeded()”如果不满足,则就不会再次触发了,对这些有个印象即可,主要看 RootWindowContainer::performSurfacePlacement 方法。

layout操作–RootWindowContainer::performSurfacePlacement

# RootWindowContainer// 这个方法加上了tracevoid performSurfacePlacement() {Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "performSurfacePlacement");try {performSurfacePlacementNoTrace();} finally {Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);}}// 主要干活的还是这个void performSurfacePlacementNoTrace() {......// 1. 如果需要,则更新焦点if (mWmService.mFocusMayChange) {mWmService.mFocusMayChange = false;mWmService.updateFocusedWindowLocked(UPDATE_FOCUS_WILL_PLACE_SURFACES, false /*updateInputWindows*/);}......// TraceTrace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "applySurfaceChanges");// 开启事务mWmService.openSurfaceTransaction();try {// 2.. 处理事务(执行窗口尺寸计算,surface状态变更等操作)applySurfaceChangesTransaction();} catch (RuntimeException e) {Slog.wtf(TAG, "Unhandled exception in Window Manager", e);} finally {// 关闭事务,做事务提交mWmService.closeSurfaceTransaction("performLayoutAndPlaceSurfaces");Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);}......// 3. Activity 切换事务处理,// 条件满足也会将窗口状态设置为HAS_DRAW 流程checkAppTransitionReady(surfacePlacer);......// 再次判断是否需要处理焦点变化if (mWmService.mFocusMayChange) {mWmService.mFocusMayChange = false;mWmService.updateFocusedWindowLocked(UPDATE_FOCUS_PLACING_SURFACES,false /*updateInputWindows*/);}......// 4. 如果过程中size或者位置变化,则通知客户端重新relayouthandleResizingWindows();......// 5. 销毁不可见的窗口i = mWmService.mDestroySurface.size();if (i > 0) {do {i--;WindowState win = mWmService.mDestroySurface.get(i);win.mDestroying = false;final DisplayContent displayContent = win.getDisplayContent();if (displayContent.mInputMethodWindow == win) {displayContent.setInputMethodWindowLocked(null);}if (displayContent.mWallpaperController.isWallpaperTarget(win)) {displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;}win.destroySurfaceUnchecked();} while (i > 0);mWmService.mDestroySurface.clear();}......}

这个方法处理的事情非常多

  • 1、焦点相关
  • 2、applySurfaceChangesTransaction 这个是当前分析的重点,主要是处理窗口大小和Surface的
  • 3、checkAppTransitionReady 是处理 Activity 切换的事务
  • 4、如果这次 layout 有窗口尺寸改变了,就需要窗口进行 resize 操作
  • 5、销毁掉不需要的窗口

这个方法比较长,干的事也比较多,而且执行的频率也很高。(可以自己加个log或者抓trace看一下)

其他的流程目前不看,只关系 applySurfaceChangesTransaction 方法。
可以看到在执行 applySurfaceChangesTransaction 方法的前后都 SurfaceTransaction 的打开和关闭,那说明这个方法内部肯定是有 Surface 事务的处理。

后面的 RootWindowContainer::applySurfaceChangesTransaction 方法是 relayoutWindow 流程的核心,在看后面之前,先对前面这块比较混乱流程整理一个流程图:

在这里插入图片描述

RootWindowContainer::applySurfaceChangesTransaction

# RootWindowContainerprivate void applySurfaceChangesTransaction() {......// 正常情况就一个屏幕final int count = mChildren.size();for (int j = 0; j < count; ++j) {final DisplayContent dc = mChildren.get(j);dc.applySurfaceChangesTransaction();}......}

遍历每个屏幕执行 DisplayContent::applySurfaceChangesTransaction

# DisplayContentprivate final LinkedList<ActivityRecord> mTmpUpdateAllDrawn = new LinkedList();void applySurfaceChangesTransaction() {......// 置空mTmpUpdateAllDrawn.clear();......// 重点* 1. 执行布局,该方法最终会调用performLayoutNoTrace,计算窗口的布局参数performLayout(true /* initial */, false /* updateInputWindows */);pendingLayoutChanges = 0;Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "applyPostLayoutPolicy");try {mDisplayPolicy.beginPostLayoutPolicyLw();// 对所有窗口执行布局策略forAllWindows(mApplyPostLayoutPolicy, true /* traverseTopToBottom */);mDisplayPolicy.finishPostLayoutPolicyLw();} finally {Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);}......Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "applyWindowSurfaceChanges");try {// 重点* 2. 遍历所有窗口,主要是改变窗口状态设置为READY_TO_SHOW,当前逻辑不满足,不会执行最终设置forAllWindows(mApplySurfaceChangesTransaction, true /* traverseTopToBottom */);} finally {Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);}// 重点* 3. finishDrawing()流程,条件满足触发提交Surface到SurfaceFlinger prepareSurfaces();......}

这个方法其实有3个重点,但是因为当前分析的是窗口执行 relayoutWindow 过来的逻辑,窗口下的 View 应用端都还没进行绘制,所以后面2个重点内部都会因为条件不满足被 return 。
不过2个重点也是后面学习需要分析的流程,所以先留下个印象。

Framework的流程很复杂,基本上没有一行代码是多余的,如果每个代码都看,每个分支都认真分析,那可能只有AI能完成了,所以分析某个流程只关心流程中主要代码就可以了。

当前流程主要关注1个重点:

performLayout :最终会执行创建大小的计算。

layout——计算窗口大小

# DisplayContent// 标记是否需要执行layoutprivate boolean mLayoutNeeded;boolean isLayoutNeeded() {return mLayoutNeeded;}// 根据前面流程,如果为false则表示不会执行循环private void clearLayoutNeeded() {if (DEBUG_LAYOUT) Slog.w(TAG_WM, "clearLayoutNeeded: callers=" + Debug.getCallers(3));mLayoutNeeded = false;}void performLayout(boolean initial, boolean updateInputWindows) {// 加上traceTrace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "performLayout");try {performLayoutNoTrace(initial, updateInputWindows);} finally {Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);}}// 主要方法private void performLayoutNoTrace(boolean initial, boolean updateInputWindows) {// 判断是否需要布局,不需要则直接返回,内部是通过mLayoutNeeded判断if (!isLayoutNeeded()) {return;}// 将mLayoutNeeded设置为flaseclearLayoutNeeded();......// 重点* 1. 对所有顶级窗口进行布局forAllWindows(mPerformLayout, true /* traverseTopToBottom */);// 重点* 2. 处理子窗口的布局forAllWindows(mPerformLayoutAttached, true /* traverseTopToBottom */);......}

DisplayContent::performLayout 方法也是为了加 trace 所以还是得看 DisplayContent::performLayoutNoTrace 方法,主要就是2个forAllWindows,这个方法在之前 Activity 启动的流程讲过类似的,就是将按照第二个参数的顺序,从上到下或者从下至上遍历每个 Window 让其执行第一个参数的 lambda 表达式,所以只要看看具体的 lambda 表达式即可。

mPerformLayout 和 mPerformLayoutAttached

# DisplayContentprivate final Consumer<WindowState> mPerformLayout = w -> {// 如果当前窗口为子窗口则直接返回if (w.mLayoutAttached) {return;}// 先判断当前窗口是否会不可见final boolean gone = w.isGoneForLayout();// 如果窗口不是不可见的,或者窗口没有框架,或者窗口需要布局if (!gone || !w.mHaveFrame || w.mLayoutNeeded) {......// 重点*1. 调用DisplayPolicy::layoutWindowLwgetDisplayPolicy().layoutWindowLw(w, null, mDisplayFrames);......if (DEBUG_LAYOUT) Slog.v(TAG, "  LAYOUT: mFrame=" + w.getFrame()+ " mParentFrame=" + w.getParentFrame()+ " mDisplayFrame=" + w.getDisplayFrame());}};private final Consumer<WindowState> mPerformLayoutAttached = w -> {// 如果不是子窗口则返回if (!w.mLayoutAttached) {return;}if ((w.mViewVisibility != GONE && w.mRelayoutCalled) || !w.mHaveFrame|| w.mLayoutNeeded) {......getDisplayPolicy().layoutWindowLw(w, w.getParentWindow(), mDisplayFrames);w.mLayoutSeq = mLayoutSeq;if (DEBUG_LAYOUT) Slog.v(TAG, " LAYOUT: mFrame=" + w.getFrame()+ " mParentFrame=" + w.getParentFrame()+ " mDisplayFrame=" + w.getDisplayFrame());}};

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • element UI学习使用(1)
  • Html、Css3动画效果
  • 1 MATLAB 绘图函数函数: plot
  • kvm 虚拟机命令行虚拟机操作、制作快照和恢复快照以及工作常用总结
  • 【Python爬虫】利用爬虫抓取双色球开奖号码,获取完整数据并通过随机森林和多层感知两种模型进行简单的预测
  • redis基本数据类型和常见命令
  • 工具使用记录-Tkinter
  • Leetcode 无重复字符的最长子串
  • slf4j依赖冲突处理
  • torchvision.transforms.ToPILImage()使用
  • MySQL 故障排查与性能优化指南
  • 韶音开放式耳机好用吗?南卡、韶音、Oladance、Cleer热门开放式耳机一周横评
  • 【Gateway】网关服务快速上手
  • uniapp数据缓存和发起网络请求
  • Unity Apple Vision Pro 开发(五):PolySpatial 2.0 导入方式
  • (十五)java多线程之并发集合ArrayBlockingQueue
  • [原]深入对比数据科学工具箱:Python和R 非结构化数据的结构化
  • 「前端」从UglifyJSPlugin强制开启css压缩探究webpack插件运行机制
  • 2018以太坊智能合约编程语言solidity的最佳IDEs
  • co模块的前端实现
  • git 常用命令
  • MD5加密原理解析及OC版原理实现
  • Node + FFmpeg 实现Canvas动画导出视频
  • Shadow DOM 内部构造及如何构建独立组件
  • 编写高质量JavaScript代码之并发
  • 基于HAProxy的高性能缓存服务器nuster
  • 开源中国专访:Chameleon原理首发,其它跨多端统一框架都是假的?
  • 腾讯优测优分享 | 你是否体验过Android手机插入耳机后仍外放的尴尬?
  • 通过来模仿稀土掘金个人页面的布局来学习使用CoordinatorLayout
  • 文本多行溢出显示...之最后一行不到行尾的解决
  • 吴恩达Deep Learning课程练习题参考答案——R语言版
  • 在GitHub多个账号上使用不同的SSH的配置方法
  • 湖北分布式智能数据采集方法有哪些?
  • ​TypeScript都不会用,也敢说会前端?
  • # 达梦数据库知识点
  • #### go map 底层结构 ####
  • #每天一道面试题# 什么是MySQL的回表查询
  • (3)STL算法之搜索
  • (Matalb时序预测)PSO-BP粒子群算法优化BP神经网络的多维时序回归预测
  • (待修改)PyG安装步骤
  • (附源码)springboot美食分享系统 毕业设计 612231
  • (四)Controller接口控制器详解(三)
  • (一) springboot详细介绍
  • (游戏设计草稿) 《外卖员模拟器》 (3D 科幻 角色扮演 开放世界 AI VR)
  • (源码分析)springsecurity认证授权
  • (转)如何上传第三方jar包至Maven私服让maven项目可以使用第三方jar包
  • (转载)PyTorch代码规范最佳实践和样式指南
  • ***详解账号泄露:全球约1亿用户已泄露
  • *ST京蓝入股力合节能 着力绿色智慧城市服务
  • .NET : 在VS2008中计算代码度量值
  • .net mvc actionresult 返回字符串_.NET架构师知识普及
  • .NET Standard 的管理策略
  • .net wcf memory gates checking failed
  • .NET 使用 ILRepack 合并多个程序集(替代 ILMerge),避免引入额外的依赖
  • .net 使用ajax控件后如何调用前端脚本