Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I’m using the source code for Surface.java 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.

like image 368
Leo Nikkilä Avatar asked Oct 28 '16 02:10

Leo Nikkilä


People also ask

When garbage collector runs?

When the JVM doesn't have necessary memory space to run, the garbage collector will run and delete unnecessary objects to free up memory. Unnecessary objects are the objects which have no other references (address) pointing to them.

Why garbage collection is important?

The garbage collector provides the following benefits: Frees developers from having to manually release memory. Allocates objects on the managed heap efficiently. Reclaims objects that are no longer being used, clears their memory, and keeps the memory available for future allocations.

Does C have garbage collection?

C does not have automatic garbage collection. If you lose track of an object, you have what is known as a 'memory leak'. The memory will still be allocated to the program as a whole, but nothing will be able to use it if you've lost the last pointer to it. Memory resource management is a key requirement on C programs.

Why is GC not running?

If the application has enough free heap memory, Major GC will not be triggered. With JConsole (comes with your JDK) you can check memory usage. Is triggering Major GCs the actual purpose of your application?


1 Answers

Surfaces are just references into the BufferQueue. They contain a Binder token, used to negotiate sending graphical buffers between producer and receiver. A relevant 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 user space 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 would 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 the Android website for device and firmware makers.

like image 159
user1643723 Avatar answered Oct 18 '22 09:10

user1643723