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

【Android 13源码分析】WindowContainer窗口层级-2-构建流程

在安卓源码的设计中,将将屏幕分为了37层,不同的窗口将在不同的层级中显示。
对这一块的概念以及相关源码做了详细分析,整理出以下几篇。

【Android 13源码分析】WindowContainer窗口层级-1-初识窗口层级树

【Android 13源码分析】WindowContainer窗口层级-2-构建流程

【Android 13源码分析】WindowContainer窗口层级-3-实例分析

【Android 13源码分析】WindowContainer窗口层级-4-Surface树

当前为第二篇,第一篇对窗口树有一个简单的认识后,本篇介绍窗口树的构建代码流程。
整个过程会相对无聊,但是不讲代码的技术文章就是耍流氓。

1. dump内容的打印

先看dump的数据在代码中是如何定义的

# WindowManagerService
    RootWindowContainer mRoot;

    private void doDump(FileDescriptor fd, PrintWriter pw, String[] args, boolean useProto) {
        ......
            // containers
            } else if ("containers".equals(cmd)) {
                synchronized (mGlobalLock) {
                    mRoot.dumpChildrenNames(pw, " ");
                    pw.println(" ");
                    mRoot.forAllWindows(w -> {pw.println(w);}, true /* traverseTopToBottom */);
                }
                return;
            } else if ("trace".equals(cmd)) {
        ......
    }
    //  打印的name
    @Override
    String getName() {
        return "ROOT";
    }

dumpChildrenNames的实现在WindowContainer的父类ConfigurationContainer中

# ConfigurationContainer
    public void dumpChildrenNames(PrintWriter pw, String prefix) {
        final String childPrefix = prefix + " ";
        pw.println(getName()
                + " type=" + activityTypeToString(getActivityType())
                + " mode=" + windowingModeToString(getWindowingMode())
                + " override-mode=" + windowingModeToString(getRequestedOverrideWindowingMode())
                + " requested-bounds=" + getRequestedOverrideBounds().toShortString()
                + " bounds=" + getBounds().toShortString());
        for (int i = getChildCount() - 1; i >= 0; --i) {
            final E cc = getChildAt(i);
            // 打印 # 加角标
            pw.print(childPrefix + "#" + i + " ");
            cc.dumpChildrenNames(pw, childPrefix);
        }
    }

可以看到从RootWindowContainer开始递归打印。 这也就是dump到的窗口容器层级树的内容。比如最开始的RootWindowContainer::getName返回的内容就是 “ROOT”。

2. 层级树的构建

2.1 调用链

SystemServer::run
    SystemServer::startOtherServices
        WindowManagerService::init
        ActivityManagerService::setWindowManager
            ActivityTaskManagerService::setWindowManager
                RootWindowContainer::setWindowManager
                    DisplayContent::init
                        DisplayContent::configureSurfaces
                            DisplayAreaPolicy.Provider::instantiate  -- 创建 DefaultTaskDisplayArea和输入法容器
                                DisplayAreaPolicy.Provider::configureTrustedHierarchyBuilder -- 开始配置图层的Feature
                                DisplayAreaPolicyBuilder::build
                                    PendingArea::instantiateChildren  -- 开始递归构建层级树
                                    RootDisplayArea::onHierarchyBuilt -- 构建完成

2.2 前期的一些调用链

调用链前面这段可以知道,在系统启动的时候就触发了这段逻辑,这也就是为什么刚进入launcher就可以dump出整个结构树的原因。
setWindowManager 方法传递的参数是WMS, WMS的启动是tartOtherServices中,而RootWindowContainer则是WMS的一个成员变量,RootWindowContainer是层级树中的跟容器,在WMS构建函数中创建。

# WindowManagerService
    // The root of the device window hierarchy.
    RootWindowContainer mRoot;
    // WMS构造函数
    private WindowManagerService(......) {
        ......
        mRoot = new RootWindowContainer(this);
        ......
    }

继续看构建流程

# RootWindowContainer
    void setWindowManager(WindowManagerService wm) {
        mWindowManager = wm;
        mDisplayManager = mService.mContext.getSystemService(DisplayManager.class);
        mDisplayManager.registerDisplayListener(this, mService.mUiHandler);
        mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);

        final Display[] displays = mDisplayManager.getDisplays();
        //  遍历每个屏幕
        for (int displayNdx = 0; displayNdx < displays.length; ++displayNdx) {
            final Display display = displays[displayNdx];
            // 重点*1:为每一个 Display 挂载一个 DisplayContent 节点
            final DisplayContent displayContent = new DisplayContent(display, this);
            addChild(displayContent, POSITION_BOTTOM);
            if (displayContent.mDisplayId == DEFAULT_DISPLAY) {
                mDefaultDisplay = displayContent;
            }
        }
    
        // 重点*2  TaskDisplayArea相关
        final TaskDisplayArea defaultTaskDisplayArea = getDefaultTaskDisplayArea();
        defaultTaskDisplayArea.getOrCreateRootHomeTask(ON_TOP);
        positionChildAt(POSITION_TOP, defaultTaskDisplayArea.mDisplayContent,
                false /* includingParents */);
    }

重点解析:

  1. 这段代码也就能看出,为什么说一个DisplayContent就代表着1个屏幕了。
  2. 处理TaskDisplayArea相关(这里窗口层级树已经构建完成了)
    上篇看层级树知道TaskDisplayArea就是放应用相关容器的,目前先不看这块,先跟踪DisplayContent下的逻辑, 现在需要看DisplayContent的构造方法,因为里面开始构造这个屏幕下层级树。(不考虑多屏幕的情况)

3. DisplayContent 开始构造当前屏幕的层级树

