【Android 14源码分析】WMS-窗口显示-第一步:addWindow

news2024/10/1 3:27:03

忽然有一天,我想要做一件事:去代码中去验证那些曾经被“灌输”的理论。
                  
                  
                                           – 服装学院的IT男

本篇已收录于Activity短暂的一生系列
欢迎一起学习讨论Android应用开发或者WMS
V:WJB6995
Q:707409815

正文

窗口显示流程一共分为以下5篇:

窗口显示-流程概览与应用端流程分析

窗口显示-第一步:addWindow

窗口显示-第二步:relayoutWindow -1

窗口显示-第二步:relayoutWindow -2

窗口显示-第三步:finishDrawingWindow

1. 流程概述

本篇开始真正看 addWindow 流程,首先从结果上对比下应用启动后窗口的区别来确认本篇的目的:

在这里插入图片描述
红色部分就是启动应用后多出来的部分,在 DefaultTaskDisplayArea 节点下多出来这么一个层级:

Task
    ActivityRecord
        WindowState

在这里插入图片描述
其中 Task 和 ActivityRecord 是如何挂载上去的在【Activity启动流程】已经介绍了,当前要分析的 addWindow 流程最重要的目标就是分析窗口对应的 WindowState 是如何创建并且挂载到窗口树中的。

也就是这一变化:在这里插入图片描述
这个流程逻辑相对简单,整个流程框图如下:

在这里插入图片描述

    1. 应用端 Activity 执行到 onResume 说明 Activity 已经可见,下面就需要处理可见的内容
    1. 应用端 Session 调用到 WindowManagerService::addWindow 方法
    1. WMS 处理 addWindow 流程也就做了2件事:
    • 创建出对应的 WindowState
    • 挂载到层级树中(挂载到对应的 WindowToken 下)

2. 流程分析

上一篇知道流程已经执行到 ViewRootImpl::setView 来触发 addWindow 流程,回忆一下应用端的调用:

# ViewRootImpl

    final W mWindow;

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
                int userId) {
                    ......
                        res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
                                getHostVisibility(), mDisplay.getDisplayId(), userId,
                                mInsetsController.getRequestedVisibleTypes(), inputChannel, mTempInsets,
                                mTempControls, attachedFrame, compatScale);
                    ......
                }

这里有几个参数比较重要:
mWindow : 用于 WMS 与应用端通信
mWindowAttributes : DecorView 的参数
getHostVisibility() :可见性
inputChannel:Input 通路

看到你这些参数有个疑问:

明明是 addWindoW 流程,但是到了 WindowManagerImpl 就变成了 addView 传递的也是 DecoreView ,再到和 WMS 通信的时候,参数里连 DecoreView 都不剩了,这怎么能叫 addWindow 流程呢?

本篇将介绍WindowManagerService是如何处理剩下逻辑的文末也会回答这个问题。

2.1 WindowManagerService::addWindow方法概览

接上篇知道 Session::addToDisplayAsUser 方法调用的是 WindowManagerService::addWindow ,先看一下这个方法。

