更多内容请查看我的个人网站
NoCodeWorld 的小地盘
UE版本:4.27.2
VS版本:2019
现在手游体量越来越大,热更下载时间越来越长,后台下载现在基本是手游必备的,但是UE对于移动端的支持非常可怜,对于后台下载这一块别说官方支持,连网上资料也基本没有,如果你也有UE4移动端后台下载的需求,我们就接着往下看吧
手机上在游戏下载中直接返回桌面,在不主动杀死后台进程的情况下需要继续下载热更内容到完成,这就是后台下载的流程,听起来很简单,实际实现上不懂移动平台开发的情况下还是非常坎坷的.
首先我们要理清楚两个思路,因为手机切换到桌面以后进程直接会被挂起,下面图中演示了安卓进程的生命周期示例,切换后台后我们的进程会被挂起,不会继续执行,所以一般做法就是把整个下载流程加上验证等操作都用Java/ObjectC 单独实现一遍,然后不管是前台后台都跑这些代码,但是因为我们已经用c++实现了整个下载流程,而且可能还会有Windows下载的需求,所以我的思路是把下载流程都还统一放到c++,安卓或者IOS端只是起一个服务,然后在服务里面执行我c++的代码,这是我的做法,大家也可以选择其他方式,我这样做是为了更加统一,不用去管每个平台的实现方式,并且可以很独立的封装成一个插件。
涉及的主要模块有 UE UPL
本文不会演示热更新相关的具体逻辑,只说明平台端后台保活的实现,后面可能会写一下热更新自动化打包和热更新下载的文章
ios相对于安卓反而非常简单,这里所使用的是简单又暴力的后台播放无声音乐保活进程的方法
首先先看一下插件目录结构
if (Target.Platform == UnrealTargetPlatform.Android)
{
AdditionalPropertiesForReceipt.Add(new ReceiptProperty("AndroidPlugin", ModuleDirectory + "/AndroidDownload_UPL.XML"));
}
package com.你起的包名;
import android.content.Intent;
import android.app.Service;
import android.os.Binder;
import android.os.IBinder;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Build;
import android.graphics.Color;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import static androidx.core.app.NotificationCompat.PRIORITY_MIN;
public class DownloadService extends Service {
public native void nativeDownloadQuit();
//用来执行c++下载逻辑的线程对象,类在DownloadThread.java声明
private DownloadThread HotUpdate_DownloadThread = null;
//在c++端控制创建
public void StartDownloadThread()
{
if (HotUpdate_DownloadThread != null) return;
HotUpdate_DownloadThread = new DownloadThread();
HotUpdate_DownloadThread.Init();
}
//在c++端控制销毁
public void StopDownloadThread()
{
if (HotUpdate_DownloadThread == null) return;
nativeDownloadQuit();
HotUpdate_DownloadThread.Shutdown();
HotUpdate_DownloadThread = null;
}
public void Shutdown()
{
//关闭前台服务
stopForeground(true);
stopSelf();
}
@Override
public IBinder onBind(Intent intent) { return new DownloadBinder(this); }
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//开始创建通知
createNotification();
return START_STICKY;
}
//创建通知
private void createNotification() {
String channelId = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
channelId = createNotificationChannel("kim.hsl", "ForegroundService");
} else {
channelId = "";
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channelId);
//这里可自己定制通知的内容,图标等,具体方法网上查
Notification notification = builder.setOngoing(true)
.setPriority(PRIORITY_MIN)
.setCategory(Notification.CATEGORY_SERVICE)
.build();
//开启前台通知
startForeground(1, notification);
}
@RequiresApi(Build.VERSION_CODES.O)
private String createNotificationChannel(String channelId, String channelName){
NotificationChannel chan = new NotificationChannel(channelId,
channelName, NotificationManager.IMPORTANCE_NONE);
chan.setLightColor(Color.BLUE);
chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
NotificationManager service = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
service.createNotificationChannel(chan);
return channelId;
}
//通过DownloadBinder在外部获取service对象
public class DownloadBinder extends Binder
{
private DownloadService HotUpdate_DownloadService = null;
public DownloadBinder(DownloadService InDownloadService)
{
HotUpdate_DownloadService = InDownloadService;
}
public DownloadService GetDownloadService()
{
return HotUpdate_DownloadService;
}
}
}
package com.你起的包名;
class DownloadThread extends Thread
{
public boolean bRunning = false;
public void Init()
{
bRunning = true;
this.start();
}
public void Shutdown()
{
bRunning = false;
}
@Override
public void run()
{
while(bRunning)
{
try
{
Thread.sleep(20);
//这里通过JNI调用自己c++的下载方法
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<root xmlns:android="http://schemas.android.com/apk/res/android">
<!-- optional files or directories to copy to Intermediate/Android/APK -->
<resourceCopies>
<copyDir src="$S(PluginDir)/Java" dst="$S(BuildDir)" />
</resourceCopies>
<!-- 这些节点我就不一一介绍了,可到官方上看下每个的用处 -->
<gameActivityImportAdditions>
<insert>
import com.你起的包名.DownloadService;
import android.content.ServiceConnection;
import android.content.ComponentName;
import android.os.IBinder;
</insert>
</gameActivityImportAdditions>
<gameActivityClassAdditions>
<insert>
<!-- 这些保存Service对象方便后续使用 -->
private DownloadService HotUpdate_DownloadService = null;
private ServiceConnection HotUpdate_Connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
HotUpdate_DownloadService = ((DownloadService.DownloadBinder)binder).GetDownloadService();
}
@Override
public void onServiceDisconnected(ComponentName name) {}
};
<!-- 这里是总得启动Service入口,可在c++中开始下载任务时调用 -->
public void AndroidThunkJava_HotUpdate_StartService()
{
Intent Hotupdate_intent = new Intent(getBaseContext(), DownloadService.class);
<!-- 这里很重要,Android版本大于多少时启动前台服务必须调用 startForegroundService -->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(Hotupdate_intent); }
else { startService(Hotupdate_intent); }
bindService(Hotupdate_intent, HotUpdate_Connection, Context.BIND_AUTO_CREATE);
}
public void AndroidThunkJava_HotUpdate_StopService()
{
if(HotUpdate_DownloadService == null) return;
unbindService(HotUpdate_Connection);
HotUpdate_DownloadService.Shutdown();
HotUpdate_DownloadService = null;
}
<!-- 这里是具体下载线程的执行入口,可在c++实际开始下载时调用 -->
public void AndroidThunkJava_HotUpdate_StartDownloadThread() { if(HotUpdate_DownloadService != null) HotUpdate_DownloadService.StartDownloadThread(); }
public void AndroidThunkJava_HotUpdate_StopDownloadThread() { if(HotUpdate_DownloadService != null) HotUpdate_DownloadService.StopDownloadThread(); }
</insert>
</gameActivityClassAdditions>
<!-- optional additions to GameActivity onCreate in GameActivity.java -->
<gameActivityOnCreateAdditions>
<insert>
</insert>
</gameActivityOnCreateAdditions>
<!-- optional additions to GameActivity onDestroy in GameActivity.java-->
<gameActivityOnDestroyAdditions>
<insert>
AndroidThunkJava_HotUpdate_StopDownloadThread();
AndroidThunkJava_HotUpdate_StopService();
</insert>
</gameActivityOnDestroyAdditions>
<!-- 重点在这里,往安卓配置里添加一个服务和前台服务权限 -->
<androidManifestUpdates>
<addElements tag="application">
<service
android:name="com.你起的包名.DownloadService"
android:exported="false" >
</service>
</addElements>
<addPermission android:name="android.permission.FOREGROUND_SERVICE"/>
</androidManifestUpdates>
</root>
📌上面演示了所有java端代码,并做了每个步骤的简单说明,核心思路很简单,就是开启一个前台服务,并且用通知保活服务,一定注意androidManifest 别忘了添加,我在手上的测试机上均已测试,后台,熄屏等情况下依然会下载至结束,包括华为等
if (Target.Platform == UnrealTargetPlatform.IOS)
{
AdditionalPropertiesForReceipt.Add(new ReceiptProperty("IOSPlugin", ModuleDirectory + "/IOSDownload_UPL.XML"));
}
//.h
#if PLATFORM_IOS
#import <AVFoundation/AVFoundation.h>
#endif
class XXX {
#if PLATFORM_IOS
AVAudioPlayer* AudioPlayer;
// IOS Handles
FDelegateHandle EnterBackgroundHandle;
FDelegateHandle EnterForegroundHandle;
#endif
}
//.cpp
void IOSStartKeepAlive()
{
#if PLATFORM_IOS
if (!EnterBackgroundHandle.IsValid()) FCoreDelegates::ApplicationWillEnterBackgroundDelegate.AddUObject(this, &HandleEnterBackground);
if (!EnterForegroundHandle.IsValid()) FCoreDelegates::ApplicationHasEnteredForegroundDelegate.AddUObject(this, &HandleEnterForeground);
#endif
}
void IOSStopKeepAlive()
{
#if PLATFORM_IOS
StopPlayAudio();
if (EnterBackgroundHandle.IsValid())
{
FCoreDelegates::ApplicationWillEnterBackgroundDelegate.Remove(EnterBackgroundHandle);
EnterBackgroundHandle.Reset();
}
if (EnterForegroundHandle.IsValid())
{
FCoreDelegates::ApplicationWillEnterBackgroundDelegate.Remove(EnterForegroundHandle);
EnterForegroundHandle.Reset();
}
#endif
}
void HandleEnterBackground()
{
#if PLATFORM_IOS
StartPlayAudio();
#endif
}
void HandleEnterForeground()
{
#if PLATFORM_IOS
StopPlayAudio();
#endif
}
void StartPlayAudio()
{
#if PLATFORM_IOS
// 1. Get the audio file
FString NativePath = FString([[NSBundle mainBundle]bundlePath] ) + TEXT("/Silence.wav");
NSURL* nsURL = nil;
nsURL = [NSURL fileURLWithPath : NativePath.GetNSString()];
if (nsURL == nil)
{
return;
}
// 2. Setting the audio session
AVAudioSession* session = [AVAudioSession sharedInstance];
NSError* error = nil;
if (@available(iOS 11.0, *))
{
[session setCategory:AVAudioSessionCategoryPlayback mode : AVAudioSessionModeDefault routeSharingPolicy : AVAudioSessionRouteSharingPolicyDefault options : AVAudioSessionCategoryOptionMixWithOthers error : &error];
}
else
{
[session setCategory:AVAudioSessionCategoryPlayback mode : AVAudioSessionModeDefault options : AVAudioSessionCategoryOptionMixWithOthers error : &error];
}
if (error)
{
return;
}
// 3. Start the audio
AudioPlayer = nil;
AudioPlayer = [[AVAudioPlayer alloc]initWithContentsOfURL:nsURL error : nil];
if (AudioPlayer == nil)
{
return;
}
else
{
AudioPlayer.numberOfLoops = -1;
AudioPlayer.volume = 0.0;
[AudioPlayer prepareToPlay];
[AudioPlayer play];
}
#endif
}
void StopPlayAudio()
{
#if PLATFORM_IOS
UE_LOG(HotUpdateLog, Display, TEXT("Audio Stop"));
if (AudioPlayer != nil)
{
[AudioPlayer stop];
AudioPlayer = nil;
}
#endif
}
<?xml version="1.0" encoding="utf-8"?>
<root>
<iosPListUpdates>
<copyFile src="$S(PluginDir)/IOS/Silence.wav" dst="$S(BuildDir)/Silence.wav" force="true"/>
<addElements tag="dict" once="true">
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
</addElements>
</iosPListUpdates>
</root>
📌注意:声音文件一定要放到Source下面的文件夹中,如果你要是放到了Content下面Mac机远程打包时默认不会上传,而且必须要通过上面copy到ipa包根目录下,不然在OC代码中获取不到虚幻里面的资源
UE4移动端后台下载的适用场景
本教程只适用于UE4.27.2的移动端后台下载,其他版本我没有测试过,这个教程在安卓和IOS端均可适用
技术难点和注意事项
安卓
这里可以看到用的是前台任务并且开启通知保活的方案;
缺点:通知栏会有一个通知常驻,有些机型不会有,通知栏网上有教程说通过卡bug的方式关 闭通知并且保活,我没成功,好像不行了,有兴趣的可以试试
优点:非常坚挺,基本什么除了进程被杀掉之外的所有情况均可高速下载,大家也可以多测试 测 试
注意:因为我不是专业安卓平台开发,我知道的是肯定是有更加完美的方案,可以做到在后台静悄 悄的下载,看不见任何东西开启,包括后台下载等方式我都试过,不过不是过段时间被杀掉就是会 有其他问题,也欢迎有知道其他方案的小伙伴在下面留言讨论
苹果
用的是后台无声音乐播放保活的方式
缺点:可能权限在苹果包提交审核时会有失败的情况,但是应该是能过审核的,因为有发现其他知 名游戏使用此方案
优点:就是简单粗暴,而且非常稳定,我也测试过很多种情况,不会打断后台下载
大家有问题的可以在下面留言,时间比较少,想写的文章比较多,可能会出现纰漏或错误,有其他方案和想法的也可以留言,谢谢
📌附件下载:无声音乐下载
Android如何降低service被杀死概率 - 简书 (jianshu.com)