Unity游戏内版本更新实现

发表于2018-12-13
评论0 5.4k浏览
大多数游戏更新都是借助LUA热更新实现的,本篇文章要分享的是在游戏内apk包更新的方法,想知道的可以看看。

ios对于应用的管理比较严格,除非热更新脚本,不太可能做到端内大版本包的更新。然而安卓端则没有此限制。因此可以做到不跳到网页或应用商店,就覆盖更新apk包。

Unity最常用的脚本语言就是C#,不做断点续传的情况下,采用C#的网络库,还是比较简单的。重点就是做好相应的异常处理。

C#用于网络访问的方法主要有两种:WebRequest和封装好的WebClient。为了将来能做更多的扩展,我采用更灵活的HttpWebRequest进行请求。为了不阻塞主线程,使用异步接口。

基本做法可参考官方文档https://msdn.microsoft.com/zh-cn/library/system.net.httpwebrequest.begingetresponse(v=vs.110).aspx

然而我们知道,Unity4.X对于多线程的支持是很弱的,不推荐使用。因此,无法在下载线程中回调相应的事件。我将回调写在主线程中,用Coroutine去轮询当前的下载状态和进度,并做相应的处理。

首先需要定义下载的状态和传入下载线程的请求状态,然后是下载的路径(可能还需要文件MD5码)以及安装路径等必要的变量,最后为了显示当前的下载进度、下载速度等,需要开启一个Coroutine或者在Update中不断查询当前下载状态,是否有异常,以及是否已经下载完毕。如果下载完毕,则校验文件,并开始安装。
using UnityEngine;
using System;
using System.Collections;
using System.Threading;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System;
public class VersionUpdater : MonoBehaviour
{
    public class RequestState
    {
        public const int BUFFER_SIZE = 1024;
        public byte[] BufferRead;
        public HttpWebRequest request;
        public HttpWebResponse response;
        public Stream responseStream;
    }
    public enum DownloadState
    {
        DOWNLOADING,
        FINISHED,
        FAILED
    }
    public delegate void ProgressCallback(long curr, long length, float rate, DownloadState state);
    public ProgressCallback progressCallback;
    string url = "";
    string installPath = "";
    string apkName = "";
    string errorMsg = "";
    private FileStream fileStream = null;
    private long length = 1;
    private long curr = 0;
    private long last = 0;
    private const float UpdateTime = 0.5f;
    private float rate = 0;
    private DownloadState downState = DownloadState.DOWNLOADING;
    public void DownloadApkAsync(string url, string md5, string path, string name)
    {
        this.url = url;
        this.installPath = path;
        this.apkName = name;
        this.errorMsg = "";
        downState = DownloadState.DOWNLOADING;
        DownloadApkAsync();
    }
    private void DownloadApkAsync()
    {
        if (string.IsNullOrEmpty(url)) return;
        if (string.IsNullOrEmpty(installPath)) return;
        if (string.IsNullOrEmpty(apkName)) return;
        string fullpath = installPath + "/" + apkName;
        IAsyncResult result = null;
        try
        {
            fileStream = new FileStream(fullpath, FileMode.Create, FileAccess.Write);
            Uri uri = new Uri(url);
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
            request.Method = "GET";
            RequestState requestState = new RequestState();
            requestState.BufferRead = new byte[RequestState.BUFFER_SIZE];
            requestState.request = request;
            curr = 0;
            length = 1;
            rate = 0.0f;
            downState = DownloadState.DOWNLOADING;
            result = (IAsyncResult)request.BeginGetResponse(new AsyncCallback(ResponeCallback), requestState);
        }
        catch (Exception e)
        {
            errorMsg = "Begin Create Exception!";
            errorMsg += string.Format("Message:{0}", e.Message);
            StopDownload(result);
            downState = DownloadState.FAILED;
        }
        StartCoroutine(updateProgress());
    }
    IEnumerator updateProgress()
    {
        while (curr <= length)
        {
            yield return new WaitForSeconds(UpdateTime);
            rate = (curr - last) / UpdateTime;
            last = curr;
            if (downState == DownloadState.FAILED)
            {
                Debug.LogError(errorMsg);
                if (fileStream != null)
                    fileStream.Close();
                if (progressCallback != null)
                    progressCallback( curr, length, rate, DownloadState.FAILED);
                break;
            }
            if (progressCallback != null)
                progressCallback( curr, length, rate, DownloadState.DOWNLOADING);
            if (downState == DownloadState.FINISHED)
            {
                if (progressCallback != null)
                    progressCallback( curr, length, rate, DownloadState.FINISHED);
                break;
            }
        }
    }
    void StopDownload(IAsyncResult result)
    {
        if (result == null) return;
        RequestState requestState = (RequestState)result.AsyncState;
        requestState.request.Abort();
    }
    void ResponeCallback(IAsyncResult result)
    {
        try
        {
            if (downState != DownloadState.FAILED)
            {
                RequestState requestState = (RequestState)result.AsyncState;
                HttpWebRequest request = requestState.request;
                requestState.response = (HttpWebResponse)request.EndGetResponse(result);
                Stream responseStream = requestState.response.GetResponseStream();
                requestState.responseStream = responseStream;
                length = requestState.response.ContentLength;
                IAsyncResult readResult = responseStream.BeginRead(requestState.BufferRead, 0, RequestState.BUFFER_SIZE, new AsyncCallback(ReadCallback), requestState);
                return;
            }
        }
        catch (Exception e)
        {
            string msg = "ResponseCallback exception!\n";
            msg += string.Format("Message:{0}", e.Message);
            StopDownload(result);
            errorMsg = msg;
            downState = DownloadState.FAILED;
        }
    }
    void ReadCallback(IAsyncResult result)
    {
        try
        {
            if (downState != DownloadState.FAILED)
            {
                RequestState requestState = (RequestState)result.AsyncState;
                Stream responseStream = requestState.responseStream;
                int read = responseStream.EndRead(result);
                if (read > 0)
                {
                    fileStream.Write(requestState.BufferRead, 0, read);
                    fileStream.Flush();
                    curr += read;
                    IAsyncResult readResult = responseStream.BeginRead(requestState.BufferRead, 0, RequestState.BUFFER_SIZE, new AsyncCallback(ReadCallback), requestState);
                    return;
                }
                else
                {
                    Debug.Log("download end");
                    responseStream.Close();
                    fileStream.Close();
                    downState = DownloadState.FINISHED;
                }
            }
        }
        catch (Exception e)
        {
            string msg = "ReadCallBack exception!";
            msg += string.Format("Message:{0}", e.Message);
            StopDownload(result);
            errorMsg = msg;
            downState = DownloadState.FAILED;
        }
    }
    public void InstallApk()
    {
#if UNITY_ANDROID && !UNITY_EDITOR
        Debug.Log("begin install");
        using (AndroidJavaObject jo = new AndroidJavaObject("com.kevonyang.androidhelper.AndroidHelper"))
        {
            if (jo == null)
            {
                WMDebug.Debug.LogError("VersionUpdater: Failed to get com.kevonyang.androidhelper.AndroidHelper");
                return;
            }
            using (AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
            {
                if (jc == null)
                {
                    WMDebug.Debug.LogError("VersionUpdater: Failed to get com.unity3d.player.UnityPlayer");
                    return;
                }
                AndroidJavaObject m_jo = jc.GetStatic<AndroidJavaObject>("currentActivity");
                if (m_jo == null)
                {
                    WMDebug.Debug.LogError("VersionUpdater: Failed to get currentActivity");
                    return;
                }
                jo.CallStatic("InstallApk", m_jo, installPath, apkName);
            }
        }
#endif
    }
}

在下载完毕后,需要写一个java类,并在里面调用安装接口。内容很简单,只需要简单的启动一个安装的Intent就可以了,随后就会出现系统提示,是否覆盖安装。至此,游戏内的下载及安装全部完成,等待覆盖安装完毕即可从新的客户端启动。
public static void InstallApk(Context context, String path, String name) {
    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setDataAndType(Uri.fromFile(new File(path, name)), "application/vnd.android.package-archive");
    context.startActivity(intent);
}

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引