AssetBundle 详解

AssetBundle 详解文章目录AssetBundle的定义和作用什么是AssetBundle用处AssetBundle使用流程图指定资源的AssetBundle属性构建AssetBundle包核心代码详解上传AB包加载AB包和包里面的资源加载本地的AB包加载服务器上的AB包AssetBundle分组策略按照逻辑实体分组按照资源类型分组按照使用分组构建AB后产生的文件AssetBund…

大家好,又见面了,我是你们的朋友全栈君。

AssetBundle 的定义和作用

什么是 AssetBundle

  • 它是一个存在于硬盘上的文件。可以称之为压缩包。这个压缩包可以认为是一个文件夹,里面包含了多个文件。这些文件可以分为两类:serialized file 和 resource files。(序列化文件和源文件)

  • serialized file:资源被打碎放在一个对象中,最后统一被写进一个单独的文件

    • serialized file 只有一个
    • 不能直接看到是什么东西,只有在游戏中加载才能看到的会被打包为序列化文件
    • 例如:prefab,材质
  • resource files:为某些资源(图片和声音)单独存储的二进制数据块,方便快速加载
    mark

  • 它是一个 AssetBundle 对象,我们可以通过代码从一个特定的压缩包加载出来的对象。这个对象包含了所有我们当初添加到这个压缩包里面的内容,我们可以通过这个对象加载出来使用。即:通过对象使用里面的资源

用处

  • AssetBundle 是一个压缩包包含模型、贴图、预制体、声音、甚至整个场景,可以在游戏运行的时候被加载;
  • AssetBundle 自身保存着互相的依赖关系;
    • 例如一个包保存着模型,一个包保存着贴图,而模型又依赖于贴图
  • 压缩包可以使用 LZMA 和 LZ4 压缩算法,可以减少包大小,更快的进行网络传输;
  • 把一些可以下载内容(DLC)放在 AssetBundle 里面,减小初始安装大小,实时更新一些资源;
    • 既然可以把资源放在 AB 里,那么就不会放在 apk 里,玩家下载安装包的时候就会很小,当玩家运行游戏的时候再在服务器上去下载资源包
    • 可以更新一些资源而不用重新安装,例如:在特殊节日的时候更换不同的封面

AssetBundle 使用流程图

mark

指定资源的 AssetBundle 属性

在 Inspector 底部,有 AssetBundle 的面板。

  • AssetBundle 名称“environment/forest”表示,将在 environment 子文件夹下创建名为 forest 的捆绑包
  • 名字不区分大小写,默认小写
  • 指定相同名字的资源会被打到一个包里面
  • 可以自行设置文件的后缀名

构建 AssetBundle 包

mark

在 Assets 文件夹中创建一个名为 Editor 的文件夹,并将包含以下内容的脚本放在该文件夹中:

  • 此脚本将在 Assets 菜单底部创建一个名为“Build AssetBundles”的菜单项,该菜单项将执行与该标签关联的函数中的代码。
  • 单击 Build AssetBundles 会将带有 AssetBundle 名称标签的所有资源,打包放在指定目录下
using System.IO;
using UnityEditor;

public class CreateAssetBundles { 
   
    [MenuItem("Assets/Build AssetBundles")]
    static void BuildAllAssetBundles() { 
   
        string assetBundleDirectory = "Assets/AssetBundles"; // 包的输出路径
        if (!Directory.Exists(assetBundleDirectory)) { 
    // 若路径不存在,则创建
            Directory.CreateDirectory(assetBundleDirectory);
        }
        // BuildPipeline:允许您以编程方式构建可从 Web 加载的播放器或 AssetBundle。
        // BuildAssetBundles():打包,Build 出来的包是有平台限制的
        BuildPipeline.BuildAssetBundles(assetBundleDirectory, BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows);
    }
}

核心代码详解

