Comment mettre en cache des bitmaps dans la mémoire native
Pour mes 10 000 points, j'ai décidé de contribuer à ce site web sympa: un mécanisme pour mettre en cache les bitmaps sur la mémoire native.
Contexte
Les appareils Android ont une quantité de mémoire très limitée pour chaque application - le tas varie de 16 Mo à 128 Mo, selon divers paramètres .
Si vous dépassez cette limite, vous obtenez un MOO, ce qui peut se produire plusieurs fois lorsque vous utilisez des bitmaps.
Plusieurs fois, une application peut avoir besoin de surmonter ces limitations, d'effectuer des opérations lourdes sur d'énormes bitmaps ou simplement de les stocker pour une utilisation ultérieure, et vous devez
Ce que j'ai trouvé, c'est une simple classe Java qui rendrait les choses un peu plus faciles à ces fins.
Il utilise JNI pour stocker les données bitmap et pouvoir les restaurer en cas de besoin.
Afin de prendre en charge plusieurs instances de la classe, j'ai dû utiliser une astuce que j'ai trouvée ( ici ).
Notes IMPORTANTES
Les données sont toujours stockées dans la RAM, donc si l'appareil n'a pas assez de RAM, l'application peut être supprimée.
N'oubliez pas de libérer la mémoire dès que possible. ce n'est pas seulement pour éviter les fuites de mémoire, mais c'est aussi pour éviter d'être priorisé par le système à tuer en premier, une fois que votre application revient en arrière-plan.
Si vous ne voulez pas oublier de libérer la mémoire, vous pouvez soit la libérer à chaque fois que vous restaurez le bitmap, soit rendre l'implémentation de la classe Closable .
Par mesure de sécurité, je l'ai fait libérer automatiquement sa mémoire native dans la méthode finalize (), mais ne le laissez pas être responsable du travail. c'est trop risqué. je l'ai également fait écrire dans le journal quand une telle chose se produit.
La façon dont cela fonctionne consiste à copier l'intégralité des données dans des objets JNI, et afin de restaurer, il crée le bitmap à partir de zéro et place les données à l'intérieur.
Les bitmaps utilisés et restaurés sont au format ARGB_8888. bien sûr, vous pouvez le changer comme vous le souhaitez, n'oubliez pas de changer le code ...
Les bitmaps volumineux peuvent prendre du temps à stocker et à restaurer, il peut donc être judicieux de le faire sur un thread d'arrière-plan.
Ce n'est pas une solution de MOO complète, mais cela pourrait aider. par exemple, vous pouvez l'utiliser en conjonction avec votre propre LruCache, tout en évitant d'utiliser la mémoire du tas pour le cache lui-même.
Le code est uniquement destiné au stockage et à la restauration. si vous devez effectuer certaines opérations, vous devrez effectuer des recherches. openCV pourrait être la réponse, mais si vous souhaitez effectuer des tâches de base, vous pouvez les implémenter vous-même (voici un exemple de grandes images rotatives à l'aide de JNI). si vous connaissez d'autres alternatives, faites-le moi savoir, ici .
J'espère que cela sera utile pour certaines personnes. veuillez noter vos commentaires.
De plus, si vous rencontrez un problème avec le code ou une suggestion d'amélioration, veuillez me le faire savoir.
Meilleure solution
Si vous souhaitez effectuer encore plus d'opérations du côté JNI, vous pouvez utiliser ce message que j'ai fait. il est basé sur le code que j'ai écrit ici, mais vous permet de faire plus d'opérations et vous pouvez facilement en ajouter d'autres.
explication
l'exemple de code montre comment stocker 2 bitmaps différents (petits, mais ce n'est qu'une démo), recycler les originaux Java ceux, puis les restaurer sur Java = instances et les utiliser.
comme vous pouvez le deviner, la mise en page a 2 imagesViews. je ne l'ai pas inclus dans le code car c'est assez évident.
n'oubliez pas de changer le code dans votre propre package si vous en avez besoin, sinon les choses ne fonctionneront pas.
MainActivity.Java - comment utiliser:
package com.example.jnibitmapstoragetest;
...
public class MainActivity extends Activity
{
@Override
protected void onCreate(final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
//
Bitmap bitmap=BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher);
final JniBitmapHolder bitmapHolder=new JniBitmapHolder(bitmap);
bitmap.recycle();
//
Bitmap bitmap2=BitmapFactory.decodeResource(getResources(),Android.R.drawable.sym_action_call);
final JniBitmapHolder bitmapHolder2=new JniBitmapHolder(bitmap2);
bitmap2.recycle();
//
setContentView(R.layout.activity_main);
{
bitmap=bitmapHolder.getBitmapAndFree();
final ImageView imageView=(ImageView)findViewById(R.id.imageView1);
imageView.setImageBitmap(bitmap);
}
{
bitmap2=bitmapHolder2.getBitmapAndFree();
final ImageView imageView=(ImageView)findViewById(R.id.imageView2);
imageView.setImageBitmap(bitmap2);
}
}
}
JniBitmapHolder.Java - le "pont" entre JNI et Java:
package com.example.jnibitmapstoragetest;
...
public class JniBitmapHolder
{
ByteBuffer _handler =null;
static
{
System.loadLibrary("JniBitmapStorageTest");
}
private native ByteBuffer jniStoreBitmapData(Bitmap bitmap);
private native Bitmap jniGetBitmapFromStoredBitmapData(ByteBuffer handler);
private native void jniFreeBitmapData(ByteBuffer handler);
public JniBitmapHolder()
{}
public JniBitmapHolder(final Bitmap bitmap)
{
storeBitmap(bitmap);
}
public void storeBitmap(final Bitmap bitmap)
{
if(_handler!=null)
freeBitmap();
_handler=jniStoreBitmapData(bitmap);
}
public Bitmap getBitmap()
{
if(_handler==null)
return null;
return jniGetBitmapFromStoredBitmapData(_handler);
}
public Bitmap getBitmapAndFree()
{
final Bitmap bitmap=getBitmap();
freeBitmap();
return bitmap;
}
public void freeBitmap()
{
if(_handler==null)
return;
jniFreeBitmapData(_handler);
_handler=null;
}
@Override
protected void finalize() throws Throwable
{
super.finalize();
if(_handler==null)
return;
Log.w("DEBUG","JNI bitmap wasn't freed nicely.please rememeber to free the bitmap as soon as you can");
freeBitmap();
}
}
Android.mk - le fichier de propriétés de JNI:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := JniBitmapStorageTest
LOCAL_SRC_FILES := JniBitmapStorageTest.cpp
LOCAL_LDLIBS := -llog
LOCAL_LDFLAGS += -ljnigraphics
include $(BUILD_SHARED_LIBRARY)
APP_OPTIM := debug
LOCAL_CFLAGS := -g
JniBitmapStorageTest.cpp - le truc "magique" va ici:
#include <jni.h>
#include <jni.h>
#include <Android/log.h>
#include <stdio.h>
#include <Android/bitmap.h>
#include <cstring>
#include <unistd.h>
#define LOG_TAG "DEBUG"
#define LOGD(...) __Android_log_print(Android_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __Android_log_print(Android_LOG_ERROR,LOG_TAG,__VA_ARGS__)
extern "C"
{
JNIEXPORT jobject JNICALL Java_com_example_jnibitmapstoragetest_JniBitmapHolder_jniStoreBitmapData(JNIEnv * env, jobject obj, jobject bitmap);
JNIEXPORT jobject JNICALL Java_com_example_jnibitmapstoragetest_JniBitmapHolder_jniGetBitmapFromStoredBitmapData(JNIEnv * env, jobject obj, jobject handle);
JNIEXPORT void JNICALL Java_com_example_jnibitmapstoragetest_JniBitmapHolder_jniFreeBitmapData(JNIEnv * env, jobject obj, jobject handle);
}
class JniBitmap
{
public:
uint32_t* _storedBitmapPixels;
AndroidBitmapInfo _bitmapInfo;
JniBitmap()
{
_storedBitmapPixels = NULL;
}
};
JNIEXPORT void JNICALL Java_com_example_jnibitmapstoragetest_JniBitmapHolder_jniFreeBitmapData(JNIEnv * env, jobject obj, jobject handle)
{
JniBitmap* jniBitmap = (JniBitmap*) env->GetDirectBufferAddress(handle);
if (jniBitmap->_storedBitmapPixels == NULL)
return;
delete[] jniBitmap->_storedBitmapPixels;
jniBitmap->_storedBitmapPixels = NULL;
delete jniBitmap;
}
JNIEXPORT jobject JNICALL Java_com_example_jnibitmapstoragetest_JniBitmapHolder_jniGetBitmapFromStoredBitmapData(JNIEnv * env, jobject obj, jobject handle)
{
JniBitmap* jniBitmap = (JniBitmap*) env->GetDirectBufferAddress(handle);
if (jniBitmap->_storedBitmapPixels == NULL)
{
LOGD("no bitmap data was stored. returning null...");
return NULL;
}
//
//creating a new bitmap to put the pixels into it - using Bitmap Bitmap.createBitmap (int width, int height, Bitmap.Config config) :
//
//LOGD("creating new bitmap...");
jclass bitmapCls = env->FindClass("Android/graphics/Bitmap");
jmethodID createBitmapFunction = env->GetStaticMethodID(bitmapCls, "createBitmap", "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
jstring configName = env->NewStringUTF("ARGB_8888");
jclass bitmapConfigClass = env->FindClass("Android/graphics/Bitmap$Config");
jmethodID valueOfBitmapConfigFunction = env->GetStaticMethodID(bitmapConfigClass, "valueOf", "(Ljava/lang/String;)Landroid/graphics/Bitmap$Config;");
jobject bitmapConfig = env->CallStaticObjectMethod(bitmapConfigClass, valueOfBitmapConfigFunction, configName);
jobject newBitmap = env->CallStaticObjectMethod(bitmapCls, createBitmapFunction, jniBitmap->_bitmapInfo.height, jniBitmap->_bitmapInfo.width, bitmapConfig);
//
// putting the pixels into the new bitmap:
//
int ret;
void* bitmapPixels;
if ((ret = AndroidBitmap_lockPixels(env, newBitmap, &bitmapPixels)) < 0)
{
LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
return NULL;
}
uint32_t* newBitmapPixels = (uint32_t*) bitmapPixels;
int pixelsCount = jniBitmap->_bitmapInfo.height * jniBitmap->_bitmapInfo.width;
memcpy(newBitmapPixels, jniBitmap->_storedBitmapPixels, sizeof(uint32_t) * pixelsCount);
AndroidBitmap_unlockPixels(env, newBitmap);
//LOGD("returning the new bitmap");
return newBitmap;
}
JNIEXPORT jobject JNICALL Java_com_example_jnibitmapstoragetest_JniBitmapHolder_jniStoreBitmapData(JNIEnv * env, jobject obj, jobject bitmap)
{
AndroidBitmapInfo bitmapInfo;
uint32_t* storedBitmapPixels = NULL;
//LOGD("reading bitmap info...");
int ret;
if ((ret = AndroidBitmap_getInfo(env, bitmap, &bitmapInfo)) < 0)
{
LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
return NULL;
}
LOGD("width:%d height:%d stride:%d", bitmapInfo.width, bitmapInfo.height, bitmapInfo.stride);
if (bitmapInfo.format != Android_BITMAP_FORMAT_RGBA_8888)
{
LOGE("Bitmap format is not RGBA_8888!");
return NULL;
}
//
//read pixels of bitmap into native memory :
//
//LOGD("reading bitmap pixels...");
void* bitmapPixels;
if ((ret = AndroidBitmap_lockPixels(env, bitmap, &bitmapPixels)) < 0)
{
LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
return NULL;
}
uint32_t* src = (uint32_t*) bitmapPixels;
storedBitmapPixels = new uint32_t[bitmapInfo.height * bitmapInfo.width];
int pixelsCount = bitmapInfo.height * bitmapInfo.width;
memcpy(storedBitmapPixels, src, sizeof(uint32_t) * pixelsCount);
AndroidBitmap_unlockPixels(env, bitmap);
JniBitmap *jniBitmap = new JniBitmap();
jniBitmap->_bitmapInfo = bitmapInfo;
jniBitmap->_storedBitmapPixels = storedBitmapPixels;
return env->NewDirectByteBuffer(jniBitmap, 0);
}
Si vous souhaitez simplement mettre en cache les bitmaps du tas, une solution plus simple consiste à utiliser la mémoire de parcelle.
C'est le Gist de celui-ci (code complet ci-dessous). Vous pouvez l'utiliser pour des instances Parcelable
autres que Bitmap
. Utilisez-le comme ceci:
private final CachedParcelable<Bitmap> cache = new CachedParcelable<>(Bitmap.CREATOR);
cache.put(bitmap);
bitmap = cache.get();
cache.close();
public final class CachedParcelable<T extends Parcelable> implements AutoCloseable {
private final Parcelable.Creator<T> creator;
private Parcel cache;
public CachedParcelable(Parcelable.Creator<T> creator) {
this.creator = creator;
}
public synchronized T get() {
if (cache == null) return null;
try {
cache.setDataPosition(0);
return creator.createFromParcel(cache);
} catch (BadParcelableException e) {
//
} catch (RuntimeException e) {
if (creator != Bitmap.CREATOR) throw e;
}
return null;
}
public synchronized void put(T value) {
if (cache != null) cache.recycle();
if (value == null) {
cache = null;
return;
}
try {
cache = Parcel.obtain();
value.writeToParcel(cache, 0);
} catch (RuntimeException e) {
if (creator != Bitmap.CREATOR) throw e;
}
}
@Override
public synchronized void close() {
if (cache != null) {
cache.recycle();
cache = null;
}
}
}