Unity DOTS中的baking(二)Baker的触发

news2024/11/27 18:38:14

Unity DOTS中的baking(二)Baker的触发

我们知道,当传入Baker的authoring component的值发生变化时,就会触发baking。不过在有些情况下,component所引用的对象没有变化,而是对象自身内部的一些属性发生了变化。这种情况下,是否会触发baking呢?我们来动手验证一下。

首先定义一个继承自ScriptableObject的ImageGeneratorInfo类:

[CreateAssetMenu(menuName = "ImageGeneratorInfo")]
public class ImageGeneratorInfo : ScriptableObject
{
    [Range(0.0f, 1.0f)]
    public float Spacing;
    public Mesh Mesh;
    public Material Material;
}

这样,我们就可以利用这个脚本创建asset了,随便给它塞点东西:

Unity DOTS中的baking(二)1

然后我们修改下baker脚本,在ECS Component中新增一个spacing,读取assets中的Spacing值:

public class MyAuthoring : MonoBehaviour
{
    public int bakeIntData = 0;
    public ImageGeneratorInfo info;

    class MyBaker : Baker<MyAuthoring>
    {
        public override void Bake(MyAuthoring authoring)
        {
            Debug.Log("==========================Bake Invoked!========================== " + authoring.name);

            if(authoring.info == null) return;

            var entity = GetEntity(TransformUsageFlags.None);
            AddComponent(entity, new MyComponent {
                value = authoring.bakeIntData,
                spacing = authoring.info.Spacing
            });
        }
    }
}

public struct MyComponent : IComponentData
{
    public int value;
    public float spacing;
}

我们在SubScene中新建一个空的GameObject,挂上MyAuthoring脚本,并在Info中设置上刚刚创建的ImageGeneratorInfo asset,随即就会触发baking:

Unity DOTS中的baking(二)2

Unity DOTS中的baking(二)3

此时,如果直接去修改asset本身,例如将asset的spacing参数设置为其他的值,并不会触发baking。这就意味着,转换后的Entity所拥有的MyComponent数据是错误的。对此,Unity官方文档给出了解释:

However, Unity doesn’t automatically track data from other sources, such as authoring components or assets. You need to add a dependency to the baker so it can track this kind of data. To do this, use the methods that the Baker class provides to access other components and GameObjects instead of the methods provided by the GameObject:

Unity只会自动地追踪依赖authoring component自身的变化,至于component所引用的资源,则需要使用DependsOn来显式定义依赖:

public override void Bake(MyAuthoring authoring)
{
    Debug.Log("==========================Bake Invoked!========================== " + authoring.name);

    DependsOn(authoring.info);

    if(authoring.info == null) return;

    var entity = GetEntity(TransformUsageFlags.None);
    AddComponent(entity, new MyComponent {
        value = authoring.bakeIntData,
        spacing = authoring.info.Spacing
    });
}

此时再修改Spacing参数,就会触发baking了:

Unity DOTS中的baking(二)4

这个DependsOn背后究竟做了什么呢?查看一下DependsOn的源码:

/// <summary>
/// This will take a dependency on Object of type T.
/// </summary>
/// <param name="dependency">The Object to take a dependency on.</param>
/// <typeparam name="T">The type of the object. Must be derived from UnityEngine.Object.</typeparam>
/// <returns>The Object of type T if a dependency was taken, null otherwise.</returns>
public T DependsOn<T>(T dependency) where T : UnityEngine.Object
{
    _State.Dependencies->DependResolveReference(_State.AuthoringSource.GetInstanceID(), dependency);

    // Transform component takes an implicit dependency on the entire parent hierarchy
    // since transform.position and friends returns a value calculated from all parents
    var transform = dependency as Transform;
    if (transform != null)
        _State.Dependencies->DependOnParentTransformHierarchy(transform);

    return dependency;
}

这里最关键的函数就是这个DependResolveReference了,再来看下它的源码:

