Unity游戏内版本更新
最近研究了一下游戏内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中不断查询当前下载状态,是否有异常,以及是否已经下载完毕。如果下载完毕,则校验文件,并开始安装。
1 using UnityEngine; 2 using System; 3 using System.Collections; 4 using System.Threading; 5 using System.IO; 6 using System.Net; 7 using System.Security.Cryptography; 8 using System.Text; 9 using System; 10 11 public class VersionUpdater : MonoBehaviour 12 { 13 public class RequestState 14 { 15 public const int BUFFER_SIZE = 1024; 16 public byte[] BufferRead; 17 public HttpWebRequest request; 18 public HttpWebResponse response; 19 public Stream responseStream; 20 } 21 22 public enum DownloadState 23 { 24 DOWNLOADING, 25 FINISHED, 26 FAILED 27 } 28 29 public delegate void ProgressCallback(long curr, long length, float rate, DownloadState state); 30 public ProgressCallback progressCallback; 31 32 string url = ""; 33 string installPath = ""; 34 string apkName = ""; 35 string errorMsg = ""; 36 37 private FileStream fileStream = null; 38 private long length = 1; 39 private long curr = 0; 40 private long last = 0; 41 private const float UpdateTime = 0.5f; 42 private float rate = 0; 43 private DownloadState downState = DownloadState.DOWNLOADING; 44 45 46 public void DownloadApkAsync(string url, string md5, string path, string name) 47 { 48 this.url = url; 49 this.installPath = path; 50 this.apkName = name; 51 this.errorMsg = ""; 52 downState = DownloadState.DOWNLOADING; 53 54 DownloadApkAsync(); 55 } 56 57 private void DownloadApkAsync() 58 { 59 if (string.IsNullOrEmpty(url)) return; 60 if (string.IsNullOrEmpty(installPath)) return; 61 if (string.IsNullOrEmpty(apkName)) return; 62 63 string fullpath = installPath + "/" + apkName; 64 65 IAsyncResult result = null; 66 try 67 { 68 fileStream = new FileStream(fullpath, FileMode.Create, FileAccess.Write); 69 70 Uri uri = new Uri(url); 71 HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri); 72 73 request.Method = "GET"; 74 75 RequestState requestState = new RequestState(); 76 requestState.BufferRead = new byte[RequestState.BUFFER_SIZE]; 77 requestState.request = request; 78 79 curr = 0; 80 length = 1; 81 rate = 0.0f; 82 downState = DownloadState.DOWNLOADING; 83 result = (IAsyncResult)request.BeginGetResponse(new AsyncCallback(ResponeCallback), requestState); 84 } 85 catch (Exception e) 86 { 87 errorMsg = "Begin Create Exception!"; 88 errorMsg += string.Format("Message:{0}", e.Message); 89 StopDownload(result); 90 downState = DownloadState.FAILED; 91 } 92 93 StartCoroutine(updateProgress()); 94 } 95 96 IEnumerator updateProgress() 97 { 98 while (curr <= length) 99 { 100 yield return new WaitForSeconds(UpdateTime); 101 102 rate = (curr - last) / UpdateTime; 103 last = curr; 104 105 if (downState == DownloadState.FAILED) 106 { 107 Debug.LogError(errorMsg); 108 if (fileStream != null) 109 fileStream.Close(); 110 if (progressCallback != null) 111 progressCallback( curr, length, rate, DownloadState.FAILED); 112 break; 113 } 114 115 if (progressCallback != null) 116 progressCallback( curr, length, rate, DownloadState.DOWNLOADING); 117 118 if (downState == DownloadState.FINISHED) 119 { 120 if (progressCallback != null) 121 progressCallback( curr, length, rate, DownloadState.FINISHED); 122 break; 123 } 124 } 125 } 126 127 void StopDownload(IAsyncResult result) 128 { 129 if (result == null) return; 130 RequestState requestState = (RequestState)result.AsyncState; 131 requestState.request.Abort(); 132 } 133 134 void ResponeCallback(IAsyncResult result) 135 { 136 try 137 { 138 if (downState != DownloadState.FAILED) 139 { 140 RequestState requestState = (RequestState)result.AsyncState; 141 HttpWebRequest request = requestState.request; 142 requestState.response = (HttpWebResponse)request.EndGetResponse(result); 143 144 Stream responseStream = requestState.response.GetResponseStream(); 145 requestState.responseStream = responseStream; 146 147 length = requestState.response.ContentLength; 148 149 IAsyncResult readResult = responseStream.BeginRead(requestState.BufferRead, 0, RequestState.BUFFER_SIZE, new AsyncCallback(ReadCallback), requestState); 150 return; 151 } 152 } 153 catch (Exception e) 154 { 155 string msg = "ResponseCallback exception!\n"; 156 msg += string.Format("Message:{0}", e.Message); 157 StopDownload(result); 158 errorMsg = msg; 159 downState = DownloadState.FAILED; 160 } 161 } 162 163 void ReadCallback(IAsyncResult result) 164 { 165 try 166 { 167 if (downState != DownloadState.FAILED) 168 { 169 RequestState requestState = (RequestState)result.AsyncState; 170 Stream responseStream = requestState.responseStream; 171 int read = responseStream.EndRead(result); 172 if (read > 0) 173 { 174 fileStream.Write(requestState.BufferRead, 0, read); 175 fileStream.Flush(); 176 curr += read; 177 178 IAsyncResult readResult = responseStream.BeginRead(requestState.BufferRead, 0, RequestState.BUFFER_SIZE, new AsyncCallback(ReadCallback), requestState); 179 return; 180 } 181 else 182 { 183 Debug.Log("download end"); 184 responseStream.Close(); 185 fileStream.Close(); 186 187 downState = DownloadState.FINISHED; 188 } 189 } 190 } 191 catch (Exception e) 192 { 193 string msg = "ReadCallBack exception!"; 194 msg += string.Format("Message:{0}", e.Message); 195 StopDownload(result); 196 errorMsg = msg; 197 downState = DownloadState.FAILED; 198 } 199 } 200 201 202 public void InstallApk() 203 { 204 #if UNITY_ANDROID && !UNITY_EDITOR 205 Debug.Log("begin install"); 206 using (AndroidJavaObject jo = new AndroidJavaObject("com.kevonyang.androidhelper.AndroidHelper")) 207 { 208 if (jo == null) 209 { 210 WMDebug.Debug.LogError("VersionUpdater: Failed to get com.kevonyang.androidhelper.AndroidHelper"); 211 return; 212 } 213 using (AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) 214 { 215 if (jc == null) 216 { 217 WMDebug.Debug.LogError("VersionUpdater: Failed to get com.unity3d.player.UnityPlayer"); 218 return; 219 } 220 AndroidJavaObject m_jo = jc.GetStatic<AndroidJavaObject>("currentActivity"); 221 if (m_jo == null) 222 { 223 WMDebug.Debug.LogError("VersionUpdater: Failed to get currentActivity"); 224 return; 225 } 226 227 jo.CallStatic("InstallApk", m_jo, installPath, apkName); 228 } 229 } 230 #endif 231 } 232 }
在下载完毕后,需要写一个java类,并在里面调用安装接口。内容很简单,只需要简单的启动一个安装的Intent就可以了,随后就会出现系统提示,是否覆盖安装。至此,游戏内的下载及安装全部完成,等待覆盖安装完毕即可从新的客户端启动。
1 public static void InstallApk(Context context, String path, String name) { 2 Intent intent = new Intent(Intent.ACTION_VIEW); 3 intent.setDataAndType(Uri.fromFile(new File(path, name)), "application/vnd.android.package-archive"); 4 context.startActivity(intent); 5 }