Question Étrange problème de mémoire insuffisante lors du chargement d'une image dans un objet Bitmap


J'ai une vue de liste avec un couple de boutons d'image sur chaque rangée. Lorsque vous cliquez sur la ligne de la liste, elle lance une nouvelle activité. J'ai dû construire mes propres onglets en raison d'un problème avec la disposition de la caméra. L'activité lancée pour le résultat est une carte. Si je clique sur mon bouton pour lancer l'aperçu de l'image (charger une image sur la carte SD) l'application revient de l'activité à la listview activité au gestionnaire de résultat pour relancer ma nouvelle activité qui n'est rien de plus qu'un widget d'image.

L'aperçu de l'image sur la liste est en train d'être fait avec le curseur et ListAdapter. Cela rend la chose assez simple, mais je ne suis pas sûr de savoir comment je peux mettre une image redimensionnée (c'est-à-dire plus petite taille de bit pas pixel comme src pour le bouton d'image à la volée. J'ai juste redimensionné l'image qui est sortie de l'appareil photo du téléphone.

Le problème est que je reçois une erreur de mémoire lorsqu'il essaie de revenir en arrière et de relancer la 2ème activité.

  • Est-il possible de construire facilement l'adaptateur de liste, rangée par rangée, où je peux redimensionner à la volée (peu sage)?

Ce serait préférable car j'ai aussi besoin d'apporter quelques modifications aux propriétés des widgets / éléments dans chaque rangée car je suis incapable de sélectionner une ligne avec écran tactile en raison de problème de mise au point. (Je peux utiliser une balle à roulettes.)

  • Je sais que je peux faire un redimensionnement hors bande et sauvegarder mon image, mais ce n'est pas vraiment ce que je veux faire, mais un exemple de code pour ça serait bien.

Dès que j'ai désactivé l'image sur la liste, cela a bien fonctionné.

FYI: Voici comment je le faisais:

String[] from = new String[] { DBHelper.KEY_BUSINESSNAME,DBHelper.KEY_ADDRESS,DBHelper.KEY_CITY,DBHelper.KEY_GPSLONG,DBHelper.KEY_GPSLAT,DBHelper.KEY_IMAGEFILENAME  + ""};
int[] to = new int[] {R.id.businessname,R.id.address,R.id.city,R.id.gpslong,R.id.gpslat,R.id.imagefilename };
notes = new SimpleCursorAdapter(this, R.layout.notes_row, c, from, to);
setListAdapter(notes);

R.id.imagefilename est un ButtonImage.

Voici mon LogCat:

01-25 05:05:49.877: ERROR/dalvikvm-heap(3896): 6291456-byte external allocation too large for this process.
01-25 05:05:49.877: ERROR/(3896): VM wont let us allocate 6291456 bytes
01-25 05:05:49.877: ERROR/AndroidRuntime(3896): Uncaught handler: thread main exiting due to uncaught exception
01-25 05:05:49.917: ERROR/AndroidRuntime(3896): java.lang.OutOfMemoryError: bitmap size exceeds VM budget
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:304)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.decodeFile(BitmapFactory.java:149)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.decodeFile(BitmapFactory.java:174)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.drawable.Drawable.createFromPath(Drawable.java:729)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ImageView.resolveUri(ImageView.java:484)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ImageView.setImageURI(ImageView.java:281)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.SimpleCursorAdapter.setViewImage(SimpleCursorAdapter.java:183)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.SimpleCursorAdapter.bindView(SimpleCursorAdapter.java:129)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.CursorAdapter.getView(CursorAdapter.java:150)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.AbsListView.obtainView(AbsListView.java:1057)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ListView.makeAndAddView(ListView.java:1616)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ListView.fillSpecific(ListView.java:1177)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ListView.layoutChildren(ListView.java:1454)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.AbsListView.onLayout(AbsListView.java:937)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1119)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.layoutHorizontal(LinearLayout.java:1108)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.onLayout(LinearLayout.java:922)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.FrameLayout.onLayout(FrameLayout.java:294)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1119)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.layoutVertical(LinearLayout.java:999)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.onLayout(LinearLayout.java:920)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.FrameLayout.onLayout(FrameLayout.java:294)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.ViewRoot.performTraversals(ViewRoot.java:771)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.ViewRoot.handleMessage(ViewRoot.java:1103)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.os.Handler.dispatchMessage(Handler.java:88)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.os.Looper.loop(Looper.java:123)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.app.ActivityThread.main(ActivityThread.java:3742)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at java.lang.reflect.Method.invokeNative(Native Method)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at java.lang.reflect.Method.invoke(Method.java:515)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:739)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:497)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at dalvik.system.NativeStart.main(Native Method)
01-25 05:10:01.127: ERROR/AndroidRuntime(3943): ERROR: thread attach failed 

