Libgdx实现跨平台热更新

游戏开发中实现热更新可以实现无须重新打包,无须发布市场,无须等待审核,只需要将更新包放到服务器上,客户端就可以直接下载更新包来实现游戏的更新,在游戏后期的维护过程中,能为开发者提供十分的便利,正所谓工欲善其事,必先利其器。这篇文章就来说说如何在Libgdx中实现游戏的热更新。

原理

要实现游戏的热更新,首先必须对编译原理有一定的了解,不用掌握技术细节,但是基本流程是必须知道的。我们知道Libgdx的开发语言是Java,Java是一种静态语言,必须先编译成字节码才能在虚拟机中执行。我们正常开发的Java程序都会被编译成class文件,这个class文件就是字节码,程序执行的时候会由操作系统启动一个Java虚拟机,虚拟机再加载字节码,然后再去执行。所以要实现热更新,首先要实现的就是字节码的动态加载,好在Java为我们提供了ClassLoader类,这个类就是专门加载字节码的,虚拟机启动后会首先创建一个ClassLoader,加载程序中已经打包好的class文件,如果我们要加载其他的class,只需创建一个新的ClassLoader即可。当然其过程中需要注意的细节很多,待会儿再来细说。需要注意上面说的是在JAVA桌面程序中,如果是在Android,字节码是以dex为后缀名的文件,它是供Android中的虚拟机(Dalvik或Art)来加载执行的。那么在IOS上呢,IOS中并没有Java虚拟机,但是libgdx的跨平台解决方案使用了Multi-OS Engine( MOE ),来在IOS中运行Java,关于MOE的细节请自行查阅资料,这里只需知道Moe为我们在IOS中提供了一个Art虚拟机,它也是执行的dex文件。这样一来,同一份dex文件可以在android和ios上执行,简直不能再完美了。
理论说太多也没用,下面跟着一起做一个Libgdx的热更新Demo吧。

框架搭建

首先我们来搭建一个基于热更新的简单框架。

1.创建工程

使用Libgdx 1.9.5创建一个基本的libgdx工程,然后在Android Stuio中打开。这个工程包含了Desktop、Android、Ios-moe三个模块,当然还有必须的Core模块。

2.添加Game模块

用AndroidStudio为我们工程增加一个Java Library模块,命名为games,我们的游戏代码将放在这个模块里,以便将需要热更新的内容和主程序分开。Game模块创建好了之后,在它下面会自动生成一个build.gradle文件,我们需要在这里添加core模块的依赖:

dependencies {
    //添加core模块的依赖
    compile project(":core")
    compile fileTree(dir: 'libs', include: ['*.jar'])
}

接下来还要在Game模块中新建一个assets文件夹,用来存放游戏的资源。注意android模块下也有一个assets文件夹,是用来存放主程序资源的。
因为只是demo,这里就把Game模块的代码一起写了吧。我新建了一个GameStage类,继承Stage,并添加了一张我网站的logo展示。注意这里的资源已经不是程序包里面的,而应该是外部资源了,所以需要从外部传入一个资源路径。

public class GameStage extends Stage {
    //需要主程序传入资源目录,传入MainGame以便能返回
    public GameStage(final MainGame mainGame, String assetsDir) {
        //添加一个logo图片,注意这里使用的是绝对路径
        Image logo = new Image(new Texture(Gdx.files.absolute(assetsDir + "logo.png")));
        logo.setPosition(getWidth() * 0.5f, getHeight() * 0.5f, Align.center);
        addActor(logo);
        //给logo添加事件
        logo.addListener(new ClickListener() {
            @Override
            public void clicked(InputEvent event, float x, float y) {
                //点击将返回到mainStage
                mainGame.changeStage(mainGame.mainStage);
            }
        });
    }
}

这个时候,Game模块应该是这样的:

这里的libs文件夹,也可以放一些Game模块需要的jar包,但是打包dex的时候,需要将jar一起打入,比较麻烦,还是建议jar包统一放到主程序中。

3.添加跨平台接口

因为android和ios实现热更新有些不同,而且core里面是无法使用dex的Loader的,所以需要定义一个跨平台的接口,然后在各个平台实现。在Core模块定义接口如下:

public interface HotUpdate {

    //获取游戏资源文件路径
    String getAssetsDir();

    //加载dex文件,并返回一个Class对象,若发成错误抛出异常
    Class loadDex(String dexPath, String className) throws Exception;
}

分别在Android和IOS模块下实现这个接口:

public class HotUpdateAndroid implements HotUpdate {
    private String assetsDir;//游戏资源文件目录
    private Context ctx;//AndroidContext

    public HotUpdateAndroid(Context ctx) {
        this.ctx = ctx;
        //Android的data目录,可以随apk卸载一起删除,并且资源文件的图片不会出现在相册中
        assetsDir = ctx.getExternalFilesDir("").getAbsolutePath() + "/";
    }

    @Override
    public String getAssetsDir() {
        return assetsDir;
    }

    @Override
    public Class loadDex(String dexPath, String className) throws Exception {
        DexClassLoader loader = new DexClassLoader(dexPath, ctx.getCacheDir().getAbsolutePath(), null, ctx.getClassLoader());
        Class claz = loader.loadClass(className);
        return claz;
    }
}
public class HotUpdateIos implements HotUpdate {

    @Override
    public String getAssetsDir() {
        //ios下资源存储目录,等效于 Gdx.files.getExternalStoragePath()
        return System.getenv("HOME") + "/Documents/";
    }

    @Override
    public Class loadDex(String dexPath, String className) throws Exception {
        //初始化一个PathClassLoader,加载dex文件
        PathClassLoader loader = new PathClassLoader(dexPath, getClass().getClassLoader());
        Class claz = loader.loadClass(className);
        return claz;
    }
}

稍微讲解一下PathClassLoader,它是android dalvik下的类,继承自Java的ClassLoader,可以用它来直接加载dex文件。和它一起的还有一个DexClassLoader,它也可以加载dex文件,同时它还能加载apk和jar中的dex。这里Android用的是DexClassLoader而IOS用的是PathClassLoader,其实都是可以的。另外不管是哪一个ClassLoader,其构造方法中有个参数是必不可少的,必须传入一个Parent ClassLoader,它的作用就是使新创建的Loader能够直接访问Parent Loader的类。
好了,接口定义完成,以后我们就只需要在core里面调用HotUpdate接口就能实现热更新的功能了。

MainGame类实现

在Core模块下创建一个MainGame类,并继承ApplicationAdapter。我在MainGame里添加了一个舞台mainStage,并添加了一张图片,点击图片就会调用HotUpdate的方法跳转到游戏界面了。因为HotUpdate只是一个接口,我们需要从各个平台传入它的实例,所以在MainGame的构造方法中传入一个参数。另外我还定义了一个切换舞台的方法,以便在游戏界面中也能返回到主界面。MainGame类代码如下:

public class MainGame extends ApplicationAdapter {
    public Stage currentStage, mainStage;
    HotUpdate hotUpdate;

    public MainGame(HotUpdate hotUpdate) {
        this.hotUpdate = hotUpdate;
    }

    @Override
    public void create() {
        //添加舞台,并添加图片
        mainStage = new Stage();
        Image img = new Image(new Texture("badlogic.jpg"));
        mainStage.addActor(img);
        changeStage(mainStage);
        //给图片添加监听
        img.addListener(new ClickListener() {
            @Override
            public void clicked(InputEvent event, float x, float y) {
                try {
                    //加载dex文件,并返回GameStage的Class对象
                    String className = "com.ayocrazy.tutorial.games.GameStage";//完整类名
                    Class claz = hotUpdate.loadDex(hotUpdate.getAssetsDir() + "game.dex", className);
                    //用Class对象初始化一个Stage
                    Stage gameStage = (Stage) claz.getConstructor(MainGame.class, String.class).newInstance(MainGame.this, hotUpdate.getAssetsDir());
                    //切换舞台,进入到gameStage
                    changeStage(gameStage);
                } catch (Exception e) {
                    //捕获异常
                    Gdx.app.log("loadDex error", e.toString());
                    e.printStackTrace();
                }
            }
        });
    }

    //切换舞台
    public void changeStage(Stage stage) {
        Gdx.input.setInputProcessor(stage);
        currentStage = stage;
    }