#  WindowManagerService
    // 保存应用端 ViewRootImpl 和 WindowState 的映射关系
    /** Mapping from an IWindow IBinder to the server's Window object. */
    final HashMap<IBinder, WindowState> mWindowMap = new HashMap<>();

    public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
            int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
            InputChannel outInputChannel, InsetsState outInsetsState,
            InsetsSourceControl[] outActiveControls) {
            ......
            // 1.1 权限检查
            int res = mPolicy.checkAddPermission(attrs.type, isRoundedCornerOverlay, attrs.packageName,
                    appOp);
            if (res != ADD_OKAY) {
                return res;
            }
            // 父窗口,应用 Activity 窗口逻辑是没有父窗口的
            WindowState parentWindow = null;
            ......
            // 拿到当前窗口类型
            final int type = attrs.type;
            ......
            synchronized (mGlobalLock) {
                    ......
                    // 1.2 如果窗口已经添加,直接return
                    if (mWindowMap.containsKey(client.asBinder())) {
                        // 日志
                        ProtoLog.w(WM_ERROR, "Window %s is already added", client);
                        return WindowManagerGlobal.ADD_DUPLICATE_ADD;
                    }
                    ......
                    
                    ActivityRecord activity = null;
                    // 是否为 hasParent
                    final boolean hasParent = parentWindow != null;
                    // 2.1 拿到token
                    WindowToken token = displayContent.getWindowToken(
                            hasParent ? parentWindow.mAttrs.token : attrs.token);
                    // Activity 没有父窗口,这里也为null
                    final int rootType = hasParent ? parentWindow.mAttrs.type : type;
                    ......
                    if (token == null) {
                        ......
                        if (hasParent) {
                            // Use existing parent window token for child windows.
                            // 2.2子窗口用父窗口的 token
                            token = parentWindow.mToken;
                        } else if (...) {
                            ......
                        } else {
                            // 2.3 系统窗口会创建 token
                            final IBinder binder = attrs.token != null ? attrs.token : client.asBinder();
                            token = new WindowToken.Builder(this, binder, type)
                                    .setDisplayContent(displayContent)
                                    .setOwnerCanManageAppTokens(session.mCanAddInternalSystemWindow)
                                    .setRoundedCornerOverlay(isRoundedCornerOverlay)
                                    .build();
                        }
                    } else if (rootType >= FIRST_APPLICATION_WINDOW
                        && rootType <= LAST_APPLICATION_WINDOW) {
                        ......
                    } else if......// 忽略其他各种创建对 token的处理

                    // 3.1 创建 WindowState
                    final WindowState win = new WindowState(this, session, client, token, parentWindow,
                            appOp[0], attrs, viewVisibility, session.mUid, userId,
                            session.mCanAddInternalSystemWindow);
                    ......
                    final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy();
                    // 调整window的参数
                    displayPolicy.adjustWindowParamsLw(win, win.mAttrs);
                    ......
                    // 1.3 验证Window是否可以添加,主要是验证权限
                    res = displayPolicy.validateAddingWindowLw(attrs, callingPid, callingUid);
                    if (res != ADD_OKAY) {
                        // 如果不满足则直接return
                        return res;
                    }   
                    final boolean openInputChannels = (outInputChannel != null
                            && (attrs.inputFeatures & INPUT_FEATURE_NO_INPUT_CHANNEL) == 0);
                    if  (openInputChannels) {
                        // 4.1 Input 事件输入通道
                        win.openInputChannel(outInputChannel);
                    }
                    ......
                    
                    // 3.2 创建SurfaceSession
                    win.attach();
                    // 3.3 窗口存入mWindowMap
                    mWindowMap.put(client.asBinder(), win);
                    ......
                    // 3.4 窗口挂载
                    win.mToken.addWindow(win);
                    displayPolicy.addWindowLw(win, attrs);
                    ......
                    // 4.2 处理窗口焦点切换
                    boolean focusChanged = false;
                    if (win.canReceiveKeys()) {
                        focusChanged = updateFocusedWindowLocked(UPDATE_FOCUS_WILL_ASSIGN_LAYERS,
                                false /*updateInputWindows*/);
                        if (focusChanged) {
                            imMayMove = false;
                        }
                    }
                    ......
                    // 调整父容器下的元素层级
                    win.getParent().assignChildLayers();
                    // 4.3 更新inut焦点
                    if (focusChanged) {
                        displayContent.getInputMonitor().setInputFocusLw(displayContent.mCurrentFocus,
                                false /*updateInputWindows*/);
                    }
                    // 4.4 更新input窗口
                    displayContent.getInputMonitor().updateInputWindowsLw(false /*force*/);
                    // 窗口添加log
                    ProtoLog.v(WM_DEBUG_ADD_REMOVE, "addWindow: New client %s"
                            + ": window=%s Callers=%s", client.asBinder(), win, Debug.getCallers(5));
                    ......
                }
                Binder.restoreCallingIdentity(origId);
                return res;
        }

这个方法就是 addWindow 流程的核心方法了,代码很多,保留了下面4个主要逻辑:

    1. 校验处理
    • 1.1 1.3 为操作权限校验
    • 1.2 为限制应用端的一个 RootView 只能执行一次 addWindow
    1. Token 处理
    • 这个 token 其实就是 WindowToken(ActivityRecord 是其子类)
    • 获取 token,如果是子窗口就从父窗口拿,没有就从参数里拿
    • 如果是系统窗口就会在2.3出根据窗口类型创建出一个 WindowToken
    1. WindowState 处理
    • 3.1 创建 WindowState
    • 2.2 执行 WindowState::attach 会创建 SurfaceSession
    • 3.3 将新建的 WindowState 和 W 映射,存入 mWindowMap
    • 3.4 窗口挂载
    1. Input 和焦点处理