public void DependResolveReference(int authoringComponent, UnityEngine.Object referencedObject)
{
    // Tricky unity details ahead:
    // A UnityEngine.Object might be
    //  - actual null (ReferenceEquals(referencedObject, null)  -> instance ID zero)
    //  - currently unavailable (referencedObject == null)
    //      - If it is unavailable, it might still have an instanceID. The object might for example be brought back through undo or putting the asset in the same path / guid in the project
    //        In that case it will be re-established with the same instanceID and hence we need to have a dependency on when an object
    //        that previously didn't exist now starts existing at the instanceID that previously mapped to an invalid object.
    //  - valid (referencedObject != null) (instanceID non-zero)
    var referencedInstanceID = ReferenceEquals(referencedObject, null) ? 0 : referencedObject.GetInstanceID();
    if (referencedInstanceID != 0)
    {
        AddObjectReference(referencedInstanceID);

        var obj = Resources.InstanceIDToObject(referencedInstanceID);
        var objTypeId = TypeManager.GetTypeIndex(referencedObject.GetType());
        AddObjectExist(new ObjectExistDependency { InstanceID = referencedInstanceID, exists = (obj != null), Type = objTypeId });

#if UNITY_EDITOR
        //@todo: How do we handle creation / destruction of assets / components?
        if (EditorUtility.IsPersistent(referencedObject))
            AddPersistentAsset(referencedObject.GetInstanceID());
#endif
    }
}

从源码的注释得知,Unity需要处理依赖的object是fake null(例如object引用为missing)的情况。ObjectExistDependency这个类记录了所引用的object对应的asset是否存在。当调用DependsOn时,就会新建一个该类的对象保存起来。之后,再检测到asset变化时,会触发CalculateObjectExistDiffsJob这个job。该job会获取记录的所有objects对应的asset当前存在的状态,与之前ObjectExistDependency保存时的状态进行比较,如果不一致,说明asset发生了变化(从无到有或者从有到无),需要重新进行baking:

// Resolve the objectIds (Get Object)
// Check if they are null (If they are null)
NativeArray<bool> objectExists = new NativeArray<bool>(objectIds.Length, Allocator.TempJob);

InstanceIDsToValidArrayMarker.Begin();
Resources.InstanceIDsToValidArray(objectIds.AsArray(), objectExists);
InstanceIDsToValidArrayMarker.End();

var diffJob = new CalculateObjectExistDiffsJob()
{
    objectExistDependencies = _StructuralObjectExistDependency,
    objectExists = objectExists,
    deduplicatedObjIds = deduplicatedObjIds,
    changedComponentsPerThread = changedComponentsPerThread
};
var diffJobHandle = diffJob.Schedule(DependenciesHashMapHelper.GetBucketSize(_StructuralObjectExistDependency), 64);

传入job的四个参数,objectExistDependencies表示之前记录的dependency,objectExists就是当前asset的状态,deduplicatedObjIds代表去重过的object instance id,changedComponentsPerThread顾名思义就是记录需要重新baking的authoring component。job具体执行的代码如下:

public void ProcessEntry(int threadIndex, in UnsafeParallelMultiHashMap<int, ObjectExistDependency> hashMap, in int key, in ObjectExistDependency value)
{
    // Add them if the exist state has changed (State has changed)
    int existsID = deduplicatedObjIds[value.InstanceID];
    if (value.exists != objectExists[existsID])
    {
        changedComponentsPerThread.Add(key, m_ThreadIndex);
        IncrementalBakingLog.RecordComponentBake(key, ComponentBakeReason.ObjectExistStructuralChange, value.InstanceID, value.Type);
    }
}

传入的key和value取自于objectExistDependencies这个数据结构,分别表示authoring component的instance id,以及记录的ObjectExistDependency对象。在解释完这些数据结构的含义之后,这里job执行的逻辑就很清晰了。

哎,等一下,这里到目前为止所说的,都是关于asset是否存在的依赖,那么asset本身是否修改,这里的依赖是在哪里注册的呢?实际上,还是在这个DependResolveReference函数里:

AddObjectReference(referencedInstanceID);

这个函数会把authoring component和所依赖的object instance id关联,最终保存到一个UnsafeParallelMultiHashMap<int, int>类型的_PropertyChangeDependency变量里。然后,当components,gameobjects,或是assets发生变化时,都会触发相应的job,对该dependency进行扫描。

