实现Android热更新

2,633 阅读8分钟

什么是热更新,然后怎么实现热更新呢?这里我向大家谈谈我的理解。热更新就是动态下发代码,就是已经上线的作品,在不发布新版本的情况下,只更新作品的一部分,实现修复BUG或者发布新功能。最开始是让开发者绕开苹果的审核机制,避免长时间的审核等待以及多次被拒造成的成本。这里给大家介绍下Egret官方文档的Android热更新方案。首先要说的是热更新的功能是要开发者自己去实现的,例如如果项目是Android项目,那么热更新就要在Android项目里用java实现,因为Android是用java开发的,所以你改Egret引擎里的东西其实是起不了多大作用的,换句话说就是热更新跟白鹭引擎没有什么很大的关系。 首先要做的就是修改config.preloadPath来指定预加载目录,开发者需要自行维护这个目录下的内容。 Android:

//MainActivity.java
nativeAndroid.config.preloadPath=”指定目录“;

可以简单地理解为,将游戏部署到手机上地目录下,然后打开这个目录下的游戏。需要更新某个资源时,只需要更新这个目录上对应的资源即可。

MainActivity.java
package org.egret.testUpdate;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.widget.Toast;

import org.egret.runtime.launcherInterface.INativePlayer;
import org.egret.egretnativeandroid.EgretNativeAndroid;

public class MainActivity extends Activity {
    private final String TAG = "MainActivity";
    private EgretNativeAndroid nativeAndroid;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        nativeAndroid = new EgretNativeAndroid(this);
        if (!nativeAndroid.checkGlEsVersion()) {
            Toast.makeText(this, "This device does not support OpenGL ES 2.0.",
                    Toast.LENGTH_LONG).show();
            return;
        }

        nativeAndroid.config.showFPS = true;
        nativeAndroid.config.fpsLogTime = 30;
        nativeAndroid.config.disableNativeRender = false;
        nativeAndroid.config.clearCache = false;
        nativeAndroid.config.loadingTimeout = 0;

        Intent intent = getIntent();
        nativeAndroid.config.preloadPath = intent.getStringExtra("preloadPath");

        setExternalInterfaces();
        
        if (!nativeAndroid.initialize("http://game.com/game/index.html")) {
            Toast.makeText(this, "Initialize native failed.",
                    Toast.LENGTH_LONG).show();
            return;
        }