# DisplayContent
    // 2个参数为Display和跟容器
    DisplayContent(Display display, RootWindowContainer root) {
        ......
        // 创建事务
        final Transaction pendingTransaction = getPendingTransaction();
        // 重点开始构建层级树
        configureSurfaces(pendingTransaction);
        // 执行事务
        pendingTransaction.apply();
        ......
    }

    private void configureSurfaces(Transaction transaction) {
        // 构建一个SurfaceControl
        final SurfaceControl.Builder b = mWmService.makeSurfaceBuilder(mSession)
                .setOpaque(true)
                .setContainerLayer()
                .setCallsite("DisplayContent");
        // 设置名字后构建 (Display 0 name="XXX")
        mSurfaceControl = b.setName(getName()).setContainerLayer().build();

        // 重点* 设置策略并构建显示区域层次结构
        if (mDisplayAreaPolicy == null) {
            // WMS的getDisplayAreaPolicyProvider方法按返回 DisplayAreaPolicy.Provider
            // 然后其 instantiate的实现 目前只有DisplayAreaPolicy的内部类DefaultProvider
            
            mDisplayAreaPolicy = mWmService.getDisplayAreaPolicyProvider().instantiate(
                    mWmService, this /* content */, this /* root */,
                    mImeWindowsContainer);
        }
        // 事务相关设置
        transaction
                .setLayer(mSurfaceControl, 0)
                .setLayerStack(mSurfaceControl, mDisplayId)
                .show(mSurfaceControl)
                .setLayer(mOverlayLayer, Integer.MAX_VALUE)
                .show(mOverlayLayer);
    }

这一块我们目前可以忽略transaction的代码只需要关心中间“getDisplayAreaPolicyProvider”这一块就好了,这段是层级树的主流程。

tips:

  1. 方法最开始的 setName设置name在层级树是能找到对应名字的
  2. 注意instantiate倒数第3第4都个参数传递的都是this,也就是DisplayContent, 因为 DisplayContent这是的父类是RootDisplayArea
# DisplayContent
    String getName() {
        return "Display " + mDisplayId + " name=\"" + mDisplayInfo.name + "\"";
    }

mDisplayId 如果只有一个屏幕就是 0 ,所以dump到层级树中的这句信息

  #0 Display 0 name="Built-in Screen"

就是在这里设置的,后面的"Built-in Screen"对应的应该就是mDisplayInfo.name了。
接下来继续看主流程:

# DisplayAreaPolicy.Provider
        @Override
        public DisplayAreaPolicy instantiate(WindowManagerService wmService,
                DisplayContent content, RootDisplayArea root,
                DisplayArea.Tokens imeContainer) {

            // 重点*1. 创建一个名为 "DefaultTaskDisplayArea" 的对象作为应用窗口的默认容器(第三个参数Feature为FEATURE_DEFAULT_TASK_CONTAINER)
            final TaskDisplayArea defaultTaskDisplayArea = new TaskDisplayArea(content, wmService,
                    "DefaultTaskDisplayArea", FEATURE_DEFAULT_TASK_CONTAINER);
            final List<TaskDisplayArea> tdaList = new ArrayList<>();
            //  实际上只有1个元素
            tdaList.add(defaultTaskDisplayArea);

            // Define the features that will be supported under the root of the whole logical
            // display. The policy will build the DisplayArea hierarchy based on this.
            // 传递RootDisplayArea(DisplayContent)构建出一个层级树的数据结构
            final HierarchyBuilder rootHierarchy = new HierarchyBuilder(root);
            // Set the essential containers (even if the display doesn't support IME).
            // 设置输入法容器
            rootHierarchy.setImeContainer(imeContainer).setTaskDisplayAreas(tdaList);

            // 这个条件满足,肯定是被信任的
            if (content.isTrusted()) {
                // 重点* 2 配置层级的支持的Feature
                configureTrustedHierarchyBuilder(rootHierarchy, wmService, content);
            }

            // Instantiate the policy with the hierarchy defined above. This will create and attach
            // all the necessary DisplayAreas to the root.
            // 重点* 3 真正开始构建层级树
            return new DisplayAreaPolicyBuilder().setRootHierarchy(rootHierarchy).build(wmService);
        }

这个方法是在DisplayContent构造函数掉进来的,注意最后2个参数,root表示跟容器,imeContainer则是输入法容器,在DisplayContent中传过来的,然后被通过setImeContainer设置给了HierarchyBuilder。
重点分析:

  1. “DefaultTaskDisplayArea” 终于出现了, 可以看到确实是TaskDisplayArea对象,然后FEATURE_DEFAULT_TASK_CONTAINER这个ID的值就是1, 那也就是在第二层,和层级树是对应的,然后构建了一个List,但是这个集合就这一个元素。
  2. 配置层级的支持的Feature
  3. 开始真正的构建

3.1 配置Feature

发现层级树中一共就出现了5个Feature就是在当前方法中配置的,分别如下:
WindowedMagnification
HideDisplayCutout
OneHanded
FullscreenMagnification
ImePlaceholder

# DisplayAreaPolicy.Provider
        private void configureTrustedHierarchyBuilder(HierarchyBuilder rootHierarchy,
                WindowManagerService wmService, DisplayContent content) {
            // WindowedMagnification should be on the top so that there is only one surface
            // to be magnified.
            rootHierarchy.addFeature(new Feature.Builder(wmService.mPolicy, "WindowedMagnification",
                    FEATURE_WINDOWED_MAGNIFICATION)
                    .upTo(TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY)
                    .except(TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY)
                    // Make the DA dimmable so that the magnify window also mirrors the dim layer.
                    .setNewDisplayAreaSupplier(DisplayArea.Dimmable::new)
                    .build());
            if (content.isDefaultDisplay) {
                // Only default display can have cutout.
                // See LocalDisplayAdapter.LocalDisplayDevice#getDisplayDeviceInfoLocked.
                rootHierarchy.addFeature(new Feature.Builder(wmService.mPolicy, "HideDisplayCutout",
                        FEATURE_HIDE_DISPLAY_CUTOUT)
                        .all()
                        .except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL, TYPE_STATUS_BAR,
                                TYPE_NOTIFICATION_SHADE)
                        .build())
                        .addFeature(new Feature.Builder(wmService.mPolicy, "OneHanded",
                                FEATURE_ONE_HANDED)
                                .all()
                                .except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL,
                                        TYPE_SECURE_SYSTEM_OVERLAY)
                                .build());
            }
            rootHierarchy
                    .addFeature(new Feature.Builder(wmService.mPolicy, "FullscreenMagnification",
                            FEATURE_FULLSCREEN_MAGNIFICATION)
                            .all()
                            .except(TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, TYPE_INPUT_METHOD,
                                    TYPE_INPUT_METHOD_DIALOG, TYPE_MAGNIFICATION_OVERLAY,
                                    TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL)
                            .build())
                    .addFeature(new Feature.Builder(wmService.mPolicy, "ImePlaceholder",
                            FEATURE_IME_PLACEHOLDER)
                            .and(TYPE_INPUT_METHOD, TYPE_INPUT_METHOD_DIALOG)
                            .build());
    }