JobHandle nonStructuralChangedComponentJobHandle = default;
if (incrementalConversionDataCache.ChangedComponents.Length > 0)
{
    var nonStructuralChangedComponentJob = new NonStructuralChangedComponentJob()
    {
        changedComponents = incrementalConversionDataCache.ChangedComponents,
        reversePropertyChangeDependency = _ReversePropertyChangeDependency,
        changedComponentsPerThread = changedComponentsPerThread
    };
    nonStructuralChangedComponentJobHandle = nonStructuralChangedComponentJob.Schedule(incrementalConversionDataCache.ChangedComponents.Length, 64, calculateGlobalReversePropertyJobHandle);
}

JobHandle nonStructuralChangedGameObjectPropertiesJobHandle = default;
if (incrementalConversionDataCache.ChangedGameObjectProperties.Length > 0)
{
    var nonStructuralChangedGameObjectPropertiesJob = new NonStructuralChangedGameObjectPropertiesJob()
    {
        changedGameObjects = incrementalConversionDataCache.ChangedGameObjectProperties,
        reversePropertyChangeDependency = _ReversePropertyChangeDependency,
        reverseGameObjectPropertyChangeDependency = _ReverseObjectPropertyDependency,
        changedComponentsPerThread = changedComponentsPerThread
    };
    nonStructuralChangedGameObjectPropertiesJobHandle = nonStructuralChangedGameObjectPropertiesJob.Schedule(incrementalConversionDataCache.ChangedGameObjectProperties.Length, 64, JobHandle.CombineDependencies(calculateGameObjectPropertyReverseJobHandle, calculateGlobalReversePropertyJobHandle));
}

JobHandle nonStructuralChangedAssetsJobHandle = default;
#if UNITY_EDITOR
if (incrementalConversionDataCache.ChangedAssets.Length > 0)
{
    var nonStructuralChangedAssetsJob = new NonStructuralChangedAssetsJob()
    {
        changedAssets = incrementalConversionDataCache.ChangedAssets,
        reversePropertyChangeDependency = _ReversePropertyChangeDependency,
        changedComponentsPerThread = changedComponentsPerThread
    };
    nonStructuralChangedAssetsJobHandle = nonStructuralChangedAssetsJob.Schedule(incrementalConversionDataCache.ChangedAssets.Length, 64, calculateGlobalReversePropertyJobHandle);
}
#endif

NonStructuralChangedComponentJob这个job为例:

public void Execute(int i)
{
    var component = changedComponents[i];
    changedComponentsPerThread.Add(component.instanceID, m_ThreadIndex);

    IncrementalBakingLog.RecordComponentBake(component.instanceID, ComponentBakeReason.ComponentChanged, component.instanceID, component.unityTypeIndex);
    IncrementalBakingLog.RecordComponentChanged(component.instanceID);

    foreach (var dep in reversePropertyChangeDependency.GetValuesForKey(component.instanceID))
    {
        changedComponentsPerThread.Add(dep, m_ThreadIndex);

        IncrementalBakingLog.RecordComponentBake(dep, ComponentBakeReason.GetComponentChanged, component.instanceID, component.unityTypeIndex);
    }
}

首先,肯定是将change component自身记录进来,它是需要重新baking的;然后,对当前记录的dependency进行扫描,取出依赖该change component的authoring components,它可能存在多个,需要进行遍历,一一记录。这样,所有依赖change component的autoring components全部都会重新baking。

此外,我们的authoring component也有可能需要获取自身GameObject上的某些component,例如我们在ECS Component中新增一个float3的position,读取transform上的position:

public override void Bake(MyAuthoring authoring)
{
    Debug.Log("==========================Bake Invoked!========================== " + authoring.name);

    DependsOn(authoring.info);

    if(authoring.info == null) return;

    var transform = authoring.transform;

    var entity = GetEntity(TransformUsageFlags.None);
    AddComponent(entity, new MyComponent {
        value = authoring.bakeIntData,
        spacing = authoring.info.Spacing,
        position = transform.position
    });
}

public struct MyComponent : IComponentData
{
    public int value;
    public float spacing;
    public float3 position;
}

然而,直接使用authoring.transform获取到的transform,如果发生了变化,并不会重新触发baking。也就是说我们也需要手动指定依赖。Unity官方推荐使用GetComponent函数来获取component。