public static AssetBundleManifest BuildAssetBundles(string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);

  • Build 的路径:随意只要是在硬盘上都可以的
  • BuildAssetBundleOptions
    • None:使用 LZMA 算法压缩,压缩的包更小,但是加载时间更长。
      • 使用之前需要整体解压,一旦被解压,这个包会使用 LZ4 重新压缩。
      • LZ4 使用资源的时候不需要整体解压。
      • 在下载的时候可以使用 LZMA 算法,一旦它被下载了之后,它会使用 LZ4 算法保存到本地上。(Unity 内置的优化策略)
    • UncompressedAssetBundle:不压缩,包大,加载快
    • ChunkBasedCompression:使用 LZ4 压缩,压缩率没有 LZMA 高,但是我们可以加载指定资源而不用解压全部。
      • LZ4 使用基于块的算法,允许按段或“块”加载 AssetBundle。解压缩单个块即可使用包含的资源,即使 AssetBundle 的其他块未解压缩也不影响。
    • 注意使用 LZ4 压缩,可以获得可以跟不压缩想媲美的加载速度,而且比不压缩文件要小。
  • BuildTarget:选择 build 出来的 AB 包要使用的平台

关于构建 AB 后产生的文件

[构建 AB 后产生的文件](#构建 AB 后产生的文件)

上传 AB 包

将资源包上传到自己的服务器,以供游戏运行时的加载。

加载 AB 包和包里面的资源

开发的时候一般会将 AB 包放在本地,因为会进行频繁的操作,发布的时候才会上传到服务器上。

加载本地的 AB 包

using System.IO;
using UnityEngine;

public class LoadFromFileExample : MonoBehaviour { 
   

    private void Start() { 
   
        AssetBundle ab = AssetBundle.LoadFromFile("Assets/AssetBundles/scene/cubewall.u3d");
        if (ab == null) { 
   
            Debug.Log("Failed to load AssetBundle!");
            return;
        }

        // 加载包里的指定物体
        var wallPre = ab.LoadAsset<GameObject>("CubeWall");
        Instantiate(wallPre); // 实例化物体
    }

    // 加载包里的所有物体
    private void LoadAllAssets(AssetBundle ab) { 
   
        Object[] objects = ab.LoadAllAssets();
        foreach (var o in objects) { 
   
            Instantiate(o);
        }
    }
}

加载服务器上的 AB 包

IEnumerator InstantiateObject()
    { 
   
        string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;
        // GetAssetBundle(string, int):获取 AssetBundle 的位置以及要下载的捆绑包的版本。
        UnityEngine.Networking.UnityWebRequest request = UnityEngine.Networking.UnityWebRequest.GetAssetBundle(uri, 0);
        yield return request.Send();
        AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
        GameObject cube = bundle.LoadAsset<GameObject>("Cube");
        GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");
        Instantiate(cube);
        Instantiate(sprite);
    }

其他详细使用可见:

[AssetBundles 的具体使用](#AssetBundles 的具体使用 )

AssetBundle 分组策略

应根据实际项目的需求来进行设置,这里只是给一个简单的参考。

这里的分组策略不是指如何划分文件夹,而是,将哪些资源打包在一个 AssetBundle 里,一个 AssetBundle 就是一组。

按照逻辑实体分组

逻辑实体分组非常适合于可下载内容 (DLC),因为通过这种方式将所有内容隔离后,可以对单个实体进行更改,而无需下载其他未更改的资源。

  • 一个 UI 界面或者所有 UI 界面一个包(这个界面里面的贴图和布局信息一个包)
  • 一个角色或者所有角色一个包(这个角色里面的模型和动画一个包)
  • 所有的场景所共享的部分一个包(包括贴图和模型)

按照资源类型分组

要构建供多个平台使用的 AssetBundle,类型分组是最佳策略之一。

例如,如果音频压缩在 Windows 和 Mac 平台上完全相同,则可以将所有音频数据打包到 AssetBundle 并重复使用这些捆绑包;而着色器往往使用更多特定于平台的选项进行编译,因此为 Mac 构建的着色器捆绑包可能无法在 Windows 上重复使用。此外,这种方法非常适合让 AssetBundle 与更多 Unity 播放器版本兼容,因为纹理压缩格式和设置的更改频率低于代码脚本或预制件。

  • 所有声音资源打成一个包,所有 shader 打成一个包,所有模型打成一个包,所有材质打成一个包

按照使用分组

  • 将需要同时加载和使用的资源捆绑在一起
    • 可以按照关卡分,一个关卡所需要的所有资源包括角色、贴图、声音等打成一个包。
    • 也可以按照场景分,一个场景所需要的资源一个包

总结

  • 把经常更新的资源放在一个单独的包里面,同不经常更新的包分离
  • 把需要同时加载的资源放在一个包里面
    • 如果一个 AssetBundle 中只有不到 50% 的资源经常同时加载,请考虑拆分该捆绑包
    • 如果不可能同时加载两组对象(例如标清资源和高清资源),请确保它们位于各自的 AssetBundle 中。
  • 可以把其他包共享的资源放在一个单独的包里面
    • 防止资源的重复打包
  • 把一些需要同时加载的小资源打包成一个包
  • 如果对于一个同一个资源有两个版本,可以考虑通过后缀来区分版本,xx.v1 xx.v2 xx.v3

构建 AB 后产生的文件

AssetBundle 文件

文件使用自定义后缀,包含在运行时为了加载资源而需要加载的内容。

此包的结构根据它是 AssetBundle 还是场景 AssetBundle 可能会略有不同。

  • 普通 AssetBundle 的结构:
    mark

  • 场景 AssetBundle 与普通 AssetBundle 的不同之处在于,它针对场景及其内容的串流加载进行了优化。

Manifest 清单文件

对于生成的每个 AB 包,都会生成关联的清单文件。清单文件包含诸如 循环冗余校验 (CRC) 数据 和 包的依赖性数据 等信息。

对于普通 AssetBundle,它们的清单文件将如下所示:

AssetBundles.manifest – AB 包的清单文件

ManifestFileVersion: 0
CRC: 4225903359
AssetBundleManifest:
  AssetBundleInfos:
    Info_0:
      Name: share.u3d
      Dependencies: {}
    Info_1:
      Name: cubewall.u3d
      Dependencies:
        Dependency_0: share.u3d
    Info_2:
      Name: spherewall.u3d
      Dependencies:
        Dependency_0: share.u3d

share.u3d.manifest – 包含贴图资源

ManifestFileVersion: 0
CRC: 4044919538
Hashes:
  AssetFileHash:
    serializedVersion: 2
    Hash: db0f6906b386d5d4413b2ddd1d9a6c61
  TypeTreeHash:
    serializedVersion: 2
    Hash: 6f165f44e4778b6c9d85e7a145a54cb1
HashAppended: 0
ClassTypes:
- Class: 21
  Script: {instanceID: 0}
- Class: 28
  Script: {instanceID: 0}
- Class: 48
  Script: {instanceID: 0}
Assets:
- Assets/Materials/Stone_floor_09.png
- Assets/Materials/Stone_floor_09.mat
Dependencies: []

cubewall.u3d.manifest – 依赖于 share

ManifestFileVersion: 0
CRC: 2862441256
Hashes:
  AssetFileHash:
    serializedVersion: 2
    Hash: 6e48f6b8e6cedd4070323880d839a3ee
  TypeTreeHash:
    serializedVersion: 2
    Hash: f49f05f36a566d50434f7d9f3fb347da
HashAppended: 0
ClassTypes:
- Class: 1
  Script: {instanceID: 0}
- Class: 4
  Script: {instanceID: 0}
- Class: 21
  Script: {instanceID: 0}
- Class: 23
  Script: {instanceID: 0}
- Class: 33
  Script: {instanceID: 0}
- Class: 43
  Script: {instanceID: 0}
- Class: 65
  Script: {instanceID: 0}
Assets:
- Assets/Prefabs/CubeWall.prefab
Dependencies:
- G:/UnityDocuments/AssetBundle/Assets/AssetBundles/share.u3d

AB 依赖

依赖打包

把共享资源放在一个包里,以节约空间。

mark

加载依赖

如果 AssetBundle 中包含依赖项,则在加载尝试实例化的对象之前,务必加载包含这些依赖项的AB包。Unity 不会自动加载依赖项。

参考以下示例,a 中的材质引用了 b 中的纹理,加载 a 和 b 的顺序无关紧要,重要的是在使用 a 中的材质前应加载 b。

在此示例中,在从 a 加载材质之前,需要将 b 加载到内存中。加载 a 和 b 的顺序无关紧要,重要的是在使用 a 中的材质前应加载 b。

mark

 private void Start() { 
   
     AssetBundle.LoadFromFile("Assets/AssetBundles/share.u3d"); // 加载依赖包
     AssetBundle ab = AssetBundle.LoadFromFile("Assets/AssetBundles/cubewall.u3d");

     var wallPre = ab.LoadAsset<GameObject>("CubeWall");
	 Instantiate(wallPre); // 实例化物体
 }

[当 AB 包存在依赖关系时,如何知道在加载原始包的资源前,需要加载哪些依赖包?](#加载 AssetBundle 清单)

AssetBundles 的具体使用

加载 AB

根据 AssetBundle 是以什么样的形式提供的,而选择具体的加载方式。

  • AssetBundle.LoadFromMemoryAsync
    • 字节数组
    • 从内存里加载
  • AssetBundle.LoadFromFile
    • 从本地加载
  • WWW.LoadFromCacheOrDownload (弃用)
  • UnityWebRequest
    • 从远程服务器加载

AssetBundle.LoadFromMemoryAsync

此函数采用包含 AssetBundle 数据的字节数组。也可以根据需要传递 CRC 值。如果捆绑包采用的是 LZMA 压缩方式,将在加载时解压缩 AssetBundle。LZ4 压缩包则会以压缩状态加载。

using System.Collections;
using System.IO;
using UnityEngine;

public class LoadFromFileExample : MonoBehaviour { 
   

    private void Start() { 
   
        StartCoroutine(LoadFromMemoryAsync("Assets/AssetBundles/cubewall.u3d"));
    }

    IEnumerator LoadFromMemoryAsync(string path) { 
   
        // 加载 AB
        AssetBundleCreateRequest createRequest = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes(path)); // 字节数组
        yield return createRequest;
        AssetBundle bundle = createRequest.assetBundle;
        // 使用里面的资源
        var prefab = bundle.LoadAsset<GameObject>("CubeWall");
        Instantiate(prefab);
    }
}

AssetBundle.LoadFromFile

  • 如果AB未压缩或采用了数据块 (LZ4) 压缩方式,LoadFromFile 将直接从磁盘加载AB包。
  • 加载完全压缩的 (LZMA) AB包将首先解压缩捆绑包,然后再将其加载到内存中。
using System.Collections;
using System.IO;
using UnityEngine;

public class LoadFromFileExample : MonoBehaviour { 
   

    private void Start() { 
   
        StartCoroutine(LoadFromFile("Assets/AssetBundles/cubewall.u3d"));
    }

    IEnumerator LoadFromFile(string path) { 
   
        // 加载 AB
        AssetBundleCreateRequest createRequest = AssetBundle.LoadFromFileAsync("Assets/AssetBundles/cubewall.u3d");
        yield return createRequest; // 等待加载完成
        AssetBundle ab = createRequest.assetBundle;
        // 使用里面的资源
        var prefab = ab.LoadAsset<GameObject>("CubeWall");
        Instantiate(prefab);
    }
}

UnityWebRequest

从远程服务器加载 AssetBundle

using System.Collections;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;

public class LoadFromFileExample : MonoBehaviour { 
   

    private void Start() { 
   
        StartCoroutine(InstantiateObject());
    }

    IEnumerator InstantiateObject() { 
   
        // 加载 AB
        //string uri = @"http://127.0.0.1/AssetBundles/cubewall.u3d";
        string uri = "file:///G:/UnityDocuments/AssetBundle/Assets/AssetBundles/cubewall.u3d";
        UnityWebRequest request = UnityWebRequest.GetAssetBundle(uri, 0);
        yield return request.SendWebRequest();
        AssetBundle ab = DownloadHandlerAssetBundle.GetContent(request);
        // 使用里面的资源
        var prefab = ab.LoadAsset<GameObject>("CubeWall");
        Instantiate(prefab);
    }
}

从 AB 加载资源

通用代码片段:

// T 是尝试加载的资源类型
T objectFromBundle = bundleObject.LoadAsset<T>(assetName);

同步加载方式:

// 加载单个游戏对象
GameObject gameObject = loadedAssetBundle.LoadAsset<GameObject>(assetName);

// 加载所有资源
Unity.Object[] objectArray = loadedAssetBundle.LoadAllAssets();

异步加载方式:

// 加载单个游戏对象
AssetBundleRequest request = loadedAssetBundleObject.LoadAssetAsync<GameObject>(assetName);
yield return request;
var loadedAsset = request.asset;

// 加载所有资源
AssetBundleRequest request = loadedAssetBundle.LoadAllAssetsAsync();
yield return request;
var loadedAssets = request.allAssets;

加载 AssetBundle 清单

当 AB 包存在依赖关系时,如何知道在加载原始包的资源前,需要加载哪些依赖包?

可以通过 Manifest 文件得到某个包的依赖,清单对象可以动态地查找加载依赖项。

using System.Collections;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;

public class LoadFromFileExample : MonoBehaviour { 
   
    private void Start() { 
   
        // 加载 ab 包
        AssetBundle manifestAB = AssetBundle.LoadFromFile("Assets/AssetBundles/AssetBundles");
        // 加载清单文件
        AssetBundleManifest manifest = manifestAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
        // 加载 ab 的所有依赖项
        string[] dependencies = manifest.GetAllDependencies("wall.u3d");
        foreach (string dependency in dependencies) { 
   
            print(dependency);
            AssetBundle.LoadFromFile("Assets/AssetBundles/" + dependency);
        }
        // 加载资源
        AssetBundle ab = AssetBundle.LoadFromFile("Assets/AssetBundles/wall.u3d");
        var wallPre = ab.LoadAsset<GameObject>("CubeWall");
        // 实例化物体
        Instantiate(wallPre); 
    }
}

AssetBundle 的卸载

了解何时加载和卸载 AssetBundle 非常重要。不正确地卸载 AssetBundle 会导致在内存中复制对象或其他不良情况,例如缺少纹理。

// 卸载 AssetBundle
AssetBundle.Unload(bool); 

AssetBundle.Unload(true)

  • 卸载所有资源,即使有资源被使用着
  • 卸载从 AssetBundle 加载的所有游戏对象(及其依赖项)。这不包括复制的游戏对象(例如实例化的游戏对象),因为它们不再属于 AssetBundle。
  • 采用此种方式卸载,从该 AssetBundle 加载的纹理(并且仍然属于它)会从场景中的游戏对象消失,因此 Unity 将它们视为缺少纹理。

AssetBundle.Unload(false)

  • 卸载所有没用被使用的资源
  • 此种方式可能会在内存中产生大量复制对象

如何选择用哪种方式

假设材质 M 是从 AssetBundle AB 加载的,如下所示。

  • 如果调用 AB.Unload(true),活动场景中的任何 M 实例也将被卸载并销毁。
  • 如果改作调用 AB.Unload(false),那么将会中断 M 和 AB 当前实例的链接关系。

mark

通常,使用 AssetBundle.Unload(false) 不会带来理想情况。大多数项目应该使用 AssetBundle.Unload(true) 来防止在内存中复制对象。

大多数项目应该使用 AssetBundle.Unload(true) 并采用一种方法来确保对象不会重复。两种常用方法是:

  • 在应用程序生命周期中具有明确定义的卸载瞬态 AssetBundle 的时间点,例如在关卡之间或在加载屏幕期间。
  • 维护单个对象的引用计数,仅当未使用所有组成对象时才卸载 AssetBundle。这允许应用程序卸载和重新加载单个对象,而无需复制内存。

如果应用程序必须使用 AssetBundle.Unload(false),则只能以两种方式卸载单个对象:

  • 在场景和代码中消除对不需要的对象的所有引用。
  • 以非附加方式加载场景。

修补 AssetBundle

修补 AssetBundle 很简单,只需要下载新的 AssetBundle 并替换现有的 AssetBundle。

如果使用 UnityWebRequest 来管理应用程序的缓存 AssetBundle,则将不同的版本参数传递给所选 API 将触发新 AssetBundle 的下载。

在修补系统中要解决的更难的问题是检测要替换的 AssetBundle。修补系统需要两个信息列表:

  • 当前已下载的 AssetBundle 及其版本控制信息的列表
  • 服务器上的 AssetBundle 及其版本控制信息的列表

修补程序应下载服务器端 AssetBundle 列表并比较这些 AssetBundle 列表。应重新下载缺少的 AssetBundle 或已更改版本控制信息的 AssetBundle。

也可以编写一个自定义系统来检测 AssetBundle 的更改。自己编写系统的大多数开发人员会选择对 AssetBundle 文件列表使用行业标准数据格式(例如 JSON)和并使用标准 c sharp 类(例如 MD5)来计算校验和。

常见问题

依赖包重复问题

  • 把需要共享的资源打包到一起
  • 分割包,这些包不是在同一时间使用的
  • 把共享部分打包成一个单独的包

图集重复问题

若不指定 Sprite 的 Packing Tag ,这些 Sprite 将会被自动打包在一个图集里面。

假如 a 包使用了这个图集里的一张图片,那么这个图集就会被打包在 a 包里面,如果 b 包也使用了图集里的一张图片,那么这个图集也会被打包在 b 包里面,这样一来就产生了图集的重复问题。

为了确保精灵图集不重复,请确保将相同精灵图集的所有精灵分配到同一个 AssetBundle。

Android 贴图问题

由于 Android 生态系统中存在严重的设备碎片,因此通常需要将纹理压缩为多种不同的格式。虽然所有 Android 设备都支持 ETC1,但 ETC1 不支持具有 Alpha 通道的纹理。如果应用程序不需要 OpenGL ES 2 支持,解决该问题的最简单方法是使用所有 Android OpenGL ES 3 设备都支持的 ETC2。

解决这个问题的一种方法是使用 Unity 5 的 AssetBundle 变体。(有关其他方案的详细信息,请参阅 Unity 的 Android 优化指南。)

Unity Asset Bundle Browser 工具

下载后,直接将 Editor 文件夹放在自己的项目目录下,此工具使用户能够查看和编辑 Unity 项目的资源包的配置。此工具将在 Window 菜单下创建 AssetBundle Browser 菜单项。

此窗口提供了一个类似资源管理器的界面,用于管理和修改项目中的资源包。

mark

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/148014.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • c++如何将字符串转为数组(将字符串转换为数组)

    1.将字符串转为byte数组stringimgData=”….,…,….,….”;string[]imgArr=imgData.Split(newchar[]{‘,’});byte[]bty=Array.ConvertAll(imgArr,delegate(strings){returnbyte.Parse(s);});2.将byte数组转为字符串主要…

  • TDD与FDD模式

    TDD与FDD模式1.TDD与FDD通过ad9361外部射频开关切换2.ad9361可一直工作在FDD模式3.FDD可同时收发,即TRX发,RX收;TDD不可同时收发,所以可复用端口,TRX收发,RX不用

  • 转的两种读音区分_category怎么读

    转的两种读音区分_category怎么读类别类别是一种为现有的类添加新方法的方式。利用Objective-C的动态运行时分配机制,可以为现有的类添加新方法,这种为现有的类添加新方法的方式称为类别catagory,他可以为任何类添加新的方法,

  • 系统运维面试题

    系统运维面试题1.什么是运维?什么是游戏运维?1)运维是指大型组织已经建立好的网络软硬件的维护,就是要保证业务的上线与运作的正常,在他运转的过程中,对他进行维护,他集合了网络、系统、数据库、开发、安全、监控于一身的技术,运维又包括很多种,有DBA运维、网站运维、虚拟化运维、监控运维、游戏运维等等2)游戏运维又有分工,分为开发运维、应用运维(业务运维)和系统运维开发运维:是给应用运维开发运维工具和运维平台的应用运维:是给业务上线、维护和做故障排除的,用开发运维开发出来的工具给业务上线、维护、做故障

  • linux删除宝塔面板_宝塔修改端口

    linux删除宝塔面板_宝塔修改端口https://www.2kss.com/45898.html

    2022年10月19日
  • Laravel 传递数据到视图

    Laravel 传递数据到视图

    2021年10月26日

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号