app: android activity lifecycle awareness

This CL fixes two bugs in the existing app implementation on android.
The first, is it assumed a single NativeActivity instance is created
per process. This is not true. If you open an app, hit the back
button, then open it again, the original activity is destroyed and a
new one is created. So only call main.main in the first onCreate.

The second bug has to do with window lifetimes. Previously we only
processed GL work while the window existed, as part of a paint cycle.
This missed GL events called as part of a lifecycle downgrade when
the window was destroyed. (I.e. the contents of onStop.) This CL
fixes this by making the main android event processing loop last for
the life of the process, not the window.

Fixes golang/go#11804.

Change-Id: Ia03e464aab5bc10ba75564b7ca11054515cda011
Reviewed-on: https://go-review.googlesource.com/12533
Reviewed-by: Nigel Tao <nigeltao@golang.org>
This commit is contained in:
David Crawshaw 2015-07-22 15:19:43 -04:00
Родитель 723de9f813
Коммит 5c91e60c93
4 изменённых файлов: 163 добавлений и 89 удалений

Просмотреть файл

@ -51,37 +51,49 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
return JNI_VERSION_1_6; return JNI_VERSION_1_6;
} }
int main_running = 0;
// Entry point from our subclassed NativeActivity. // Entry point from our subclassed NativeActivity.
// //
// By here, the Go runtime has been initialized (as we are running in // By here, the Go runtime has been initialized (as we are running in
// -buildmode=c-shared) but main.main hasn't been called yet. // -buildmode=c-shared) but the first time it is called, Go's main.main
// hasn't been called yet.
//
// The Activity may be created and destroyed multiple times throughout
// the life of a single process. Each time, onCreate is called.
void ANativeActivity_onCreate(ANativeActivity *activity, void* savedState, size_t savedStateSize) { void ANativeActivity_onCreate(ANativeActivity *activity, void* savedState, size_t savedStateSize) {
JNIEnv* env = activity->env; if (!main_running) {
JNIEnv* env = activity->env;
// Note that activity->clazz is mis-named. // Note that activity->clazz is mis-named.
JavaVM* current_vm = activity->vm; JavaVM* current_vm = activity->vm;
jobject current_ctx = activity->clazz; jobject current_ctx = activity->clazz;
setCurrentContext(current_vm, (*env)->NewGlobalRef(env, current_ctx)); setCurrentContext(current_vm, (*env)->NewGlobalRef(env, current_ctx));
// Set TMPDIR. // Set TMPDIR.
jmethodID gettmpdir = find_method(env, current_ctx_clazz, "getTmpdir", "()Ljava/lang/String;"); jmethodID gettmpdir = find_method(env, current_ctx_clazz, "getTmpdir", "()Ljava/lang/String;");
jstring jpath = (jstring)(*env)->CallObjectMethod(env, current_ctx, gettmpdir, NULL); jstring jpath = (jstring)(*env)->CallObjectMethod(env, current_ctx, gettmpdir, NULL);
const char* tmpdir = (*env)->GetStringUTFChars(env, jpath, NULL); const char* tmpdir = (*env)->GetStringUTFChars(env, jpath, NULL);
if (setenv("TMPDIR", tmpdir, 1) != 0) { if (setenv("TMPDIR", tmpdir, 1) != 0) {
LOG_INFO("setenv(\"TMPDIR\", \"%s\", 1) failed: %d", tmpdir, errno); LOG_INFO("setenv(\"TMPDIR\", \"%s\", 1) failed: %d", tmpdir, errno);
}
(*env)->ReleaseStringUTFChars(env, jpath, tmpdir);
// Call the Go main.main.
uintptr_t mainPC = (uintptr_t)dlsym(RTLD_DEFAULT, "main.main");
if (!mainPC) {
LOG_FATAL("missing main.main");
}
callMain(mainPC);
main_running = 1;
} }
(*env)->ReleaseStringUTFChars(env, jpath, tmpdir);
// Call the Go main.main.
uintptr_t mainPC = (uintptr_t)dlsym(RTLD_DEFAULT, "main.main");
if (!mainPC) {
LOG_FATAL("missing main.main");
}
callMain(mainPC);
// These functions match the methods on Activity, described at // These functions match the methods on Activity, described at
// http://developer.android.com/reference/android/app/Activity.html // http://developer.android.com/reference/android/app/Activity.html
//
// Note that onNativeWindowResized is not called on resize. Avoid it.
// https://code.google.com/p/android/issues/detail?id=180645
activity->callbacks->onStart = onStart; activity->callbacks->onStart = onStart;
activity->callbacks->onResume = onResume; activity->callbacks->onResume = onResume;
activity->callbacks->onSaveInstanceState = onSaveInstanceState; activity->callbacks->onSaveInstanceState = onSaveInstanceState;
@ -97,7 +109,5 @@ void ANativeActivity_onCreate(ANativeActivity *activity, void* savedState, size_
activity->callbacks->onConfigurationChanged = onConfigurationChanged; activity->callbacks->onConfigurationChanged = onConfigurationChanged;
activity->callbacks->onLowMemory = onLowMemory; activity->callbacks->onLowMemory = onLowMemory;
// Note that onNativeWindowResized is not called on resize. Avoid it.
// https://code.google.com/p/android/issues/detail?id=180645
onCreate(activity); onCreate(activity);
} }

Просмотреть файл

@ -42,7 +42,6 @@ import "C"
import ( import (
"log" "log"
"os" "os"
"runtime"
"time" "time"
"unsafe" "unsafe"
@ -79,12 +78,6 @@ func callMain(mainPC uintptr) {
go callfn.CallFn(mainPC) go callfn.CallFn(mainPC)
} }
//export onCreate
func onCreate(activity *C.ANativeActivity) {
config := windowConfigRead(activity)
pixelsPerPt = config.pixelsPerPt
}
//export onStart //export onStart
func onStart(activity *C.ANativeActivity) { func onStart(activity *C.ANativeActivity) {
} }
@ -106,6 +99,16 @@ func onPause(activity *C.ANativeActivity) {
func onStop(activity *C.ANativeActivity) { func onStop(activity *C.ANativeActivity) {
} }
//export onCreate
func onCreate(activity *C.ANativeActivity) {
// Set the initial configuration.
//
// Note we use unbuffered channels to talk to the activity loop, and
// NativeActivity calls these callbacks sequentially, so configuration
// will be set before <-windowRedrawNeeded is processed.
windowConfigChange <- windowConfigRead(activity)
}
//export onDestroy //export onDestroy
func onDestroy(activity *C.ANativeActivity) { func onDestroy(activity *C.ANativeActivity) {
} }
@ -132,19 +135,17 @@ func onNativeWindowRedrawNeeded(activity *C.ANativeActivity, window *C.ANativeWi
//export onNativeWindowDestroyed //export onNativeWindowDestroyed
func onNativeWindowDestroyed(activity *C.ANativeActivity, window *C.ANativeWindow) { func onNativeWindowDestroyed(activity *C.ANativeActivity, window *C.ANativeWindow) {
windowDestroyed <- true windowDestroyed <- window
} }
var queue *C.AInputQueue
//export onInputQueueCreated //export onInputQueueCreated
func onInputQueueCreated(activity *C.ANativeActivity, q *C.AInputQueue) { func onInputQueueCreated(activity *C.ANativeActivity, q *C.AInputQueue) {
queue = q inputQueue <- q
} }
//export onInputQueueDestroyed //export onInputQueueDestroyed
func onInputQueueDestroyed(activity *C.ANativeActivity, q *C.AInputQueue) { func onInputQueueDestroyed(activity *C.ANativeActivity, q *C.AInputQueue) {
queue = nil inputQueue <- nil
} }
//export onContentRectChanged //export onContentRectChanged
@ -213,7 +214,8 @@ func onLowMemory(activity *C.ANativeActivity) {
} }
var ( var (
windowDestroyed = make(chan bool) inputQueue = make(chan *C.AInputQueue)
windowDestroyed = make(chan *C.ANativeWindow)
windowCreated = make(chan *C.ANativeWindow) windowCreated = make(chan *C.ANativeWindow)
windowRedrawNeeded = make(chan *C.ANativeWindow) windowRedrawNeeded = make(chan *C.ANativeWindow)
windowRedrawDone = make(chan struct{}) windowRedrawDone = make(chan struct{})
@ -223,21 +225,3 @@ var (
func init() { func init() {
registerGLViewportFilter() registerGLViewportFilter()
} }
func main(f func(App)) {
// Preserve this OS thread for the GL context created in windowDraw.
runtime.LockOSThread()
donec := make(chan struct{})
go func() {
f(app{})
close(donec)
}()
for w := range windowCreated {
if windowDraw(w, queue, donec) {
return
}
}
panic("unreachable")
}

Просмотреть файл

@ -80,11 +80,7 @@ func (app) EndPaint() {
// This enforces that the final receive (for this paint cycle) on // This enforces that the final receive (for this paint cycle) on
// gl.WorkAvailable happens before the send on endPaint. // gl.WorkAvailable happens before the send on endPaint.
gl.Flush() gl.Flush()
endPaint <- struct{}{}
select {
case endPaint <- struct{}{}:
default:
}
} }
var filters []func(interface{}) interface{} var filters []func(interface{}) interface{}

Просмотреть файл

@ -5,7 +5,6 @@
package app package app
/* /*
#include <android/log.h>
#include <android/native_activity.h> #include <android/native_activity.h>
#include <android/native_window.h> #include <android/native_window.h>
#include <android/input.h> #include <android/input.h>
@ -24,57 +23,67 @@ const EGLint RGB_888[] = {
EGL_NONE EGL_NONE
}; };
EGLDisplay display; EGLDisplay display = NULL;
EGLSurface surface; EGLSurface surface = NULL;
#define LOG_ERROR(...) __android_log_print(ANDROID_LOG_ERROR, "Go", __VA_ARGS__) char* initEGLDisplay() {
display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (!eglInitialize(display, 0, 0)) {
return "EGL initialize failed";
}
return NULL;
}
void createEGLWindow(ANativeWindow* window) { char* createEGLSurface(ANativeWindow* window) {
char* err;
EGLint numConfigs, format; EGLint numConfigs, format;
EGLConfig config; EGLConfig config;
EGLContext context; EGLContext context;
display = eglGetDisplay(EGL_DEFAULT_DISPLAY); if (display == 0) {
if (!eglInitialize(display, 0, 0)) { if ((err = initEGLDisplay()) != NULL) {
LOG_ERROR("EGL initialize failed"); return err;
return; }
} }
if (!eglChooseConfig(display, RGB_888, &config, 1, &numConfigs)) { if (!eglChooseConfig(display, RGB_888, &config, 1, &numConfigs)) {
LOG_ERROR("EGL choose RGB_888 config failed"); return "EGL choose RGB_888 config failed";
return;
} }
if (numConfigs <= 0) { if (numConfigs <= 0) {
LOG_ERROR("EGL no config found"); return "EGL no config found";
return;
} }
eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format); eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format);
if (ANativeWindow_setBuffersGeometry(window, 0, 0, format) != 0) { if (ANativeWindow_setBuffersGeometry(window, 0, 0, format) != 0) {
LOG_ERROR("EGL set buffers geometry failed"); return "EGL set buffers geometry failed";
return;
} }
surface = eglCreateWindowSurface(display, config, window, NULL); surface = eglCreateWindowSurface(display, config, window, NULL);
if (surface == EGL_NO_SURFACE) { if (surface == EGL_NO_SURFACE) {
LOG_ERROR("EGL create surface failed"); return "EGL create surface failed";
return;
} }
const EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE }; const EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE };
context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs); context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE) { if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE) {
LOG_ERROR("eglMakeCurrent failed"); return "eglMakeCurrent failed";
return;
} }
return NULL;
} }
#undef LOG_ERROR char* destroyEGLSurface() {
if (!eglDestroySurface(display, surface)) {
return "EGL destroy surface failed";
}
return NULL;
}
*/ */
import "C" import "C"
import ( import (
"fmt"
"log" "log"
"runtime"
"golang.org/x/mobile/event/config" "golang.org/x/mobile/event/config"
"golang.org/x/mobile/event/lifecycle" "golang.org/x/mobile/event/lifecycle"
@ -84,7 +93,18 @@ import (
"golang.org/x/mobile/gl" "golang.org/x/mobile/gl"
) )
func windowDraw(w *C.ANativeWindow, queue *C.AInputQueue, donec chan struct{}) (done bool) { func main(f func(App)) {
// Preserve this OS thread for the GL context created below.
runtime.LockOSThread()
donec := make(chan struct{})
go func() {
f(app{})
close(donec)
}()
var q *C.AInputQueue
// Android can send a windowRedrawNeeded event any time, including // Android can send a windowRedrawNeeded event any time, including
// in the middle of a paint cycle. The redraw event may have changed // in the middle of a paint cycle. The redraw event may have changed
// the size of the screen, so any partial painting is now invalidated. // the size of the screen, so any partial painting is now invalidated.
@ -97,15 +117,27 @@ func windowDraw(w *C.ANativeWindow, queue *C.AInputQueue, donec chan struct{}) (
// windowRedrawDone is signalled, allowing onNativeWindowRedrawNeeded // windowRedrawDone is signalled, allowing onNativeWindowRedrawNeeded
// to return. // to return.
var redrawGen, paintGen uint32 var redrawGen, paintGen uint32
for { for {
processEvents(queue) if q != nil {
processEvents(q)
}
select { select {
case <-windowCreated:
case q = <-inputQueue:
case <-donec: case <-donec:
return true return
case cfg := <-windowConfigChange: case cfg := <-windowConfigChange:
// TODO save orientation // TODO save orientation
pixelsPerPt = cfg.pixelsPerPt pixelsPerPt = cfg.pixelsPerPt
case w := <-windowRedrawNeeded: case w := <-windowRedrawNeeded:
newWindow := C.surface == nil
if newWindow {
if errStr := C.createEGLSurface(w); errStr != nil {
log.Printf("app: %s (%s)", C.GoString(errStr), eglGetError())
return
}
}
sendLifecycle(lifecycle.StageFocused) sendLifecycle(lifecycle.StageFocused)
widthPx := int(C.ANativeWindow_getWidth(w)) widthPx := int(C.ANativeWindow_getWidth(w))
heightPx := int(C.ANativeWindow_getHeight(w)) heightPx := int(C.ANativeWindow_getHeight(w))
@ -116,28 +148,43 @@ func windowDraw(w *C.ANativeWindow, queue *C.AInputQueue, donec chan struct{}) (
HeightPt: geom.Pt(float32(heightPx) / pixelsPerPt), HeightPt: geom.Pt(float32(heightPx) / pixelsPerPt),
PixelsPerPt: pixelsPerPt, PixelsPerPt: pixelsPerPt,
} }
if paintGen == 0 { redrawGen++
paintGen++ if newWindow {
C.createEGLWindow(w) // New window, begin paint loop.
// TODO(crawshaw): If we get a <-windowCreated in between sending a
// paint.Event and receiving on endPaint, we can double-paint. (BUG)
paintGen = redrawGen
eventsIn <- paint.Event{} eventsIn <- paint.Event{}
} }
redrawGen++
case <-windowDestroyed: case <-windowDestroyed:
if C.surface != nil {
if errStr := C.destroyEGLSurface(); errStr != nil {
log.Printf("app: %s (%s)", C.GoString(errStr), eglGetError())
return
}
}
C.surface = nil
sendLifecycle(lifecycle.StageAlive) sendLifecycle(lifecycle.StageAlive)
return false
case <-gl.WorkAvailable: case <-gl.WorkAvailable:
gl.DoWork() gl.DoWork()
case <-endPaint: case <-endPaint:
if paintGen == redrawGen { if paintGen == redrawGen {
// eglSwapBuffers blocks until vsync. if C.surface != nil {
C.eglSwapBuffers(C.display, C.surface) // eglSwapBuffers blocks until vsync.
if C.eglSwapBuffers(C.display, C.surface) == C.EGL_FALSE {
log.Printf("app: failed to swap buffers (%s)", eglGetError())
}
}
select { select {
case windowRedrawDone <- struct{}{}: case windowRedrawDone <- struct{}{}:
default: default:
} }
} }
if C.surface != nil {
redrawGen++
eventsIn <- paint.Event{}
}
paintGen = redrawGen paintGen = redrawGen
eventsIn <- paint.Event{}
} }
} }
} }
@ -186,3 +233,40 @@ func processEvent(e *C.AInputEvent) {
log.Printf("unknown input event, type=%d", C.AInputEvent_getType(e)) log.Printf("unknown input event, type=%d", C.AInputEvent_getType(e))
} }
} }
func eglGetError() string {
switch errNum := C.eglGetError(); errNum {
case C.EGL_SUCCESS:
return "EGL_SUCCESS"
case C.EGL_NOT_INITIALIZED:
return "EGL_NOT_INITIALIZED"
case C.EGL_BAD_ACCESS:
return "EGL_BAD_ACCESS"
case C.EGL_BAD_ALLOC:
return "EGL_BAD_ALLOC"
case C.EGL_BAD_ATTRIBUTE:
return "EGL_BAD_ATTRIBUTE"
case C.EGL_BAD_CONTEXT:
return "EGL_BAD_CONTEXT"
case C.EGL_BAD_CONFIG:
return "EGL_BAD_CONFIG"
case C.EGL_BAD_CURRENT_SURFACE:
return "EGL_BAD_CURRENT_SURFACE"
case C.EGL_BAD_DISPLAY:
return "EGL_BAD_DISPLAY"
case C.EGL_BAD_SURFACE:
return "EGL_BAD_SURFACE"
case C.EGL_BAD_MATCH:
return "EGL_BAD_MATCH"
case C.EGL_BAD_PARAMETER:
return "EGL_BAD_PARAMETER"
case C.EGL_BAD_NATIVE_PIXMAP:
return "EGL_BAD_NATIVE_PIXMAP"
case C.EGL_BAD_NATIVE_WINDOW:
return "EGL_BAD_NATIVE_WINDOW"
case C.EGL_CONTEXT_LOST:
return "EGL_CONTEXT_LOST"
default:
return fmt.Sprintf("Unknown EGL err: %d", errNum)
}
}