I got the following mail from a user of my app:

Hi,

Comic strip is a great app and i love it very much. But there are couple of problem

  1. It force close when i apply FX
  2. When i preview the strip, the pictures turn out black

Please fix it My handphone is samsung galaxy beam andriod 2.1

This user has paid for the "pro" version. He also added the following comment in the market:

(2 stars) on February 18, 2012 (Samsung Galaxy Beam with version 1.5.0) It force close when i fx the pics and the pics turn out blank in the preview page.. Will upgrade to 5 stars when fixed

Interesting use of both carrot ("will upgrade to 5 stars"") and stick (current rating: 2 stars). I think the rating mechanism is pretty harsh on developers, but that's a topic for another post :)

Black images in preview

I started by looking into problem (2) - the black images in preview. This one sounded unusual - I've had no other reports of this problem at all.

I set up an Android 2.1 device in my emulator and set about trying to replicate the issue. I allowed it a 24Mb heap per app, and started to look into the black images in preview.

Galaxy-Beam virtual device

I was pretty surprised to see that there was indeed a problem. In all my testing using other Android API levels I hadn't encountered any such issue. Every time I tried to re-load one of the images for preview I saw the following error in log-cat:

Resolve uri failed on bad Bitmap uri: ...

The strange thing, of course, is that the uri was perfectly fine, works great in all API levels greater than 7, was created by the system using Uri.fromFile(file), and is working fine even in API level 7 when I reload the image in the scene editor activity!

Fix for "resolve uri failed on bad bitmap uri"

Given that the scene-editor was able to load the image just fine, I compared the code I was using to load images in the scene-editor with the code in the preview-activity. I had the following:

// scene-editor-activity snippet - works fine!
Bitmap _b = BitmapFactory.decodeStream(
    rslv.openInputStream(aScene.getBackgroundUri()), null, _opts
);
_img.setImageBitmap(_b);

// preview-activity snippet - fails with 'bad bitmap uri'
ImageView _image = new ImageView(ctx);  
_image.setImageURI(aScene.getBackgroundUri());

It seems that ImageView in Android versions less than 2.2 (API level 8) has a problem with directly resolving perfectly valid Bitmap uri's.

In the scene-editor I was always resolving the Uri to an InputStream using ContentResolver (which you can obtain from the Activity with getContentResolver()), whilst in my preview activity I was simply expecting ImageView to resolve the uri.

The fix for all Android versions was to use the slightly more laborious method of loading the Bitmap via ContentResolver and setting the Bitmap to the ImageView, like this:

// Resolve a uri to a Bitmap
private Bitmap getImageBitmap(Uri aUri) {
    try {
        BitmapFactory.Options _opts = new BitmapFactory.Options();
        _opts.inScaled = false;
        _opts.inSampleSize = 1;
        _opts.inPreferredConfig = config;

        return BitmapFactory.decodeStream(
            rslv.openInputStream(aUri), null, _opts
        );
    } catch (Exception anExc) {
        L.e("loading bitmap", anExc);
        return null;
    }
}

// set a bitmap instead of a uri...
_image.setImageBitmap(getImageBitmap(aScene));

If the Uri really resolves to a missing file then I'll still get black images, but this is only likely in fairly extreme circumstances, and at least the app doesn't crash :). I suppose a better solution would be to return the app icon in such cases.

So to the next problem...

Force-Close while applying FX

My first guess was that this was going to be a VM budget issue. I've had them before, but with the v1.5.0 release I seemed to have largely solved them (no crash reports at all since). Here are some of the issues:

  1. Older Android devices only allow 16Mb to each running app. The generation of devices from about 2 years ago (e.g. original Motorola Droid) often allow 24Mb per app, which is still pretty small for dealing with large images. Current generation (e.g. Samsung Galaxy Mini and S2) allow 64Mb (yay!).
  2. One of the nice things about Android devices is the way in which they integrate with Google's eco-system. For example, the "Gallery" app on most devices shows images from your Picasa Web Albums, as well as photos taken directly on the device. Of course, this means that the phone has access to potentially very large images taken with a "real" camera.
  3. Many mobiles these days have 8MP camera's built in, therefore a single photo can be very large!
  4. The nature of my app (making comic strips from your own images) means that I am dealing with potentially many images at any given time. Applying FX requires at least two such images concurrently in-memory (the source, and the target). The finished strips are rendered as rows of 350x350 images, so the size of that final bitmap depends on how many frames you add to your strip.

A quick investigation revealed that I wasn't exceeding the VM budget - nowhere near in fact: the app crashed frequently with the VM size still less than 7Mb. Switching back and forth between API levels 7 and 8 showed that this was definitely only a problem at API level 7 (Android 2.1).

In 2.2 and above I can go through all of the FX several times over with no problems. In 2.1 the app usually crashes at the application of the second effect, but sometimes goes at the first or third attempt.