当然这个方法实际上做的事肯定不止这些,只是根据我的个人理解整理出了几个比较重要的处理点。

当前分析的 addWindow 主流程,所以分析2,3两点,也就是 Token 和 WindowState 的处理逻辑。

3 Token相关

首先给一个结论,当前分析的场景,这个 Token 就是 Activity 启动流程中创建 ActivityRecord 时创建的 Token ,而 ActivityRecord 是 WindowToken 的子类。
在 【WindowContainer窗口树】介绍过,WindowState 的父节点大部分情况是 WindowToken ,而且在上一篇看到 dump 启动应前后的窗口树区别,明确知道 WindowState 是挂载到 ActivityRecord 下的,
在分析 WindowState 的创建和挂载前,需要先给它找到它的父节点: WindowToken 。这也是 WindowManagerService::addWindow 方法中比较靠前执行的逻辑。

根据上一小节的分析,当前场景的 Token 来自参数“attrs.token” 。这个参数是应用端传递过来的,上一篇在分析 WindowManagerGlobal::addView 方法的时候提到在Window::adjustLayoutParamsForSubWindow 方法对赋值 token 给参数,现在看一下这个方法。

# Window
    
    // 应用Token
    private IBinder mAppToken;

    void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
        ......
        if (wp.token == null) {
            wp.token = mContainer == null ? mAppToken : mContainer.mAppToken; // activity的window在这里设置token
        }
        ......
    }

mContainer 唯一赋值的地方在 Window::setContainer 方法,当前没调用,所以 wp.token 最终的值是为 mAppToken ,而mAppToken 的赋值在给 Window 设置 WindowManager 的时候赋值,也就是setWindowManager 方法,这里的 token 就是 ActivityRecord 的 token 。

下面这张图可以更直观的看到 Token 的传递:

在这里插入图片描述

    1. WindowToken 内部有个成本变量 token ,ActivityRecord 是其子类
    1. Activity 启动过程中会先创建 ActivityRecord ,在创建 ActivityRecord 的时候会创建一个匿名 Token 对象,并保存在变量 token 中
    1. 随着启动流程的执行,会在 ActivityTaskSupervisor::realStartActivityLocked 方法里构建事务,这个时候 token 就被保存在 ClientTransaction 的成员变量 mActivityToken
    1. ClientTransaction 提供了一个 getActivityToken 方法返回 mActivityToken 。这个方法在具体的事务执行时,比如 LaunchActivityItem::execute 方法执行,会作为参数传递过去
    1. LaunchActivityItem::execute 方法会构建一个 ActivityClientRecord ,构建方法需要 Token 参数,这个时候 Token 就被保存在 ActivityClientRecord 的成员变量 token 中
    1. 接下里就到了应用进程,应用进程执行 ActivityThread::performLaunchActivity 方法开始处理 Activity 启动流程,ActivityClientRecord 作为参数被传递了过来
    1. ActivityThread::performLaunchActivity 方法内部会执行 Activity::attach 方法,这个方法需要一个 Token 作为参数,传递的就是从 ActivityClientRecord 里取出的 token
    1. Activity::attach 方放会将 Token 赋值给成员变量 mToken
    1. Window 创建后会执行 Window::setWindowManager ,这个时候会将 mToken 作为参数传递进去,保存在 Window 的成员变量 mAppToken 中
    1. 在执行 WindowManagerGlobal::addView 时会执行 Window::adjustLayoutParamsForSubWindow 调整参数,这个时候 Token 就被复制到 WindowManager.LayoutParams 下的 token 变量中
    1. 执行 addWindow 流程时,WindowManager.LayoutParams 会被传递到 WMS ,这样 Token 也就被传递了过去

3.1 补充 生命周期事务流程图

这里补充下 system_service 是如何通过 ClientTransaction 来完成应用端生命周期相关执行的流程,具体代码不是当前重点,就不具体分析了。

在这里插入图片描述

4. WindowState的创建与挂载

addWindow 流程中 WindowState 的创建与挂载是重点,回顾一下这一流程层级树的变化:
在这里插入图片描述

4.1 WindowState的创建

在 WindowManagerService::addWindow 方法中,执行了 WindowState 的创建,代码如下:

# WindowManagerService

    public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
            int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
            InputChannel outInputChannel, InsetsState outInsetsState,
            InsetsSourceControl[] outActiveControls) {
            ......// WindowToken相关处理
            // 创建WindowState
            final WindowState win = new WindowState(this, session, client, token, parentWindow,
                    appOp[0], attrs, viewVisibility, session.mUid, userId,
                    session.mCanAddInternalSystemWindow);
            ......
        }

这里注意几个参数,然后直接看WindowState的构造方法

# WindowState

    final IWindow mClient;
    @NonNull WindowToken mToken;
    
    // The same object as mToken if this is an app window and null for non-app windows.
    // 与 mToken 相同的对象(如果这是应用程序窗口),而对于非应用程序窗口为null
    // 说人话就是应用窗口才有ActivityRecord
    ActivityRecord mActivityRecord;
    // 层级
    final int mBaseLayer;

    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) {
            ......
            mClient = c;
            ......
            // 保存token
            mToken = token;
            // 只有 ActivityRecord 重写了 asActivityRecord 其他默认返回努力了
            mActivityRecord = mToken.asActivityRecord();
            ......
            //子窗口处理
            if (mAttrs.type >= FIRST_SUB_WINDOW && mAttrs.type <= LAST_SUB_WINDOW){
                ......
            }else {
                // Activity的窗口指为  2 * 10000 + 1000  = 21000
                mBaseLayer = mPolicy.getWindowLayerLw(parentWindow)
                        * TYPE_LAYER_MULTIPLIER + TYPE_LAYER_OFFSET;
                ......
            }
        }

创建 WindowState 有2个重要的参数 :client,和 token 。这个 client 代表着客户端也就是 ViewRootImpl 的内部类 W ,另一个参数就是上节的 Token 。

WindowState 以后会经常看到,不过当前只要知道在 WindowManagerService::addWindow 会创建出一个 WindowState 对象即可。

4.2 WindowState的挂载

WindowState 创建好后自然是需要挂载到窗口树的,操作也很简单,直接添加到对应的 (ActivityRecord)WindowToken 下就好。

# WindowManagerService
    // ViewRootImpl和WindowState的map
    final HashMap<IBinder, WindowState> mWindowMap = new HashMap<>();

    public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
            int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
            InputChannel outInputChannel, InsetsState outInsetsState,
            InsetsSourceControl[] outActiveControls) {
            ......
            // 窗口已经添加,直接return
            if (mWindowMap.containsKey(client.asBinder())) {
                // 打印log
                ProtoLog.w(WM_ERROR, "Window %s is already added", client);
                return WindowManagerGlobal.ADD_DUPLICATE_ADD;
            }
            ......// WindowToken相关处理
            ......// WindowState的创建
            // WindowState的挂载
            win.attach();
            // 1. 存进map
            mWindowMap.put(client.asBinder(), win);
            ......
            // 2. 挂载
            win.mToken.addWindow(win);
            ......
        }
    1. 在看挂载前先看一下 mWindowMap 这个数据结构,key 是一个 IBinder,value 是 WindowState ,这边将新创建的 WindowState 作为 value 添加到了 map 中,前面说过 client是应用端 ViewRootImpl 下的 “W”这个类,也就是说在 WMS 中应用端的这个 ViewRootImpl 和为其创建的 WindowState 已经被记录在 mWindowMap 中了。

在执行WMS::addWindow方法开始的时候就会尝试通过 clent 从 mWindowMap 获取值,如果获取到了说明已经执行过 addWindow 则进行 return 不执行后面逻辑。

    1. 这里是窗口的挂载,“win.mToken” 这里的 mToken 刚刚看到是创建 WindowState 的时候传递的 token 也就是 ActivityRecord (WindowToken)。

也就是说调用的是 ActivityRecord::addWindow 方法进行挂载的。

# ActivityRecord
    @Override
    void addWindow(WindowState w) {
        super.addWindow(w);
        checkKeyguardFlagsChanged();
    }

直接调用其父类方法,ActivityRecord 父类是 WindowToken