    @Override
    public void render() {
        Gdx.gl.glClearColor(0.9f, 0.9f, 0.9f, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        if (currentStage != null) {
            currentStage.act();
            currentStage.draw();
        }
    }
}

最后,分别在AndroidLauncher和IOSLauncher两个类中,将HotUpdateAndroid和HotUpdateIOS的实例传入到MainGame。

public class AndroidLauncher extends AndroidApplication {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        AndroidApplicationConfiguration config = new AndroidApplicationConfiguration();
        //传入HotUpdateAndroid的实例
        initialize(new MainGame(new HotUpdateAndroid(this)), config);
    }
}
public class IOSMoeLauncher extends IOSApplication.Delegate {

    protected IOSMoeLauncher(Pointer peer) {
        super(peer);
    }

    @Override
    protected IOSApplication createApplication() {
        IOSApplicationConfiguration config = new IOSApplicationConfiguration();
        config.useAccelerometer = false;
        //传入HotUpdateIOS的实例
        return new IOSApplication(new MainGame(new HotUpdateIos()), config);
    }

    public static void main(String[] argv) {
        UIKit.UIApplicationMain(0, null, null, IOSMoeLauncher.class.getName());
    }
}

至此,框架搭建已经完成,我们可以尝试运行一下看看。
我在ios上进入主程序正常,但是点击图片进入游戏界面无效果,打印了两句log:

System 5 ClassLoader referenced unknown path: /Users/ayo/Library/Developer/CoreSimulator/Devices/2DB6D358-14A6-4D64-A3DE-93E8DCFF7322/data/Containers/Data/Application/9FF34C72-7937-4604-97C4-E138783441AB/Documents/game.dex
loadDex error 4 java.lang.ClassNotFoundException: Didn't find class "GameStage" on path: DexPathList[[],nativeLibraryDirectories=[]]

第一句是ClassLoader报错,提示dex的路径不对,第二句是我捕获的异常,提示没有找到GameStage类。继续往下走。

生成dex

现在我们需要做的就是把游戏代码(这里只有GameStage类)生成dex文件,然后放到服务器上供主程序下载,这里作为演示,直接把生成的dex文件放到目标文件夹中。那么现在问题来了,如何生成dex文件了?原来Android SDK已经为我们提供了便利的工具,在Android SDK的build-tools目录下,有一个dx工具,它的作用是将jar或者class文件转换成dex,所以我们还需要将游戏代码先编译成class文件或者打包成jar,这就要用到jdk为我们提供的javac命令了。是不是好麻烦?限于篇幅,这里不去讨论这些工具的使用。但是我提供一个gradle脚本,可以实现直接将java文件打包成dex,其实原理上还是使用的javac和dx,只不过使用脚本要方便许多,写好一次,以后直接运行就能生成dex文件了。
继续吧!在Game模块下的build.gradle文件里空白处添加一个task,代码如下:

//添加类型为Exec的任务,并依赖java插件提供的classes任务
task packDex(dependsOn: classes, type: Exec) {
    //获取sdk的目录
    def sdkDir
    def btVersion = "25.0.0"//build-tools版本号,需要换成你自己的
    def localFile = file("../local.properties")
    if (localFile.exists()) {
        Properties localProp = new Properties()
        localFile.withInputStream { instr ->
            localProp.load(instr)
        }
        sdkDir = localProp.getProperty('sdk.dir')
        if (!sdkDir) {
            sdkDir = "$System.env.ANDROID_HOME"
        }
    }
    //dx工具路径,win下需改为dx.bat
    def dx = sdkDir + "/build-tools/$btVersion/dx"
    //要打包的class文件目录,classes任务默认会将java文件编译到这个目录
    def input = file("build/classes/main")
    //输出的dex文件目录
    def output = file("build/dex");
    //用于检测文件是否变动
    inputs.files input
    outputs.dir output
    //创建目录
    file(output).mkdirs();
    //执行命令行
    commandLine "$dx", '--dex', "--output=$output/game.dex", input
}