这里执行了5次addFeature,每次对应一个Feature刚好是5个。Feature.Builder构造一个Feature对象,代码如下

# DisplayAreaPolicy.Feature.Builder
            Builder(WindowManagerPolicy policy, String name, int id) {
                mPolicy = policy;
                mName = name;
                mId = id;
                mLayers = new boolean[mPolicy.getMaxWindowLayer() + 1];
            }

注意后面2个参数,第二个为name,就是名字,后面的是ID,根据使用的地方肯定是定义了对应ID的。
留意下mLayers,mPolicy.getMaxWindowLayer()返回36所以是定义了一个长度为37的boolean类型数组,如果为ture表示这个图层支持这个Feature,为false反之。

5个Feature对应的ID如下,并且有相应的注释:

    # DisplayAreaOrganizer
        /**
         * Display area that can be magnified in
         * ......
         */
        public static final int FEATURE_WINDOWED_MAGNIFICATION = FEATURE_SYSTEM_FIRST + 4;

        /**
         * Display area for hiding display cutout feature
         * @hide
         */
        public static final int FEATURE_HIDE_DISPLAY_CUTOUT = FEATURE_SYSTEM_FIRST + 6;

        /**
         * Display area for one handed feature
         */
        public static final int FEATURE_ONE_HANDED = FEATURE_SYSTEM_FIRST + 3;

        /**
         * Display area that can be magnified in
         * ......
         */
        public static final int FEATURE_FULLSCREEN_MAGNIFICATION = FEATURE_SYSTEM_FIRST + 5;

        /**
         * Display area that the IME container can be placed in. Should be enabled on every root
         * hierarchy if IME container may be reparented to that hierarchy when the IME target changed.
         * @hide
         */
        public static final int FEATURE_IME_PLACEHOLDER = FEATURE_SYSTEM_FIRST + 7;

根据注释能知道这个Feature代表这个图层具体用于什么特征了。

然后还看到all(),and(),except()等方法。

3.1.1 all,and,except方法

    # DisplayAreaPolicy.Feature.Builder

         Builder all() {
            Arrays.fill(mLayers, true);
            return this;
        }

        Builder and(int... types) {
            for (int i = 0; i < types.length; i++) {
                int type = types[i];
                set(type, true);
            }
            return this;
        }

        Builder except(int... types) {
            for (int i = 0; i < types.length; i++) {
                int type = types[i];
                set(type, false);
            }
            return this;
        }

        Builder upTo(int typeInclusive) {
            // 根据传入的type计算到图层
            final int max = layerFromType(typeInclusive, false);
            for (int i = 0; i < max; i++) {
                mLayers[i] = true;
            }
            set(typeInclusive, true);
            return this;
        
        }
        private void set(int type, boolean value) {
            mLayers[layerFromType(type, true)] = value;
            ......
        }

        private int layerFromType(int type, boolean internalWindows) {
            return mPolicy.getWindowLayerFromTypeLw(type, internalWindows);
        }

        Feature build() {
            // 默认为true
            if (mExcludeRoundedCorner) {
                // Always put the rounded corner layer to the top most layer.
                mLayers[mPolicy.getMaxWindowLayer()] = false;
            }
            return new Feature(mName, mId, mLayers.clone(), mNewDisplayAreaSupplier);
        }

mLayers前面说过是一个长度为37的数组,set方法就是将参数的这个图层,对应的boolean设置为true, 换句话说就是指定某个图层是否支持这个Feature。

all():将所有数组所有值都设为true,表示每个图层都支持这个Feature

and(): 将指定某个图层支持这个Feature

except():将指定某个图层不支持这个Feature

upTo(): 将支持Feature的图层设置为从0到typeInclusive

build():将数组的最后最后一个设置为false,剔除最后一层

这里的几个方法都会调用到layerFromType,根据layerFromType方法的调用知道具体逻辑在WindowManagerPolicy::getWindowLayerFromTypeLw方法控制的.
这段代码有点长是因为好多case,但是总体逻辑并不复杂,主要关注传入的WindowType和返回的Layertype,其实就是返回层级树中所在的图层。