# WindowToken
    void addWindow(final WindowState win) {
        // WindowState 挂载日志
        ProtoLog.d(WM_DEBUG_FOCUS,
                "addWindow: win=%s Callers=%s", win, Debug.getCallers(5));

        if (win.isChildWindow()) {
            // Child windows are added to their parent windows.
            // 子窗口的父类应该是WindowState所以不执行后续
            return;
        }
        // This token is created from WindowContext and the client requests to addView now, create a
        // surface for this token.
        // 真正添加进子容器
        if (!mChildren.contains(win)) {
            // 日志
            ProtoLog.v(WM_DEBUG_ADD_REMOVE, "Adding %s to %s", win, this);
            // 挂载(添加进孩子容器),有一个比较方法
            addChild(win, mWindowComparator);
            // 记录有窗口边框
            mWmService.mWindowsChanged = true;
            // TODO: Should we also be setting layout needed here and other places?
        }
    }

执行完 WindowContainer::addChild 方法后 WindowState 已经被添加到层级树中了,挂在到对应的 ActivityRecord 下。

当然这里需要注意 WindowToken::addWindow 最终也是调用父类 WindowContainer::addChild 将 WindowState 添加到自己的孩子中,这里传递了一个mWindowComparator。

4.3 挂载的位置

WindowContainer::addChild 方法被定义在基类,也就是容器添加孩子时都会按一定规则添加,当然默认其实还是按顺序,但是有的时候也有特殊情况,所以这个方法提供了一个参数,使得可以在具体场景控制具体的添加逻辑。

# WindowContainer
    protected void addChild(E child, Comparator<E> comparator) {
        // 检查子元素是否已经被其他容器拥有,如果是,则抛出异常
        if (!child.mReparenting && child.getParent() != null) {
            throw new IllegalArgumentException("addChild: container=" + child.getName()
                    + " is already a child of container=" + child.getParent().getName()
                    + " can't add to container=" + getName());
        }
        // 初始化插入位置为-1,表示尚未找到合适的插入位置
        int positionToAdd = -1;
        // 如果有比较器则进行比较
        
        // 遍历当前容器中的所有子元素
        if (comparator != null) {
            final int count = mChildren.size();
            // 使用比较器比较待插入的子元素和当前容器中的子元素
            for (int i = 0; i < count; i++) {

                // 如果比较结果小于0,表示待插入元素应该位于当前元素之前
                if (comparator.compare(child, mChildren.get(i)) < 0) {
                    positionToAdd = i;
                    break;
                }
            }
        }
        // 没有比较器或者比较的结果还是-1 ,则添加到最后(大部分场景)
        if (positionToAdd == -1) {
            mChildren.add(child);
        } else {
            // 如果比较器计算出了准确位置,则按要求添加
            mChildren.add(positionToAdd, child);
        }

        // Set the parent after we've actually added a child in case a subclass depends on this.
        // 调用孩子容器设置当前容器为其父节点
        child.setParent(this);
    }
    1. 方法目的就是添加子元素到父容器中,但是可以根据 comparator 比较规则添加到正确的位置
    1. 比较方式很简单,拿当前需要添加的元素和容器内其他元素逐个比较,如果比较 comparator 返回值小于0,则添加到“被比较”的元素前面
    1. 有2种情况,是按顺序添加到容器末尾
    • 3.1 没有比较器。positionToAdd 为默认值 -1
    • 3.2 和每个元素比较的返回值都大于0,说明要添加其后面,这个时候 positionToAdd 还是为默认值 -1
    1. setParent 调用孩子容器设置当前容器为其父节点,另外还会将 mSyncState 变量设置为 SYNC_STATE_WAITING_FOR_DRAW

当前场景,父容器 ActivityRecord 还是是空的,所以没什么意义。

不过既然看到这里,就继续分析,根据分析,当前逻辑调用的比较器是 WindowToken下的 mWindowComparator 。

4.3.1 addWindow是顺序-- WindowToken下的 mWindowComparator

# WindowToken

    private final Comparator<WindowState> mWindowComparator =
            (WindowState newWindow, WindowState existingWindow) -> {
        ......
        return isFirstChildWindowGreaterThanSecond(newWindow, existingWindow) ? 1 : -1;
    };
    protected boolean isFirstChildWindowGreaterThanSecond(WindowState newWindow,
            WindowState existingWindow) {
        // 就是比较两个窗口的mBaseLayer
        return newWindow.mBaseLayer >= existingWindow.mBaseLayer;
    }

