鳩. 需求:在安卓平台的软件内部直接进行版本更新,不需要用户去应用商店重新下载apk了。简单点说,就是从服务端获取一个APK文件到本地,然后直接进行安装并覆盖当前的软件,从而实现版本更新。(针对安卓平台) 参考博客1:https://www.cnblogs.com/szyx/p/14890735.html 参考视频1:唐老狮的《Unity移动平台相关》课程的第8节和第9节。 参考视频2:https://www.bilibili.com/video/BV1d44y1B7Ja/?spm_id_from=333.1007.top_right_bar_window_custom_collection.content.click&vd_source=13caff7d8cef9587ceb26d9b306a5661 以下为实现的全过程(详细到每一个细节): 1;首先是创建一个新的Unity项目,这里我用的是2021.3.26f1c1版本,2d、3d项目无所谓,我这里创建了一个3d项目。 2;在Assets目录下创建存放安卓插件的目录,如下 3;将平台转换到安卓平台(如果发现无法切换安卓平台的话,就去UnityHub自己下载安卓模块) 4;打开Player Settings->Other Settings,打开后往下滑就能找到下图的内容,可以按照自己喜欢修改包名和版本,要注意一下包名的名字和Minimum API Level,后面是要用到的。 5;打开Android Studio,新创建一个空项目。选中Empty Activity之后,直接点Next就可以了。(如果连Android Studio都不知道是什么,或者还没有安装的话,推荐看唐老狮的课程来了解一下) 6;这一步是对Android Studio的项目进行设置,具体怎么设置看下图。(如果Minimum SDK没有API 22的这个选项的话,可以自己去下载一下,具体可以自己搜一下,或者看唐老狮的视频) 7;进入项目后,等待加载完毕,就可以把这下面这几个都给选中删掉。 8;发现还没有删除干净,继续把下面的两个也给删掉。 9;删除结束后如下图所示 10;切换到项目,进行如下点击操作即可。 11;修改build.gradle文件,修改结束要点击一下Sync Now,这样才算是修改完毕。 12;找到自己Unity项目的编辑器目录,比如说我这里是用的Unity 2021.3.26f1c1,就打开目录就行了。 13;进行如下点击操作即可。(因为项目是mono所以,选择了mono目录,如果是il2cpp的话就选择下面第三张图的目录中的classes.jar) 14;把复制的文件粘贴到Android Studio项目里面(复制的位置就是在libs下面) 15;选中classes.jar,然后点击右键,再点击下图所示的选项,出现一个弹窗点OK就行了。 16;打开build.gradle看到如下红圈圈起来的代码的时候,就说明classes.jar配置成功了。 17;再次来到Unity 2021.3.26f1c1的如下目录,并且复制一个文件夹。 18;将复制的文件夹直接粘贴到jave目录下(选中java,然后右键,选择Paste就可以了),出现如下图所示的结果就说明成功了。 19;修改MainActivity里面的代码。 20;在MainActivity中增加一个安装函数,让Unity可以直接调用这个函数来安装APK(到时候只需要把APK所在地路径给传进来,然后就能安装了,下面的安装函数有一个返回的bool值,是用来判断是否安装成功的)。以下是MainActivity的所有代码: package com.test.test; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.util.Log; import androidx.core.content.FileProvider; import com.unity3d.player.UnityPlayer; import com.unity3d.player.UnityPlayerActivity; import java.io.File; public class MainActivity extends UnityPlayerActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } public boolean InstallAPK(String apkPath){ File apkFile = new File(apkPath); if (apkFile.exists()) { Intent intent = new Intent(Intent.ACTION_VIEW); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); Uri contentUri = FileProvider.getUriForFile(UnityPlayer.currentActivity, UnityPlayer.currentActivity.getPackageName()+".fileprovider", apkFile); intent.setDataAndType(contentUri, "application/vnd.android.package-archive"); } else { intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } UnityPlayer.currentActivity.startActivity(intent); return true; } else { Log.d("TAG", "文件不存在"+apkPath); return false; } } } 21;在res目录下面新增加一个文件夹xml 22;在xml文件夹下新增加一个文件:provider_paths.xml 23;这是provider_paths.xml内应该填写的内容 <?xml version="1.0" encoding="utf-8"?> <paths> <external-path name="publicDir" path="."/> </paths> 24;修改AndroidManifest.xml文件,注意其中的package要修改为自己的包名,我这里就是com.test.test了 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.test.test" > <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <application> android:requestLegacyExternalStorage="true" <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <meta-data android:name="unityplayer.UnityActivity" android:value="true"/> </activity> <meta-data android:name="com.google.android.actions" android:resource="@xml/provider_paths" /> <!-- 适配android 7.0以及以上更新APK路径 \--> <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true" > <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths" /> </provider> </application> </manifest> 25;最后再点击一下 26;Build成功后,就可以发现app下面多了一个build文件夹,然后往下面找就可以看到一个app-debug.aar包,把aar包复制下来,这个aar包就是unity需要使用到安卓包。 27;把app-debug.aar复制到Unity的下面这个路径就可以了,注意,还有一个AndroidManifest.xml文件,这个文件也是我从Andoid Studio项目里面直接复制过来的,内容就和上面第24步的xml的内容一模一样。(要在资源管理器里面粘贴,因为Unity里面是没法粘贴的) 28;打开资源管理器,然后双击app-debug.aar文件,如果双击没办法打开的话那么你就需要用到一个软件(WinRAR,如果没有的话可以去这个网址里面按照教程下载:https://mp.weixin.qq.com/s/-eJGDlgs97Ot5XuXNJA8Pw)。双击打开app-debug.aar文件以后,就把libs下面的那个classes.jar包给删除了,因为这个包是多余的,如果不删打包的时候就会报错。 29;继续删除app-debug.aar里面的内容。在app-debug.aar包里面继续双击classes.jar文件,对classes.jar文件进行操作,步骤在图中。(注意,当操作完毕,关闭压缩包之后,一定要保存) 30;看一下这个包是如果是安卓的,那就没什么问题。 31;在Player Settings->Publishing Settings中往下滑,看到如下图的地方后,就点一下箭头标注的地方,打上勾即可。 32;可以看到下图这里多了两个文件,就是因为我们上面点了两个勾勾才出现的,接下来就是要修改这两个文件,可以用vs或rider或者sublime Text打开都行,只要能对文件进行修改就可以,首先是修改mainTemplate.gradle文件。 implementation 'androidx.appcompat:appcompat:1.2.0' 33;再来修改gradleTemplate.properties文件。 org.gradle.jvmargs=-Xmx**JVM_HEAP_SIZE**M org.gradle.parallel=true android.enableR8=**MINIFY_WITH_R_EIGHT** unityStreamingAssets=**STREAMING_ASSETS** **ADDITIONAL_PROPERTIES** android.overridePathCheck=true android.useAndroidX=true android.enableJetifier=true 34;打开这个路径的AndroidManifest.xml文件,还有一个地方要修改。需要把一个启动入口删掉,因为aar包中也有一个AndroidManifest.xml文件,两个文件有两个启动入口的话,就会在安卓机上生成两个应用图标,所以需要删掉一个启动入口。(我卡在这一步很久,在这里要特地感谢一下唐老师帮我解惑,经过唐老师的指点,我才发现原来唐老师已经在《Unity移动平台相关》课程的第9节讲解过相关的知识点了) 35;到这里为止,配置相关就已经结束了,接下来将正式开始进行Unity调用相关。后面就讲得比较粗略一点了。首先是创建一个Test.cs脚本,挂载在摄像机上(随便挂哪都行)。然后创建两个UI,一个Button,一个Slider,Button是用来点击后执行更新版本的操作的,Slider则是用来显示下载的进度的(因为新版本的APK都是从服务器上拉取来的,所以肯定需要进行下载,然后有一个进度条,就可以让用户看到下载多少了)。如下图所示: 36;如下是Test内的代码(记得在Unity里面把对应的UI组件拖拽到代码上进行关联,代码应该不难,有点基础都能看懂。(唯一提一点,apk一定要下载在Application.persistentDataPath这个路径下,血的教训): using System; using System.Collections; using System.IO; using System.Linq; using UnityEngine; using UnityEngine.Networking; using UnityEngine.Serialization; using UnityEngine.UI; using Button = UnityEngine.UI.Button; using Debug = UnityEngine.Debug; public class Test : MonoBehaviour { private AndroidJavaClass ajc; private AndroidJavaObject ajo; public Button UpdateButton; public Slider ProcessSlider; private void Awake() { try { ajc = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); ajo = ajc.GetStatic<AndroidJavaObject>("currentActivity"); } catch (Exception e) { Debug.Log(e.Message); } } private void Start() { UpdateButton.onClick.AddListener(() => { UpdateButton.interactable = false; DownloadApk( "填写你自己的APK下载网址"); }); } public void DownloadApk(string url) { UnityWebRequest request = UnityWebRequest.Get(url); StartCoroutine(DownloadApkRoutine(request)); } private IEnumerator DownloadApkRoutine(UnityWebRequest request) { //显示下载的进度条 StartCoroutine(LoadProcess(request)); yield return request.SendWebRequest(); if (request.isDone) { if(request.result!=UnityWebRequest.Result.Success) { Debug.LogError("Error downloading apk: " + request.error ); } else { string dataPath = Application.persistentDataPath; string fileName = request.url.Split('/').Last(); string path = dataPath + "/download" + fileName; byte [] data = request.downloadHandler.data; bool isSuccess = true; using (FileStream fs = File.Create(path)) { try { fs.Write(data, 0, data.Length); } catch (Exception e) { Debug.Log("写入数据失败!"); isSuccess = false; } } //只有成功了才能安装apk,否则的话就不要安装了 if(isSuccess)InstallAPK(path); } } else { Debug.Log("下载APK失败"); } } private void InstallAPK(string Path) { bool isSuccess = ajo.Call<bool>("InstallAPK",Path); if (isSuccess) { Debug.Log("更新成功"); } else { Debug.Log("更新失败"); } } private IEnumerator LoadProcess(UnityWebRequest request) { //直接来一个While循环,一直要等到下载结束后才会结束 while (!request.isDone) { ProcessSlider.value = request.downloadProgress; yield return null; } } } 37;最后一步就是打包,然后用安卓机进行测试。安装后,打开应用,然后点击更新按钮,就可以看到进度条在不断的走(要保证是联网的状态),就说明正在下载apk,当进度条走完之后,就会直接跳出安装界面,点击就可以安装apk了,这样用户就可以在应用里面点击更新按钮直接联网更新版本,就不用去应用商店下载了。而且如果是有的项目刚开始根本不考虑热更新,导致后面想要再修改用Lua语言,相当于是要把代码推翻重写,这就太浪费时间了,而直接更新版本,也不失为一个好办法。 2024.1.9更新 有两点需要注意一下: 1;首先就是密钥的问题,如果想要覆盖当前软件进行版本更新的话,那么安装包的密钥要和当前软件的密钥一样,不然就会安装失败,所以得提前设定好密钥,这个也可以去看唐老师的移动平台相关的视频,有详细介绍密钥是怎么设定的,在这里我就不多说了。 2;应用下载在Application.persistentDataPath这个目录下后,版本更新并不会导致这个目录的文件被清空,所以如果版本更新很多次,这个目录就会存在很多从服务端下载下来的apk,我采取的办法就是,直接把所有版本的apk都进行同一个命名,这样一来这个目录下最多就只会存在一个apk,用户的存储空间也不会被占用太多。 2024.1.14更新 之前忘记说了,我用的Android Studio版本是:Android Studio Arctic Fox | 2020.3.1
蒋佳李 分享一个我所使用的包: Tools-release.aar 包。 tools-release.zip6kB Unity上的代码: using UnityEngine; namespace Fantasy { /// <summary> /// 安卓库包的接口脚本 /// </summary> public static class AndroidTools { #region 对外公开使用的接口 /// <summary> /// 重启应用 /// </summary> public static void RestartApp() { MJavaObject.Call("restart",1); MJavaObject.Dispose(); } /// <summary> /// 打开浏览器 /// </summary> public static void OpenBrowser(string url) { MJavaObject.Call("openBrowser", url); } /// <summary> /// 震动反馈 /// /// long[] pattern = {0, 200, 200, 200}; // 强烈震动的模式:开启0毫秒,震动200毫秒,休息200毫秒,然后重复 /// </summary> /// <param name="pattern">震动模式</param> /// <param name="repeat">重复次数</param> public static void AppVibrator(long[] pattern, int repeat) { MJavaObject.Call("vibrator", pattern, repeat); } /// <summary> /// 默认震动反馈 /// </summary> public static void AppVibrator() { long[] pattern = {0, 20}; AppVibrator(pattern,-1); } #endregion 对外公开使用的接口 #region 私有变量 private static AndroidJavaObject _appTool; /// <summary> /// 获取安卓库的类 /// </summary> private static AndroidJavaObject MJavaObject { get { if (_appTool != null) return _appTool; _appTool = new AndroidJavaObject("com.jiangjiali.tools.AppTool"); return _appTool; } } #endregion } }