3.1.2 重点:getWindowLayerFromTypeLw方法 (决定窗口挂载在哪一层)

   # WindowManagerPolicy
       default int getWindowLayerFromTypeLw(int type, boolean canAddInternalSystemWindow,
               boolean roundedCornerOverlay) {
           // Always put the rounded corner layer to the top most.
           // 第二个参数为false,这里忽略
           if (roundedCornerOverlay && canAddInternalSystemWindow) {
               return getMaxWindowLayer();
           }
           // 根据这2个type名字也知道表示 APP图层,对应的值是1-99,如果处于这直接就返回APPLICATION_LAYER =2
           if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
               return APPLICATION_LAYER;
           }
           // 然后开始根据各个WindowType,去返回其在层级树中所在的图层
           switch (type) {
               case TYPE_WALLPAPER:
                   // wallpaper is at the bottom, though the window manager may move it.
                   return  1;
               case TYPE_PRESENTATION:
               case TYPE_PRIVATE_PRESENTATION:
               case TYPE_DOCK_DIVIDER:
               case TYPE_QS_DIALOG:
               case TYPE_PHONE:
                   return  3;
               case TYPE_SEARCH_BAR:
                   return  4;
               case TYPE_INPUT_CONSUMER:
                   return  5;
               case TYPE_SYSTEM_DIALOG:
                   return  6;
               case TYPE_TOAST:
                   // toasts and the plugged-in battery thing
                   return  7;
               case TYPE_PRIORITY_PHONE:
                   // SIM errors and unlock.  Not sure if this really should be in a high layer.
                   return  8;
               case TYPE_SYSTEM_ALERT:
                   // like the ANR / app crashed dialogs
                   // Type is deprecated for non-system apps. For system apps, this type should be
                   // in a higher layer than TYPE_APPLICATION_OVERLAY.
                   return  canAddInternalSystemWindow ? 12 : 9;
               case TYPE_APPLICATION_OVERLAY:
                   return  11;
               case TYPE_INPUT_METHOD:
                   // on-screen keyboards and other such input method user interfaces go here.
                   return  13;
               case TYPE_INPUT_METHOD_DIALOG:
                   // on-screen keyboards and other such input method user interfaces go here.
                   return  14;
               case TYPE_STATUS_BAR:
                   return  15;
               case TYPE_STATUS_BAR_ADDITIONAL:
                   return  16;
               case TYPE_NOTIFICATION_SHADE:
                   return  17;
               case TYPE_STATUS_BAR_SUB_PANEL:
                   return  18;
               case TYPE_KEYGUARD_DIALOG:
                   return  19;
               case TYPE_VOICE_INTERACTION_STARTING:
                   return  20;
               case TYPE_VOICE_INTERACTION:
                   // voice interaction layer should show above the lock screen.
                   return  21;
               case TYPE_VOLUME_OVERLAY:
                   // the on-screen volume indicator and controller shown when the user
                   // changes the device volume
                   return  22;
               case TYPE_SYSTEM_OVERLAY:
                   // the on-screen volume indicator and controller shown when the user
                   // changes the device volume
                   return  canAddInternalSystemWindow ? 23 : 10;
               case TYPE_NAVIGATION_BAR:
                   // the navigation bar, if available, shows atop most things
                   return  24;
               case TYPE_NAVIGATION_BAR_PANEL:
                   // some panels (e.g. search) need to show on top of the navigation bar
                   return  25;
               case TYPE_SCREENSHOT:
                   // screenshot selection layer shouldn't go above system error, but it should cover
                   // navigation bars at the very least.
                   return  26;
               case TYPE_SYSTEM_ERROR:
                   // system-level error dialogs
                   return  canAddInternalSystemWindow ? 27 : 9;
               case TYPE_MAGNIFICATION_OVERLAY:
                   // used to highlight the magnified portion of a display
                   return  28;
               case TYPE_DISPLAY_OVERLAY:
                   // used to simulate secondary display devices
                   return  29;
               case TYPE_DRAG:
                   // the drag layer: input for drag-and-drop is associated with this window,
                   // which sits above all other focusable windows
                   return  30;
               case TYPE_ACCESSIBILITY_OVERLAY:
                   // overlay put by accessibility services to intercept user interaction
                   return  31;
               case TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY:
                   return 32;
               case TYPE_SECURE_SYSTEM_OVERLAY:
                   return  33;
               case TYPE_BOOT_PROGRESS:
                   return  34;
               case TYPE_POINTER:
                   // the (mouse) pointer layer
                   return  35;
               default:
                   Slog.e("WindowManager", "Unknown window type: " + type);
                   return 3;
           }
       }

代码很长不用一个个看,直接根据参数找就好,比如当参数是TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY,那对应的返回就是32。
后面看到的对应的TYPE,直接复制WindowManagerPolicy类下搜索即可。

现在再重新看看configureTrustedHierarchyBuilder方法里5个Feature到底是什么。

    # DisplayAreaPolicy.Provider
            private void configureTrustedHierarchyBuilder(HierarchyBuilder rootHierarchy,
                    WindowManagerService wmService, DisplayContent content) {
                // WindowedMagnification should be on the top so that there is only one surface
                // to be magnified.
                rootHierarchy.addFeature(new Feature.Builder(wmService.mPolicy, "WindowedMagnification",
                        FEATURE_WINDOWED_MAGNIFICATION)
                        .upTo(TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY) // 0-32
                        .except(TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY)// 32
                        // Make the DA dimmable so that the magnify window also mirrors the dim layer.
                        .setNewDisplayAreaSupplier(DisplayArea.Dimmable::new)
                        .build()); // 0-31
                if (content.isDefaultDisplay) {
                    // Only default display can have cutout.
                    // See LocalDisplayAdapter.LocalDisplayDevice#getDisplayDeviceInfoLocked.
                    rootHierarchy.addFeature(new Feature.Builder(wmService.mPolicy, "HideDisplayCutout",
                            FEATURE_HIDE_DISPLAY_CUTOUT)
                            .all() //  0-36
                            .except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL, TYPE_STATUS_BAR,
                                    TYPE_NOTIFICATION_SHADE)//  24 25  15  17
                            .build())// 0-14   16  18-23 26-35
                            .addFeature(new Feature.Builder(wmService.mPolicy, "OneHanded",
                                    FEATURE_ONE_HANDED)
                                    .all()
                                    .except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL,
                                            TYPE_SECURE_SYSTEM_OVERLAY)//24  25  33
                                    .build());// 0-23   26-32  34-35
                }
                rootHierarchy
                        .addFeature(new Feature.Builder(wmService.mPolicy, "FullscreenMagnification",
                                FEATURE_FULLSCREEN_MAGNIFICATION)
                                .all() // 0-36
                                .except(TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, TYPE_INPUT_METHOD,
                                        TYPE_INPUT_METHOD_DIALOG, TYPE_MAGNIFICATION_OVERLAY,
                                        TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL)// 32  13   14  28  24 25
                                .build())// 0-12 15-23  26-27 29-31 33-35
                        .addFeature(new Feature.Builder(wmService.mPolicy, "ImePlaceholder",
                                FEATURE_IME_PLACEHOLDER)
                                .and(TYPE_INPUT_METHOD, TYPE_INPUT_METHOD_DIALOG)// 13-14
                                .build());// 13-14
            }
        }

3.2 Feature总结

WindowedMagnification
拥有特征的层级: 0-31
特征描述: 支持窗口缩放的一块区域,一般是通过辅助服务进行缩小或放大

HideDisplayCutout
拥有特征的层级: 0-14 16 18-23 26-35
特征描述:隐藏剪切区域,即在默认显示设备上隐藏不规则形状的屏幕区域,比如在代码中打开这个功能后,有这个功能的图层就不会延伸到刘海屏区域。

OneHanded
拥有特征的层级:0-23 26-32 34-35
特征描述:表示支持单手操作的图层,这个功能在手机上还是挺常见的

FullscreenMagnification
拥有特征的层级:0-12 15-23 26-27 29-31 33-35
特征描述:支持全屏幕缩放的图层,和上面的不同,这个是全屏缩放,前面那个可以局部

ImePlaceholder
拥有特征的层级: 13-14
特征描述:输入法相关