这里的 newWindow 和 existingWindow 当然是一个当前需要添加进容器的 WindowState 和上一个存在的 WindowState

    1. 对 WindowToken 的子窗口进行比较排序,1和-1表示相对顺序
    1. 返回 true,则是 1,表示插入在后面。 也就是 newWindow 的 mBaseLayer 大于原来的
    1. 返回 false,则是 -1,表示插入在前面,也就是 newWindow 的 mBaseLayer 小于原来的

简单来说就是比较 mBaseLayer 来判断当前新添加的是放在哪个位置。 正常情况都是按顺添加,也就是后添加的在最上面。

这个 mBaseLayer 在 创建 WindowState 赋值,逻辑也比较简单,应用窗口的值计算后就是 21000 ,假设有2个应用窗口,那值都是一样的,就按序添加了。

这个值自己可以根据窗口类型计算,也可以使用命名 “adb shell dumpsys window windows” dump,然后搜 “mBaseLayer=” 确认。

addWindow 流程到这也就结束了,在三个流程里算比较简单的了,就做了2件事:

    1. 创建 WindowState
    1. 挂载到窗口树上

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

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

相关文章

项目管理专业资质认证ICB 3中关于项目经理素质的标准

项目管理专业资质认证ICB 3中关于项目经理素质的标准&#xff0c;的确很全面&#xff0c;下面摘录之&#xff1a;

三款专业的英文文献翻译工具,翻译论文不在话下

阅读英文论文文献时免不了要借用一些翻译软件来帮助理解&#xff0c;但因为论文文献的特殊性&#xff0c;普通的翻译软件不能很好的翻译一些专业名词和术语&#xff0c;所以这里给大家分享三款可以胜任文献翻译的专业翻译工具&#xff0c;可以快速准确的完成英文文献翻译工作。…

CDGA|2024年数据治理的六个关键建议

随着数字经济的快速发展&#xff0c;数据已成为企业运营和决策的核心资产。在2024年&#xff0c;做好数据治理对于提升企业的竞争力和运营效率至关重要。以下是六个关键建议&#xff0c;帮助企业有效应对数据治理的挑战。 1. 制定明确的数据治理策略 首先&#xff0c;企业需要…

遥感影像-实例分割数据集:iSAID 从切图到YOLO格式数据集制作详细介绍

背景介绍 开源数据集isaid标注包含实例分割&#xff0c;但是原始影像太大&#xff0c;很吃显存&#xff0c;一般显卡无法用原始影像直接训练&#xff0c;所以需要对影像进行裁剪&#xff0c;并生成对应的标签&#xff0c;因为想用yolo系列跑模型&#xff0c;所以将标签需要转为…

【设计模式-模板】

定义 模板方法模式是一种行为设计模式&#xff0c;它在一个方法中定义了一个算法的骨架&#xff0c;并将一些步骤延迟到子类中实现。通过这种方式&#xff0c;模板方法允许子类在不改变算法结构的情况下重新定义算法中的某些特定步骤。 UML图 组成角色 AbstractClass&#x…

Java 为什么使用 UTF-16 而不是更节省内存的 UTF-8?

Java 选择 UTF-16 编码而不是更节省内存的 UTF-8 这一决定&#xff0c;涉及多个层面的设计权衡&#xff0c;包括历史原因、虚拟机&#xff08;JVM&#xff09;实现的复杂度、性能和字符处理的一致性。要理解这个问题&#xff0c;我们需要从 Java 语言的设计初衷、JVM 的工作机制…

C++:笔试题

1.什么是虚函数&#xff1f;什么是纯虚函数&#xff1f; 虚函数是类中的一个成员函数&#xff0c;使用关键字virtual在函数名前声明。 虚函数主要目的是允许子类重写父类中的同名函数&#xff0c;从而实现多态性&#xff0c;并且子函数重写的是虚函数表中的函数。 当通过父类的…

七、添加攻击音效

