Unity发布Android App Bundle详解(三)快速转换Addressables

2024-01-13 23:59

本文主要是介绍Unity发布Android App Bundle详解(三)快速转换Addressables,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

2021.12.28更新

经群友提醒,目前Unity官方已经支持直接不进行任何更改打出aab包了。

支持的Unity版本:

  • 2021 → 2021.2.0b4 以上
  • 2020 → 2020.3.15f2 以上
  • 2019 → 2019.4.29f1 以上

将Split Application Binary选项勾选

在ProjectSettings → Android → Publish Settings 最底下有个 Split Application Binary,将其勾选

原本这是会让 APK 产生 APK Expansion Files (.oob) 的选项,但在build target选AAB的情况下会变成使用Play Asset Delivery。

详细操作方式按照官方文档操作即可

也可以查看该文章解决:https://medium.com/akatsuki-taiwan-technology/unity-play-asset-delivery-1d468fd90c2d

-----------------------------------------------------------------------------------------------------

概述

对于项目本身就使用AssetBundle的来说,打包新格式aab是很容易的,上篇文章已经详细说过了。

对于项目之初没有考虑AssetBundle热更新的项目怎么办呢?

项目都是采用Resources加载,并且是同步加载的,unity场景资源也较多,没有做好分包设计的怎么办呢?

这篇文章我们讲怎么处理。

难点

  • 场景采用同步/异步加载
    SceneManager.LoadScene ("xx",LoadSceneMode.Single);
  • 资源采用Resources.Load加载
    GameObject prefab = Resources.Load<GameObject> (path);

方案

采用Unity自带的可寻址系统插件Addressables(该插件已经替代AssetBundle作为Unity推荐的热更新方案)

采用Addressables的方式将项目快速转换为热更新。

此方案改动较少,对于之前的文件路径也不需要修改太多逻辑,几乎能完美移植。

插件导入

在项目中使用Package Manager,找到Addressables安装即可。

目标

如果项目本身加载场景/资源都采用异步加载,也就是SceneManager.LoadSceneAsync和Resources.LoadAsync,并且已经实现了等待过程处理,那这种转换起来还是蛮简单的。如果没有也没关系,因为Addressables可寻址系统也支持同步加载(最开始没有,1.17以上加了)

  •  场景加载API替换

  • Resources.Load加载资源的API替换

  • 实例化逻辑替换

  • 内存释放

请注意: 因为之前Resources.Load不需要考虑内存释放的问题,但是Addressables系统需要自己管理内存释放,否则内存会一直留着,导致内存不足可能会导致闪退情况。

资源可寻址

将插件导入后,需要开始配置可寻址路径,只有进行正确的配置,才能实现快速转换。

目标:

  • Resources文件目录转移(Addressables自行处理),否则也会打包到包体内
  • 场景变为可寻址

 配置:

  1. 打开Window/Asset Management/Addressables/Groups
  2. 将需要分包的资源目录/文件拖到组里面,会自动将资源移动到Resources_moved目录,防止Unity打包到包体内



    注意:加载路径有所修改,后面需要加上.prefab后缀名才能读取到
  3. 对于场景,直接将场景目录或.unity文件直接拖入即可。(名字可以自行更改)

代码配置

如果想通过代码配置,Addressables官方没有提供简单的方式,但是我们也可以通过这种方式实现。

为此我自己封装了这个类,大家调用接口即可通过代码方式把资源变为可寻址。