再放上之前画的层级树更加清晰了

在这里插入图片描述

3.3 构建层级树 DisplayAreaPolicyBuilder::build

上面只是将5个Feature添加到了rootHierarchy的mFeatures这个集合中

    # HierarchyBuilder
        private final ArrayList<DisplayAreaPolicyBuilder.Feature> mFeatures = new ArrayList<>();

            HierarchyBuilder addFeature(DisplayAreaPolicyBuilder.Feature feature) {
                mFeatures.add(feature);
                return this;
            }

DisplayAreaPolicyBuilder::setRootHierarchy方法很简单,就是把添加了ImeContainer和5个Feature的HierarchyBuilder设置给DisplayAreaPolicyBuilder

    # DisplayAreaPolicyBuilder
        DisplayAreaPolicyBuilder setRootHierarchy(HierarchyBuilder rootHierarchyBuilder) {
            mRootHierarchyBuilder = rootHierarchyBuilder;
            return this;
        }

然后开始执行DisplayAreaPolicyBuilder::build

    # DisplayAreaPolicyBuilder
        // 这个可以忽略,没有值
        private final ArrayList<HierarchyBuilder> mDisplayAreaGroupHierarchyBuilders =
                new ArrayList<>();

        Result build(WindowManagerService wmService) {
            //  对输入参数进行验证,确保它们是有效的
            validate();

            // Attach DA group roots to screen hierarchy before adding windows to group hierarchies.
            // 重点:构建层级树
            mRootHierarchyBuilder.build(mDisplayAreaGroupHierarchyBuilders);
            // 因为mDisplayAreaGroupHierarchyBuilders没有值,后面的都可以忽略
            ......
            return new Result(wmService, mRootHierarchyBuilder.mRoot, displayAreaGroupRoots,
                    mSelectRootForWindowFunc);
        }

4. 层级结构树构造

这个mRootHierarchyBuilder就是上一小节操作的RootHierarchyBuilder,然后执行其build方法,这个方法非常重要!!!

在这里插入图片描述

在构造层级树一共分为2步:

  1. 构建PendingArea树
    1. 构建Feature相关
    2. 构建Leaf相关
  2. 根据PendingArea树构建最终的DisplayAreas树,也就是层级树

通过2个类的名字也能感觉到一些关系,毕竟叫Pending。既然要先构造PendingPendingArea,那肯定需要先看看PendingArea这个数据结构

4.1 数据结构 PendingArea简介

    # DisplayAreaPolicyBuilder.PendingArea

        static class PendingArea {
            final int mMinLayer; // 最小层级
            final ArrayList<PendingArea> mChildren = new ArrayList<>();// 有Children说明也是一个容器
            final Feature mFeature; //  当前支持的Feature
            final PendingArea mParent; // 有父亲
            int mMaxLayer; // 最大层级
            // 从这几个成员变量其实能感觉到和上一篇画的层级树的图有点那味了
            @Nullable DisplayArea mExisting; // 当前存在的容器
            boolean mSkipTokens = false; // 只有输入法和应用会为true

            PendingArea(Feature feature, int minLayer, PendingArea parent) {
                mMinLayer = minLayer;
                mFeature = feature;
                mParent = parent;
            }
            ......
        }

PendingArea后面还有一些方法,等后面会再次具体分析,当前只有PendingArea这个数据结构是什么样就好了。

4.2 构建PendingArea树