一、添加动画事件 1、在动画事件中添加音效 2、添加音频组件 3、代码 public void PlayAttackSound() {AudioSource1.PlayOneShot(AudioClip1, SoundValue);//PlayOneShot播放一个音频剪辑&#xff08;AudioClip&#xff09;一次 }

Oracle 日志文件多路复用

多路复用 PRODCDB 数据库的所有日志组中的 redo log 文件&#xff0c;存放目录&#xff1a; /u01/app/oracle/oradata/MREDO 1.创建目录 mkdir -p /u01/app/oracle/oradata/MREDO 2.查看日志文件路径 select group#,member from v$logfile; 3.增加日志组文件 alter database a…

ElementUI el-tree 树组件 增加辅助线

需求 项目需求给elementUI的el-tree添加辅助线&#xff0c;并且不能使用其他插件&#xff0c;没办法只能该样式了。 效果 代码 html <template><div><el-scrollbar class"long-content"><el-tree node-key"id":data"deptTre…

《程序猿之Redis缓存实战 · 有序集合类型》

&#x1f4e2; 大家好&#xff0c;我是 【战神刘玉栋】&#xff0c;有10多年的研发经验&#xff0c;致力于前后端技术栈的知识沉淀和传播。 &#x1f497; &#x1f33b; CSDN入驻不久&#xff0c;希望大家多多支持&#xff0c;后续会继续提升文章质量&#xff0c;绝不滥竽充数…

OpenGL ES 索引缓冲区(4)

OpenGL ES 索引缓冲区(4) 简述 本节会介绍索引缓冲区&#xff0c;索引缓冲区和顶点缓冲区类似&#xff0c;也是显存上的一段内存&#xff0c;只不过上面的数据用处不同&#xff0c;索引缓冲区故名思义里面的数据是用于索引&#xff0c;主要作用是用于复用顶点缓冲区里的数据。…

Kd-tree介绍和使用

GeoHash原理介绍以及在redis中的应用-CSDN博客 这边文章中介绍了GeoHash编码原理以及它的一个应用——利用GeoHash编码可以建立一个索引&#xff0c;从而实现快速的空间搜索。今天&#xff0c;我们介绍一个常见的数据结构Kd-Tree&#xff0c;利用它也可以快速实现多位数据的搜索…

调用智谱AI,面试小助手Flask简单示例

文章目录 1.接入AI获取API密钥Python代码 2.小助手的实现流程3.Flask应用示例Python文件.pyindex.html运行Flask应用地址栏输入 http://localhost:5000/ 1.接入AI 获取API密钥 在智谱AI的官方网站上注册&#xff0c;右上角点击API密钥&#xff0c;新建并复制一个 API Key&…

掌握未来:产品经理学习AI大模型的重要性解析

前言 在AI大模型时代&#xff0c;技术的迅猛进步正在重塑各行各业的面貌。作为产品经理&#xff0c;我们不仅要紧跟时代步伐&#xff0c;更要深入探索与运用这一前沿技术。学习大模型等AI技术&#xff0c;不仅是为了理解其背后的工作原理和应用潜力&#xff0c;更是为了将智能…

天选思路怎能不会!小波变换+CNN完美融合,最新idea发了CV顶会!

今天给大家推荐一个涨点发顶会的好方向&#xff1a;小波变换CNN。这俩热点的结合可以轻松实现“11&#xff1e;2”的效果。 这是因为&#xff0c;一方面小波变换可以作为预处理步骤&#xff0c;提取出关键的局部特征&#xff0c;加速CNN收敛并提升性能&#xff1b;另一方面&am…

配置树莓派打开SSH服务

在树莓派终端中查看IP 在终端中输入命令来查看IP地址。最常用的命令是&#xff1a;hostname -I注意&#xff0c;这里的参数I是大写的&#xff0c;它表示查看本机上所有配置的IP地址&#xff08;包括IPv4和IPv6&#xff0c;如果有的话&#xff09;。如果你只需要查看IPv4地址&am…

Linux:磁盘管理

一、静态分区管理 静态的分区方法不可以动态的增加或减少分区的容量。 1、磁盘分区-fdisk 该命令是用于查看磁盘分区情况&#xff0c;和分区管理的命令 命令格式&#xff1a;fdisk [选项] 设备文件名常用命令&#xff1a; -h&#xff1a;查看分区信息 fdisk系统常用命令&…

19、网络安全合规复盘

数据来源&#xff1a;5.网络安全合规复盘_哔哩哔哩_bilibili

山大电力研发费用率远弱同行,先分红上亿再补流9000万?

《港湾商业观察》施子夫 8月9日&#xff0c;证监会网站披露深交所已向山东山大电力技术股份有限公司&#xff08;以下简称&#xff0c;山大电力&#xff09;发出第三轮审核问询函。据悉&#xff0c;2023年6月&#xff0c;山大电力递表深交所&#xff0c;保荐机构为兴业证券。 …