阅读 255
Android CocosNative单进程方案

Android CocosNative单进程方案

背景

CocosCreator 创建的小游戏,打成Android原生包后,官方仅支持 Activity方式,由于资源释放存在较多问题,大多数公司会采用多进程方式,在游戏页面关闭时,直接杀掉进程,确保资源释放彻底。但这种方式有很大的局限性,比如在现有的直播间Activity嵌入cocos游戏,这类场景其实是需要将cocos游戏作为view的方式添加,本篇文章就是为解决这个场景。

效果如下: 1.gif

用Profile 做了一次内存测试,基本没有泄漏,另外 V8引擎的占用确实比较小,常驻不影响实际使用。

2.png

接下来,我们进入正题,这篇文章应该是全网最实诚的内容,因为它将马上解决这个难题。

环境准备

Cocos Creator: 2.1.3 和 2.4.2 实测有效,其余版本有时间会再尝试

VS Code: 最新版本即可,主要用于查看和编辑 C++引擎代码

Android Studio: 最新版本即可,用于封装核心工具类和示例代码

MacBook Pro:编程电脑

生成Android工程

使用Creator项目构建,配置如下:

3.png

注意配置Android SDK 和 NDK版本,在CocosCreator的偏好设置中:

4.png

顺利的话,你应该可以运行起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限制

5.png

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, null0, 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(thisthis.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>
复制代码

配置小游戏包:

6.png

在 build.gradle 中配置如下:

7.png

好了,以上是全部操作,试试吧~

后记

接下来正式商用,还需要更细化的分析内存占用情况。另外小游戏作为主进程一部分了,游戏异常有可能会导致App闪退,这块还有一些工作需要优化。

参考资料

文章分类
前端