java - ¿Cómo gestiona Surface la recolección de basura después de parcelarse en Android?
garbage-collection jni (1)
Estoy usando el código fuente de Surface.java
como referencia para esta pregunta.
Surface implementa la interfaz Parcelable, y también tiene un identificador para un objeto en el lado nativo.
Me interesa saber cómo se maneja la recolección de basura en este caso:
Una superficie (A) se crea y se escribe en un paquete. No hay referencias a eso luego.
Una copia de la superficie original (B) se lee del paquete; digamos que esto sucede en otro hilo usado para renderizar. Esta instancia ahora se mantiene en el mismo identificador nativo que (A) y hay una fuerte referencia a esta instancia en alguna parte.
Se produce un GC y (A) se recoge porque ya no se hace referencia a él. se ejecuta
finalize()
, que llama arelease()
, que a su vez llama anativeRelease(long)
para el identificador nativo.
Una mirada superficial sobre el código fuente me hizo pensar que ahora (B) también debería dar un puntapié al cubo y dejar de funcionar ya que se libera el identificador nativo, pero después de intentar replicar esto no parece ser el caso. (A) se recoge pero (B) permanece vivo y sigue siendo utilizable.
Ahora tengo la sensación de que hay un conteo de referencias con el objeto nativo, o alguna otra magia en el lado nativo del proceso de parcelación.
Independientemente de si mi suposición es correcta, estoy buscando una visión general sobre las causas de este comportamiento, preferiblemente con algunas referencias al código fuente del marco. También estoy tangencialmente interesado en cómo funciona el bloqueo de superficie en casos similares.
Las superficies son solo referencias en BufferQueue . Contienen un token Binder , utilizado para negociar el envío de búferes gráficos entre el productor y el receptor. Un código JNI relevante :
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());
}
Puede ver claramente cómo se lee un token de Binder desde Parcel y se convierte en la interfaz IPC de IGraphicBufferProducer .
Los tokens de enlace se cuentan de referencia en kernel, destruyendo una de las referencias de espacio de usuario no hace nada mientras exista más.
Cuando está dentro del mismo proceso, la semántica de bloqueo no cambia, porque la Surface
nativa mantiene un caché de instancias :
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;
}
Cada instancia de Java Surface
, creada parcelling / unparcelling dentro del mismo proceso, se refiere a la misma Surface
nativa, lo que significa que los bloqueos aún deben tener efecto: obtendrá una excepción en caso de contención.
Intentar obtener simultáneamente Superficies sin par de múltiples procesos fallaría porque el contrato IGraphicBufferProducer
prohíbe explícitamente eso :
// 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;
Puede encontrar más detalles sobre la arquitectura de pila gráfica de nivel inferior en el sitio web de Android para fabricantes de dispositivos y firmware .