背景
CocosCreator 创建的小游戏,打成Android原生包后,官方仅支持 Activity方式,由于资源释放存在较多问题,大多数公司会采用多进程方式,在游戏页面关闭时,直接杀掉进程,确保资源释放彻底。但这种方式有很大的局限性,比如在现有的直播间Activity嵌入cocos游戏,这类场景其实是需要将cocos游戏作为view的方式添加,本篇文章就是为解决这个场景。
效果如下:
用Profile 做了一次内存测试,基本没有泄漏,另外 V8引擎的占用确实比较小,常驻不影响实际使用。
接下来,我们进入正题,这篇文章应该是全网最实诚的内容,因为它将马上解决这个难题。
环境准备
Cocos Creator: 2.1.3 和 2.4.2 实测有效,其余版本有时间会再尝试
VS Code: 最新版本即可,主要用于查看和编辑 C++引擎代码
Android Studio: 最新版本即可,用于封装核心工具类和示例代码
MacBook Pro:编程电脑
生成Android工程
使用Creator项目构建,配置如下:
注意配置Android SDK 和 NDK版本,在CocosCreator的偏好设置中:
顺利的话,你应该可以运行起Android小游戏了,但现在只能以Activity存在,并且整个进程就只有一个小游戏。接下来跟着我逐步调整~
引擎调整
引擎目录: /Applications/CocosCreator/Creator/2.4.2/CocosCreator.app/Contents/Resources/cocos2d-x
cocos/platform/android/CCApplication-android.cpp
Application::~Application()
{
#if USE_AUDIO
AudioEngine::end();
#endif
// 以下为新增代码
LOGD(">>>>>>>> ~Application");
Configuration::destroyInstance();
_scheduler->unscheduleAll();
EventDispatcher::destroy();
se::ScriptEngine::getInstance()->cleanup();
// 注释掉这一行,V8引擎不要释放,实测内存占用非常少
// se::ScriptEngine::destroyInstance();
delete _renderTexture;
_renderTexture = nullptr;
Application::_instance = nullptr;
}
cocos/platform/android/jni/JniImp.cpp
// 新增JNI方法,用于主动关闭游戏
JNIEXPORT void JNICALL JNI_RENDER(nativeExit)(JNIEnv* env)
{
LOGD("nativeExitApp");
exitApplication();
restartJSVM();
}
cocos/scripting/js-bindings/jswrapper/v8/ScriptEngine.cpp
bool ScriptEngine::isDebuggerEnabled() const
{
// 关闭V8的调试模式
return false;
// return !_debuggerServerAddr.empty() && _debuggerServerPort > 0;
}
Java层封装
目录: /Applications/CocosCreator/Creator/2.4.2/CocosCreator.app/Contents/Resources/cocos2d-x/cocos/platform/android/java/src/org/cocos2dx/lib
cocos/platform/android/java/src/org/cocos2dx/lib/CocosHelper.java
新增类:CocosHelper,搬移 Cocos2dxActivity 中逻辑,摆脱Activity限制
package org.cocos2dx.lib;
import android.app.Activity;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.widget.RelativeLayout;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLDisplay;
public class CocosHelper implements Cocos2dxHelper.Cocos2dxHelperListener {
private Activity context;
private RelativeLayout container;
private Cocos2dxGLSurfaceView mGLSurfaceView = null;
private Cocos2dxRenderer renderer = null;
private int[] mGLContextAttrs = {8,8,8,8,0,0,0};
public Cocos2dxGLSurfaceView getGLSurfaceView(){
return mGLSurfaceView;
}
public CocosHelper(Activity context, RelativeLayout container) {
this.context = context;
this.container = container;
Utils.setActivity(this.context);
Utils.hideVirtualButton();
onLoadNativeLibraries();
Cocos2dxHelper.init(this.context, this);
CanvasRenderingContext2DImpl.init(this.context);
}
public void start(final String path) {
this.renderer = this.createRenderer();
this.renderer.setScreenWidthAndHeight(this.container.getWidth(), this.container.getHeight());
this.renderer.setDefaultResourcePath(path);
this.container.addView(this.mGLSurfaceView);
}
public void pause() {
Cocos2dxHelper.onPause();
mGLSurfaceView.onPause();
}
public void resume() {
Utils.hideVirtualButton();
Cocos2dxHelper.onResume();
mGLSurfaceView.onResume();
}
public void destroy() {
if (this.renderer != null) {
this.renderer.exit();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
CocosHelper.this.renderer = null;
CocosHelper.this.container.removeView(CocosHelper.this.mGLSurfaceView);
CocosHelper.this.mGLSurfaceView = null;
}
}, 500);
}
}
private void onLoadNativeLibraries() {
try {
ApplicationInfo ai = this.context.getPackageManager().getApplicationInfo(this.context.getPackageName(), PackageManager.GET_META_DATA);
Bundle bundle = ai.metaData;
String libName = bundle.getString("android.app.lib_name");
System.loadLibrary(libName);
} catch (Exception e) {
e.printStackTrace();
}
}
private Cocos2dxRenderer createRenderer() {
this.mGLSurfaceView = this.createSurfaceView();
this.mGLSurfaceView.setPreserveEGLContextOnPause(true);
mGLSurfaceView.setBackgroundColor(Color.TRANSPARENT);
Cocos2dxRenderer renderer = new Cocos2dxRenderer();
this.mGLSurfaceView.setCocos2dxRenderer(renderer);
return renderer;
}
private Cocos2dxGLSurfaceView createSurfaceView() {
Cocos2dxGLSurfaceView glSurfaceView = new Cocos2dxGLSurfaceView(this.context);
//this line is need on some device if we specify an alpha bits
if(this.mGLContextAttrs[3] > 0) glSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
Cocos2dxEGLConfigChooser chooser = new Cocos2dxEGLConfigChooser(this.mGLContextAttrs);
glSurfaceView.setEGLConfigChooser(chooser);
return glSurfaceView;
}
@Override
public void showDialog(final String pTitle, final String pMessage) {
}
@Override
public void runOnGLThread(final Runnable runnable) {
this.mGLSurfaceView.queueEvent(runnable);
}
public class Cocos2dxEGLConfigChooser implements GLSurfaceView.EGLConfigChooser {
protected int[] configAttribs;
public Cocos2dxEGLConfigChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize)
{
configAttribs = new int[] {redSize, greenSize, blueSize, alphaSize, depthSize, stencilSize};
}
public Cocos2dxEGLConfigChooser(int[] attribs)
{
configAttribs = attribs;
}
private int findConfigAttrib(EGL10 egl, EGLDisplay display,
EGLConfig config, int attribute, int defaultValue) {
int[] value = new int[1];
if (egl.eglGetConfigAttrib(display, config, attribute, value)) {
return value[0];
}
return defaultValue;
}
class ConfigValue implements Comparable<ConfigValue> {
public EGLConfig config = null;
public int[] configAttribs = null;
public int value = 0;
private void calcValue() {
// depth factor 29bit and [6,12)bit
if (configAttribs[4] > 0) {
value = value + (1 << 29) + ((configAttribs[4]%64) << 6);
}
// stencil factor 28bit and [0, 6)bit
if (configAttribs[5] > 0) {
value = value + (1 << 28) + ((configAttribs[5]%64));
}
// alpha factor 30bit and [24, 28)bit
if (configAttribs[3] > 0) {
value = value + (1 << 30) + ((configAttribs[3]%16) << 24);
}
// green factor [20, 24)bit
if (configAttribs[1] > 0) {
value = value + ((configAttribs[1]%16) << 20);
}
// blue factor [16, 20)bit
if (configAttribs[2] > 0) {
value = value + ((configAttribs[2]%16) << 16);
}
// red factor [12, 16)bit
if (configAttribs[0] > 0) {
value = value + ((configAttribs[0]%16) << 12);
}
}
public ConfigValue(int[] attribs) {
configAttribs = attribs;
calcValue();
}
public ConfigValue(EGL10 egl, EGLDisplay display, EGLConfig config) {
this.config = config;
configAttribs = new int[6];
configAttribs[0] = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE, 0);
configAttribs[1] = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0);
configAttribs[2] = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0);
configAttribs[3] = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0);
configAttribs[4] = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0);
configAttribs[5] = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0);
calcValue();
}
@Override
public int compareTo(ConfigValue another) {
if (value < another.value) {
return -1;
} else if (value > another.value) {
return 1;
} else {
return 0;
}
}
@Override
public String toString() {
return "{ color: " + configAttribs[3] + configAttribs[2] + configAttribs[1] + configAttribs[0] +
"; depth: " + configAttribs[4] + "; stencil: " + configAttribs[5] + ";}";
}
}
@Override
public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display)
{
int[] EGLattribs = {
EGL10.EGL_RED_SIZE, configAttribs[0],
EGL10.EGL_GREEN_SIZE, configAttribs[1],
EGL10.EGL_BLUE_SIZE, configAttribs[2],
EGL10.EGL_ALPHA_SIZE, configAttribs[3],
EGL10.EGL_DEPTH_SIZE, configAttribs[4],
EGL10.EGL_STENCIL_SIZE,configAttribs[5],
EGL10.EGL_RENDERABLE_TYPE, 4, //EGL_OPENGL_ES2_BIT
EGL10.EGL_NONE
};
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
boolean eglChooseResult = egl.eglChooseConfig(display, EGLattribs, configs, 1, numConfigs);
if (eglChooseResult && numConfigs[0] > 0)
{
return configs[0];
}
// there's no config match the specific configAttribs, we should choose a closest one
int[] EGLV2attribs = {
EGL10.EGL_RENDERABLE_TYPE, 4, //EGL_OPENGL_ES2_BIT
EGL10.EGL_NONE
};
eglChooseResult = egl.eglChooseConfig(display, EGLV2attribs, null, 0, numConfigs);
if(eglChooseResult && numConfigs[0] > 0) {
int num = numConfigs[0];
ConfigValue[] cfgVals = new ConfigValue[num];
// convert all config to ConfigValue
configs = new EGLConfig[num];
egl.eglChooseConfig(display, EGLV2attribs, configs, num, numConfigs);
for (int i = 0; i < num; ++i) {
cfgVals[i] = new ConfigValue(egl, display, configs[i]);
}
ConfigValue e = new ConfigValue(configAttribs);
// bin search
int lo = 0;
int hi = num;
int mi;
while (lo < hi - 1) {
mi = (lo + hi) / 2;
if (e.compareTo(cfgVals[mi]) < 0) {
hi = mi;
} else {
lo = mi;
}
}
if (lo != num - 1) {
lo = lo + 1;
}
Log.w("cocos2d", "Can't find EGLConfig match: " + e + ", instead of closest one:" + cfgVals[lo]);
return cfgVals[lo].config;
}
return null;
}
}
}
cocos/platform/android/java/src/org/cocos2dx/lib/Cocos2dxHelper.java
public static void runOnGLThread(final Runnable r) {
// ((Cocos2dxActivity)sActivity).runOnGLThread(r);
Cocos2dxHelper.sCocos2dxHelperListener.runOnGLThread(r);
}
// 调整init方法,增加listener参数
public static void init(final Activity activity) {
Cocos2dxHelper.init(activity, (Cocos2dxHelperListener)activity);
}
public static void init(final Activity activity, Cocos2dxHelperListener listener) {
sActivity = activity;
Cocos2dxHelper.sCocos2dxHelperListener = (Cocos2dxHelperListener)listener;
... ....
}
public static void endApplication() {
// 避免直接退出Activity
// if (sActivity != null)
// sActivity.finish();
}
//////////////// 代码替换 /////////////////////
Cocos2dxActivity.getContext() => getActivity()
Cocos2dxHelper.sActivity => getActivity()
cocos/platform/android/java/src/org/cocos2dx/lib/Cocos2dxRenderer.java
// 增加exit方法
public void exit() {
Cocos2dxRenderer.nativeExit();
}
private static native void nativeExit();
编写示例代码
Java工程,新建一个MainAcivity,并设置为启动Activity。
package org.cocos2d.examplecases;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.RelativeLayout;
import org.cocos2dx.lib.CocosHelper;
public class MainActivity extends AppCompatActivity {
private RelativeLayout gameContainer;
private CocosHelper helper;
private int index = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!isTaskRoot()) {
finish();
return;
}
setContentView(R.layout.activity_main);
this.findViewById(R.id.switchBtn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
MainActivity.this.start();
}
});
this.findViewById(R.id.closeBtn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
MainActivity.this.stop();
}
});
this.gameContainer = this.findViewById(R.id.gameContianer);
this.helper = new CocosHelper(this, this.gameContainer);
}
private void start() {
String resPath = "";
if (index == 0) {
index = -1;
resPath = "@assets/game/bubble";
} else {
index = 0;
resPath = "@assets/game/game1";
}
this.helper.start(resPath);
}
private void stop() {
this.helper.destroy();
}
}
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#009688"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/switchBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="切换游戏"
android:textColor="#00695C"
tools:ignore="TextContrastCheck" />
<Button
android:id="@+id/closeBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="关闭游戏" />
</LinearLayout>
<RelativeLayout
android:id="@+id/gameContianer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#bbbbbb"
>
</RelativeLayout>
</LinearLayout>
</android.support.constraint.ConstraintLayout>
配置小游戏包:
在 build.gradle 中配置如下:
好了,以上是全部操作,试试吧~
后记
接下来正式商用,还需要更细化的分析内存占用情况。另外小游戏作为主进程一部分了,游戏异常有可能会导致App闪退,这块还有一些工作需要优化。