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

相关文章

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

高效+灵活,万博智云全球发布AWS无代理跨云容灾方案!

摘要 近日,万博智云推出了基于AWS的无代理跨云容灾解决方案,并与拉丁美洲,中东,亚洲的合作伙伴面向全球开展了联合发布。这一方案以AWS应用环境为基础,将HyperBDR平台的高效、灵活和成本效益优势与无代理功能相结合,为全球企业带来实现了更便捷、经济的数据保护。 一、全球联合发布 9月2日,万博智云CEO Michael Wong在线上平台发布AWS无代理跨云容灾解决方案的阐述视频,介绍了

电脑桌面文件删除了怎么找回来?别急,快速恢复攻略在此

在日常使用电脑的过程中,我们经常会遇到这样的情况:一不小心,桌面上的某个重要文件被删除了。这时,大多数人可能会感到惊慌失措,不知所措。 其实,不必过于担心,因为有很多方法可以帮助我们找回被删除的桌面文件。下面,就让我们一起来了解一下这些恢复桌面文件的方法吧。 一、使用撤销操作 如果我们刚刚删除了桌面上的文件,并且还没有进行其他操作,那么可以尝试使用撤销操作来恢复文件。在键盘上同时按下“C

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

hdu 4565 推倒公式+矩阵快速幂

题意 求下式的值: Sn=⌈ (a+b√)n⌉%m S_n = \lceil\ (a + \sqrt{b}) ^ n \rceil\% m 其中: 0<a,m<215 0< a, m < 2^{15} 0<b,n<231 0 < b, n < 2^{31} (a−1)2<b<a2 (a-1)^2< b < a^2 解析 令: An=(a+b√)n A_n = (a +

Vue3项目开发——新闻发布管理系统(六)

文章目录 八、首页设计开发1、页面设计2、登录访问拦截实现3、用户基本信息显示①封装用户基本信息获取接口②用户基本信息存储③用户基本信息调用④用户基本信息动态渲染 4、退出功能实现①注册点击事件②添加退出功能③数据清理 5、代码下载 八、首页设计开发 登录成功后,系统就进入了首页。接下来,也就进行首页的开发了。 1、页面设计 系统页面主要分为三部分,左侧为系统的菜单栏,右侧

K8S(Kubernetes)开源的容器编排平台安装步骤详解

K8S(Kubernetes)是一个开源的容器编排平台,用于自动化部署、扩展和管理容器化应用程序。以下是K8S容器编排平台的安装步骤、使用方式及特点的概述: 安装步骤: 安装Docker:K8S需要基于Docker来运行容器化应用程序。首先要在所有节点上安装Docker引擎。 安装Kubernetes Master:在集群中选择一台主机作为Master节点,安装K8S的控制平面组件,如AP