        setContentView(nativeAndroid.getRootFrameLayout());
    }

    @Override
    protected void onPause() {
        super.onPause();
        nativeAndroid.pause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        nativeAndroid.resume();
    }

    @Override
    public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            nativeAndroid.exitGame();
        }

        return super.onKeyDown(keyCode, keyEvent);
    }

    private void setExternalInterfaces() {
        nativeAndroid.setExternalInterface("sendToNative", new INativePlayer.INativeInterface() {
            @Override
            public void callback(String message) {
                String str = "Native get message: ";
                str += message;
                Log.d(TAG, str);
                nativeAndroid.callExternalInterface("sendToJS", str);
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
    

热更新的实现代码:

LaunchActivity.java
package org.egret.testUpdate;

import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public class LaunchActivity extends Activity {

    private Button btn_load;
    private Button btn_game;

    private final String gameUrl = "http://game.com/game/index.html";
    private final String zipUrl = "http://tool.egret-labs.org/Weiduan/game/game2.zip";
    private final String preloadPath = "/sdcard/egretGame/";

    private static String[] permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_launch);

        btn_load = (Button)findViewById(R.id.btn_load);
        btn_game = (Button)findViewById(R.id.btn_game);

        btn_load.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                btn_load.setEnabled(false);
                btn_game.setEnabled(false);
                preloadGame();
            }
        });

        btn_game.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(LaunchActivity.this, MainActivity.class);
                intent.putExtra("preloadPath", preloadPath);
                startActivity(intent);
            }
        });

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            int check = checkSelfPermission(permissions[0]);
            if (check != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(permissions, 111);
            }
        }
    }

    private void preloadGame() {
        String dir = preloadPath + getFileDirByUrl(gameUrl);
        File dirFile = new File(dir);
        if (!dirFile.exists()) {
            dirFile.mkdirs();
        }
        downloadGameRes(zipUrl, dir);
    }

    private void downloadGameRes(final String zipUrl, String targetDir) {
        String tempZipFileName = targetDir + "game.zip";
        final File file = new File(tempZipFileName);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                InputStream inputStream = null;
                FileOutputStream outputStream = null;
                HttpURLConnection connection = null;
                boolean finish = false;

                try {
                    URL url = new URL(zipUrl);
                    connection = (HttpURLConnection)url.openConnection();

                    int code = connection.getResponseCode();
                    if (code == 200) {
                        inputStream = connection.getInputStream();
                        outputStream = new FileOutputStream(file, true);

                        byte[] buffer = new byte[4096];
                        int length;

                        while ((length = inputStream.read(buffer)) != -1) {
                            outputStream.write(buffer, 0, length);
                        }
                    }
                    finish = true;
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    try {
                        if (outputStream != null) {
                            outputStream.close();
                        }
                        if (inputStream != null) {
                            inputStream.close();
                        }
                        if (connection != null) {
                            connection.disconnect();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                        return;
                    }
                }

                if (finish) {
                    unzip(file);
                }
            }
        };
        new Thread(runnable).start();
    }

    private void unzip(File file) {
        int BUFFER = 4096;
        String strEntry;
        String targetDir = file.getParent() + "/";
        try {
            BufferedOutputStream dest = null;
            FileInputStream fis = new FileInputStream(file.getAbsolutePath());
            ZipInputStream zis = new ZipInputStream(new BufferedInputStream(fis));
            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                try {
                    int count;
                    byte data[] = new byte[BUFFER];
                    strEntry = entry.getName();
                    File entryFile = new File(targetDir + strEntry);
                    if (strEntry.endsWith("/")) {
                        entryFile.mkdirs();
                        continue;
                    }
                    File entryDir = new File(entryFile.getParent());
                    if (!entryDir.exists()) {
                        entryDir.mkdirs();
                    }
                    FileOutputStream fos = new FileOutputStream(entryFile);
                    dest = new BufferedOutputStream(fos, BUFFER);
                    while ((count = zis.read(data, 0, BUFFER)) != -1) {
                        dest.write(data, 0, count);
                    }
                    dest.flush();
                    dest.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                    return;
                }
            }
            zis.close();
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }

        file.delete();
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                btn_game.setEnabled(true);
            }
        });
    }

    private static String getFileDirByUrl(String urlString) {
        int lastSlash = urlString.lastIndexOf('/');
        String server = urlString.substring(0, lastSlash + 1);
        return server.replaceFirst("://", "/").replace(":", "#0A");
    }

}

什么是热更新,然后怎么实现热更新呢?这里我向大家谈谈我的理解。热更新就是动态下发代码,就是已经上线的作品,在不发布新版本的情况下,只更新作品的一部分,实现修复BUG或者发布新功能。最开始是让开发者绕开苹果的审核机制,避免长时间的审核等待以及多次被拒造成的成本。这里给大家介绍下Egret官方文档的Android热更新方案。首先要说的是热更新的功能是要开发者自己去实现的,例如如果项目是Android项目,那么热更新就要在Android项目里用java实现,因为Android是用java开发的,所以你改Egret引擎里的东西其实是起不了多大作用的,换句话说就是热更新跟白鹭引擎没有什么很大的关系。 首先要做的就是修改config.preloadPath来指定预加载目录,开发者需要自行维护这个目录下的内容。 Android: //MainActivity.java nativeAndroid.config.preloadPath=”指定目录“; 可以简单地理解为,将游戏部署到手机上地目录下,然后打开这个目录下的游戏。需要更新某个资源时,只需要更新这个目录上对应的资源即可。 MainActivity.java package org.egret.testUpdate;

import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.KeyEvent; import android.widget.Toast;

import org.egret.runtime.launcherInterface.INativePlayer; import org.egret.egretnativeandroid.EgretNativeAndroid;

public class MainActivity extends Activity { private final String TAG = "MainActivity"; private EgretNativeAndroid nativeAndroid;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    nativeAndroid = new EgretNativeAndroid(this);
    if (!nativeAndroid.checkGlEsVersion()) {
        Toast.makeText(this, "This device does not support OpenGL ES 2.0.",
                Toast.LENGTH_LONG).show();
        return;
    }

