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());}};
相关文章:

Android Framework(五)WMS-窗口显示流程——窗口布局与绘制显示
文章目录 relayoutWindow流程概览应用端处理——ViewRootImpl::setView -> relayoutWindowViewRootImpl::setViewViewRootImpl::performTraversalsViewRootImpl::relayoutWindow Surface的创建WindowManagerService::relayoutWindow了解容器类型和Buff类型的SurfaceBuff类型…...

【计网】计算机网络基础
当自律变成一种本能的习惯, 你就会享受到它的快乐。 --- 村上春树 --- 初识计算机网络 1 初识协议1.1 协议分层1.2 OSI七层模型1.3 TCP / IP协议 2 初识局域网2.1 什么是局域网2.2 MAC地址2.3 局域网通信 3 简单认识IP地址 1 初识协议 1.1 协议分层 首先&#…...

秃姐学AI系列之:实战Kaggle比赛:图像分类(CIFAR-10)
目录 准备工作 整理数据集 将验证集从原始的训练集中拆分出来 整理测试集 使用函数 图像增广 读取数据集 定义模型 定义训练函数 训练和验证数据集 对测试集进行分类并提交结果 准备工作 首先导入竞赛需要的包和模块 import collections import math import os i…...

nginx: [error] invalid PID number ““ in “/run/nginx.pid“
出现这个报错的原因 : 空值:“/run/nginx.pid” 文件为空或者内容不是有效的PID数字 文件损坏:如果PID文件被意外修改,例如被其他程序覆盖了内容,可能会显示为无效。 路径错误:Nginx无法找到指定的PID文件…...

Java使用Apache POI向Word文档中填充数据
Java使用Apache POI向Word文档中填充数据 向一个包含占位符的Word文档中填充数据,并保存为新的文档。 准备工作 环境搭建 在项目中添加Apache POI依赖。在pom.xml中添加如下依赖: <dependencies><dependency><groupId>org.apache.po…...

Gitflow基础知识
0.理想状态 现状 听完后的理想状态 没使用过 git 知道 git 是什么,会用 git 基础流程命令 用过 git,但只通过图形化界面操作 脱离图形化界面操作,通过 git 命令操作 会 git 命令 掌握 gitflow 规范,合理使用 rebase 和解决…...

NLP基础及其代码-tokenizer
基础知识 NLP-分词器:SentencePiece【参考Chinese-LLaMA-Alpaca在通用中文语料上训练的20K中文词表并与原版LLaMA模型的32K词表进行合并的代码】_sentencepiece 中文训练-CSDN博客 【OpenLLM 008】大模型基础组件之分词器-万字长文全面解读LLM中的分词算法与分词器…...

OpenCV结构分析与形状描述符(8)点集凸包计算函数convexHull()的使用
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 查找一个点集的凸包。 函数 cv::convexHull 使用斯克拉斯基算法(Sklansky’s algorithm)来查找一个二维点集的凸包&#…...

灰光模块,彩光模块-介绍
1. 引用 知识分享系列一:5G基础知识-CSDN博客 5G前传的最新进展-CSDN博客 灰光和彩光_通信行业5G招标系列点评之二:一文读懂5G前传-光纤、灰光、彩光、CWDM、LWDM、MWDM...-CSDN博客 ADOP带你了解:CWDM、DWDM、MWDM、LWDM:快速…...

python-新冠病毒
题目描述 假设我们掌握了特定时间段内特定城市的新冠病毒感染病例的信息。在排名 i 的当天有 i 个案例,即: 第一天有一例感染第二天有两例感染第三天有三例感染以此类推...... 请计算 n 天内的感染总数和每天平均感染数。 输入 整数 n 表示天数&…...

2023年408真题计算机网络篇
https://zhuanlan.zhihu.com/p/6954228062023年网络规划设计师上午真题解析TCP流量计算_哔哩哔哩_bilibili 1 1在下图所示的分组交换网络中,主机H1和H2通过路由器互联,2段链路的数据传输速率为100 Mb/s、时延带宽积 (即单向传播时延带宽&am…...

分类学习器(Classification Learner App)MATLAB
在MATLAB中,分类学习器用于构建和评估分类模型。MATLAB提供了一些工具和功能,帮助你进行分类任务,例如分类学习器应用程序、统计和机器学习工具箱中的函数等。 数据集介绍 不同的人被要求在平板电脑上写字母"J"、“V"和&quo…...

DolphinDB 基准性能测试工具:金融模拟数据生成模块合集
测试 DolphinDB 数据库性能时,往往需要快速写入一些测试数据。为方便用户快速完成简单的基准性能测试,金融 Mock 数据生成模块覆盖了常用的金融数据集,满足用户生成模拟数据的需求。基于本模块生成的模拟数据不具有实际意义,建议仅…...

BUUCTF—[BJDCTF2020]The mystery of ip
题解 打开环境点击上面的flag可以看到这个IP页面。 抓个包看看有啥东西无,可以看到在返回包有IP。 看到IP就想到X-Forwarded-For这个玩意,我们用X-Forwarded-For随便添加个IP看看。可以看到返回的IP内容变成了123。 X-Forwarded-For:123 推测它会输出我…...

leecode100题-双指针-三数之和
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k ,同时还满足 nums[i] nums[j] nums[k] 0 。请你返回所有和为 0 且不重复的三元组。 答案中不可以包含重复的三元组。 示例 1: 输入…...