It is important to access other authoring components by using the GetComponent methods, because doing so registers dependencies. If we would have used authoring.transform here instead, a dependency would not have been registered, and moving the authoring GameObject while live baking would not rerun the baker as it should.

那我们就换用GetComponent<Transform>()来测试一下:

Unity DOTS中的baking(二)5

的确修改transform就会触发baking了。秉着知其然知其所以然的精神,我们来看看GetComponent背后的代码:

/// <summary>
/// Retrieves the component of Type T in the GameObject
/// </summary>
/// <param name="gameObject">The GameObject to get the component from</param>
/// <typeparam name="T">The type of component to retrieve</typeparam>
/// <returns>The component if a component matching the type is found, null otherwise</returns>
/// <remarks>This will take a dependency on the component</remarks>
private T GetComponentInternal<T>(GameObject gameObject) where T : Component
{
    var hasComponent = gameObject.TryGetComponent<T>(out var returnedComponent);

    _State.Dependencies->DependOnGetComponent(gameObject.GetInstanceID(), TypeManager.GetTypeIndex<T>(), hasComponent ? returnedComponent.GetInstanceID() : 0, BakeDependencies.GetComponentDependencyType.GetComponent);

    // Transform component takes an implicit dependency on the entire parent hierarchy
    // since transform.position and friends returns a value calculated from all parents
    var transform = returnedComponent as Transform;
    if (transform != null)
        _State.Dependencies->DependOnParentTransformHierarchy(transform);

    return returnedComponent;
}

不难发现关键代码就在DependOnGetComponent这个函数了:

public void DependOnGetComponent(int gameObject, TypeIndex type, int returnedComponent, GetComponentDependencyType dependencyType)
{
    if (returnedComponent != 0)
        AddObjectReference(returnedComponent);

    AddGetComponent(new GetComponentDependency {GameObject = gameObject, Type = type, ResultComponent = returnedComponent, DependencyType = dependencyType});
}

returnedComponent代表获取到的component的instance id,如果component存在,就需要追踪它本身是否发生变化,因此这里就和前面一样,使用AddObjectReference这个函数记录依赖。AddGetComponent会把要获取的GameObject,component,component的type index,以及依赖类型给记录下来,最终保存到_StructuralGetComponentDependency这个hashmap中。CalculateStructuralGetComponentDependencyJob会用到保存的数据,当检测到components,gameobjects,或是assets变化时,job就会触发。

public void ProcessEntry(int threadIndex, in UnsafeParallelMultiHashMap<int, GetComponentDependency> hashMap, in int key, in GetComponentDependency value)
{
    if (!value.IsValid(ref components, ref hierarchy))
    {
        changedComponentsPerThread.Add(key, m_ThreadIndex);
        IncrementalBakingLog.RecordComponentBake(key, ComponentBakeReason.GetComponentStructuralChange, value.ResultComponent, value.Type);
    }
}

从字面含义来看,只有GetComponentDependency类型的value判断为invalid时,才会把对应的key标记为需要重新baking。这里的key和value就是hashmap中的键值对,key表示authoring component,value里记录了当时使用GetComponent获取到的component依赖。那么,什么时候IsValid返回的是false呢?

public bool IsValid(ref GameObjectComponents components, ref SceneHierarchy hierarchy)
{
    switch (DependencyType)
    {
        case GetComponentDependencyType.GetComponentInParent:
            return GameObjectComponents.GetComponentInParent(ref components, ref hierarchy, GameObject, Type) == ResultComponent;
        case GetComponentDependencyType.GetComponent:
            return components.GetComponent(GameObject, Type) == ResultComponent;
        case GetComponentDependencyType.GetComponentInChildren:
            return GameObjectComponents.GetComponentInChildren(ref components, ref hierarchy, GameObject, Type) == ResultComponent;
    }
    return false;
}

这里的DependencyType为GetComponentDependencyType.GetComponent,可以看到这里会再去获取一下记录的GameObject身上最新的component,如果和之前记录的component instance id不同,说明component经历了从无到有或者从有到无的过程,它已经不是之前的那个对象了,此时就会返回false。总结一下,GetComponentInternal函数,不仅对component内部的值发生变化进行了依赖注册,还对component自身引用发生变化也进行了依赖注册。