SIGSEGV while recycling Bitmap's

My app allows you to apply some image effects to the photos you select for each frame, to give a more comic-book feel. For example, you can apply a half-tone print effect, or a quantised and outlined "cartoon" effect.

To process these effects I have to juggle multiple Bitmap's and Canvas's, and - because of the resource-constrained environment of a mobile device - clean up the memory that these objects were using as soon as they are no longer needed.

To make the user-experience more friendly the FX are processed in a background thread. On the UI thread I show a dialog with a spinner to let the user know something is happening. This is nothing special - I'm using the AsyncTask class provided by the Android framework for exactly this purpose.

In Android - pre Honeycomb - Bitmap memory is allocated off-heap by JNI calls in the Bitmap class. It doesn't gain you extra memory to play with in your VM - the bitmap pixel data is still counted within the total memory used by your app (witness the number of StackOverflow questions pertaining to Bitmap's and VM budget!). In Honeycomb the bitmap pixel data has moved into the VM

As soon as you're done with a Bitmap, you are supposed to let the Runtime know, by invoking Bitmap.recycle(), then null'ing the reference to the Bitmap. Fine, my app works great on API levels above 7 - no crashes, no warnings, no memory leaks.

At API level 7 (Android 2.1) however, this is what happens:

02-19 09:41:19.710: I/DEBUG(28): *** *** *** *** *** *** *** *** *** 
    *** *** *** *** *** *** ***
02-19 09:41:19.710: I/DEBUG(28): Build fingerprint: 
    'generic/sdk/generic/:2.1-update1/ECLAIR/35983:eng/test-keys'
02-19 09:41:19.710: I/DEBUG(28): pid: 224, tid: 234  
    >>> com.roundwoodstudios.comicstripitpro <<<
02-19 09:41:19.710: I/DEBUG(28): signal 11 (SIGSEGV), fault addr 00000028
02-19 09:41:19.720: I/DEBUG(28):  
    r0 00000000  r1 0012715c  r2 00000000  r3 0012715c
02-19 09:41:19.720: I/DEBUG(28):  
    r4 00137e18  r5 0012719c  r6 00000000  r7 00000000
02-19 09:41:19.720: I/DEBUG(28):  
    r8 00000001  r9 00000000  10 00000000  fp 00000000
02-19 09:41:19.720: I/DEBUG(28):  
    ip ff000000  sp 47285c58  lr 00000000  pc ac065288  
    cpsr 60000010
02-19 09:41:19.840: I/DEBUG(28):  #00  pc 00065288  /system/lib/libskia.so
02-19 09:41:19.840: I/DEBUG(28):  #01  pc 00065dcc  /system/lib/libskia.so
02-19 09:41:19.840: I/DEBUG(28):  #02  pc 00064148  /system/lib/libskia.so
02-19 09:41:19.840: I/DEBUG(28):  #03  pc 00041986  
    /system/lib/libandroid_runtime.so
02-19 09:41:19.850: I/DEBUG(28):  #04  pc 0000f1f4  /system/lib/libdvm.so
02-19 09:41:19.850: I/DEBUG(28):  #05  pc 00037f90  /system/lib/libdvm.so
02-19 09:41:19.850: I/DEBUG(28):  #06  pc 00031612  /system/lib/libdvm.so
02-19 09:41:19.860: I/DEBUG(28):  #07  pc 00013f58  /system/lib/libdvm.so
02-19 09:41:19.860: I/DEBUG(28):  #08  pc 00019888  /system/lib/libdvm.so
02-19 09:41:19.860: I/DEBUG(28):  #09  pc 00018d5c  /system/lib/libdvm.so
02-19 09:41:19.880: I/DEBUG(28):  #10  pc 0004d6d0  /system/lib/libdvm.so
02-19 09:41:19.880: I/DEBUG(28):  #11  pc 0004d702  /system/lib/libdvm.so
02-19 09:41:19.880: I/DEBUG(28):  #12  pc 00041c78  /system/lib/libdvm.so
02-19 09:41:19.890: I/DEBUG(28):  #13  pc 00010000  /system/lib/libc.so
02-19 09:41:19.890: I/DEBUG(28):  #14  pc 0000fad4  /system/lib/libc.so
02-19 09:41:19.890: I/DEBUG(28): code around pc:
02-19 09:41:19.890: I/DEBUG(28): ac065278 e1d4e2f4 e1d472f6 e5946004 e197200e 
02-19 09:41:19.890: I/DEBUG(28): ac065288 e5969028 e596a024 0a00002e e59db00c 
02-19 09:41:19.900: I/DEBUG(28): ac065298 e2848028 e1a0c008 e8bb000f e8ac000f 
02-19 09:41:19.900: I/DEBUG(28): code around lr:
02-19 09:41:19.900: I/DEBUG(28): stack:
02-19 09:41:19.900: I/DEBUG(28):     47285c18  4001d001  
    /dev/ashmem/mspace/dalvik-heap/zygote/0 (deleted)