J'ai aussi une nouvelle erreur lors de l'affichage d'une image:

01-25 22:13:18.594: DEBUG/skia(4204): xxxxxxxxxxx jpeg error 20 Improper call to JPEG library in state %d
01-25 22:13:18.604: INFO/System.out(4204): resolveUri failed on bad bitmap uri: 
01-25 22:13:18.694: ERROR/dalvikvm-heap(4204): 6291456-byte external allocation too large for this process.
01-25 22:13:18.694: ERROR/(4204): VM won't let us allocate 6291456 bytes
01-25 22:13:18.694: DEBUG/skia(4204): xxxxxxxxxxxxxxxxxxxx allocPixelRef failed

1105


origine


Réponses:


le Formation Android classe, "Affichage efficace des bitmaps", offre de bonnes informations pour comprendre et gérer l'exception java.lang.OutOfMemoryError: bitmap size exceeds VM budget lors du chargement de Bitmaps.


Lire les dimensions et le type de bitmap

le BitmapFactory La classe fournit plusieurs méthodes de décodage (decodeByteArray(), decodeFile(), decodeResource(), etc.) pour créer un Bitmap de diverses sources. Choisissez la méthode de décodage la plus appropriée en fonction de votre source de données d'image. Ces méthodes tentent d'allouer de la mémoire pour le bitmap construit et peuvent donc facilement entraîner une OutOfMemory exception. Chaque type de méthode de décodage possède des signatures supplémentaires qui vous permettent de spécifier des options de décodage via le BitmapFactory.Options classe. Réglage de la inJustDecodeBounds propriété à true tandis que le décodage évite l'allocation de mémoire, le retour null pour l'objet bitmap, mais le réglage outWidth, outHeight et outMimeType. Cette technique vous permet de lire les dimensions et le type des données d'image avant la construction (et l'allocation de mémoire) de l'image bitmap.

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

Éviter java.lang.OutOfMemory exceptions, vérifiez les dimensions d'un bitmap avant de le décoder, à moins que vous ne fassiez absolument confiance à la source pour vous fournir des données d'image de taille prévisible qui s'intègrent confortablement dans la mémoire disponible.


Charger une version réduite dans la mémoire

Maintenant que les dimensions de l'image sont connues, elles peuvent être utilisées pour décider si l'image complète doit être chargée en mémoire ou si une version sous-échantillonnée doit être chargée à la place. Voici quelques facteurs à considérer:

  • Estimation de l'utilisation de la mémoire lors du chargement de l'image complète en mémoire.
  • La quantité de mémoire que vous êtes prêt à consacrer au chargement de cette image compte tenu des autres besoins en mémoire de votre application.
  • Dimensions du composant ImageView ou UI cible dans lequel l'image doit être chargée.
  • Taille et densité de l'écran de l'appareil actuel

Par exemple, il ne vaut pas la peine de charger une image de 1024 x 768 pixels dans la mémoire si elle sera éventuellement affichée dans une miniature de 128 x 96 pixels dans un ImageView.