下面这段代码比较长我再代码里加了很多注释,其实这一块就是java的循环对数据结构的处理。就和刚学java的时候看2个for循环一样。
这个方法其实是构建整个树的方法,先看一眼,后面会再具体分析。

    # DisplayAreaPolicyBuilder.HierarchyBuilder

            private final RootDisplayArea mRoot;

            private void build(@Nullable List<HierarchyBuilder> displayAreaGroupHierarchyBuilders) {
                final WindowManagerPolicy policy = mRoot.mWmService.mPolicy;
                // 定义最大层级数  37 = 36+1 
                final int maxWindowLayerCount = policy.getMaxWindowLayer() + 1;
                // 存储每个窗口层级对应的 DisplayArea.Tokens,一共37个,后续窗口挂载也是在这个数据结构上找
                // 在方法底部执行instantiateChildren的时候调用
                final DisplayArea.Tokens[] displayAreaForLayer =
                        new DisplayArea.Tokens[maxWindowLayerCount];

                // 存储每个特性对应的 DisplayArea 列表
                // mFeatures就是在configureTrustedHierarchyBuilder配置的Feature大小,很明显一共是5个
                final Map<Feature, List<DisplayArea<WindowContainer>>> featureAreas =
                        new ArrayMap<>(mFeatures.size());
                for (int i = 0; i < mFeatures.size(); i++) {
                    // 为每个feature,创建其对应的DisplayArea列表
                    featureAreas.put(mFeatures.get(i), new ArrayList<>());
                }
                // 到这里featureAreas里一共是5个值,key就是上节提到的5个Feature,value目前就是个空的List

                // -------构建PendingArea树----
                // *1 创建 PendingArea 数组,用于临时存储每个窗口层级对应的 PendingArea,也是37个
                // 后面需要关注这个areaForLayer成员的变化
                PendingArea[] areaForLayer = new PendingArea[maxWindowLayerCount];
                // 先创建个PendingArea 再让areaForLayer的37个数据都默认为root的PendingArea
                // 注意第一个参数feature为null
                final PendingArea root = new PendingArea(null, 0, null);
                Arrays.fill(areaForLayer, root);

                // 2. 构建Features的树
                // mFeatures.size为5,所以有5个大循环
                final int size = mFeatures.size();
                for (int i = 0; i < size; i++) {
                    // 拿到当前需要处理的Feature
                    final Feature feature = mFeatures.get(i);
                    PendingArea featureArea = null;
                    // 内部循环,37次
                    for (int layer = 0; layer < maxWindowLayerCount; layer++) {
                        // 如果这个层级,支持当前Feature
                        if (feature.mWindowLayers[layer]) {
                            //判断是否复用 PendingArea (同一个feature才复用,否则创建新的)
                            if (featureArea == null || featureArea.mParent != areaForLayer[layer]) {
                                // 创建新的 PendingArea,作为上一层级的子节点,用于当前层级,并且双向奔赴,设置为各自的孩子或者父亲
                                // 注意第一个参数feature
                                featureArea = new PendingArea(feature, layer, areaForLayer[layer]);
                                areaForLayer[layer].mChildren.add(featureArea);
                            }
                            areaForLayer[layer] = featureArea;
                        } else {
                            // 如果该特性不应用于当前窗口层级,则featureArea置为空。用于上面if的判断
                            featureArea = null;
                        }
                    }
                }
                // 到这里,areaForLayer这个37层就按照feature分类,有自己对应的PendingArea了。


                // 3. 构建叶子节点相关的PendingArea,注意还是操作areaForLayer数组,但是操作的是内部元素的mChildren的值

                // 定义一个叶子节点用的 PendingArea
                PendingArea leafArea = null;
                int leafType = LEAF_TYPE_TOKENS;// 定义leafType
               
                for (int layer = 0; layer < maxWindowLayerCount; layer++) {
                    //  获取每层的type,从这看type是和所在层级有关系的
                    int type = typeOfLayer(policy, layer);
                    // // 检查是否可以复用前一个层级的 Tokens,和前面的循环类似
                    if (leafArea == null || leafArea.mParent != areaForLayer[layer]
                            || type != leafType) {
                        // 创建PendingArea,注意参数,featur为null
                        leafArea = new PendingArea(null /* feature */, layer, areaForLayer[layer]);
                        // 注意是添加到孩子,而不是跟上一次循环直接修改areaForLayer
                        areaForLayer[layer].mChildren.add(leafArea);
                        leafType = type;
                        // 应用类型处理
                        if (leafType == LEAF_TYPE_TASK_CONTAINERS) {
                            // 添加 TaskDisplayArea 到应用程序层级
                            addTaskDisplayAreasToApplicationLayer(areaForLayer[layer]);
                             // 添加 DisplayAreaGroup 到应用程序层级
                            addDisplayAreaGroupsToApplicationLayer(areaForLayer[layer],
                                    displayAreaGroupHierarchyBuilders);
                            // 跳过创建 Tokens,即不创建 Tokens,即使没有 Task
                            leafArea.mSkipTokens = true;
                        } else if (leafType == LEAF_TYPE_IME_CONTAINERS) {
                            // 输入法处理
                            leafArea.mExisting = mImeContainer;
                             // 跳过
                            leafArea.mSkipTokens = true;
                        }
                    }
                    leafArea.mMaxLayer = layer;
                }
                // 计算根节点的最大层级
                root.computeMaxLayer();
                // -------构建DisplayAreas树----
                // 4. 根据之前定义的PendingArea生成最后的 DisplayAreas 树
                // 注意参数
                // We built a tree of PendingAreas above with all the necessary info to represent the
                // hierarchy, now create and attach real DisplayAreas to the root.
                root.instantiateChildren(mRoot, displayAreaForLayer, 0, featureAreas);

                // 通知根节点已经完成了所有DisplayArea的添加 (将displayAreaForLayer保存在RootDisplayArea成员变量roomAreaForLayer中,供后面逻辑使用)
                mRoot.onHierarchyBuilt(mFeatures, displayAreaForLayer, featureAreas);
            }

4.2.1 构建Feature相关

这边根据具体的执行画了几张图,先看上面Features的循环逻辑,在执行循环前数组areaForLayer和执行第一次大循环后集合如下
第一个Feature是WindowedMagnification拥有特征的层级 0-31,也就是其 前面32个为true。

在这里插入图片描述
在层级树,如果某一块都是支持同一Feature的话,可以写成 “name 起始层:结束层 ”的形式,转换后如下
转换成层级树的方式就是
在这里插入图片描述

然后第二个大循环

第二Feature是HideDisplayCutout拥有特征的层级 0-14 16 18-23 26-35

在这里插入图片描述

太长了所以第24开始换到了下一排, 规律就是结合上一次的循环,0-31以内,HideDisplayCutout的父亲都是上一次循环的WindowedMagnification,然后32之后的父亲就是默认的root了。
再转成层级树的表示形式如下:
在这里插入图片描述
按照这个规则Feature 5次全执行完后,层级树的图就是下面这个,不过做了下顺序的调整,从小到达排序

在这里插入图片描述

4.2.2 构建Leaf相关

在构建叶子节点的时候,又有一个新的东西,leafType,对应的就是叶子节点的类型,默认是LEAF_TYPE_TOKENS,一共也只定义了3个

    # DisplayAreaPolicyBuilder.HierarchyBuilder
            private static final int LEAF_TYPE_TASK_CONTAINERS = 1; // APP
            private static final int LEAF_TYPE_IME_CONTAINERS = 2; // 输入法
            private static final int LEAF_TYPE_TOKENS = 0; // 默认

然后是通过typeOfLayer方法根据当前层级返回type

    # DisplayAreaPolicyBuilder.HierarchyBuilder

            private static int typeOfLayer(WindowManagerPolicy policy, int layer) {
                if (layer == APPLICATION_LAYER) {
                    return LEAF_TYPE_TASK_CONTAINERS;
                } else if (layer == policy.getWindowLayerFromTypeLw(TYPE_INPUT_METHOD)
                        || layer == policy.getWindowLayerFromTypeLw(TYPE_INPUT_METHOD_DIALOG)) {
                    return LEAF_TYPE_IME_CONTAINERS;
                } else {
                    return LEAF_TYPE_TOKENS;
                }
            }

逻辑还是比较简单的除了输入法(13-14)和应用(2)所在的层级,均返回LEAF_TYPE_TOKENS。
经过第二个for循环后,相当于给每个Feature 都加上了一个Leaf , 然后对输入法和应用图做了单独的处理。
先看输入法的, 是把mExisting设置为了最开始从DisplayConten传进来的mImeContainer,然后mSkipTokens设置为false,表示后续的操作可以跳过。
然后看对应用图层的处理,除了也将mSkipTokens设置为false外还执行了2个方法其中第二個方法。addDisplayAreaGroupsToApplicationLayer因为内部依赖displayAreaGroupHierarchyBuilders,而目前也没看到对这个对象操作的地方,所以长度为0,可以忽略,所以只看

addTaskDisplayAreasToApplicationLayer方法即可

addTaskDisplayAreasToApplicationLayer

