我们开发了一个Android的3D应用,界面部分使用Java,渲染部分使用了C++,但发布后,应用在前后台切换时总是再渲染功能上出现错误导致程序崩溃,经过几天的奋战,终于整清楚了。
Android在进行前后台切换时,如果程序中不显示调用View的onPause和onResume,那么系统不会自动调用其onPause和onResume,在我们的程序中,在activity的onPause和onResume分别调用view相应的函数。
查看GLSurfaceView的源码发现Android在进行前后台切换时,自动重新创建EGLSurface和EGLContext,即在suspend(onPause)时删除现有的对象,然后在resume时再创建新的对象,虽然有一个属性setPreserveEGLContextOnPause,但不能保证百分百好用,与具体手机有关,根据我们测试的情况是,如果setPreserveEGLContextOnPause(true),那么程序不会调用Renderer的onSurfaceCreated,但会发生EGL_CONTEXT_LOST,
从而导致EGLSurface和EGLContext被重建(虽然重建了,但仍然不会调用onSurfaceCreated),因此为了能满足所有情况,
还是setPreserveEGLContextOnPause(false)。
根据GLSurfaceView中的文档描述,渲染环境重建时,会自动删除OpenGL的相关资源,因此不需要我们手动调用释放资源的动作。
我们的应用要适应现有的情况,则必须:
1.在渲染环境删除前保存一些运行时资源,
2.然后在渲染环境重建后,第一时间恢复这些运行时资源,并把在前一个渲染环境中创建的OpenGL对象置为初始状态(只需要置状态,不需要调用OpenGL的删除资源函数,
注意:一定不能调用删除函数,否则后续渲染就会出现未知的效果错误,比如花屏,贴图缺失等奇异错误)
对于恢复的时机比较容易找到,那就是onSurfaceCreated,但保存运行资源的位置却没有明显的时机,首先调用保存动作,必须在渲染环境环境删除前,而且必须是在渲染线程中执行,我们初步确定了几个位置onPause,EGLWindowSurfaceFactory.destroySurface,EGLContextFactory.destroyContext,我们都试了,但居然不好使,始终提示设备环境无效,经过一番努力终于搞定,在onPause中调用,具体代码如下:
@Override
public void onPause() {
queueEvent(new Runnable() {
@Override
public void run() {
//保存运行时资源
...
}
});
super.onPause();
}
就是必须在GLSurfaceView的onPause之前调用自己的代码,在activity中也是类似,在super.onPause()前调用view的onPause, activity的onResume正好相反,在super.onResume之后调用view的onResume.
发布程序,终于好用了!
有个小插曲,根据我的经验一般重建渲染环境,需要重建窗口,但android在没有重建窗口的情况下居然也能重建渲染环境,让我感到很诧异,后来查看ndk的程序
ANativeActivity的callbacks有一个回调onNativeWindowCreated,在程序从后台切换到前台时会调用onNativeWindowCreated,说明符合之前的经验,
而我们在程序中保存了ANativeWindow对象的指针,因此在程序从后台切换到前台时,也需要重新获取ANativeWindow指针,否则会出错:
在我们程序中唯一使用ANativeWindow的地方就是取得窗口尺寸:
h = ANativeWindow_getHeight(win_handle_);
w = ANativeWindow_getWidth(win_handle_);
如果不重新获取这个指针,w和h得到的都是负数,我们的程序前端接的GLSurfaceView,从GLSurfaceView中无法获得ANativeWindow指针,必须先在JAVA程序中
取得Surface对象:
Surface surf = GLSurfaceView的对象.getHolder().getSurface();
然后在C++中从Surface获取ANativeWindow指针:
ANativeWindow* window = ANativeWindow_fromSurface(env, surface);
这个程序切到前台时也必须刷新ANativeWindow指针,才能取到正确的窗口尺寸。