using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.AddressableAssets.Settings;
#endif
using System;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.SceneManagement;public class AddressablesUtils
{public static T LoadAsset<T>(string path){var request = LoadAssetAsync<T>(path);return request.WaitForCompletion();}public static AsyncOperationHandle<T> LoadAssetAsync<T>(string path){try{var request = Addressables.LoadAssetAsync<T>(path);return request;}catch (Exception e){Debug.Log(e.Message);}return default;}public static AsyncOperationHandle<SceneInstance> LoadSceneAsync(string level,LoadSceneMode loadSceneMode){var async = Addressables.LoadSceneAsync(level, loadSceneMode);return async;}public static GameObject Instantiate(string path, Transform parent){var request = InstantiateAsync(path, parent);return request.WaitForCompletion();}public static AsyncOperationHandle<GameObject> InstantiateAsync(string path,Transform parent){try{return Addressables.InstantiateAsync(path, parent);}catch (Exception e){Debug.Log(e.Message);}return default;}public static void ReleaseInstance(GameObject instance){Addressables.ReleaseInstance(instance);}public static void Release<T>(T target){Addressables.Release(target);}#if UNITY_EDITORpublic static void AddToAddressable(string path,string address="",string groupName="PlayAssetDelivery",bool isAdd=true){if (string.IsNullOrEmpty(path)) return;var assetSettingPath = "Assets/AddressableAssetsData/AddressableAssetSettings.asset";AddressableAssetSettings addressableAssetSettings = AssetDatabase.LoadAssetAtPath<AddressableAssetSettings>(assetSettingPath);SetAaEntry(addressableAssetSettings, groupName, path, isAdd, address);}static void SetAaEntry(AddressableAssetSettings aaSettings,string groupName, string path, bool create,string address=""){AddressableAssetGroup assetGroup = null;if (!string.IsNullOrEmpty(groupName)){assetGroup = aaSettings.FindGroup(groupName);}else{assetGroup = aaSettings.DefaultGroup;}if (create && assetGroup.ReadOnly){Debug.LogError("Current default group is ReadOnly.  Cannot add addressable assets to it");return;}Undo.RecordObject(aaSettings, "AddressableAssetSettings");var guid = string.Empty;//if (create || EditorUtility.DisplayDialog("Remove Addressable Asset Entries", "Do you want to remove Addressable Asset entries for " + targets.Length + " items?", "Yes", "Cancel")){var entriesAdded = new List<AddressableAssetEntry>();var modifiedGroups = new HashSet<AddressableAssetGroup>();Type mainAssetType;guid = AssetDatabase.AssetPathToGUID(path);if (create){var e = aaSettings.CreateOrMoveEntry(guid, assetGroup, false, false);if (!string.IsNullOrEmpty(address)) e.address = address;entriesAdded.Add(e);modifiedGroups.Add(e.parentGroup);}else{aaSettings.RemoveAssetEntry(guid);}if (create){foreach (var g in modifiedGroups)g.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, entriesAdded, false, true);aaSettings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, entriesAdded, true, false);}}}
#endif
}

增加到Unity右键菜单

选中目录,直接添加可寻址,并去除后缀

因为unity通过Resources.Load加载的资源路径不需要后缀的,而拖文件夹的方式后缀名又无法去除,所以写了个菜单,自动将选中的目录下的文件加到可寻址,并去除后缀名。