如果DependsOn和GetComponent所依赖的component,它是个transform,Unity还会调用DependOnParentTransformHierarchy这个函数做一些额外的依赖工作。这是因为transform本身就不是独立的,它依赖整个parent hierarchy。这个事情是必要的,比如我们给当前挂有MyAuthoring脚本的GameObject建立一个父节点,如果父节点的transform发生变化,或者层级关系发生了变化,那么子节点的world transform必然也发生了变化,理应就要再次触发baking。

Unity DOTS中的baking(二)6

DependOnParentTransformHierarchy的实现如下:

public void DependOnParentTransformHierarchy(Transform transform)
{
    if (transform != null)
    {
        var hashGenerator = new xxHash3.StreamingState(false);
        GameObject go = transform.gameObject;
        int goInstanceID = go.GetInstanceID();

        // We take the dependency on the parent hierarchy.
        transform = transform.parent;
        while (transform != null)
        {
            hashGenerator.Update(transform.gameObject.GetInstanceID());

            AddObjectReference(transform.GetInstanceID());
            transform = transform.parent;
        }

        var hash = new Hash128(hashGenerator.DigestHash128());
        AddGetHierarchy(new GetHierarchyDependency {GameObject = goInstanceID, Hash = hash, DependencyType = GetHierarchyDependencyType.Parent});
    }
}

可以看到,Unity会对当前节点到根节点整个parent hierarchy进行扫描,对每个父节点的transform都建立了依赖关系,然后还记录了每个父节点GameObject的instance id,把它们汇总在一起生成一个128位的hash值。这个hash值就是用来标记当前的hierarchy。值得一提的是,Unity ECS 1.0.16版本之前的这段代码是有问题的,之前的版本也会把当前节点的instance id也统计进去,这点在Unity的changelog里有说明:

Changelog

[1.0.16] - 2023-09-11

Fixed

  • Fixed a hash mismatch on DependOnParentTransformHierarchy

之后,CalculateStructuralGetHierarchyDependencyJob这个job会使用之前记录的GetHierarchyDependency,判断hierarchy是否发生了变化(如果是1.0.16之前的版本,这里就必然会发生变化了,两边计算的方式压根就不一致)

public void ProcessEntry(int threadIndex, in UnsafeParallelMultiHashMap<int, GetHierarchyDependency> hashMap, in int key, in GetHierarchyDependency value)
{
    if (!value.IsValid(ref hierarchy))
    {
        changedComponentsPerThread.Add(key, m_ThreadIndex);
        IncrementalBakingLog.RecordComponentBake(key, ComponentBakeReason.GetHierarchyStructuralChange, 0, default);
    }
}

具体的判断逻辑位于IsValid

public Hash128 GetParentsHash(ref SceneHierarchy hierarchy, int instanceId)
{
    var hashGenerator = new xxHash3.StreamingState(false);

    if (hierarchy.TryGetIndexForInstanceId(instanceId, out int currentIndex))
    {
        while (currentIndex != -1)
        {
            int parentIndex = hierarchy.GetParentForIndex(currentIndex);
            if (parentIndex != -1)
            {
                int parentInstanceID = hierarchy.GetInstanceIdForIndex(parentIndex);
                hashGenerator.Update(parentInstanceID);
            }
            currentIndex = parentIndex;
        }
    }
    return new Hash128(hashGenerator.DigestHash128());
}

public bool IsValid(ref SceneHierarchy hierarchy)
{
    Hash128 returnValue = default;
    switch (DependencyType)
    {
        case GetHierarchyDependencyType.Parent:
            returnValue = GetParentsHash(ref hierarchy, GameObject);
            break;
        case GetHierarchyDependencyType.ImmediateChildren:
            returnValue = GetChildrenHash(ref hierarchy, GameObject, false);
            break;
        case GetHierarchyDependencyType.AllChildren:
            returnValue = GetChildrenHash(ref hierarchy, GameObject, true);
            break;
    }
    return (returnValue == Hash);
}