mTaskDisplayAreas看到应该联想到前面创建的name为“DefaultTaskDisplayArea”的那一个TaskDisplayArea,事实上也就是在那创建的,这个是和应用最相关的图层,
从代码上看也能证明:

    # DisplayAreaPolicyBuilder.HierarchyBuilder
        private final ArrayList<TaskDisplayArea> mTaskDisplayAreas = new ArrayList<>();

        HierarchyBuilder setTaskDisplayAreas(List<TaskDisplayArea> taskDisplayAreas) {
            mTaskDisplayAreas.clear();
            mTaskDisplayAreas.addAll(taskDisplayAreas);
            return this;
        }
        private void addTaskDisplayAreasToApplicationLayer(PendingArea parentPendingArea) {
            // 已知长度为1,
            final int count = mTaskDisplayAreas.size();
                
            for (int i = 0; i < count; i++) {
                PendingArea leafArea =
                        new PendingArea(null /* feature */, APPLICATION_LAYER, parentPendingArea);
                // 所以就是把“DefaultTaskDisplayArea”这个设置为mExisting
                leafArea.mExisting = mTaskDisplayAreas.get(i);
                leafArea.mMaxLayer = APPLICATION_LAYER;
                // parentPendingArea.mChildren本来为大家都一样的Leaf,又添加了一个DefaultTaskDisplayArea
                parentPendingArea.mChildren.add(leafArea);
            }
        }

这段对应用图层的处理非常的重要了,特别最下面对parentPendingArea.mChildren再次添加DefaultTaskDisplayArea的操作

经过这个循环的处理,每个Feature下面都有了对应的叶子节点,如图:

在这里插入图片描述
第二层应用层目前是有2个孩子的,一个是Lead,另一个就是DefaultTaskDisplayArea。
到目前为止,层级树雏形是有了。但是比较还是一个PendingArea数组,另外 Leaf 0:1 这种目前在代码上也还没有得到体现。

4.3 真正DisplayAreas树 PendingArea::instantiateChildren

其实从PendingArea::instantiateChildren上面源码给的2个注释也知道,前面的2个循环,只是构建了一个PendingAreas树,接下来才是真正构建层级树(DisplayAreas)
并把这个树添加到root(DisplayContent)

    # DisplayAreaPolicyBuilder.PendingArea

            void instantiateChildren(DisplayArea<DisplayArea> parent, DisplayArea.Tokens[] areaForLayer,
                    int level, Map<Feature, List<DisplayArea<WindowContainer>>> areas) {
                // 1. 子区域按照它们的最小层级进行升序排列
                mChildren.sort(Comparator.comparingInt(pendingArea -> pendingArea.mMinLayer));
                // 2. 遍历孩子将PendingArea转换成DisplayArea
                for (int i = 0; i < mChildren.size(); i++) {
                    final PendingArea child = mChildren.get(i);
                    final DisplayArea area = child.createArea(parent, areaForLayer);
                    if (area == null) {
                        // TaskDisplayArea and ImeContainer can be set at different hierarchy, so it can
                        // be null.
                        continue;
                    }
                    // 将返回的area设置为孩子,第一次执行的时候root就是DisplayContent
                    parent.addChild(area, WindowContainer.POSITION_TOP);
                    if (child.mFeature != null) {
                        // 让Feature对应的容器里添加创建的DisplayArea
                        areas.get(child.mFeature).add(area);
                    }
                    // 开始迭代构建
                    child.instantiateChildren(area, areaForLayer, level + 1, areas);
                }
            }

先解释一下3个参数
parent:根据上面代码的代码逻辑,root就是DisplayContent
areaForLayer: 这个是build方法开始创建的displayAreaForLayer
level:从哪级开始
areas: 这个也是build方法创建的map集合,key是Feature。

  1. 上来就执行了个排序,这个mChildren是啥呢?咋一看好像一点印象都没有,但是根据这个方法调用处看,他是root.instantiateChildren,
    而这个root是构建PendingAreas树时最开始创建的root,也就是我们上面图片PendingAreas树里的 root 0:0。所以他的孩子就是2次循环处理后,父亲是他的PendingArea,也就是那些feature或者leaf
  2. 这一步就是将那些PendingArea的数据结构转换为DisplayArea
    之前看过PendingArea的成员变量和构造方法,现在看看

    # DisplayAreaPolicyBuilder.PendingArea
            @Nullable
            private DisplayArea createArea(DisplayArea<DisplayArea> parent,
                    DisplayArea.Tokens[] areaForLayer) {
                // 只有输入法和应用层mExisting有值
                if (mExisting != null) {
                    if (mExisting.asTokens() != null) { 
                        // 只有输入法满足
                        // Store the WindowToken container for layers
                        fillAreaForLayers(mExisting.asTokens(), areaForLayer);
                    }
                    // 然后将mExisting作为结果返回
                    return mExisting;
                }
                // mSkipTokens为true则返回,应用和IME创建的PendingArea
                if (mSkipTokens) {
                    return null;
                }
                // 2. 定义DisplayArea的type
                DisplayArea.Type type;
                if (mMinLayer > APPLICATION_LAYER) {
                    type = DisplayArea.Type.ABOVE_TASKS;
                } else if (mMaxLayer < APPLICATION_LAYER) {
                    type = DisplayArea.Type.BELOW_TASKS;
                } else {
                    type = DisplayArea.Type.ANY;
                }
                
                if (mFeature == null) {
                    // // 3. 构建返回的leaf    注意第三个参数格式
                    final DisplayArea.Tokens leaf = new DisplayArea.Tokens(parent.mWmService, type,
                            "Leaf:" + mMinLayer + ":" + mMaxLayer);
                    fillAreaForLayers(leaf, areaForLayer); // 给对应覆盖的层级都需要赋值
                    return leaf;
                } else {
                    // 对有Feature的PendingArea返回构建
                    return mFeature.mNewDisplayAreaSupplier.create(parent.mWmService, type,
                            mFeature.mName + ":" + mMinLayer + ":" + mMaxLayer, mFeature.mId);
                }
            }

注意这里的参数areaForLayer这个是一个build方法创建的集合,也是最终层级树的体现。

  1. 方法前面mExisting.asTokens, 这个asTokens,方法定义在DisplayArea中默认返回null,只有DisplayArea.Tokens返回本身。 而ImeContainer是继承DisplayArea.Tokens的,所以有返回值。
    而对于应用层mExisting是TaskDisplayArea,不是DisplayArea.Tokens的子类,所以这个不满足,也就是说只有IME的PendingArea才会执行下面fillAreaForLayers的逻辑