计算机毕业设计PySpark+Django考研分数线预测 考研院校推荐系统 考研推荐系统 考研爬虫 考研大数据 Hadoop 大数据毕设 机器学习 深度学习
《PySparkDjango考研分数线预测与推荐系统》开题报告 一、研究背景与意义 随着教育水平的提高和就业竞争的加剧,越来越多的学生选择继续深造,参加研究生入学考试(考研)。然而,考研信息繁杂,选择专业和院校…...

Go语言多态实践以及gin框架c.BindJSON序列化遇到的坑
遇到的问题 如果定义的接收结构体字段是interface{},在调用gin的 c.BindJSON 方法后会直接转为map, 导致无法断言为其他类型 场景 在创建工程请求中,根据工程类别的不同会有多种创建参数,比如 // A 类型需要编译 所以有这些字…...

SpringCloud神领物流学习笔记:项目概述(一)
SpringCloud神领物流学习笔记:项目概述(一) 文章目录 SpringCloud神领物流学习笔记:项目概述(一)1、项目介绍2、基本业务流程3、系统架构4、技术架构 1、项目介绍 神领物流是一个基于微服务架构体系的【…...

RocketMQ异步报错:No route info of this topic
在SpringBoot中发送RocketMQ异步消息的时候报错了,提示org.apache.rocketmq.client.exception.MQClientException: No route info of this topic, testTopic1 这里给出具体的解决方案 一、Broker模块不支持自动创建topic,并且topic没有被手动创建过 R…...

Node.js学习记录(一)
目录 一、文件读取 readFile 二、写入文件 writeFile 三、动态路径 __dirname:表示当前文件所处的目录、path.join 四、获取路径文件名 path.basename 五、提取某文件中的css、JS、html 六、http 七、启动创建web服务器 服务器响应 八、将资源请求的 url 地…...

【AI】Pytorch_模型构建
建议点赞收藏关注!持续更新至pytorch大部分内容更完。 本文已达到10w字,故按模块拆开,详见目录导航。 整体框架如下 数据及预处理 模型及其构建 损失函数及优化器 本节目录 模型线性回归逻辑回归LeNetAlexNet 构建模块组织复杂网络初始化网络…...

FFmpeg源码:avcodec_descriptor_get函数分析
一、avcodec_descriptor_get函数的声明 avcodec_descriptor_get函数声明在FFmpeg源码(本文演示用的FFmpeg源码版本为7.0.1)的头文件libavcodec/codec_desc.h中: /*** return descriptor for given codec ID or NULL if no descriptor exist…...

为数据仓库构建Zero-ETL无缝集成数据分析方案(下篇)
对于从事数据分析的小伙伴们来说,最头疼的莫过于数据处理的阶段。在我们将数据源的原始数据导入数据仓储进行分析之前,我们通常需要进行ETL流程对数据格式进行统一转换,这个流程需要分配专业数据工程师基于业务情况完成,整个过程十…...

ElMessageBox消息确认框组件在使用时如何设置第三个或多个自定义按钮
ElMessageBox自带两个按钮一个确认一个取消,当还想使用该组件还想再加个功能组件时,就需要自定义个按钮加到组件里 第二种方法可以通过编写自定义弹窗来完成,个人觉得代码量增多过于繁琐,当然也可以实现 先定义方法负责获取dom父节点,创建新的子元素加…...

javaWeb【day04】--(MavenSpringBootWeb入门)
01. Maven课程介绍 1.1 课程安排 学习完前端Web开发技术后,我们即将开始学习后端Web开发技术。做为一名Java开发工程师,后端Web开发技术是我们学习的重点。 1.2 初识Maven 1.2.1 什么是Maven Maven是Apache旗下的一个开源项目,是一款用于…...

[Linux]:文件(下)
✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 重定向原理 在明确了文件描述符的概念及其分配规则后,我们就可…...

【学习笔记】手写Tomcat 一
目录 HTTP协议请求格式 HTTP协议响应格式 Socket 解读代码 服务端优化 解读代码 作业 1. 响应一个 HTML 页面给客户端,游览器把接收到的内容进行渲染 2. 文件的媒体类型是写死的,肯定不行,怎么变成动态? 昨天作业答案 …...

springboot基础-Druid数据库连接池使用
文章目录 引入Druid组件(maven)配置数据源Druid配置项1. 数据源配置2. 监控配置3. 安全配置4. SQL拦截配置 示例配置相关地址 Druid是阿里巴巴开源的一个高性能的Java数据库连接池组件,它提供了强大的监控统计功能和工具支持。Druid不仅可以作…...

C语言文件操作全攻略:从打开fopen到读写r,w,一网打尽
前言 在C语言中,文件操作是一项基础而强大的功能,它允许程序与存储在硬盘上的数据进行交互。无论是读取配置文件、处理日志文件,还是创建新的数据文件,C语言都提供了丰富的函数库来支持这些操作。本文将整合并详细介绍fopen(), 对…...

【0328】Postgres内核之 “User ID state”
1. User ID state 我们必须追踪与“用户ID(user ID)”概念相关的多个不同值。Postgres内核中有共有以下几个 User ID。 ✔ AuthenticatedUserId ✔ SessionUserId ✔ OuterUserId ✔ CurrentUserId 1.1 User ID 概念相关的不同值 AuthenticatedUserId AuthenticatedUserId…...