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

news2024/9/20 13:28:26

文章目录

  • 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
      • mLayoutAttached 变量的意义
    • 计算窗口大小——DisplayPolicy::layoutWindowLw
      • 真正计算-- computeFrames
  • 计算结束后的赋值
    • 参数解释
    • 设置窗口大小--WindowState::setFrames
  • 返回窗口大小给应用端
  • 小结
    • 调用链分析
      • Activity 启动触发2个事务的调用链
    • 应用端 ViewRootImpl::setView
    • system_service端调用链
    • 流程图

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::setView
   ViewRootImpl::requestLayout
      ViewRootImpl::scheduleTraversals             
            ViewRootImpl.TraversalRunnable::run          -- Vsync相关--scheduleTraversals
               ViewRootImpl::doTraversal
                  ViewRootImpl::performTraversals 
                     ViewRootImpl::relayoutWindow        -- 第二步:relayoutWindow
                        Session::relayout                -- 跨进程执行 relayoutWindow流程
                        ViewRootImpl::updateBlastSurfaceIfNeeded
                           Surface::transferFrom         -- 应用端Surface赋值
                        ViewRootImpl::setFrame           -- 应用端窗口大小赋值
                     ViewRootImpl::performMeasure        -- View绘制三部曲
                     ViewRootImpl::performLayout
                     ViewRootImpl::performDraw        
                     ViewRootImpl::createSyncIfNeeded    --- 第三步:绘制完成 finishDrawingWindow
   Session.addToDisplayAsUser                     --- 第一步:addWindow

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

ViewRootImpl::setView

