NeatoCode Techniques
Supporting Low Cost Android Phones

The Android operating system now runs 75% of all smartphones shipped, covering a wide variety of different hardware and price points. Make sure you aren’t crashing on many of them! One odd crash report recently came in from an Alcatel Venture smartphone:

java.lang.OutOfMemoryError: bitmap size exceeds VM budget
  at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
  at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:460)
  at android.graphics.BitmapFactory.decodeResourceStream(BitmapFactory.java:336)
  at android.graphics.drawable.Drawable.createFromResourceStream(Drawable.java:697)
  at android.content.res.Resources.loadDrawable(Resources.java:1709)
  at android.content.res.Resources.getDrawable(Resources.java:581)
  at com.android.internal.policy.impl.PhoneWindow.generateLayout(PhoneWindow.java:2242)
  at com.android.internal.policy.impl.PhoneWindow.installDecor(PhoneWindow.java:2277)
  at com.android.internal.policy.impl.PhoneWindow.getDecorView(PhoneWindow.java:1443)
  at com.actionbarsherlock.internal.ActionBarSherlockCompat.installDecor(ActionBarSherlockCompat.java:898)
  at com.actionbarsherlock.internal.ActionBarSherlockCompat.initActionBar(ActionBarSherlockCompat.java:138)
  at com.actionbarsherlock.internal.ActionBarSherlockCompat.getActionBar(ActionBarSherlockCompat.java:128)
  at com.actionbarsherlock.app.SherlockActivity.getSupportActionBar(SherlockActivity.java:37)
  ...

This phone is a low cost, prepaid phone with a low screen density and little process memory that came out earlier this year. The crash is due to the phone not having enough memory to scale all the graphics resources in the app down to fit the screen, In this particular case it is failing when the ActionBarSherlock library tries to place an ActionBar at the top of the window.

You can help reduce crashes like this by pre-scaling your graphics assets to low density. Here is an example of doing that for normal PNG files in ActionBarSherlock using the Cygwin command prompt with ImageMagick installed:

Lance@LanceLaptop /cygdrive/c/mine/projects/GitHub/Shared/JakeWharton-ActionBarSherlock-4.2.0-0-g90939dc/library/res/drawable-mdpi
$ find -type f -not -name \*.9.png\* -and -exec convert {} -resize %75 ../drawable-ldpi/{} \;

And here is using a cool script I wrote to scale down the remaining Nine Patch PNG files, which have special guideline borders:

Lance@LanceLaptop /cygdrive/c/mine/projects/GitHub/Shared/JakeWharton-ActionBarSherlock-4.2.0-0-g90939dc/library/res/drawable-mdpi
$ find -name \*.9.png\* -exec ../nine-patch-downscale.bsh {} %75 ../drawable-ldpi/{} \;

This is the script:

#!/bin/bash
#
# This shell script scales down an image that is in Android Nine-Patch format 
# ( http://developer.android.com/tools/help/draw9patch.html ). It is useful 
# in situations where low density Android phones with little memory are 
# crashing when trying to scale down resources at runtime and you want to 
# package pre-scaled resources.