稍微阐述一下,classes是Java Gradle插件的内置任务,会将Java代码打包成class文件,并放到build/classes/main路径下,然后我们写的这个packDex任务将该路径下的class文件打包成dex,放到build/dex路径下。使用方法是先刷新Gradle,然后在Android Studio的右侧找到Gradle的任务列表,在games/other里能找到我们定义的packDex任务,双击执行就可以了。如果你有配置Gradle的环境变量,也可以直接在工程目录下输入gradle packDex命令来打包,更加快捷。打包成功后,就能在Game模块下的build/dex目录看到dex文件了。此时的Game模块如图:

打包上传

dex生成好了,还差资源文件了。如果是在正式环境,可以将dex和Game模块下的assets文件夹一起打包成zip包,上传到服务器,然后由主程序下载并解压。并且,我们同样可以使用Gradle脚本任务来进行打包操作,十分方便。
这里作为Demo,我直接将dex和资源文件复制到了ios模拟器里。

加载运行

在运行之前还有一个至关重要的事情要做,因为我们的GameStage类引用了MainGame类,然而moe在打包的时候默认是开启混淆的,所以MainGame类在主程序中被混淆了而GameStage类里的MainGame类是没被混淆的,实际运行会报错。所以我们还需要关闭MainGame类的混淆,这里我直接关闭了com.ayocrazy.toturial包的混淆,在ios-moe模块下的proguard.append.cfg文件中加入:

-keep class com.badlogic.** { *; }
-keep enum com.badlogic.** { *; }
-keep class com.ayocrazy.tutorial.** { *; }

好了,激动人心的时刻到了,赶快运行起来看看效果吧:

在上图中,第一次点击后我将GameStage的代码改了,然后重新打包成dex放入到模拟器里覆盖之前的文件,再次进入的时候已经是新的代码了,可以看到我给logo添加了一个动画,完美实现热更新。
Android下效果是一样的,已经验证,这里不截图了,可以自己运行查看。

效率

关于libgdx的热更新到这里其实已经算是讲完了,但是用这种方式投入生产开发的话效率很低,所以下面讨论下如何提升开发效率:

  1. 调试。首先dex的内容是没法实现断点调试的,ide不支持,如果你足够强大可以自己写debug工具。但是我们也可以用桌面项目来进行调试,让桌面项目运行的时候将Game模块的内容包含进去,运行desktop模块的时候就不用dex了,也就不存在热更新,况且本来desktop也是没法加载dex的。
  2. 资源。我们使用AssetManager管理资源的时候需要特别注意,在主程序中使用的是internal路径,而在游戏中是absolute路径,所以无法使用同一个AssetManager来管理,只能分别管理。这个时候就要特别注意主程序和游戏的生命周期,小心处理内存问题。
  3. 工具。这里主要指Gradle,擅用Gradle可以让你的开发效率大大提升。除了前面说的用gradle生成dex,你还可以用它来打zip包,上传服务器,复制文件到Android手机、IOS模拟器都是可以的,包括第1点里提到的让desktop运行的时候包含Game模块的内容也是可以使用Gradle脚本实现的。当然这里只提供思路,细节还需自行研究,可以告诉大家的是这些功能在我的项目中都已经实现。如果有疑问,可以留言一起探讨。
  4. 架构。良好的架构可以让程序的稳定性、可维护性大大增强。给游戏添加热更新会提高开发的复杂度,如果没有一个良好的架构支持,必然导致开发过程中痛苦万分,那样就本末倒置了。

最后,作个总结吧。Libgdx实现跨平台的热更新可以归纳为四个步骤:模块分离->代码编写->dex生成->热更加载。 技术上实现并不难,难的它打破了原有的开发流程,在给我们带来强大功能的同时,也失去了一些便利性,所以也不要盲目追求热更新,还是面向需求编程吧。

时间: 2024-08-19 20:59:52

Libgdx实现跨平台热更新的相关文章

是什么逼得苹果对开发者们下"热更新"的最后通牒

苹果用一封邮件对"热更新"下达了最后的通牒,也让iOS开发者们度过了坐立不安的一天. "热更新"也就是动态下发代码,它可以使开发者在不发布版本的情况下,修复BUG和发布功能.这让开发者绕开了苹果的审核机制,避免长时间的审核等待以及多次被拒造成的成本开销. 但现在,苹果正在对"热更新"实行更严厉的审查.昨天,不少开发者收到了来自苹果的邮件.苹果在邮件中表示,将不再允许使用动态下发代码的机制.苹果要求被警告的开发者在下个版本中去除能动态改变应用行为和