static List<string> SelectsPath{get{List<string> pathList = new List<string>();var selectionList = Selection.assetGUIDs;foreach (var guid in selectionList){var assetPath = AssetDatabase.GUIDToAssetPath(guid);var fullPath = SVNProjectPath + "/" + assetPath;pathList.Add(fullPath);}List<string> result = new List<string>(pathList);for (int i = 0; i < pathList.Count; i++){string svnPath = pathList[i];//移除掉所有比自己长并且部分完全包含自己的路径(也就是移除所有子路径)result.RemoveAll((path) =>{return path.Length > svnPath.Length && path.StartsWith(svnPath);});}return result;}}
public static string SVNProjectPath{get{System.IO.DirectoryInfo parent = System.IO.Directory.GetParent(Application.dataPath);return parent.ToString();}}
[MenuItem("Assets/Add To Addressables", false)]static void AddToAddressables(){List<string> selectPath = SelectsPath;foreach (var path in selectPath){if (FileControll.FolderExist(path)){var files = FileControll.GetFolderFiles(path,true);int totalLength = files.Count;int i = 0;foreach (var filePath in files){i++;float progress = ((float)i / (float)totalLength);bool isCancel = EditorUtility.DisplayCancelableProgressBar (string.Format("正在处理..."), string.Format("处理资源({0}/{1})",i,totalLength), progress);if (isCancel){EditorUtility.ClearProgressBar();break;}if (filePath.EndsWith(".meta")) continue;string fPath = FileControll.GetRelativePath(filePath, Application.dataPath.Replace("Assets",""));string address = fPath.Replace(new FileInfo(fPath).Extension,"");address = FileControll.MakePathPerfect(address);if (address.StartsWith("Assets/Game/Resources_moved")){address = address.Replace("Assets/Game/Resources_moved/", "");}//Debug.Log(fPath+"==="+Application.dataPath);AddressablesUtils.AddToAddressable(fPath,address);}EditorUtility.ClearProgressBar();}}}
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System;
using System.Text;public static class FileControll{/// <summary>/// 创建文件/// </summary>/// <param name="filePath"></param>/// <returns></returns>public static bool CreateFile(string filePath){if (!FileExist (filePath)) {File.Create (filePath);return FileExist(filePath);}return false;}/// <summary>/// 创建目录/// </summary>/// <param name="folderPath"></param>/// <returns></returns>public static bool CreateFolder(string folderPath){if (!FolderExist(folderPath)) {DirectoryInfo info = Directory.CreateDirectory (folderPath);return info.Exists;}return false;}/// <summary>/// 删除文件/// </summary>/// <param name="filePath"></param>public static void DeleteFile(string filePath){if (FileExist (filePath)) {File.Delete (filePath);}}/// <summary>/// 删除目录/// </summary>/// <param name="folderPath"></param>public static void DeleteFolder(string folderPath){DeleteFolder (folderPath, true);}/// <summary>/// 删除目录/// </summary>/// <param name="folderPath"></param>/// <param name="recursive">是否递归删除</param>public static void DeleteFolder(string folderPath,bool recursive){if(FolderExist(folderPath)){Directory.Delete (folderPath, recursive);}}/// <summary>/// 目录是否存在/// </summary>/// <param name="folderPath"></param>/// <returns></returns>public static bool FolderExist(string folderPath){return Directory.Exists (folderPath);}/// <summary>/// 文件是否存在/// </summary>/// <param name="filePath"></param>/// <returns></returns>public static bool FileExist(string filePath){return File.Exists (filePath);}/// <summary>/// 获取文件的目录/// </summary>/// <param name="filePath"></param>/// <returns></returns>public static string GetFileFolder(string filePath){FileInfo fileInfo = new FileInfo (filePath);return fileInfo.Directory.ToString ();}/// <summary>/// 获取子目录/// </summary>/// <param name="folderPath"></param>/// <returns></returns>public static List<string> GetSubFolders(string folderPath){List<string> result = new List<string> ();GetSubFolders (folderPath, ref result);return result;}/// <summary>/// 获取子目录/// </summary>/// <param name="path"></param>/// <param name="result"></param>static void GetSubFolders(string path,ref List<string> result){result.Add (path);if (Directory.Exists(path)){foreach (string sub in Directory.GetDirectories(path)) {GetSubFolders (sub + "/", ref result);}}}/// <summary>/// 获取对应路径的相对路径(如absolutePath=E:/AB/c.txt,relativeTo=E:/AB/,输出c.txt)/// </summary>/// <param name="absolutePath"></param>/// <param name="relativeTo"></param>/// <returns></returns>public static string GetRelativePath(string absolutePath,string relativeTo){var fileInfo = new FileInfo(relativeTo);var fullFileInfo = new FileInfo(absolutePath);string absoluteName = fullFileInfo.FullName;absoluteName = MakePathPerfect(absoluteName);string relative = fileInfo.FullName;relative = MakePathPerfect(relative);string result = absoluteName.Replace(relative, "");return result;}/// <summary>/// 获取目录下的所有文件/// </summary>/// <param name="folderPath"></param>/// <param name="recursive">是否递归获取</param>/// <param name="endWith"></param>/// <returns></returns>public static List<string> GetFolderFiles(string folderPath, bool recursive,string endWith=""){List<string> fileList;if (recursive){fileList = new List<string>();List<string> subFolderList = GetSubFolders (folderPath);foreach (var subFolder in subFolderList){List<string> subFileList = GetFolderFiles(subFolder, endWith);fileList.AddRange (subFileList);}}else{fileList = GetFolderFiles(folderPath, endWith);}return fileList;}/// <summary>/// 获取目录下的所有文件/// </summary>/// <param name="folderPath"></param>/// <param name="endWith"></param>/// <returns></returns>public static List<string> GetFolderFiles(string folderPath,string endWith=""){List<string> result = new List<string> ();if (Directory.Exists(folderPath)){foreach (string file in Directory.GetFiles(folderPath)){if (!string.IsNullOrEmpty(endWith) && !file.ToLower().EndsWith(endWith.ToLower())) continue;result.Add (file);}}return result;}/// <summary>/// 写入文件/// </summary>/// <param name="path"></param>/// <param name="bytes"></param>public static void WriteFile(string path,byte[] bytes){WriteFile (path, bytes, FileMode.Create);}/// <summary>/// 写入文件/// </summary>/// <param name="path"></param>/// <param name="bytes"></param>/// <param name="fileMode"></param>public static void WriteFile(string path,byte[] bytes,FileMode fileMode){
#if UNITY_EDITOR || (!UNITY_WINRT)try {FileStream fs = new FileStream(path, fileMode);fs.Write (bytes, 0, bytes.Length);fs.Close();} catch (Exception ex) {Debug.LogError ("文件写入失败" + path + ":" + ex.Message);}
#endif}/// <summary>/// 写入文件/// </summary>/// <param name="path"></param>/// <param name="append"></param>/// <param name="infos"></param>public static void WriteFile(string path,bool append,List<string> infos){try {StreamWriter sw = new StreamWriter (path, append);if (infos!=null) {foreach (string info in infos) {sw.WriteLine(info);	}	}sw.Close ();sw.Dispose ();} catch (Exception ex) {Debug.LogError ("文件写入失败" + path + ":" + ex.Message);}}/// <summary>/// 写入Txt文件/// </summary>/// <param name="path"></param>/// <param name="content"></param>/// <param name="encoding"></param>public static void WriteTxtFile(string path, string content,Encoding encoding){File.WriteAllText(path,content,encoding);}/// <summary>/// 写入Txt文件/// </summary>/// <param name="path"></param>/// <param name="content"></param>public static void WriteTxtFile(string path, string content){File.WriteAllText(path,content);}/// <summary>/// 复制文件到/// </summary>/// <param name="path"></param>/// <param name="toPath"></param>/// <param name="overwrite">是否覆盖</param>public static void CopyFile(string path,string toPath,bool overwrite){try {File.Copy(path,toPath,overwrite);} catch (Exception ex) {Debug.LogError ("拷贝文件失败:"+ex.Message);}}/// <summary>/// 复制目录到/// </summary>/// <param name="from"></param>/// <param name="to"></param>public static void CopyFolder(string from, string to){if (!Directory.Exists(to))Directory.CreateDirectory(to);// 子文件夹foreach (string sub in Directory.GetDirectories(from))CopyFolder(sub + "/", to + Path.GetFileName(sub) + "/");// 文件foreach (string file in Directory.GetFiles(from)){try {File.Copy(file, to + Path.GetFileName(file), true);} catch (Exception ex) {Debug.LogWarning ("拷贝失败:" + ex.Message);}}}/// <summary>/// 读取文件/// </summary>/// <param name="path"></param>/// <returns></returns>public static byte[] ReadFile(string path){
#if UNITY_EDITOR || (!UNITY_WINRT)if (!File.Exists (path))return null;FileStream fs = new FileStream(path, FileMode.Open);long size = fs.Length;byte[] array = new byte[size];//将文件读到byte数组中fs.Read(array, 0, array.Length);fs.Close();return array;
#elsereturn null;
#endif}/// <summary>/// 读取Txt数据/// </summary>/// <param name="path"></param>/// <returns></returns>public static string ReadTxtFile(string path){if (!File.Exists (path))return null;return File.ReadAllText (path);}/// <summary>/// 读取Txt文件/// </summary>/// <param name="path"></param>/// <param name="encoding"></param>/// <returns></returns>public static string ReadTxtFile(string path,System.Text.Encoding encoding){if (!File.Exists (path))return null;return File.ReadAllText (path, encoding);}/// <summary>/// 读取Txt行/// </summary>/// <param name="path"></param>/// <returns></returns>public static List<string> ReadTxtFileLine(string path){if (!File.Exists (path))return null;try{using(StreamReader sr = new StreamReader (path)){List<string> dataList=new List<string>();string line;while ((line = sr.ReadLine()) != null) {dataList.Add(line);}return dataList;}}catch(Exception e){Debug.Log("文件未能读取"+e.Message);return null;}}/// <summary>/// 检测并矫正CSV格式/// </summary>/// <param name="path">csv文件路径</param>/// <param name="fileEncoding"></param>/// <returns>是否矫正</returns>public static bool CheckAndCollectCSVFormat(string path,Encoding fileEncoding=null){if (fileEncoding == null){TextUtil.EncodingType encodingType = TextUtil.GuessFileEncoding(path);if (encodingType == TextUtil.EncodingType.Unknown){fileEncoding = TextUtil.ANSI_CHINESE; //默认用ANSI编码}else fileEncoding = TextUtil.GetEncoding(encodingType);}if (!fileEncoding.CodePage.Equals(Encoding.UTF8.CodePage)){var content = File.ReadAllText(path, fileEncoding);File.WriteAllText(path,content,Encoding.UTF8);return true;}return false;}/// <summary>/// 合并路径/// </summary>/// <param name="folderPath"></param>/// <param name="fileName"></param>/// <returns></returns>public static string CombineFilePath(string folderPath, string fileName){if (string.IsNullOrEmpty(folderPath)){return "";}DirectoryInfo directoryInfo=new DirectoryInfo(folderPath);if (!directoryInfo.Exists){Debug.LogError("目录不存在:"+folderPath);return "";}return directoryInfo.FullName + "\\" + fileName;}/// <summary>/// 使路径规范化(都变成这样的格式:Assets/Game/Source)/// 如Assets\Game\Source变成Assets/Game/Source/// </summary>/// <param name="path"></param>/// <returns></returns>public static string MakePathPerfect(string path){return path.Replace("\\", "/");}/// <summary>/// 移动文件/// </summary>/// <param name="from"></param>/// <param name="to"></param>public static void MoveFile(string @from, string to){@from = from.Replace('\\', '/');@to = to.Replace('\\', '/');if (Directory.Exists(to)){FileControll.CopyFolder(@from, to);FileControll.DeleteFolder(@from);Debug.Log($"覆盖文件夹:{@from} ===> {to}");}else if (File.Exists(to)){FileControll.CopyFile(@from, to, true);FileControll.DeleteFile(@from);Debug.Log($"覆盖文件:{@from} ===> {to}");}else{string fileLastPath = to;if (to.Contains(".")){fileLastPath=to.GetFileLastPath();}if (!Directory.Exists(fileLastPath)){Directory.CreateDirectory(fileLastPath);}File.Move(@from, to);Debug.Log($"移动文件:{@from} ===> {to}");}}
}

场景加载API替换

代码示例

void LoadScene(string sceneName)
{SceneManager.LoadSceneAsync(sceneName,LoadSceneMode.Single);
}

替换后:

void LoadScene(string sceneName)
{Addressables.LoadSceneAsync(sceneName, LoadSceneMode.Single);
}

注意:

对于LoadSceneMode.Single,Addressables系统会在切换到其他场景时自动卸载内存,所以无需内存管理。而对于LoadSceneMode.Additive模式需要卸载场景时调用Addressables.UnloadSceneAsync卸载场景释放内存。

 Resources资源API替换

代码示例

T LoadAsset<T>(string path)where T:Object
{return Resources.Load<T>(path);
}

 替换后

使用request.WaitForCompletion可以将线程卡死,等待加载完成释放(该行为有风险,可能会导致一些意外情况)相当于是同步过程。

参考官方文档:Synchronous Workflow | Addressables | 1.17.17

T LoadAsset<T>(string path)where T:Object
{var request = Addressables.LoadAssetAsync<T>(path);request.WaitForCompletion();return request.Result;
}

注意:

  •  加载后的资源需要在恰当的时候释放掉,否则会导致内存泄露问题

释放接口如下: 

//释放实例
public static void ReleaseInstance(GameObject instance)
{Addressables.ReleaseInstance(instance);
}//释放加载的对象
public static void Release<T>(T target)
{Addressables.Release(target);
}

实例化逻辑替换

对于使用Instantiate实例化预制,需要修改,否则释放不了

代码示例

void InstancePrefab(string path,Transform parent)
{var prefab=Resources.Load<GameObject>(path);var go = GameObject.Instantiate(prefab, parent);
}

 替换后

void InstancePrefab(string path,Transform parent)
{var request = Addressables.InstantiateAsync(path);var go=request.WaitForCompletion();
}

 注意:

只有通过这种方式Instance出来的对象,才能进行释放

public static void ReleaseInstance(GameObject instance)
{Addressables.ReleaseInstance(instance);
}

内存管理

还有一步重要的设置,就是需要自行管理内存释放,否则随着游戏进程的增加,会导致内存泄露,玩家设备内存不足,会出现闪退等错误情况。

如何知道内存情况呢?

  1. 打开EventView,可以检测内存情况。
    Window/Addressables/Event Viewer
  2. 打开设置,选中配置文件,勾选Send Profiler Events,只有打开这个设置,才会检测资源情况

  3. 资源卸载时可以分析资源是否在列表里,在列表里说明没有释放,还占用着内存

测试

至此,转换工作顺利完成,可以开始测试。

  1. 打包AssetBundle
  2. 打包本体
  3. 打包后运行测试

结语

Addressables转换完成了,资源也分包好了,后面的工作就是进行谷歌aab合并上传。通过Play Asset Delivery可以快速的进行分发。

因为Google提供的插件是针对AssetBundle的,对于Addressables系统还有很多问题,而Unity这方面也没有过多的解释,所以后续还有蛮多工作要做的。

后续我们再细讲。

上一篇:Unity发布Android App Bundle详解(二)Play Asset Delivery介绍

下一篇:Unity发布Android App Bundle详解(四)Addressables+Play Asset Delivery分发

系列文章索引

Unity发布Android App Bundle详解(一)Unity .aab支持情况

Unity发布Android App Bundle详解(二)Play Asset Delivery介绍

Unity发布Android App Bundle详解(三)快速转换Addressables

Unity发布Android App Bundle详解(四)Addressables+Play Asset Delivery分发

这篇关于Unity发布Android App Bundle详解(三)快速转换Addressables的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/603237

相关文章

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

Spring Cloud LoadBalancer 负载均衡详解

《SpringCloudLoadBalancer负载均衡详解》本文介绍了如何在SpringCloud中使用SpringCloudLoadBalancer实现客户端负载均衡,并详细讲解了轮询策略和... 目录1. 在 idea 上运行多个服务2. 问题引入3. 负载均衡4. Spring Cloud Load

Springboot中分析SQL性能的两种方式详解

《Springboot中分析SQL性能的两种方式详解》文章介绍了SQL性能分析的两种方式:MyBatis-Plus性能分析插件和p6spy框架,MyBatis-Plus插件配置简单,适用于开发和测试环... 目录SQL性能分析的两种方式:功能介绍实现方式:实现步骤:SQL性能分析的两种方式:功能介绍记录

在 Spring Boot 中使用 @Autowired和 @Bean注解的示例详解

《在SpringBoot中使用@Autowired和@Bean注解的示例详解》本文通过一个示例演示了如何在SpringBoot中使用@Autowired和@Bean注解进行依赖注入和Bean... 目录在 Spring Boot 中使用 @Autowired 和 @Bean 注解示例背景1. 定义 Stud

定价129元!支持双频 Wi-Fi 5的华为AX1路由器发布

《定价129元!支持双频Wi-Fi5的华为AX1路由器发布》华为上周推出了其最新的入门级Wi-Fi5路由器——华为路由AX1,建议零售价129元,这款路由器配置如何?详细请看下文介... 华为 Wi-Fi 5 路由 AX1 已正式开售,新品支持双频 1200 兆、配有四个千兆网口、提供可视化智能诊断功能,建

使用Python快速实现链接转word文档

《使用Python快速实现链接转word文档》这篇文章主要为大家详细介绍了如何使用Python快速实现链接转word文档功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 演示代码展示from newspaper import Articlefrom docx import

如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解

《如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解》:本文主要介绍如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别的相关资料,描述了如何使用海康威视设备网络SD... 目录前言开发流程问题和解决方案dll库加载不到的问题老旧版本sdk不兼容的问题关键实现流程总结前言作为

SQL 中多表查询的常见连接方式详解

《SQL中多表查询的常见连接方式详解》本文介绍SQL中多表查询的常见连接方式,包括内连接(INNERJOIN)、左连接(LEFTJOIN)、右连接(RIGHTJOIN)、全外连接(FULLOUTER... 目录一、连接类型图表(ASCII 形式)二、前置代码(创建示例表)三、连接方式代码示例1. 内连接(I

Go路由注册方法详解

《Go路由注册方法详解》Go语言中,http.NewServeMux()和http.HandleFunc()是两种不同的路由注册方式,前者创建独立的ServeMux实例,适合模块化和分层路由,灵活性高... 目录Go路由注册方法1. 路由注册的方式2. 路由器的独立性3. 灵活性4. 启动服务器的方式5.

Java中八大包装类举例详解(通俗易懂)

《Java中八大包装类举例详解(通俗易懂)》:本文主要介绍Java中的包装类,包括它们的作用、特点、用途以及如何进行装箱和拆箱,包装类还提供了许多实用方法,如转换、获取基本类型值、比较和类型检测,... 目录一、包装类(Wrapper Class)1、简要介绍2、包装类特点3、包装类用途二、装箱和拆箱1、装