if [ $# -ne 3 ]
then
 echo usage $0 INPUT_FILE SCALE_FACTOR OUTPUT_FILE
 echo e.g.
 echo ./$0 abs__ab_bottom_solid_dark_holo.9.png 75% ../drawable-ldpi/abs__ab_bottom_solid_dark_holo.9.png
 exit 1
fi

INPUT_FILE=$1
SCALE_FACTOR=$2
OUTPUT_FILE=$3

WIDTH=`convert $INPUT_FILE -print "%[fx:w]" null:`
HEIGHT=`convert $INPUT_FILE -print "%[fx:h]" null:`
RIGHT_BORDER_POSITION=`expr $WIDTH - 1`
BOTTOM_BORDER_POSITION=`expr $HEIGHT - 1`
CONTENT_WIDTH=`expr $WIDTH - 2`
CONTENT_HEIGHT=`expr $HEIGHT - 2`

#Break 9-patch into guidelines and contents. 
convert $INPUT_FILE -crop 1x${CONTENT_HEIGHT}+0+1 +repage /tmp/left_border.png
convert $INPUT_FILE -crop 1x${CONTENT_HEIGHT}+${RIGHT_BORDER_POSITION}+1 +repage /tmp/right_border.png
convert $INPUT_FILE -crop ${CONTENT_WIDTH}x1+1+0 +repage /tmp/top_border.png
convert $INPUT_FILE -crop ${CONTENT_WIDTH}x1+1+${BOTTOM_BORDER_POSITION} +repage /tmp/bottom_border.png
convert $INPUT_FILE -crop ${CONTENT_WIDTH}x${CONTENT_HEIGHT}+1+1 +repage /tmp/contents.png

#Scale them down.
convert /tmp/left_border.png -scale 1x${SCALE_FACTOR} /tmp/left_border_scaled.png
convert /tmp/right_border.png -scale 1x${SCALE_FACTOR} /tmp/right_border_scaled.png
convert /tmp/top_border.png -scale ${SCALE_FACTOR}x1 /tmp/top_border_scaled.png
convert /tmp/bottom_border.png -scale ${SCALE_FACTOR}x1 /tmp/bottom_border_scaled.png
convert /tmp/contents.png -scale ${SCALE_FACTOR} /tmp/contents_scaled.png

#Create scaled guidelines discarding any partial pixels.
convert /tmp/left_border_scaled.png -channel A -threshold 1 +channel /tmp/left_border_scaled_bw_conservative.png
convert /tmp/right_border_scaled.png -channel A -threshold 1 +channel /tmp/right_border_scaled_bw_conservative.png
convert /tmp/bottom_border_scaled.png -channel A -threshold 1 +channel /tmp/bottom_border_scaled_bw_conservative.png
convert /tmp/top_border_scaled.png -channel A -threshold 1 +channel /tmp/top_border_scaled_bw_conservative.png

#Create scaled guidelines keeping any partial pixels.
convert /tmp/left_border_scaled.png -channel A -threshold 99% +channel /tmp/left_border_scaled_bw_greedy.png
convert /tmp/right_border_scaled.png -channel A -threshold 99% +channel /tmp/right_border_scaled_bw_greedy.png
convert /tmp/bottom_border_scaled.png -channel A -threshold 99% +channel /tmp/bottom_border_scaled_bw_greedy.png
convert /tmp/top_border_scaled.png -channel A -threshold 99% +channel /tmp/top_border_scaled_bw_greedy.png

#Count the number of black pixels in the conservative scaling, use greedy scaling if there are none.
#Negate all pixels turning black into white, fill everything not white with black, count white.
BLACK_PIXELS=`convert /tmp/left_border_scaled_bw_conservative.png -negate -fill black +opaque white -print "%[fx:w*h*mean]" null:`
if [[ $BLACK_PIXELS == '0' ]] ; then LEFT_OUTPUT=left_border_scaled_bw_greedy.png ; else LEFT_OUTPUT=left_border_scaled_bw_conservative.png ; fi
BLACK_PIXELS=`convert /tmp/right_border_scaled_bw_conservative.png -negate -fill black +opaque white -print "%[fx:w*h*mean]" null:`
if [[ $BLACK_PIXELS == '0' ]] ; then RIGHT_OUTPUT=right_border_scaled_bw_greedy.png ; else RIGHT_OUTPUT=right_border_scaled_bw_conservative.png ; fi
BLACK_PIXELS=`convert /tmp/top_border_scaled_bw_conservative.png -negate -fill black +opaque white -print "%[fx:w*h*mean]" null:`
if [[ $BLACK_PIXELS == '0' ]] ; then TOP_OUTPUT=top_border_scaled_bw_greedy.png ; else TOP_OUTPUT=top_border_scaled_bw_conservative.png ; fi
BLACK_PIXELS=`convert /tmp/bottom_border_scaled_bw_conservative.png -negate -fill black +opaque white -print "%[fx:w*h*mean]" null:`
if [[ $BLACK_PIXELS == '0' ]] ; then BOTTOM_OUTPUT=bottom_border_scaled_bw_greedy.png ; else BOTTOM_OUTPUT=bottom_border_scaled_bw_conservative.png ; fi

#Put the image back together.
convert /tmp/$TOP_OUTPUT /tmp/contents_scaled.png /tmp/$BOTTOM_OUTPUT -background transparent -gravity center -append /tmp/output_vertical_parts.png
convert /tmp/$LEFT_OUTPUT /tmp/output_vertical_parts.png /tmp/$RIGHT_OUTPUT -background transparent -gravity center +append $OUTPUT_FILE

I can do a better job by hand, but there are 65 nine patch images in that directory and they would have to be updated any time the higher resolution ones change. The script produces results like this, where the grid in the image represents a single pixel:

Pre-scaling like this helps speed up the user experience on lower density devices, and avoid running out of memory. It’s great that we’re getting so many new users from so many different devices, but be careful that your app still provides a good experience! Expect to see an example of the changes live soon in our fun dice game, Lock n Roll.

  1. neatocode posted this
Blog comments powered by Disqus