DependOnParentTransformHierarchy注册的dependency type为GetHierarchyDependencyType.Parent,因此这里计算出的hash值表示当前的parent hierarchy。如果两个hash值不同,说明两个hierarchy不同,那么authoring component需要重新运行baking。而且xxhash算法是一种非常快速的非加密哈希算法,它基本不可能发生碰撞,所以这里如果两个hash值相等,就可以认为两个hierarchy是完全相同的。

最后,我们来实际演示一下:

Unity DOTS中的baking(二)7

Reference

[1] Baker overview

[2] EntityComponentSystemSamples

[3] xxHash

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

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

相关文章

DNS--windows sever 2012 r2

1 安装dns服务 得到 2 配置正向解析文件 设置正向解析文件的别名 配置(1)主机名 (2)别名&#xff08;3)邮件交换器 最终得到 3 配置反向文件 建立指针 得到 4 验证

【百科物理】-1.弹力和压力

导入 问题&#xff1a; 为什么在蹦床上弹得更高&#xff1f; 现象背后的原理&#xff0c;因为有力。 力&#xff1a;物体(物质)与物体(物质)之间推、拉、挤压的相互作用。力可以改变物体的运动状态&#xff08;比如踢足球&#xff09;&#xff0c;力可以改变物体的形状&#xf…

ProtoBuf一些踩坑记录

一、Protobuf学习基础 学习的资料很多也很全&#xff0c;这里添加几个链接进行Protobuf的基础学习的链接&#xff0c;链接中的案例使用C编辑&#xff1a; 链接&#xff1a;Protobuf介绍及简单使用(上&#xff09;_google_protobuf_version-CSDN博客 Protobuf介绍及简单使用(下&…

行云部署成长之路 -- 慢 SQL 优化之旅 | 京东云技术团队

当项目的SQL查询慢得像蜗牛爬行时&#xff0c;用户的耐心也在一点点被消耗&#xff0c;作为研发&#xff0c;我们可不想看到这样的事。这篇文章将结合行云部署项目的实践经验&#xff0c;带你走进SQL优化的奇妙世界&#xff0c;一起探索如何让那些龟速的查询飞起来&#xff01;…

three.js 学习笔记(学习中1.7更新) |

文章目录 three.js 学习笔记入门基础概念透视相机 第一个three.js应用threejs画布尺寸和布局canvas画布宽高度动态变化 坐标辅助器 THREE.AxesHelper实现动画效果requestAnimationFrame时间相关属性和方法 THREE.Clock类 相机控件 轨道控制器OrbitControls 灯光点光源点光源辅助…

【ArcGIS微课1000例】0087:经纬度格式转换(度分秒转度、度转度分秒)

ArcGIS软件可以很方便的直接实现度分秒转度、度转度分秒。 文章目录 一、转换预览二、工具介绍三、案例解析一、转换预览 借助ArcGIS快速实现度分秒与度及其他格式的坐标转换。例如:度分秒→度 度分秒: 度: 二、工具介绍 转换坐标记法:将一个或两个字段包含的坐标记法从一…

PAT 乙级 1049 数列的片段和

分数 20 作者 CAO, Peng 单位 Google 给定一个正数数列&#xff0c;我们可以从中截取任意的连续的几个数&#xff0c;称为片段。例如&#xff0c;给定数列 { 0.1, 0.2, 0.3, 0.4 }&#xff0c;我们有 (0.1) (0.1, 0.2) (0.1, 0.2, 0.3) (0.1, 0.2, 0.3, 0.4) (0.2) (0.2, 0.3) …

【JAVA】Iterator 和 ListIterator 有什么区别?

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a; JAVA ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 在Java中&#xff0c;遍历集合是日常编程中常见的任务&#xff0c;而Iterator和ListIterator作为遍历集合的两个主要接口&#xff0…

【MySQL】索引基础

文章目录 1. 索引介绍2. 创建索引 create index…on…2.1 explain2.2 创建索引create index … on…2.3 删除索引 drop index … on 表名 3. 查看索引 show indexes in …4. 前缀索引4.1 确定最佳前缀长度&#xff1a;索引的选择性 5. 全文索引5.1 创建全文索引 create fulltex…

Angular学习第二天--问题记录

一、问题 1.用脚手架搭建完项目之后&#xff0c;缺少app.modules.ts文件&#xff0c; 2.解决办法&#xff1a; 在终端继续输入命令 ng new 项目名称 --no-standalone --routing --ssrfalse 3.完整目录&#xff1a; 二、问题 1.问题来源&#xff0c;源代码&#xff1a; <fo…