02-19 09:41:19.900: I/DEBUG(28):     47285c1c  ad04d21d  /system/lib/libdvm.so
02-19 09:41:19.900: I/DEBUG(28):     47285c20  00000000  
02-19 09:41:19.910: I/DEBUG(28):     47285c24  00010002  [heap]
02-19 09:41:19.910: I/DEBUG(28):     47285c28  00010002  [heap]
02-19 09:41:19.910: I/DEBUG(28):     47285c2c  418ab254  
    /dev/ashmem/dalvik-LinearAlloc (deleted)
02-19 09:41:19.910: I/DEBUG(28):     47285c30  0012a0f8  [heap]
02-19 09:41:19.910: I/DEBUG(28):     47285c34  ad04d6d9  /system/lib/libdvm.so
02-19 09:41:19.910: I/DEBUG(28):     47285c38  ad07ff50  /system/lib/libdvm.so
02-19 09:41:19.910: I/DEBUG(28):     47285c3c  42ab4edd  
    /data/dalvik-cache/system@framework@framework.jar@classes.dex
02-19 09:41:19.910: I/DEBUG(28):     47285c40  47285c48  
02-19 09:41:19.910: I/DEBUG(28):     47285c44  00000001  
02-19 09:41:19.910: I/DEBUG(28):     47285c48  00000001  
02-19 09:41:19.910: I/DEBUG(28):     47285c4c  00000007  
02-19 09:41:19.910: I/DEBUG(28):     47285c50  df002777  
02-19 09:41:19.920: I/DEBUG(28):     47285c54  e3a070ad  
02-19 09:41:19.920: I/DEBUG(28): #00 47285c58  44ebe8a0  
    /dev/ashmem/mspace/dalvik-heap/2 (deleted)
02-19 09:41:19.920: I/DEBUG(28):     47285c5c  0012a0f8  [heap]
02-19 09:41:19.920: I/DEBUG(28):     47285c60  418ab254  
    /dev/ashmem/dalvik-LinearAlloc (deleted)
02-19 09:41:19.920: I/DEBUG(28):     47285c64  00127174  [heap]
02-19 09:41:19.920: I/DEBUG(28):     47285c68  47285c70  
02-19 09:41:19.920: I/DEBUG(28):     47285c6c  47285cd4  
02-19 09:41:19.930: I/DEBUG(28):     47285c70  000000f0  
02-19 09:41:19.930: I/DEBUG(28):     47285c74  00127128  [heap]
02-19 09:41:19.930: I/DEBUG(28):     47285c78  000000e4  
02-19 09:41:19.930: I/DEBUG(28):     47285c7c  0012a0f8  [heap]
02-19 09:41:19.930: I/DEBUG(28):     47285c80  00000001  
02-19 09:41:19.930: I/DEBUG(28):     47285c84  00000007  
02-19 09:41:19.930: I/DEBUG(28):     47285c88  00000001  
02-19 09:41:19.941: I/DEBUG(28):     47285c8c  ad040a89  /system/lib/libdvm.so
02-19 09:41:19.941: I/DEBUG(28):     47285c90  00000000  
02-19 09:41:19.941: I/DEBUG(28):     47285c94  0012a0f8  [heap]
02-19 09:41:19.941: I/DEBUG(28):     47285c98  ad07ecc0  /system/lib/libdvm.so
02-19 09:41:19.941: I/DEBUG(28):     47285c9c  ad03775b  /system/lib/libdvm.so
02-19 09:41:19.941: I/DEBUG(28):     47285ca0  ad037745  /system/lib/libdvm.so
02-19 09:41:19.941: I/DEBUG(28):     47285ca4  47285d2c  
02-19 09:41:19.941: I/DEBUG(28):     47285ca8  47285cd0  
02-19 09:41:19.941: I/DEBUG(28):     47285cac  00127128  [heap]
02-19 09:41:19.941: I/DEBUG(28):     47285cb0  00000000  
02-19 09:41:19.941: I/DEBUG(28):     47285cb4  00000001  
02-19 09:41:19.950: I/DEBUG(28):     47285cb8  00000000  
02-19 09:41:19.950: I/DEBUG(28):     47285cbc  00000000  
02-19 09:41:19.950: I/DEBUG(28):     47285cc0  00000000  
02-19 09:41:19.950: I/DEBUG(28):     47285cc4  ac065dd0  
    /system/lib/libskia.so