Pour indiquer au décodeur de sous-échantillonner l'image, en chargeant une version plus petite dans la mémoire, réglez inSampleSize à true dans ton BitmapFactory.Options objet. Par exemple, une image avec la résolution 2048x1536 qui est décodée avec un inSampleSize de 4 produit un bitmap d'environ 512x384. Chargement en mémoire utilise 0,75 Mo au lieu de 12 Mo pour l'image complète (en supposant une configuration bitmap de ARGB_8888). Voici une méthode pour calculer une valeur de taille d'échantillon qui est une puissance de deux basée sur une largeur et une hauteur cibles:

public static int calculateInSampleSize(
        BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

Remarque: Une puissance de deux valeur est calculée car le décodeur utilise un   arrondi à la puissance la plus proche de deux, conformément à la    inSampleSize Documentation.

Pour utiliser cette méthode, commencez par décoder avec inJustDecodeBounds mis à true, passez les options à travers puis décoder à nouveau en utilisant le nouveau inSampleSize valeur et inJustDecodeBounds mis à false:

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
    int reqWidth, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

Cette méthode facilite le chargement d'un bitmap de taille arbitrairement grande dans un ImageView qui affiche une miniature de 100 x 100 pixels, comme indiqué dans l'exemple de code suivant:

mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

Vous pouvez suivre un processus similaire pour décoder les bitmaps d'autres sources, en remplaçant BitmapFactory.decode* méthode au besoin.


558



Pour corriger l'erreur OutOfMemory, vous devriez faire quelque chose comme ceci:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 8;
Bitmap preview_bitmap = BitmapFactory.decodeStream(is, null, options);

Ce inSampleSize option réduit la consommation de mémoire.

Voici une méthode complète. D'abord, il lit la taille de l'image sans décoder le contenu lui-même. Ensuite, il trouve le meilleur inSampleSize valeur, il devrait être une puissance de 2, et enfin l'image est décodée.

// Decodes image and scales it to reduce memory consumption
private Bitmap decodeFile(File f) {
    try {
        // Decode image size
        BitmapFactory.Options o = new BitmapFactory.Options();
        o.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(new FileInputStream(f), null, o);

        // The new size we want to scale to
        final int REQUIRED_SIZE=70;

        // Find the correct scale value. It should be the power of 2.
        int scale = 1;
        while(o.outWidth / scale / 2 >= REQUIRED_SIZE && 
              o.outHeight / scale / 2 >= REQUIRED_SIZE) {
            scale *= 2;
        }

        // Decode with inSampleSize
        BitmapFactory.Options o2 = new BitmapFactory.Options();
        o2.inSampleSize = scale;
        return BitmapFactory.decodeStream(new FileInputStream(f), null, o2);
    } catch (FileNotFoundException e) {}
    return null;
}

856



J'ai fait une petite amélioration au code de Fedor. Il fait essentiellement la même chose, mais sans (à mon avis) laide tout en boucle et il en résulte toujours une puissance de deux. Bravo à Fedor pour avoir fait la solution originale, j'étais bloqué jusqu'à ce que je trouve le sien, et ensuite j'ai pu faire celui-ci :)

 private Bitmap decodeFile(File f){
    Bitmap b = null;

        //Decode image size
    BitmapFactory.Options o = new BitmapFactory.Options();
    o.inJustDecodeBounds = true;

    FileInputStream fis = new FileInputStream(f);
    BitmapFactory.decodeStream(fis, null, o);
    fis.close();

    int scale = 1;
    if (o.outHeight > IMAGE_MAX_SIZE || o.outWidth > IMAGE_MAX_SIZE) {
        scale = (int)Math.pow(2, (int) Math.ceil(Math.log(IMAGE_MAX_SIZE / 
           (double) Math.max(o.outHeight, o.outWidth)) / Math.log(0.5)));
    }

    //Decode with inSampleSize
    BitmapFactory.Options o2 = new BitmapFactory.Options();
    o2.inSampleSize = scale;
    fis = new FileInputStream(f);
    b = BitmapFactory.decodeStream(fis, null, o2);
    fis.close();

    return b;
}