K8S API访问控制之RBAC利用

前言 K8S对于API的访问安全提供了访问控制&#xff0c;主要为4个阶段&#xff0c;本文为第二个阶段——鉴权的RBAC。RBAC是基于角色的访问控制&#xff0c;使用kubeadm安装集群1.6版本以上的都默认开启了RBAC。本文主要研究集群可能存在的利用点及相对应的利用手法。 API访问…

kettle的基本介绍和使用

1、 kettle概述 1.1 什么是kettle Kettle是一款开源的ETL工具&#xff0c;纯java编写&#xff0c;可以在Window、Linux、Unix上运行&#xff0c;绿色无需安装&#xff0c;数据抽取高效稳定。 1.2 Kettle核心知识点 1.2.1 Kettle工程存储方式 以XML形式存储以资源库方式存储…

Flutter 图片和资源的高效使用指南

文章目录 指定资源什么是 [pubspec.yaml](https://dart.cn/tools/pub/pubspec) 文件 图片图片常用的配置属性加载本地图片通过 pubspec.yml 文件进行配置图片目录使用 Image.asset 小部件加载本地图片 加载网络图片通过 Image.network小部件加载网络图片&#xff1a;使用Image.…

Idea将xml文件配置为模板

在配置mybatis的mapper映射文件的时候&#xff0c;通常需要到官网拷贝配置文件的内容&#xff0c;这里直接将xml的文件配置为模板&#xff0c;下次可以直接进行创建。

解锁Mac的无限可能:Sensei for Mac - 你的全能系统优化清理助手

你是否经常为Mac的缓慢速度和不断积累的缓存文件而感到烦恼&#xff1f;不用担心&#xff0c;因为今天&#xff0c;我要向您介绍一款全新的系统优化清理工具 - Sensei for Mac。 作为一款卓越的系统清理工具&#xff0c;Sensei for Mac在保持您的Mac系统流畅运行的同时&#x…

1-04C语言执行过程

一、概述 本小节主要讲解一个C程序从源代码到最终执行的过程&#xff0c;这个过程又可以细分为两部分&#xff1a; 源代码到可执行文件的过程可执行文件在内存中执行 本小节是C语言基础当中&#xff0c;比较容易被初学者忽视的知识点。而实际上&#xff1a; 熟悉C程序从源文…

【flink番外篇】9、Flink Table API 支持的操作示例(14)- 时态表的join(java版本)

Flink 系列文章 一、Flink 专栏 Flink 专栏系统介绍某一知识点&#xff0c;并辅以具体的示例进行说明。 1、Flink 部署系列 本部分介绍Flink的部署、配置相关基础内容。 2、Flink基础系列 本部分介绍Flink 的基础部分&#xff0c;比如术语、架构、编程模型、编程指南、基本的…

模板管理支持批量操作,DataEase开源数据可视化分析平台v2.2.0发布

2024年1月8日&#xff0c;DataEase开源数据可视化分析平台正式发布v2.2.0版本。 这一版本的功能升级包括&#xff1a;在“模板管理”页面中&#xff0c;用户可以通过模板管理的批量操作功能&#xff0c;对已有模板进行快速重新分类、删除等维护操作&#xff1b;数据大屏中&…

Flutter 小技巧之升级适配 Xcode15

美好的 2024 从「适配」开始&#xff0c;按照苹果的尿性&#xff0c;2024 春季开始大家将不得使用 Xcode15 来构建 App &#xff0c;另外根据《2024 的 iOS 的隐私清单》 要求&#xff0c;使用 Flutter 的开发者是无法逃避适配 Xcode15 更新的命运。 另外&#xff0c;众所周知…

NFS 共享存储实验

一、服务器部署 第一步、安装nfs和rpcbind包 [rootserver ~]# yum install -y nfs-utils rpcbind截图&#xff1a; 第二步、这里选择一个 lvm 挂载点做 NFS 共享目录 [rootserver ~]# df -HT截图&#xff1a; 第三步、修改配置文件 [rootserver ~]# vi /etc/exports /home …