iOS热更新解读(三)—— JSPatch 之于 Swift

继承自 NSObject 的 Swift 类 修改属性 新建 Swift 工程 SwiftJSPatch.AppDelegate.swift: // in AppDelegate.swift ---------------- func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { let path = NS

iOS 热更新解读(二)—— JSPatch 源码解析

关于 JSPatch 的实现原理,JSPatch 作者本人 bang 已经有一系列文章阐述: JSPatch 实现原理详解 <一> 核心 JSPatch 实现原理详解 <二> 细节 JSPatch 实现原理详解 <三> 扩展 JSPatch 实现原理详解 <四> 新特性 JSPatch 实现原理详解 <五> 优化 这些文章是对 JSPatch 内部实现原理和细节诸如"require实现"."property实现&qu

React Native热更新方案

一.目标 React Native热更新就是下载新RN包替换老RN包,那么我们需要考虑的点就是增量更新,要达到增量更新的目的,就需要把老RN包与新RN包的差异找出来,并且可以将这些差异与老RN包还原出新RN包.bsdiff和bspatch相关技术能实现我们的目标. 二.基本流程 1.服务器 使用bsdiff算法将老RN包和新RN包生成一个补丁patch文件,供客户端下载. 2.客户端 下载patch文件,使用bspatch算法将补丁patch文件和老RN包生成一个新RN包. 3.目前情况 暂时由

iOS 热更新解读(一)APatch &amp; JavaScriptCore

iOS 动态更新的几种方案 WebView 加载 HTML5 动态更新. React Native/weex js 动态更新. lua 脚本文件控制动态更新(代表框架 WaxPatch ). js 脚本文件控制动态更新(代表框架 JSPatch). framework 实现功能模块动态更新. 其中 WaxPatch 和 JSPatch 是使用较广泛的两种热修复方案.而苹果 review guideline 提到只允通过JavaScriptCore.framework或WebKit执行脚本,因此

React Native热更新及混合开发

随着 React Native 的不断发展完善,越来越多的公司选择使用 React Native 替代 iOS/Android 进行部分业务线的开发,也有不少使用 Hybrid 技术的公司转向了 React Native .虽然React Native在目前来说仍有不少的坑,不过对于以应用开发为主的App来说完全可以胜任. 概述 在iOS应用开发中,由于Apple严格的审核标准和低效率,iOS应用的发版速度极慢,这对于大多数团队来说是不能接受的,所以热更新对于iOS应用来说就显得尤其重要.而就在

框架-iOS 动态库 热更新 审核

问题描述 iOS 动态库 热更新 审核 目前我用到了热更新这一个说法,因为业务的需求,公司需要我将iOS不用提交appStore就能更新 就能更新,我排除了其他js的更新方式,用了动态库(frameWork)经过一段时间的研究,我柑橘动态库研究的差不多了!但是问题也是变多了!动态库里面我封装了几个控制器在里面,而且控制器里面是需要去网络请求数据的,我主工程里面用的AFN,我动态库里面无法去公用AFN并且,我从很多帖子上看到说苹果不允许动态库加载更新,但是2104年iOS8出来的时候,苹果不是开放

iOS第三方类库JSPatch(热更新)

---------------------------------------------------------------------------------------------------------------------------- 更新记录 2016年3月4日 JSPatch官方网址:http://jspatch.com/ OC转JS代码工具:http://bang590.github.io/JSPatchConvertor/ -------------------------

游戏-java项目热更新如何实现热更新

问题描述 java项目热更新如何实现热更新 大虾们的项目都是如何实现热更新的呢,比如游戏服务端,修改后如何实现不停服更新呢 解决方案 http://www.oschina.net/code/snippet_993989_33592 解决方案二: 有些服务器都支持热部署的啊,例如Tomcat就支持热部署的. 解决方案三: 如果是开发环境,除了设置Tomcat热部署为可用,还需设置Eclipse, Eclipse --> Project --> 勾选 Build Automatically