352



Je viens de l'expérience iOS et j'étais frustré de découvrir un problème avec quelque chose d'aussi basique que le chargement et la présentation d'une image. Après tout, tout le monde qui a ce problème essaie d'afficher des images de taille raisonnable. Quoi qu'il en soit, voici les deux changements qui ont résolu mon problème (et rendu mon application très réactif).

1) Chaque fois que vous faites BitmapFactory.decodeXYZ(), assurez-vous de passer dans un BitmapFactory.Options avec inPurgeable mis à true (et de préférence avec inInputShareable également mis à true).

2) N'utilisez JAMAIS Bitmap.createBitmap(width, height, Config.ARGB_8888). Je veux dire JAMAIS! Je n'ai jamais eu cette chose ne pas augmenter l'erreur de mémoire après quelques passages. Aucune quantité de recycle(), System.gc(), tout ce qui a aidé. Il a toujours soulevé l'exception. L'autre façon qui fonctionne réellement est d'avoir une image fictive dans vos drawables (ou un autre bitmap que vous avez décodé en utilisant l'étape 1 ci-dessus), redimensionnez-le comme vous voulez, puis manipulez le Bitmap résultant (comme le transmettre à un Canvas pour plus de plaisir). Donc, ce que vous devriez utiliser à la place est: Bitmap.createScaledBitmap(srcBitmap, width, height, false). Si pour une raison quelconque, vous devez utiliser la méthode brute force create, alors au moins passer Config.ARGB_4444.

C'est presque garanti de vous faire économiser des heures sinon des jours. Tout ce qui parle de la mise à l'échelle de l'image, etc. ne fonctionne pas vraiment (sauf si vous envisagez d'obtenir une mauvaise taille ou une image dégradée une solution).


220



C'est un bug connu, ce n'est pas à cause de gros fichiers. Depuis Android met en cache les Drawables, il est à court de mémoire après l'utilisation de quelques images. Mais j'ai trouvé un moyen alternatif, en sautant le système de cache par défaut Android.

Solution: Déplacez les images vers le dossier "assets" et utilisez la fonction suivante pour obtenir BitmapDrawable:

public static Drawable getAssetImage(Context context, String filename) throws IOException {
    AssetManager assets = context.getResources().getAssets();
    InputStream buffer = new BufferedInputStream((assets.open("drawable/" + filename + ".png")));
    Bitmap bitmap = BitmapFactory.decodeStream(buffer);
    return new BitmapDrawable(context.getResources(), bitmap);
}

84



J'ai eu ce même problème et l'ai résolu en évitant les fonctions BitmapFactory.decodeStream ou decodeFile et à la place utilisé BitmapFactory.decodeFileDescriptor

decodeFileDescriptor On dirait qu'il appelle différentes méthodes natives que le decodeStream / decodeFile.

Quoi qu'il en soit, ce qui a fonctionné était ceci (notez que j'ai ajouté quelques options comme ci-dessus, mais ce n'est pas ce qui a fait la différence. BitmapFactory.decodeFileDescriptor au lieu de decodeStream ou decodeFile):