# DisplayAreaPolicyBuilder.PendingArea
            private void fillAreaForLayers(DisplayArea.Tokens leaf, DisplayArea.Tokens[] areaForLayer) {
                for (int i = mMinLayer; i <= mMaxLayer; i++) {
                    areaForLayer[i] = leaf;
                }
            }

fillAreaForLayers方法也比较简单,就是将这个PendingArea的所有图层都设置传进来的leaf。那当前逻辑只处理IME的话,就是把13,14层都设置这个mExisting.
另外应用层不执行到fillAreaForLayers,执行后面的return mExisting, 这里也有个很重要的点,因为前面知道应用层的Feature有2个孩子,但是mExisting却是为DefaultTaskDisplayArea,
这也就是为什么最终层级树的第二层只有DefaultTaskDisplayArea的原因

  1. 定义了个DisplayArea的type, 也不复杂, 如果当前区域最小的图层都大于应用图层(2),那type就是ABOVE_TASKS,如果最大图层还小于应用图层(2)就是BELOW_TASKS(这个只有壁纸了),
    其他的就是ANY。目前还不知道具体用处,我认为了解即可

  2. mFeature == null的条件,在上面build方法里有2个for循环都创建了PendingArea对象,第二个创建叶子节点的时候是没有传递mFeature的。
    直接创建DisplayArea.Tokens,最重要的是第三个参数,是一个字符串,就是构建这个对象的name,看格式也是非常的清楚。其实就是层级树的Leaf节点,比如“Leaf:0:1 ”。(舒服了)

  3. 这里处理的是第一次循环对Feature构建出来的PendingArea,
    这里比较好奇的是这个mNewDisplayAreaSupplier是什么,那么就需要看Feature的定义了


    # DisplayAreaPolicyBuilder
        static class Feature {
            private final String mName;
            private final int mId;
            private final boolean[] mWindowLayers;
            private final NewDisplayAreaSupplier mNewDisplayAreaSupplier;
            // 构造函数
            private Feature(String name, int id, boolean[] windowLayers,
                NewDisplayAreaSupplier newDisplayAreaSupplier) {
                mName = name;
                mId = id;
                mWindowLayers = windowLayers;
                mNewDisplayAreaSupplier = newDisplayAreaSupplier;
            }
                static class Builder {
                ......
                // 默认为DisplayArea对象
                private NewDisplayAreaSupplier mNewDisplayAreaSupplier = DisplayArea::new;
                private boolean mExcludeRoundedCorner = true;
                    Feature build() {
                    ......
                    return new Feature(mName, mId, mLayers.clone(), mNewDisplayAreaSupplier);
                }
                }
        }
         /** Supplier interface to provide a new created {@link DisplayArea}. */
        interface NewDisplayAreaSupplier {
            DisplayArea create(WindowManagerService wms, DisplayArea.Type type, String name,
                    int featureId);
        }

mNewDisplayAreaSupplier这个对象的赋值是在Feature的构造方法,而根据代码分析,添加的5个Feature是通过Builder的方式,所以我们现在分析的
mNewDisplayAreaSupplier的值,就是定义在Feature.Builder下的默认值也就是DisplayArea对象
所以这一步就是返回了一个DisplayArea对象,然后name就是 “mFeature.mName + “:” + mMinLayer + “:” + mMaxLayer” 比如 “HideDisplayCutout:32:35”

到现在为止,层级树每个成员是如何构建,以及里面的字符串名字是怎么来的,就全都清楚了。
后面迭代也只是方法的递归而已,经过一层一层的迭代后,整个层级结构树就构建好了。
现在的层级树如下:

在这里插入图片描述

这个层级树和上一篇看 不太一样那是因为Leaf下没有内容了,应用层“DefaultTask
DisplayArea”和壁纸层也没有内容,那是因为Leaf后面的内容都是具体业务添加上去的。
所以其实对应Window的add流程,其实也就是真没添加到这个层级树的流程。后面具体分析业务的时候肯定是会有具体案例的。

5. 小结

窗口层级树这一块的代码有点抽象,代码虽然不多但是也挺绕。我写的也水平有限,学习这块最好是自己也能跟着画出一个层级树的图来。当然就算画不了,也问题不大,再怎么不济现在对层级树的概念肯定也是有了解的,也知道怎么命令看,以后实际业务经常会需要比对层级树的变化,看到多了,自如而且就清除了。虽然层级树打印的内容比较多,但是只要关注DefaultTaskDisplayArea下的内容,这一块的内容也就那么点。


http://www.kler.cn/a/306052.html

相关文章:

  • Flink调优----反压处理
  • C++----------类的设计
  • openwrt 负载均衡方法 openwrt负载均衡本地源接口
  • 【MinIO系列】MinIO Client (mc) 完全指南
  • 微服务篇-深入了解 XXL-JOB 分布式任务调度的具体使用(XXL-JOB 的工作流程、框架搭建)
  • Web 第一次作业 初探html 使用VSCode工具开发
  • 详细介绍 Servlet 基本概念——以餐厅服务员为喻
  • Linux下write函数
  • PG表空间
  • Android命令行查看CPU频率和温度
  • 鲸天科技外卖会员卡系统更专业
  • Spring源码(12)-- Aop源码
  • 【Linux 从基础到进阶】自动化部署工具(Jenkins、GitLab CI/CD)
  • jdk知识
  • Excel数据清洗工具:提高数据处理效率的利器
  • verilog运算符优先级
  • TCP/IP网络编程概念及Java实现TCP/IP通讯Demo
  • 论文速递!Auto-CNN-LSTM!新的锂离子电池(LIB)剩余寿命预测方法
  • WEB打点
  • Metacritic 网站中的游戏开发者和类型信息爬取
  • OpenCV-轮廓检测
  • 《深度学习》PyTorch 手写数字识别 案例解析及实现 <下>
  • 编写并运行第一个spark java程序
  • 【JavaEE】初识⽹络原理
  • 计算机毕业设计 二手闲置交易系统的设计与实现 Java实战项目 附源码+文档+视频讲解
  • python-古籍翻译