    nativeAndroid.config.showFPS = true;
    nativeAndroid.config.fpsLogTime = 30;
    nativeAndroid.config.disableNativeRender = false;
    nativeAndroid.config.clearCache = false;
    nativeAndroid.config.loadingTimeout = 0;

    Intent intent = getIntent();
    nativeAndroid.config.preloadPath = intent.getStringExtra("preloadPath");

    setExternalInterfaces();
    
    if (!nativeAndroid.initialize("http://game.com/game/index.html")) {
        Toast.makeText(this, "Initialize native failed.",
                Toast.LENGTH_LONG).show();
        return;
    }

    setContentView(nativeAndroid.getRootFrameLayout());
}

@Override
protected void onPause() {
    super.onPause();
    nativeAndroid.pause();
}

@Override
protected void onResume() {
    super.onResume();
    nativeAndroid.resume();
}

@Override
public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) {
    if (keyCode == KeyEvent.KEYCODE_BACK) {
        nativeAndroid.exitGame();
    }

    return super.onKeyDown(keyCode, keyEvent);
}

private void setExternalInterfaces() {
    nativeAndroid.setExternalInterface("sendToNative", new INativePlayer.INativeInterface() {
        @Override
        public void callback(String message) {
            String str = "Native get message: ";
            str += message;
            Log.d(TAG, str);
            nativeAndroid.callExternalInterface("sendToJS", str);
        }
    });
}

@Override
protected void onDestroy() {
    super.onDestroy();
}

热更新的实现代码: LaunchActivity.java package org.egret.testUpdate;

import android.Manifest; import android.app.Activity; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button;

import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream;

public class LaunchActivity extends Activity {

private Button btn_load;
private Button btn_game;

private final String gameUrl = "http://game.com/game/index.html";
private final String zipUrl = "http://tool.egret-labs.org/Weiduan/game/game2.zip";
private final String preloadPath = "/sdcard/egretGame/";

private static String[] permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE};

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_launch);

    btn_load = (Button)findViewById(R.id.btn_load);
    btn_game = (Button)findViewById(R.id.btn_game);

    btn_load.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            btn_load.setEnabled(false);
            btn_game.setEnabled(false);
            preloadGame();
        }
    });

    btn_game.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent intent = new Intent(LaunchActivity.this, MainActivity.class);
            intent.putExtra("preloadPath", preloadPath);
            startActivity(intent);
        }
    });

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        int check = checkSelfPermission(permissions[0]);
        if (check != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(permissions, 111);
        }
    }
}

private void preloadGame() {
    String dir = preloadPath + getFileDirByUrl(gameUrl);
    File dirFile = new File(dir);
    if (!dirFile.exists()) {
        dirFile.mkdirs();
    }
    downloadGameRes(zipUrl, dir);
}

private void downloadGameRes(final String zipUrl, String targetDir) {
    String tempZipFileName = targetDir + "game.zip";
    final File file = new File(tempZipFileName);
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            InputStream inputStream = null;
            FileOutputStream outputStream = null;
            HttpURLConnection connection = null;
            boolean finish = false;

            try {
                URL url = new URL(zipUrl);
                connection = (HttpURLConnection)url.openConnection();

                int code = connection.getResponseCode();
                if (code == 200) {
                    inputStream = connection.getInputStream();
                    outputStream = new FileOutputStream(file, true);

                    byte[] buffer = new byte[4096];
                    int length;

                    while ((length = inputStream.read(buffer)) != -1) {
                        outputStream.write(buffer, 0, length);
                    }
                }
                finish = true;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (outputStream != null) {
                        outputStream.close();
                    }
                    if (inputStream != null) {
                        inputStream.close();
                    }
                    if (connection != null) {
                        connection.disconnect();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    return;
                }
            }

            if (finish) {
                unzip(file);
            }
        }
    };
    new Thread(runnable).start();
}