02-19 09:41:19.950: I/DEBUG(28): #01 47285cc8  00000000  
02-19 09:41:19.950: I/DEBUG(28):     47285ccc  00000000  
02-19 09:41:19.950: I/DEBUG(28):     47285cd0  00000000  
02-19 09:41:19.950: I/DEBUG(28):     47285cd4  afe0f2c0  /system/lib/libc.so
02-19 09:41:19.950: I/DEBUG(28):     47285cd8  47285d28  
02-19 09:41:19.950: I/DEBUG(28):     47285cdc  00000000  
02-19 09:41:19.950: I/DEBUG(28):     47285ce0  00000000  
02-19 09:41:19.950: I/DEBUG(28):     47285ce4  00000000  
02-19 09:41:19.950: I/DEBUG(28):     47285ce8  00127128  [heap]
02-19 09:41:19.960: I/DEBUG(28):     47285cec  afe0f3b0  /system/lib/libc.so
02-19 09:41:19.960: I/DEBUG(28):     47285cf0  00000000  
02-19 09:41:19.960: I/DEBUG(28):     47285cf4  afe0f2c0  /system/lib/libc.so
02-19 09:41:19.960: I/DEBUG(28):     47285cf8  00000003  
02-19 09:41:19.960: I/DEBUG(28):     47285cfc  afe3b9bc  
02-19 09:41:19.960: I/DEBUG(28):     47285d00  00137e18  [heap]
02-19 09:41:19.960: I/DEBUG(28):     47285d04  47285d2c  
02-19 09:41:19.960: I/DEBUG(28):     47285d08  00127128  [heap]
02-19 09:41:19.960: I/DEBUG(28):     47285d0c  00000003  
02-19 09:41:19.960: I/DEBUG(28):     47285d10  ffffffff  
02-19 09:41:19.960: I/DEBUG(28):     47285d14  47285d88  
02-19 09:41:19.960: I/DEBUG(28):     47285d18  42f0cd88  
02-19 09:41:19.970: I/DEBUG(28):     47285d1c  42f0cd74  
02-19 09:41:19.980: I/DEBUG(28):     47285d20  0012a0f8  [heap]
02-19 09:41:19.980: I/DEBUG(28):     47285d24  ac06414c  /system/lib/libskia.so
02-19 09:41:21.230: D/Zygote(30): Process 224 terminated by signal (11)
02-19 09:41:21.230: I/WindowManager(52): WIN DEATH: 
    Window{44d330a0 Just a sec! paused=false}
02-19 09:41:21.240: I/ActivityManager(52): Process 
    com.roundwoodstudios.comicstripitpro (pid 224) has died.
02-19 09:41:21.250: I/WindowManager(52): WIN DEATH: Window{44d72738
    com.roundwoodstudios.comicstripitpro/
        com.roundwoodstudios.comicstripit.SceneActivity paused=false}
02-19 09:41:21.320: I/UsageStats(52): Unexpected resume of com.android.launcher 
    while already resumed in com.roundwoodstudios.comicstripitpro

Yep, that's a seg-fault of the Dalvik VM triggered in the libskia library (Android's graphics lib), so I'm pretty screwed here - there's no catch and recover strategy for that! I've tried all sorts of things to try to work around it for Eclair, but so far no joy.

I got quite a few hits on StackOverflow for similar problems. Most seemed to be related to calling recycle, but I often hit the problem even before I recycle - I get blow-outs when creating Bitmaps (and yes, I'm still well within the VM budget, and I even tried allowing a 64Mb heap per app).

This looks like a monstrous bug in Android-2.1 to me. If I can't work around it I'll refund my user (he's a user of the paid version), but I doubt if that will lead to recovery of my previously 4.8 star rating.

Did I mention that I thought the rating mechanism was pretty harsh on developers? :(

Update - a few hours later :)

After some more debugging, I isolated the problem and created the simplest re-construction possible. The following code crashes reliably under API level 7, but runs to completion under API level 8 or above:

package com.roundwoodstudios.bitmaptest;

import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;

public class BitmapActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        for (int i=0; i<100; i++) {
            Log.i("Bitmap Test", "Iteration: " + i);
            Canvas _c = new Canvas();
            _c.drawColor(Color.WHITE);
        }
    }
}

When you look at it like that its fairly clear what's wrong: the canvas isn't really initialised properly yet - it doesn't know how large it is, for example. If I set a bitmap to it first it runs fine even under API level 7:

package com.roundwoodstudios.bitmaptest;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;

public class BitmapActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        for (int i=0; i<100; i++) {
            Log.i("Bitmap Test", "Iteration: " + i);
            Canvas _c = new Canvas();
            Bitmap _b = Bitmap.createBitmap(350, 350, Bitmap.Config.ARGB_8888);
            _c.setBitmap(_b);
            _c.drawColor(Color.WHITE);
        }
    }
}

Phew, that's a relief :)

blog comments powered by Disqus