private void showImage(String path)   {
    Log.i("showImage","loading:"+path);
    BitmapFactory.Options bfOptions=new BitmapFactory.Options();
    bfOptions.inDither=false;                     //Disable Dithering mode
    bfOptions.inPurgeable=true;                   //Tell to gc that whether it needs free memory, the Bitmap can be cleared
    bfOptions.inInputShareable=true;              //Which kind of reference will be used to recover the Bitmap data after being clear, when it will be used in the future
    bfOptions.inTempStorage=new byte[32 * 1024]; 


    File file=new File(path);
    FileInputStream fs=null;
    try {
        fs = new FileInputStream(file);
    } catch (FileNotFoundException e) {
        //TODO do something intelligent
        e.printStackTrace();
    }

    try {
        if(fs!=null) bm=BitmapFactory.decodeFileDescriptor(fs.getFD(), null, bfOptions);
    } catch (IOException e) {
        //TODO do something intelligent
        e.printStackTrace();
    } finally{ 
        if(fs!=null) {
            try {
                fs.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
    //bm=BitmapFactory.decodeFile(path, bfOptions); This one causes error: java.lang.OutOfMemoryError: bitmap size exceeds VM budget

    im.setImageBitmap(bm);
    //bm.recycle();
    bm=null;



}

Je pense qu'il y a un problème avec la fonction native utilisée dans decodeStream / decodeFile. J'ai confirmé qu'une méthode native différente est appelée lors de l'utilisation de decodeFileDescriptor. Aussi ce que j'ai lu est "que les images (bitmaps) ne sont pas allouées d'une manière Java standard, mais via des appels natifs, les allocations sont faites en dehors du tas virtuel, mais sont compté contre!"


69



Je pense que la meilleure façon d'éviter le OutOfMemoryError est d'y faire face et de le comprendre.

J'ai fait un application intentionnellement provoquer OutOfMemoryErroret surveillez l'utilisation de la mémoire.

Après avoir fait beaucoup d'expériences avec cette application, j'ai les conclusions suivantes:

Je vais parler des versions de SDK avant Honey Comb en premier.

  1. Le bitmap est stocké dans le tas natif, mais il sera automatiquement récupéré, l'appel de recycle () est inutile.

  2. Si {Taille du segment VM} + {Mémoire segmentée native allouée}> = {Limite de taille de segment VM pour le périphérique} et que vous essayez de créer une image bitmap, OOM sera lancé.

    AVIS: VM HEAP SIZE est comptabilisé à la place de VM MEMORY ALLOCATED.

  3. La taille du tas VM ne rétrécira jamais après la croissance, même si la mémoire VM allouée est réduite.

  4. Vous devez donc conserver la mémoire de pointe de la machine virtuelle aussi faible que possible pour éviter que la taille du tas VM ne devienne trop importante pour économiser la mémoire disponible pour les bitmaps.

  5. Appeler manuellement System.gc () n'a aucun sens, le système l'appellera en premier avant d'essayer d'augmenter la taille du tas.

  6. La taille de tas native ne rétrécira jamais aussi, mais elle n'est pas comptabilisée pour MOO, donc pas besoin de s'inquiéter à ce sujet.

Ensuite, parlons de SDK Starts de Honey Comb.

  1. Bitmap est stocké dans le segment VM, la mémoire native n'est pas comptabilisée pour MOO.

  2. La condition de MOO est beaucoup plus simple: {taille de segment VM}> = {limite de taille de tas VM pour le périphérique}.

  3. Vous avez donc plus de mémoire disponible pour créer un bitmap avec la même limite de taille de segment, OOM est moins susceptible d'être lancé.

Voici quelques unes de mes observations à propos de Garbage Collection et Memory Leak.

Vous pouvez le voir vous-même dans l'application. Si une activité exécute une asyncTask qui était toujours en cours d'exécution après la destruction de l'activité, l'activité ne récupère pas les données parasites avant la fin de la tâche asynchrone.

C'est parce que AsyncTask est une instance d'une classe interne anonyme, il contient une référence de l'activité.

L'appel de AsyncTask.cancel (true) n'arrêtera pas l'exécution si la tâche est bloquée dans une opération IO dans le thread d'arrière-plan.

Les rappels sont également des classes internes anonymes. Par conséquent, si une instance statique de votre projet les contient et ne les libère pas, la mémoire risque de fuir.

Si vous planifiez une tâche répétée ou retardée, par exemple un temporisateur, et que vous n'appelez pas cancel () et purge () dans onPause (), la mémoire risque de fuir.


64



J'ai vu beaucoup de questions sur les exceptions de MOO et la mise en cache ces derniers temps. Le guide du développeur a un très bon article sur ce point, mais certains ont tendance à échouer à le mettre en œuvre de manière appropriée.

Pour cette raison, j'ai écrit un exemple d'application qui illustre la mise en cache dans un environnement Android. Cette implémentation n'a pas encore obtenu de MOO.

Regardez la fin de cette réponse pour un lien vers le code source.

Exigences:

  • Android API 2.1 ou supérieur (je n'ai tout simplement pas réussi à obtenir la mémoire disponible pour une application dans l'API 1.6 - c'est le seul morceau de code qui ne fonctionne pas dans l'API 1.6)
  • Pack de support Android

Screenshot

Caractéristiques:

  • Conserve le cache s'il y a un changement d'orientation, en utilisant un singleton
  • Utilisation un huit de la mémoire de l'application affectée au cache (modifiez si vous le souhaitez)
  • Grandes bitmaps obtient à l'échelle (vous pouvez définir le nombre maximal de pixels que vous souhaitez autoriser)
  • Contrôles qu'il y a une connexion Internet disponible avant de télécharger les bitmaps
  • S'assure que vous instanciez seulement une tâche Par rangée
  • Si tu lances la ListView loin, il ne téléchargera tout simplement pas les bitmaps entre

Cela n'inclut pas:

  • Mise en cache de disque Cela devrait être facile à mettre en œuvre de toute façon - pointez simplement sur une tâche différente qui récupère les bitmaps du disque

Exemple de code:

Les images téléchargées sont des images (75x75) de Flickr. Cependant, placez les urls d'images que vous voulez traiter, et l'application réduira la taille si elle dépasse le maximum. Dans cette application, les URL sont simplement dans un Stringtableau.

le LruCache a un bon moyen de gérer les bitmaps. Cependant, dans cette application, j'ai mis une instance d'un LruCache dans une autre classe de cache que j'ai créée pour rendre l'application plus faisable.

Les choses critiques de Cache.java (le loadBitmap() la méthode est la plus importante):

public Cache(int size, int maxWidth, int maxHeight) {
    // Into the constructor you add the maximum pixels
    // that you want to allow in order to not scale images.
    mMaxWidth = maxWidth;
    mMaxHeight = maxHeight;

    mBitmapCache = new LruCache<String, Bitmap>(size) {
        protected int sizeOf(String key, Bitmap b) {
            // Assuming that one pixel contains four bytes.
            return b.getHeight() * b.getWidth() * 4;
        }
    };

    mCurrentTasks = new ArrayList<String>();    
}

/**
 * Gets a bitmap from cache. 
 * If it is not in cache, this method will:
 * 
 * 1: check if the bitmap url is currently being processed in the
 * BitmapLoaderTask and cancel if it is already in a task (a control to see
 * if it's inside the currentTasks list).
 * 
 * 2: check if an internet connection is available and continue if so.
 * 
 * 3: download the bitmap, scale the bitmap if necessary and put it into
 * the memory cache.
 * 
 * 4: Remove the bitmap url from the currentTasks list.
 * 
 * 5: Notify the ListAdapter.
 * 
 * @param mainActivity - Reference to activity object, in order to
 * call notifyDataSetChanged() on the ListAdapter.
 * @param imageKey - The bitmap url (will be the key).
 * @param imageView - The ImageView that should get an
 * available bitmap or a placeholder image.
 * @param isScrolling - If set to true, we skip executing more tasks since
 * the user probably has flinged away the view.
 */
public void loadBitmap(MainActivity mainActivity, 
        String imageKey, ImageView imageView,
        boolean isScrolling) {
    final Bitmap bitmap = getBitmapFromCache(imageKey); 

    if (bitmap != null) {
        imageView.setImageBitmap(bitmap);
    } else {
        imageView.setImageResource(R.drawable.ic_launcher);
        if (!isScrolling && !mCurrentTasks.contains(imageKey) && 
                mainActivity.internetIsAvailable()) {
            BitmapLoaderTask task = new BitmapLoaderTask(imageKey,
                    mainActivity.getAdapter());
            task.execute();
        }
    } 
}

Vous ne devriez pas avoir besoin d'éditer quoi que ce soit dans le fichier Cache.java, sauf si vous voulez implémenter la mise en cache disque.

L'essentiel de MainActivity.java:

public void onScrollStateChanged(AbsListView view, int scrollState) {
    if (view.getId() == android.R.id.list) {
        // Set scrolling to true only if the user has flinged the       
        // ListView away, hence we skip downloading a series
        // of unnecessary bitmaps that the user probably
        // just want to skip anyways. If we scroll slowly it
        // will still download bitmaps - that means
        // that the application won't wait for the user
        // to lift its finger off the screen in order to
        // download.
        if (scrollState == SCROLL_STATE_FLING) {
            mIsScrolling = true;
        } else {
            mIsScrolling = false;
            mListAdapter.notifyDataSetChanged();
        }
    } 
}

// Inside ListAdapter...
@Override
public View getView(final int position, View convertView, ViewGroup parent) {           
    View row = convertView;
    final ViewHolder holder;

    if (row == null) {
        LayoutInflater inflater = getLayoutInflater();
        row = inflater.inflate(R.layout.main_listview_row, parent, false);  
        holder = new ViewHolder(row);
        row.setTag(holder);
    } else {
        holder = (ViewHolder) row.getTag();
    }   

    final Row rowObject = getItem(position);

    // Look at the loadBitmap() method description...
    holder.mTextView.setText(rowObject.mText);      
    mCache.loadBitmap(MainActivity.this,
            rowObject.mBitmapUrl, holder.mImageView,
            mIsScrolling);  

    return row;
}

getView() est appelé très souvent. Ce n'est normalement pas une bonne idée de télécharger des images là-bas si nous n'avons pas implémenté un contrôle qui nous assure que nous ne commencerons pas une quantité infinie de threads par ligne. Cache.java vérifie si le rowObject.mBitmapUrl est déjà dans une tâche et si c'est le cas, il n'en commencera pas un autre. Par conséquent, nous ne dépassons probablement pas la restriction de la file d'attente de travail AsyncTask bassin.

Télécharger:

Vous pouvez télécharger le code source de https://www.dropbox.com/s/pvr9zyl811tfeem/ListViewImageCache.zip.


Derniers mots:

Je l'ai testé pendant quelques semaines maintenant, je n'ai pas encore reçu une seule exception de MOO. J'ai testé cela sur l'émulateur, sur mon Nexus One et sur mon Nexus S. J'ai testé des URL d'images contenant des images en qualité HD. Le seul goulot d'étranglement est que cela prend plus de temps à télécharger.

Il y a seulement un scénario possible où je peux imaginer que le MOO apparaîtra, et si nous téléchargeons beaucoup, vraiment de grandes images, et avant qu'elles soient mises à l'échelle et cachées, prendront simultanément plus de mémoire et causerons un MOO. Mais ce n'est même pas une situation idéale et il ne sera probablement pas possible de le résoudre d'une manière plus réaliste.

Signalez les erreurs dans les commentaires! :-)


58



J'ai fait ce qui suit pour prendre l'image et la redimensionner à la volée. J'espère que cela t'aides

Bitmap bm;
bm = Bitmap.createScaledBitmap(BitmapFactory.decodeFile(filepath), 100, 100, true);
mPicture = new ImageView(context);
mPicture.setImageBitmap(bm);    

36