Leo Nikkilä Leo Nikkilä - 1 month ago 6
Java Question

How does Surface handle garbage collection after being parcelled on Android?

I’m using the source code for

as a reference for this question.

Surface implements the Parcelable interface, and it also holds a handle to an object on the native side.

I’m interested in knowing how garbage collection is handled in this case:


  1. A Surface (A) is created and written into a Parcel. There are no references to it afterwards.

  2. A copy of the original Surface (B) is read from the Parcel; let’s say this happens on another thread used for rendering. This instance now holds onto the same native handle as (A) and there’s a strong reference to this instance somewhere.

  3. A GC occurs and (A) is collected since it’s no longer referenced.
    finalize()
    is run, which calls
    release()
    , which in turn calls
    nativeRelease(long)
    for the native handle.



A cursory look over the source code made me think that now (B) should also kick the bucket and stop working since the native handle is released, but after trying to replicate this it doesn’t seem to be the case. (A) does get collected but (B) lives on and remains usable.

Now I have a feeling that there’s some reference counting going on with the native object, or some other magic on the native side of the parcelling process.

Regardless of whether my assumption is correct, I’m looking for an overview on what causes this behaviour, preferably with some references to the framework source code. I’m also tangentially interested in how Surface locking works in similar cases.

Answer

Surfaces are just references into the BufferQueue. They contain a Binder token, used to negotiate sending graphical buffers between producer and receiver. A relevent JNI code:

static jlong nativeReadFromParcel(JNIEnv* env, jclass clazz, jlong nativeObject, jobject parcelObj) {
  Parcel* parcel = parcelForJavaObject(env, parcelObj);
  if (parcel == NULL) {
    doThrowNPE(env);
    return 0;
  }

  android::view::Surface surfaceShim;

  // Calling code in Surface.java has already read the name of the Surface
  // from the Parcel
  surfaceShim.readFromParcel(parcel, /*nameAlreadyRead*/true);

  sp<Surface> self(reinterpret_cast<Surface *>(nativeObject));

  // update the Surface only if the underlying IGraphicBufferProducer
  // has changed.
  if (self != nullptr
        && (IInterface::asBinder(self->getIGraphicBufferProducer()) ==
                IInterface::asBinder(surfaceShim.graphicBufferProducer))) {
      // same IGraphicBufferProducer, return ourselves
      return jlong(self.get());
  }

  sp<Surface> sur;
  if (surfaceShim.graphicBufferProducer != nullptr) {
    // we have a new IGraphicBufferProducer, create a new Surface for it
    sur = new Surface(surfaceShim.graphicBufferProducer, true);
    // and keep a reference before passing to java
    sur->incStrong(&sRefBaseOwner);
  }

  if (self != NULL) {
    // and loose the java reference to ourselves
    self->decStrong(&sRefBaseOwner);
  }

  return jlong(sur.get());
}

You can clearly see, how a Binder token is read from Parcel and converted to IGraphicBufferProducer IPC interface.

Binder tokens are reference-counted in kernel, destroying one of userspace references does nothing as long as more exists.

When you are within the same process, locking semantics do not change, because native Surface maintains a cache of instances:

sp<Surface> Surface::readFromParcel(const Parcel& data) {
  Mutex::Autolock _l(sCachedSurfacesLock);
  sp<IBinder> binder(data.readStrongBinder());
  sp<Surface> surface = sCachedSurfaces.valueFor(binder).promote();
  if (surface == 0) {
   surface = new Surface(data, binder);
   sCachedSurfaces.add(binder, surface);
  } else {
    // The Surface was found in the cache, but we still should clear any
    // remaining data from the parcel.
    data.readStrongBinder();  // ISurfaceTexture
    data.readInt32();         // identity
  }
  if (surface->mSurface == NULL && surface->getISurfaceTexture() == NULL) {
    surface = 0;
  }
  cleanCachedSurfacesLocked();
  return surface;
}

Every Java Surface instance, created by parcelling/unparcelling within the same process, refers to the same native Surface, which means that locks should still have effect: you will get an exception in case of contention.

Attempting to simultaneously draw to unparcelled Surfaces from multiple processes should fail because IGraphicBufferProducer contract explicitly forbids that:

// connect attempts to connect a client API to the IGraphicBufferProducer.
// This must be called before any other IGraphicBufferProducer methods are
// called except for getAllocator.
//
// This method will fail if the connect was previously called on the
// IGraphicBufferProducer and no corresponding disconnect call was made.
//
// outWidth, outHeight and outTransform are filled with the default width
// and height of the window and current transform applied to buffers,
// respectively. The token needs to be any binder object that lives in the
// producer process -- it is solely used for obtaining a death notification
// when the producer is killed.
virtual status_t connect(const sp<IBinder>& token,
        int api, bool producerControlledByApp, QueueBufferOutput* output) = 0;

You can find more details about lower-level graphical stack architecture on Android website for device and firmware makers.