# ViewRootImpl
   // 重点* 1. 应用端这个View树的 Surface
   public final Surface mSurface = new Surface();
   // 对应的SurfaceControl
   private 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) {
            // 当前第一次执行肯定为null
            if (mView == null) {
               mView = view;
               ......
               int res; // 定义稍后跨进程add返回的结果
               // 重点* 3. 第二步:会触发relayoutWindow
               requestLayout();  
               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 的代码:

# ViewRootImpl

   boolean mLayoutRequested;

   @Override
   public void requestLayout() {
         if (!mHandlingLayoutInLayoutRequest) {
            ......
            // 只有主线程才能更新UI
            checkThread();
            // 正确请求layout
            mLayoutRequested = 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()
# ViewRootImpl

   final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

   //  是否在执行scheduleTraversals
   public boolean mTraversalScheduled;

   void scheduleTraversals() {
         // 如果遍历操作尚未被调度 
         if (!mTraversalScheduled) {
            // 将调度标志设置为true,表示遍历操作已被调度
            mTraversalScheduled = true;
            // 设置同步屏障 
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // 重点 * 执行mTraversalRunnable
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            // 通知渲染器有一个新的帧即将开始处理
            notifyRendererOfFramePending();
            // 根据需要戳一下绘制锁
            pokeDrawLockIfNeeded();
         }
   }

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

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

# ViewRootImpl

   final class TraversalRunnable implements Runnable {
         @Override
         public void run() {
            doTraversal();
         }
   }

   void doTraversal() {
         if (mTraversalScheduled) {
            // 正在执行或已经执行完毕 
            mTraversalScheduled = false;
            // 移除同步屏障
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            .....
            performTraversals();
            ......
         }
      }

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

ViewRootImpl::performTraversals

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

# ViewRootImpl
   private SurfaceSyncGroup mActiveSurfaceSyncGroup;

   private void performTraversals() {
      ......
      // mWinFrame保存的是当前窗口的尺寸
      Rect frame = mWinFrame;
      ----1.1 硬绘相关----
      // 硬件加速是否初始化
      boolean hwInitialized = false;
      ......
      ----2. relayoutWindow流程----
      // 内部会将经过WMS计算后的窗口尺寸给mWinFrame
      relayoutResult = 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 方法如下:

# ViewRootImpl

   public 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 和 BlastBufferQueue
                     mAttachInfo.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 方法:

# ViewRootImpl

    private 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
    @Override
    public 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代码如下:

# WindowManagerService

    public 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中获取WindowState
            final 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 = VISIBLE
            win.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. 创建SurfaceControl
                    result = 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::relayoutWindow
   WindowManagerService::createSurfaceControl
      WindowStateAnimator::createSurfaceLocked -- 创建“Buff” 类型Layer
         WindowStateAnimator::resetDrawState   -- DRAW_PENDING
         WindowSurfaceController::init
            SurfaceControl.Builder::build
               SurfaceControl::init
      WindowSurfaceController::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
# WindowManagerService

   private 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”类型Surface
         Trace.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 的。

# WindowStateAnimator

   WindowSurfaceController 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_PENDING
      resetDrawState();
      ......
         // 重点* 2. 创建WindowSurfaceController
         mSurfaceController = new WindowSurfaceController(attrs.getTitle().toString(), format,
                    flags, this, attrs.type);
      ......
      return mSurfaceController;
   }

这里有2个重点:

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

设置窗口状态——DRAW_PENDIND

# WindowStateAnimator
   void resetDrawState() {
      // 设置windowState状态为DRAW_PENDING
      mDrawState = 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

# WindowSurfaceController

   SurfaceControl mSurfaceControl;

   WindowSurfaceController(String name, int format, int flags, WindowStateAnimator animator,
            int windowType) {
      mAnimator = animator;
      // 1. 也会作为Surface的name
      title = name;

      mService = animator.mService;
      // 2. 拿到WindowState
      final 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);
      // 高版本都为BLAST
      if (useBLAST) {
         // 4. 重点* 设置为“Buff”图层
         b.setBLASTLayer();
      }
      // 触发build
      mSurfaceControl = 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 时完成。

# WindowSurfaceController

    void getSurfaceControl(SurfaceControl outSurfaceControl) {
      // 将framework层的SurfaceControl copy给应用层传递过来的outSurfaceControl
      outSurfaceControl.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定义如下:

# WindowState
   public class WindowState extends WindowContainer<WindowState> implements
         WindowManagerPolicy.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::performSurfacePlacement
    WindowSurfacePlacer::performSurfacePlacementLoop
        RootWindowContainer::performSurfacePlacement
            RootWindowContainer::performSurfacePlacementNoTrace
                RootWindowContainer::applySurfaceChangesTransaction
                    DisplayContent::applySurfaceChangesTransaction
                        DisplayContent::performLayout 
                            DisplayContent::performLayoutNoTrace
                                DisplayContent::mPerformLayout 
                                    DisplayPolicy::layoutWindowLw 
                                        WindowLayout::computeFrames   -- 计算窗口大小,保存在 sTmpClientFrames中
                                        WindowState::setFrames        -- 将计算结果 sTmpClientFrames 的数据设置给窗口

在这里插入图片描述

WindowSurfacePlacer::performSurfacePlacement

# WindowSurfacePlacer

    // 控制是否需要继续执行 performSurfacePlacementLoop方法
    private boolean mTraversalScheduled;
    // 延迟layout就会+1
    private int mDeferDepth = 0;

    final void performSurfacePlacement(boolean force) {
        if (mDeferDepth > 0 && !force) {
            mDeferredRequests++;
            return;
        }
        // 最大次数循环为6次
        int loopCount = 6;
        do {
            // 设置为false
            mTraversalScheduled = 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 方法的内容。

# WindowSurfacePlacer
    private 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 的逻辑。

# WindowSurfacePlacer
    void 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

    // 这个方法加上了trace
    void 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*/);
        }
        ......
        // Trace
        Trace.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或者位置变化,则通知客户端重新relayout
        handleResizingWindows();
        ......
        // 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

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

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

# DisplayContent
    private 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
    // 标记是否需要执行layout
    private 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) {
        // 加上trace
        Trace.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设置为flase
        clearLayoutNeeded();
        ......
        // 重点* 1. 对所有顶级窗口进行布局
        forAllWindows(mPerformLayout, true /* traverseTopToBottom */);

        // 重点* 2. 处理子窗口的布局
        forAllWindows(mPerformLayoutAttached, true /* traverseTopToBottom */);
        ......
    }

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

mPerformLayout 和 mPerformLayoutAttached

# DisplayContent
    private final Consumer<WindowState> mPerformLayout = w -> {
        // 如果当前窗口为子窗口则直接返回
        if (w.mLayoutAttached) {
            return;
        }

        // 先判断当前窗口是否会不可见
        final boolean gone = w.isGoneForLayout();
        // 如果窗口不是不可见的,或者窗口没有框架,或者窗口需要布局
        if (!gone || !w.mHaveFrame || w.mLayoutNeeded) {
            ......
            // 重点*1. 调用DisplayPolicy::layoutWindowLw
            getDisplayPolicy().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());
        }
    };

mLayoutAttached 变量的意义

这2个 lambda 表达式基本上是一样的主要就是执行“getDisplayPolicy().layoutWindowLw”,但是区别在于方法最前面对 w.mLayoutAttached 的判断,这个属性是什么意思呢?
这个属性在WindowState构造的时候赋值。

# WindowState
    WindowState(WindowManagerService service, Session s, IWindow c, WindowToken token,
            WindowState parentWindow, int appOp, WindowManager.LayoutParams a, int viewVisibility,
            int ownerId, int showUserId, boolean ownerCanAddInternalSystemWindow,
            PowerManagerWrapper powerManagerWrapper) {
                ......
                if (mAttrs.type >= FIRST_SUB_WINDOW && mAttrs.type <= LAST_SUB_WINDOW) {
                    ......
                    mLayoutAttached = mAttrs.type !=
                            WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
                    ......
                } else {
                    ......
                    mLayoutAttached = false;
                    ......
                }
                ......
            }

其实直接理解为:只要不是子窗口都为 false 就可以,根据我的 debug 和堆栈信息也确实如此。那也就是说 mPerformLayoutAttached 是不会执行的,真正执行的是 mPerformLayout 这个lambda 表达式,然后内部走到 DisplayPolicy::layoutWindowLw 方法。

计算窗口大小——DisplayPolicy::layoutWindowLw

# DisplayPolicy

    public void layoutWindowLw(WindowState win, WindowState attached, DisplayFrames displayFrames) {
        // 判断是否需要跳过布局
        if (win.skipLayout()) {
            return;
        }

        // 获取DisplayFrames
        displayFrames = win.getDisplayFrames(displayFrames);
        // 获取某个方向的窗口布局参数
        final WindowManager.LayoutParams attrs = win.getLayoutingAttrs(displayFrames.mRotation);
        final Rect attachedWindowFrame = attached != null ? attached.getFrame() : null;

        // If this window has different LayoutParams for rotations, we cannot trust its requested
        // size. Because it might have not sent its requested size for the new rotation.
        final boolean trustedSize = attrs == win.mAttrs;
        // 应用端请求的宽高信息
        final int requestedWidth = trustedSize ? win.mRequestedWidth : UNSPECIFIED_LENGTH;
        final int requestedHeight = trustedSize ? win.mRequestedHeight : UNSPECIFIED_LENGTH;
        // 重点* 1. 调用WindowLayout.computeFrames计算窗口布局大小
        mWindowLayout.computeFrames(attrs, win.getInsetsState(), displayFrames.mDisplayCutoutSafe,
                win.getBounds(), win.getWindowingMode(), requestedWidth, requestedHeight,
                win.getRequestedVisibleTypes(), win.mGlobalScale, sTmpClientFrames);
        // 重点* 2. 将计算的布局参数赋值给windowFrames
        win.setFrames(sTmpClientFrames, win.mRequestedWidth, win.mRequestedHeight);
    }
  • 1、WindowLayout::computeFrames 计算窗口大小的。里面非常的复杂了,有一套计算规则
  • 2、WindowState::setFrames 然后将计算好后的大小设置给 WindowState ,这个方法好像也不复杂就3个参数,其实2个还是宽高

真正计算-- computeFrames

首先要说明2点:

1、执行这个方法的逻辑不止上面分析的这一处,在ViewRootImpl::setView 方法里也出现过
2、这个方法里是具体计算窗口大小和位置的,计算规则也很复杂,个人觉得适当了解即可,遇到窗口的大小和位置显示异常的问题再详细研究这个方法就好。

下面的代码很复杂,对于这种复杂代码,学习阶段一般建议以黑盒的方式掌握即可,不需要知道具体的执行,更不用记代码,google 没准哪天改了这里的代码就白记了。
我是没记住下面的逻辑,但是我把相关的注释加在代码中,至少我知道了这个方法是干什么的大概做了什么事,遇到问题的时候知道往这看。

知道这个方法就计算窗口位置,然后还考虑了一下刘海屏的情况就差不多了。

计算结束后的赋值

WindowLayout::computeFrames 方法很长,最后一个参数:frames 传进去的是 DisplayPolicy 下的静态变量 sTmpClientFrames 。也就是说根据一套计算规则计算后,sTmpClientFrames 这个变量中就保存了最新的窗口大小信息。

然后调用 WindowState::setFrames 方法把数据设置给 WindowState。

参数解释

第一个参数有正确的大小信息,后面2个参数是请求的宽高,那是谁请求的呢?是应用端请求的。 和 View 绘制一样,子 View 会有一个自己期望的宽高,但是计算规则会根据实际情况来设置 View最终的宽高。
那么窗口也是一样,应用端会在执行 relayoutWindow 流程的时候就把它期望的宽高传递了过来,但是 WMS 毕竟管理的是整个手机上各个窗口,它需要根据实际情况来设置最终的窗口大小,并返回给应用端。
道理知道了,现在从代码中来确认。
DisplayPolicy::layoutWindowLw 方法中是这样调用的

win.setFrames(sTmpClientFrames, win.mRequestedWidth, win.mRequestedHeight);

后面2个参数也是在 WindowState 内。

# WindowState
    /**
     * The window size that was requested by the application.  These are in
     * the application's coordinate space (without compatibility scale applied).
     * 应用程序请求的窗口大小
     */
    int mRequestedWidth;
    int mRequestedHeight;
    // 赋值
    void setRequestedSize(int requestedWidth, int requestedHeight) {
        if ((mRequestedWidth != requestedWidth || mRequestedHeight != requestedHeight)) {
            mLayoutNeeded = true;
            mRequestedWidth = requestedWidth;
            mRequestedHeight = requestedHeight;
        }
    }

那其实只有看哪里调用 setRequestedSize 方法就好了,答案就是 WindowManagerService::relayoutWindow 方法,这个其实上一篇代码里有了,再看一眼:

# 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) {
                    ......
                    if (viewVisibility != View.GONE) {
                    // 把应用端请求的大小,保存到WindowState下
                    win.setRequestedSize(requestedWidth, requestedHeight);
                    }
                    ......
                    if (shouldRelayout) {
                        try {
                            // 重点* 1. 创建SurfaceControl
                            result = createSurfaceControl(outSurfaceControl, result, win, winAnimator);
                        } catch (Exception e) {
                            ......
                            return 0;
                        }
                    }

                    // 重点* 2. 计算窗口的大小 (极其重要的方法)
                    mWindowPlacerLocked.performSurfacePlacement(true /* force */);
                    ......
                    // 重点* 3. 给应用端SurfaceControl赋值
                    winAnimator.mSurfaceController.getSurfaceControl(outSurfaceControl);
                    ......
                    // 重点* 4. 填充WMS计算好后的数据,返回应用端
                    win.fillClientWindowFramesAndConfiguration(outFrames, outMergedConfiguration,
                            false /* useLatestConfig */, shouldRelayout);
                    ......
            }

现在知道这3个参数是什么了开始看 WindowState::setFrames 方法。

设置窗口大小–WindowState::setFrames

窗口 WindowState 下有个成员变量 mWindowFrames 是 WindowFrames 类型,保存在窗口尺寸信息。先看看这个类:

# WindowFrames
    /**
     * The frame to be referenced while applying gravity and MATCH_PARENT.
     * 父容器矩形位置
     */
    public final Rect mParentFrame = new Rect();

    /**
     * The bounds that the window should fit.
     * 屏幕的大小,包括状态栏导航栏这些
     */
    public final Rect mDisplayFrame = new Rect();

    /**
     * "Real" frame that the application sees, in display coordinate space.
     * 表示应用程序或视图在屏幕上的实际可见区域。
     * 窗口的大小就是这个变量,应用端绘制的大小也是这个
     */
    final Rect mFrame = new Rect();

    /**
     * mFrame but relative to the parent container.
     * 这个矩形与mFrame类似,但它是相对于父容器的坐标系统而不是屏幕坐标。
     */
    final Rect mRelFrame = new Rect();

同 WindowLayout::computeFrames 方法一样,也是大量代码,而且也会被 google 修改,我还是建议黑盒形式知道这个方法会把计算出的尺寸设置给窗口 WindowState 就好了。

# WindowState
// clientWindowFrames: 经过WindowLayout::computeFrames 方法计算出来的窗口尺寸
// requestedWidth: 应用端请求的宽度
// requestedHeight : 应用端请求的高度
void setFrames(ClientWindowFrames clientWindowFrames, int requestedWidth, int requestedHeight) {
        // 获取当前窗口的框架信息实例
        final WindowFrames windowFrames = mWindowFrames;
        // 将父窗口框架信息复制到临时变量 mTmpRect
        mTmpRect.set(windowFrames.mParentFrame);

        // 3个变量的赋值
        windowFrames.mDisplayFrame.set(clientWindowFrames.displayFrame);
        windowFrames.mParentFrame.set(clientWindowFrames.parentFrame);
        windowFrames.mFrame.set(clientWindowFrames.frame);

        // 设置兼容框架信息与窗口框架相同
        windowFrames.mCompatFrame.set(windowFrames.mFrame);
        // 如果需要兼容缩放或者硬件调整大小功能启用
        if (hasCompatScale()/** M: Add for App Resolution Tuner @{ */
            || mNeedHWResizer/** @}*/) {
            // Also, the scaled frame that we report to the app needs to be adjusted to be in
            // its coordinate space.
            对windowFrames.mCompatFrame缩放,确保在应用坐标空间内正确展示
            windowFrames.mCompatFrame.scale(mInvGlobalScale);
        }

        // 设置是否父窗口框架被显示切割区域裁剪标志位
        windowFrames.setParentFrameWasClippedByDisplayCutout(
                clientWindowFrames.isParentFrameClippedByDisplayCutout);

        // Calculate relative frame
        // 计算相对框架信息
        windowFrames.mRelFrame.set(windowFrames.mFrame);
        // 将窗口框架转换为相对于父容器的坐标系
        WindowContainer<?> parent = getParent();
        int parentLeft = 0;
        int parentTop = 0;
        if (mIsChildWindow) {
            // 如果当前窗口是子窗口,则根据其父窗口计算偏移量
            parentLeft = ((WindowState) parent).mWindowFrames.mFrame.left;
            parentTop = ((WindowState) parent).mWindowFrames.mFrame.top;
        } else if (parent != null) {
            // 若当前窗口不是子窗口但有父容器,则从父容器边界获取偏移量
            final Rect parentBounds = parent.getBounds();
            parentLeft = parentBounds.left;
            parentTop = parentBounds.top;
        }
        windowFrames.mRelFrame.offsetTo(windowFrames.mFrame.left - parentLeft,
                windowFrames.mFrame.top - parentTop);

        // 检查请求的宽高是否改变,以及父窗口框架是否有变化
        if (requestedWidth != mLastRequestedWidth || requestedHeight != mLastRequestedHeight
                || !mTmpRect.equals(windowFrames.mParentFrame)) {

            // 更新最后请求的宽高值,并标记内容发生变化
            mLastRequestedWidth = requestedWidth;
            mLastRequestedHeight = requestedHeight;
            windowFrames.setContentChanged(true);
        }

        // 如果窗口类型为 DOCK_DIVIDER 类型
        // 目前我知道的是分屏应用中间的分割线是这个类型
        if (mAttrs.type == TYPE_DOCK_DIVIDER) {
            if (!windowFrames.mFrame.equals(windowFrames.mLastFrame)) {
                // 如果当前帧与上次帧不相等,则表示窗口移动过
                mMovedByResize = true;
            }
        }

        // 如果当前窗口是壁纸
        if (mIsWallpaper) {
            final Rect lastFrame = windowFrames.mLastFrame;
            final Rect frame = windowFrames.mFrame;
            if (lastFrame.width() != frame.width() || lastFrame.height() != frame.height()) {
                mDisplayContent.mWallpaperController.updateWallpaperOffset(this, false /* sync */);
            }
        }
        // 更新源框架信息
        updateSourceFrame(windowFrames.mFrame);

        ......
        // 如果存在 ActivityRecord 并且当前窗口不是子窗口,则调用 layoutLetterbox 方法
        if (mActivityRecord != null && !mIsChildWindow) {
            mActivityRecord.layoutLetterbox(this);
        }
        // 标记需要进行 Surface 的放置操作
        mSurfacePlacementNeeded = true;
        // 标记已经具有有效的帧信息
        mHaveFrame = true;
    }

返回窗口大小给应用端

经过 WindowState::setFrames 方法后,窗口自己就有了一个确定的尺寸了,但是绘制内容是在应用端,所以现在需要把这个窗口尺寸传递给应用端。 WindowManagerService::relayoutWindow 方法中知道,经过 WindowSurfacePlacer::performSurfacePlacement 方法计算出窗口尺寸后, 会执行 WindowState::fillClientWindowFramesAndConfiguration 方法就尺寸信息填充到参数 outFrames 中,也就是传递给应用端。

# WindowState
    /**
     * Fills the given window frames and merged configuration for the client.
     *
     * @param outFrames The frames that will be sent to the client.
     * @param outMergedConfiguration The configuration that will be sent to the client.
     * @param useLatestConfig Whether to use the latest configuration.
     * @param relayoutVisible Whether to consider visibility to use the latest configuration.
     */
    void fillClientWindowFramesAndConfiguration(ClientWindowFrames outFrames,
            MergedConfiguration outMergedConfiguration, boolean useLatestConfig,
            boolean relayoutVisible) {
        // 尺寸信息设置给应用端
        outFrames.frame.set(mWindowFrames.mCompatFrame);
        outFrames.displayFrame.set(mWindowFrames.mDisplayFrame);

        // 缩放处理
        if (mLayoutAttached) {
            if (outFrames.attachedFrame == null) {
                outFrames.attachedFrame = new Rect();
            }
            outFrames.attachedFrame.set(getParentWindow().getFrame());
            if (mInvGlobalScale != 1f) {
                outFrames.attachedFrame.scale(mInvGlobalScale);
            }
        }
        ......
        // 标记已向客户端报告过配置信息
        mLastConfigReportedToClient = true;
    }

这个 outFrames 就是应用端 ViewRootImpl执行 relayout 方法触发WMS::relayoutWindow 传递的参数 mTmpFrames。

小结

relayoutWindow 流程就分析完了,代码很多流程很长,而且中间很多核心方法还有其他的分支,但是只当前主流程还是比较清晰的。

在这里插入图片描述
上一篇分析了Surface的创建,本篇分析了窗口大小的计算和保存, 代码虽多但是其实 WindowLayout::computeFrames 和 WindowState::setFrames 这2个方法知道是干啥的就可以了,完全不需要记住。等实际解决问题的时候知道在哪看就好了。

调用链分析

Activity 启动触发2个事务的调用链

其中 ResumeActivityItem 会触发 ViewRootImpl::setView 执行,也是触发relayoutWindow 的逻辑。

LaunchActivityItem::execute
   ActivityThread::handleLaunchActivity
         ActivityThread::performLaunchActivity
            Instrumentation::newActivity      --- 创建Activity
            Activity::attach                  --- 创建Window
               Window::init
               Window::setWindowManager
            Instrumentation::callActivityOnCreate  
               Activity::performCreate
                  Activity::onCreate       --- onCreate

ResumeActivityItem::execute
   ActivityThread::handleResumeActivity
        ActivityThread::performResumeActivity   
            Activity::performResume     
               Instrumentation::callActivityOnResume
                  Activity::onResume       --- onResume
        WindowManagerImpl::addView           --- 创建ViewRootImpl
            WindowManagerGlobal::addView   
               ViewRootImpl::setView        ---WMS通信触发窗口的显示逻辑


应用端 ViewRootImpl::setView

ViewRootImpl::setView 后续的调用链如下:

ViewRootImpl::setView
   ViewRootImpl::requestLayout
      ViewRootImpl::scheduleTraversals             
            ViewRootImpl.TraversalRunnable::run          --- Vsync相关--scheduleTraversals
               ViewRootImpl::doTraversal
                  ViewRootImpl::performTraversals 
                     ViewRootImpl::relayoutWindow        --- 第二步:relayoutWindow
                        Session::relayout                --- 跨进程调用
                        ViewRootImpl::updateBlastSurfaceIfNeeded
                           Surface::transferFrom         -- 应用端Surface赋值
                     ViewRootImpl::performMeasure        --- View绘制三部曲
                     ViewRootImpl::performLayout
                     ViewRootImpl::performDraw        
                     ViewRootImpl::createSyncIfNeeded    --- 第三步:绘制完成 finishDrawingWindow
   Session.addToDisplayAsUser                     --- 第一步:addWindow

其中窗口显示的三步都是这里触发的。

所以说 ViewRootImpl 真的很重要,其实没有Activity,没有Window, 只通过 ViewRootImpl 也能在屏幕上显示一个窗口

system_service端调用链

WindowManagerService::relayoutWindow
   WindowManagerService::createSurfaceControl
      WindowStateAnimator::createSurfaceLocked -- 创建“Buff” 类型Surface  (上一篇)
         WindowStateAnimator::resetDrawState   -- 设置窗口状态为DRAW_PENDING
         WindowSurfaceController::init
            SurfaceControl.Builder::build
               SurfaceControl::init
    WindowSurfacePlacer::performSurfacePlacement  -- 计算窗口大小 (本篇)
        WindowSurfacePlacer::performSurfacePlacementLoop
            RootWindowContainer::performSurfacePlacement
                RootWindowContainer::performSurfacePlacementNoTrace
                    RootWindowContainer::applySurfaceChangesTransaction
                        DisplayContent::applySurfaceChangesTransaction
                            DisplayContent::performLayout 
                                DisplayContent::performLayoutNoTrace
                                    DisplayContent::mPerformLayout 
                                        DisplayPolicy::layoutWindowLw 
                                            WindowLayout::computeFrames   -- 计算窗口大小,保存在 sTmpClientFrames中
                                            WindowState::setFrames        -- 将计算结果 sTmpClientFrames 的数据设置给窗口
   WindowSurfaceController::getSurfaceControl  -- 给应用端Surface赋值
   WindowState::fillClientWindowFramesAndConfiguration  -- 给应用端窗口大小赋值

流程图

一次layout中,关于一个屏幕下所有窗口大小计算的流程图如下:
在这里插入图片描述
在这里插入图片描述
这部分也是 relayoutWindow 流程中关于窗口计算的核心流程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2135907.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

并发编程 - GCD的栅栏(dispatch_barrier_async)

引言 Grand Central Dispath&#xff08;GCD&#xff09;是苹果提供的强大工具&#xff0c;它几乎涵盖了多线程编程的所有方面。通过GCD&#xff0c;我们可以轻松地创建队列、管理线程&#xff0c;并以更优雅的方式处理并发任务。在前面的博客中&#xff0c;我们已经深入探讨了…

基于SpringBoot+Vue+MySQL的校园健康驿站管理系统

系统展示 用户前台界面 管理员后台界面 系统背景 本文设计并实现了一个基于SpringBoot后端、Vue前端与MySQL数据库的校园健康驿站管理系统。该系统旨在通过数字化手段&#xff0c;全面管理学生的健康信息&#xff0c;包括体温监测、疫苗接种记录、健康状况申报等&#xff0c;为…

【Canvas与表盘】绘制黄蓝两色简约表盘

【成图】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>黄蓝卡通手表</title><style type"text/css">…

【我的Android进阶之旅】解决CardView四个圆角有白边的问题

文章目录 一、问题描述二、分析CardView出现白边的原因三、如何解决这个问题?3.1 如何修复?3.2 为什么这样可以修复?3.3 示例代码3.4 总结一、问题描述 在实现一个RecycleView的Item时候,样式需要用到卡片式效果,于是想到用CardView来实现,但是最终发现运行出来的效果,…

(微服务项目)新闻头条——Day1

最近发生了很多事情&#xff0c;躺了一阵子&#xff0c;也是终于振作起来做自己的事情了.... 有的人追求精彩而活&#xff0c;而即使瘦若浮游&#xff0c;仍旧痴迷71种滋味&#xff0c;而有的人寿命明却装醉不得自由虚度自己的光阴&#xff0c;年华终究在最后一刻幡然醒悟&…

Android Studio新建工程(Java语言环境)

一、新建工程流程(java语言环境) 1、File->New->New Project 2、选择“Empty Views Activity” -> Next 3、创建项目名称/项目路径/语言环境 1&#xff09;项目名称&#xff1a;使用默认Name 或 修改Name 2) Package name&#xff1a;每个项目的这个名称唯一&…

MySQL 创建数据库和表全攻略

一、MySQL 创建数据库与表的重要性 MySQL 作为广泛应用的关系型数据库管理系统&#xff0c;创建数据库和表具有至关重要的意义。 在数据存储方面&#xff0c;数据库就如同一个巨大的仓库&#xff0c;为各类数据提供了安全、有序的存储环境。通过创建数据库&#xff0c;可以将相…

计算机毕业设计体育资讯个性化推荐网站网站内容留言评论前台注册后台管理/springboot/javaWEB/J2EE/MYSQL数据库/vue前后分离小程序

一、网站内容 ‌个性化推荐‌&#xff1a;根据用户浏览历史和兴趣&#xff0c;推送相关体育资讯。‌丰富资讯‌&#xff1a;包含体育赛事直播、新闻报道、专栏评论等。 二、留言评论系统 ‌用户互动‌&#xff1a;允许用户对资讯进行留言和评论&#xff0c;增强社区互动性。…

python基础知识 (五)--容器、索引、切片、字符串的遍历、查找、修改元素

目录 容器 容器大总结 索引 切片 字符串的遍历 for循环语法&#xff1a; while循环语法&#xff1a; 查找元素 修改元素 例题 1.验证码 2.抽取大红包 3.a和b互换位置 容器 在Python中&#xff0c;常见容器有&#xff1a; &#xff08;1&#xff09;字符串&#x…

k8s的加密配置secret和应用配置configmap

目录 加密配置 secret的三种类型 创建opaque类型的两种方式 方法一 方法二 如何把secret挂载到pod当中 把secret作为环境变量传到pod当中 指定harbor私有仓库加密的secret配置 应用配置 configmap 创建configmap的方式 在pod里面用configmap做pod的环境变量 **用c…

[Linux入门]---使用exec函数实现简易shell

文章目录 1.简易实现2.人机交互&#xff0c;获取命令行3.命令行分割4.执行命令5.内建命令6.myshell代码 1.简易实现 2.人机交互&#xff0c;获取命令行 代码如下&#xff1a; int quit0; #define LEFT "[" #define RIGHT "]" #define LABLE "#&quo…

如何免费试用OpenAI o1 preview大模型

OpenAI于 2024 年 9 月 12 日推出 o1&#xff08;以前称为 Strawberry 项目&#xff09; 。这一系列新推理模型旨在更有效地解决复杂问题。ChatGPT Plus 和 Team 用户可以访问 o1-preview 和 o1-mini&#xff0c;但消息量有限。 OpenAI o1-preview与 GPT-o对比 o1-previewGPT…

Excel 基础知识-操作手册2

十、查找与引用函数 Excel中的查找与引用函数非常丰富&#xff0c;以下是一些主要的函数及其使用示例&#xff1a; 1. **VLOOKUP** - 语法&#xff1a;VLOOKUP(lookup_value, table_array, col_index_num, [range_lookup]) - 示例&#xff1a;假设A列是员工编号&#xff0c;B…

27 顺序表 · 链表

目录 一、单链表 &#xff08;一&#xff09;概念 1、节点 2、链表的性质 &#xff08;二&#xff09;单链表的实现 &#xff08;三&#xff09;单链表算法题 1、移除链表元素 2、反转链表 3、链表的中间节点 4、合并两个有序的单链表 5、链表分割 6、链表的回文结构…

pdf怎么加页码?5种pdf添加页码指南分享,快来领取!

如何在一个包含大量页面的大型pdf文件中快速找到特定的页面或信息呢&#xff1f;最简便的方法就是为pdf添加页码。pdf添加页码能够清晰显示页面顺序&#xff0c;帮助读者轻松浏览大型pdf文档&#xff0c;同时也便于寻找特定章节和确定整体长度。然而&#xff0c;并非所有pdf文件…

VirtualBox Install MacOS

环境搭建 git clone https://github.com/myspaghetti/macos-virtualbox 脚本配置 修改macos-guest-virtualbox.sh部分内容为 vm_name"macOS" # name of the VirtualBox virtual machine macOS_release_name"Catalina" # install &quo…

PHP 环境搭建教程

搭建一个稳定的PHP开发环境是开发Web应用的基础。在Linux系统上&#xff0c;LAMP&#xff08;Linux, Apache, MySQL/MariaDB, PHP&#xff09;堆栈是最广泛使用的组合。本文将详细介绍如何在Linux上搭建PHP开发环境&#xff0c;涵盖安装步骤、配置和测试。更多内容&#xff0c;…

Docker操作MySQL

1&#xff0c;拷贝&#xff1b; docker cp mysql01:/etc/mysql .2&#xff0c;修改conf.d和mysql.conf.d文件 3&#xff0c; vim mysql/my.cnf 4&#xff0c;拷贝并替换my.cnf文件 5&#xff0c;mysql镜像重启命令&#xff1a; docker exec -it mysql01 -uroot -p0000006&…

LOAM学习

LOAM Ceres Solver 中的LocalParameterization理解ALOAM雷达里程计主要步骤论文A-LOAM laser Odometry代码LiDAR Odometry寻找角点特征代码流程分析寻找面点特征 求解器设置 Ceres Solver 中的LocalParameterization理解 该LocalParameterization类用来解决非线性优化中的过参…

最全的软件测试面试题(含答案)

软件的生命周期(prdctrm) 计划阶段(planning)-〉需求分析(requirement)-〉设计阶段(design)-〉编码(coding)->测试(testing)->运行与维护(running maintrnacne) 测试用例 用例编号  测试项目  测试标题  重要级别  预置条件  输入数据  执行步骤   预期结果 1…