private void unzip(File file) {
    int BUFFER = 4096;
    String strEntry;
    String targetDir = file.getParent() + "/";
    try {
        BufferedOutputStream dest = null;
        FileInputStream fis = new FileInputStream(file.getAbsolutePath());
        ZipInputStream zis = new ZipInputStream(new BufferedInputStream(fis));
        ZipEntry entry;
        while ((entry = zis.getNextEntry()) != null) {
            try {
                int count;
                byte data[] = new byte[BUFFER];
                strEntry = entry.getName();
                File entryFile = new File(targetDir + strEntry);
                if (strEntry.endsWith("/")) {
                    entryFile.mkdirs();
                    continue;
                }
                File entryDir = new File(entryFile.getParent());
                if (!entryDir.exists()) {
                    entryDir.mkdirs();
                }
                FileOutputStream fos = new FileOutputStream(entryFile);
                dest = new BufferedOutputStream(fos, BUFFER);
                while ((count = zis.read(data, 0, BUFFER)) != -1) {
                    dest.write(data, 0, count);
                }
                dest.flush();
                dest.close();
            } catch (Exception ex) {
                ex.printStackTrace();
                return;
            }
        }
        zis.close();
    } catch (Exception e) {
        e.printStackTrace();
        return;
    }

    file.delete();
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            btn_game.setEnabled(true);
        }
    });
}

private static String getFileDirByUrl(String urlString) {
    int lastSlash = urlString.lastIndexOf('/');
    String server = urlString.substring(0, lastSlash + 1);
    return server.replaceFirst("://", "/").replace(":", "#0A");
}

} 这里给大家简单的理解下,这里热更新的实现是靠2个按钮触发的,一个是 btn_load按钮这个按钮就是热更新的触发按钮,一个是btn_game按钮这个按钮就是进入游戏的按钮。当第一次点击btn_game按钮时,就是进入初始的游戏界面,就是没有经过热更新的游戏界面。当点击了btn_load按钮后,再点击btn_game按钮就是进入了已经热更新后的游戏界面。点击btn_load按钮后,手机会自动下载解压压缩包,很多人说egret官方文档的示例是错的,其实不是,在手机里的文件管理里没有找到热更新的压缩包,其实照成这样的原因是下载的压缩包已经被解压了,当然也就不存在了,再怎么找也找不到。 我这里测试的地址是一个空的不存在的地址,game.com/index.html,…

就如预想的那样显示这个地址找不到。(ps:特别要注意的一点就是进入游戏首先不能先点击btn_load按钮,这个热更新按钮。要先点击btn_game按钮,因为我们要得到的是没有热更新的游戏界面。) 然后再点击btn_load按钮,进行下载新的压缩包,更新游戏界面。点击后就会出现如图画面。

这是就是已经热更新了,这时就是已经下载并解压了压缩包。然后我们再点击btn_game按钮,进入游戏界面,显示如图:

这就是热更新后的游戏界面。看来是成功了。 这里要说的就是如果你退出这个程序,再点击btn_game按钮就会显示已经热更新后的游戏画面,而不是404的画面。你再点击btn_load按钮,再进入游戏,游戏画面依然不会变。有很多人就以为是热更新没有实现,其实并不是,而是你第一次运行这个程序,已经实现了热更新的功能,你再次进入游戏的时候,这个游戏已经热更新了,你再次点击热更新,更新相同的资源,显示的画面也是一样的。所以说官网的示例是没毛病的。热更新的现象,只有第一次运行这个游戏,进行热更新才会显示出来。当你再次运行这个游戏的是时候,这个游戏都是已经进行了热更新了,所以没有热更新的现象显示。这个问题一直困惑了我好久,我一直以为是示例的热更新代码出问题,可是看了琢磨了好久,发现热更新的代码的逻辑是行得通得,没有什么毛病,一直想不到问题会出在哪,后来玩王者荣耀时,刚好也要更新,更新后进入游戏的界面就变了,然后退了,再次进入游戏的时候,显示就是更新后的游戏界面,而不会显示更新前的游戏界面,突然间就想同了这个问题,实在的困惑了好久,一直在测试,一直在看代码,就是不知道哪里出了问题。现在解决了,真的是一身轻松。 如果有什么疑问,欢迎在帖子下留言。