Thursday, August 23, 2012

[Android] Which alternative image resource will be chosen by the system?

This is a note from my experiments.

Let's say I've supplied images only in drawable-xhdpi and drawable-hdpi folders and now my app is running on device with mdpi screen density. My question is which folder will be chosen by Android system to scale an image to display in mdpi device. Below is a note from my experiments about how Android find the best matching image resource.

Drawable folders structure
drawable-xhdpi (density = 320)
drawable-hdpi (density = 240)
drawable-mdpi (density = 160)
drawable-ldpi (density = 120)

CAUTION: Screen density qualifiers are not supported in Android 1.5 and lower.

Code snippet
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
            
Log.v(TAG, "Options value before decode Bitmap");
Log.v(TAG, String.format("inDensity %d, inScaled %b, inScreenDensity %d, inTargetDensity %d", options.inDensity, options.inScaled, options.inScreenDensity, options.inTargetDensity));
            
BitmapFactory.decodeResource(getResources(), R.drawable.my_image, options);
            
Log.v(TAG, "Options value after decode Bitmap");
Log.v(TAG, String.format("inDensity %d, inScaled %b, inScreenDensity %d, inTargetDensity %d", options.inDensity, options.inScaled, options.inScreenDensity, options.inTargetDensity));

Experiment #1 Does the system scales image to matches screen density?

This experiment try to answers the question "Does the system chooses image from another folder if there is no image for current device screen density?"

A.      Supply only image in xhdpi folder.

Log result

o    On xhdpi device
inDensity 320, inScaled true, inScreenDensity 0, inTargetDensity 320
Meaning
System chooses image from xhdpi folder (inDensity = 320) for xhdpi density (inTargetDensity = 320)
 
o    On hdpi device
inDensity 320, inScaled true, inScreenDensity 0, inTargetDensity 240
Meaning
System chooses image from xhdpi folder (inDensity = 320) and scales it to current hdpi density (inTargetDensity = 240)


Conclusion from experiment #1
System scales image to matches current screen density.


Experiment #2 Does the system chooses bitmap from the most closely matches drawable folder?

This experiment try to answers the question "Does the system choose image from drawable folder that most closely matches device screen density?"

A.      Supply image in xhdpi and hdpi

Log result

o    On mdpi device
inDensity 240, inScaled true, inScreenDensity 0, inTargetDensity 160

Meaning
System chooses image from hdpi folder (inDensity = 240) which is the most closely matches current mdpi density (inTargetDensity = 160)
 
o    On ldpi device
inDensity 240, inScaled true, inScreenDensity 0, inTargetDensity 120
 
Meaning
System chooses image from hdpi folder (inDensity = 240) which is the most closely matches current ldpi density (inTargetDensity = 120) than xhdpi.

B.      Supply image in hdpi and mdpi

Log result
 
o    On xhdpi device
inDensity 240, inScaled true, inScreenDensity 0, inTargetDensity 320

Meaning
System chooses image from hdpi folder (inDensity = 240) which is the most closely matches current xhdpi density (inTargetDensity = 320) than mdpi.
 
o    On ldpi device
inDensity 160, inScaled true, inScreenDensity 0, inTargetDensity 120

Meaning
System chooses image from mdpi folder (inDensity = 160) which is the most closely matches current ldpi density (inTargetDensity = 120) than hdpi.
 
C.      Supply image in mdpi and ldpi

Log result

o    On xhdpi device
inDensity 160, inScaled true, inScreenDensity 0, inTargetDensity 320

Meaning
System chooses image from mdpi folder (inDensity = 160) which is the most closely matches current xhdpi density (inTargetDensity = 320) than ldpi.

o    On hdpi device
inDensity 160, inScaled true, inScreenDensity 0, inTargetDensity 240

Meaning
System chooses image from mdpi folder (inDensity = 160) which is the most closely matches current hdpi density (inTargetDensity = 240) than ldpi.

D.      Supply image in xhdpi and ldpi

Log result

o    On hdpi device
inDensity 320, inScaled true, inScreenDensity 0, inTargetDensity 240

Meaning
System chooses image from xhdpi folder (inDensity = 320) which is the most closely matches current hdpi density (inTargetDensity = 240) than ldpi
 
o    On mdpi device
inDensity 320, inScaled true, inScreenDensity 0, inTargetDensity 160

Meaning
System chooses image from xhdpi folder (inDensity = 320) which is not the most closely matches current mdpi density (inTargetDensity = 160).
In this case, the chosen image should be the image in ldpi folder (density = 120) which is the most closely matches mdpi but system picks an image from xhdpi folder. This could be the effect from scaling factor which describes in Android document that system prefer to scales down the image.


Conclusion from experiment #2

·         System is not always choose the image from the most closely matches folder. In the experiment #2-D mdpi device case, system chooses to scale down the image from xhdpi folder instead of scale up the image from ldpi folder which is the most closely matches. This could be the effect from the scaling factor which scale down the image can produces better image quality than scale it up. We will try experiment more about scaling factor in the experiment #3.

·         Another interesting case is experiment #2-B ldpi device case. The system chooses image from mdpi folder and scale it down. This should not be a problem since mdpi is the most closely matches but, according to this Android document which describes this case that system would prefer to scale down the hdpi image instead of mdpi one because of scaling hdpi to ldpi by a factor of 0.5 has fewer artifacts compared to scaling mdpi to ldpi by a factor of 0.75. I'm not sure why my experiment result is conflicted with the one in Android document perhaps I'm running my test case on Android emulator (Froyo) or something else?

Experiment #3 Which one to be chosen? Scale up or scale down?

This experiment try to find the answer in the situation that the system cannot find an image for the current screen density but the images exist in drawable folders which are next to the current device screen density folder  e.g. current screen density is hdpi but the images are stored in drawable-xhdpi and drawable-mdpi, system will scale down an image from xhdpi folder or scale up an image from mdpi folder?

A.      Supply image in xhdpi and ldpi (experiment #2-D)

Log result

o    On hdpi device
inDensity 320, inScaled true, inScreenDensity 0, inTargetDensity 240

Meaning
System scales down the image from xhdpi folder (inDensity = 320) to current hdpi density (inTargetDensity = 240).

o    On mdpi device
inDensity 320, inScaled true, inScreenDensity 0, inTargetDensity 160

Meaning
System scales down the image from xhdpi folder (inDensity = 320) to current mdpi density (inTargetDensity = 160).

B.      Supply image in xhdpi and mdpi

Log result

o    On hdpi device
inDensity 320, inScaled true, inScreenDensity 0, inTargetDensity 240

Meaning
System scales down the image from xhdpi folder (inDensity = 320) to current hdpi density (inTargetDensity = 240).

o    On ldpi device
inDensity 160, inScaled true, inScreenDensity 0, inTargetDensity 120

Meaning
System scales down the image from mdpi folder (inDensity = 160) to current ldpi density (inTargetDensity = 160) which is also the most closely matches.

C.      Supply image in hdpi and ldpi

Log result

o    On mdpi device
inDensity 240, inScaled true, inScreenDensity 0, inTargetDensity 160

Meaning
System scales down image from hdpi folder (inDensity = 240) to current mdpi density (inTargetDensity = 160).

o    On xhdpi device
inDensity 240, inScaled true, inScreenDensity 0, inTargetDensity 320

Meaning
System scales up image from hdpi folder (inDensity = 240) to current xhdpi density (inTargetDensity = 320) which is also the most closely matches.

Conclusion from experiment #3
System prefer to scales down the image.

Experiment #4 Three image candidates.

The goal of this experiment is to determine which image will be chosen by the system if all folders has an image but the current device density folder.

A.      Supply image in xhdpi, hdpi and mdpi

Log result on ldpi device
inDensity 160, inScaled true, inScreenDensity 0, inTargetDensity 120

Meaning
System scales down image from mdpi folder (inDensity = 160) which is most closely matches current ldpi density (inTargetDensity = 120).

B.      Supply image in xhdpi, hdpi and ldpi

Log result
inDensity 240, inScaled true, inScreenDensity 0, inTargetDensity 160

Meaning
System scales down image from hdpi folder (inDensity = 240) to current mdpi density (inTargetDensity = 160).

C.      Supply image in xhdpi, mdpi and ldpi.

Log result
inDensity 320, inScaled true, inScreenDensity 0, inTargetDensity 240

Meaning
System scales down image from xhdpi folder (inDensity = 320) to current hdpi density (inTargetDensity = 240).

D.      Supply image in hdpi, mdpi and ldpi.

Log result
inDensity 240, inScaled true, inScreenDensity 0, inTargetDensity 320

Meaning
System scales up image from hdpi folder (inDensity = 240) which is most closely matches current xhdpi density (inTargetDensity = 320).

Conclusion from experiment #3
System prefer to scales down from the larger image in the most closely matches folder or scales up from the smaller image in the most closely matches folder if there are no image in higher resolution.

Conclusion
From all experiment results, below is the behavior of the Android system when finding best matching image.

Precondition
The desired image does not exist in the drawable folder of the current device screen density.

1.       System chooses the higher resolution image which is most closely matches the current device screen density.
2.       If no higher resolution image exist then chooses the lower resolution image which is most closely matches the current device screen density.





Thursday, July 26, 2012

What's happening in the JVM level when you concatenate the Strings?


  Have you ever been told by someone about concatenate Strings in Java? Of course, there are many ways to do it but, what is the difference between each approach?

  In this blog post, I disassemble the example Java bytecodes (.class files) into Jasmin (simple assembler-like syntax with JVM instruction set). Each example demonstrates the different way to concatenate Strings(but not cover all the ways that exist in the world ^^).

Lets' disassemble and see what will be happened in the JVM level !

Note: I'm using JasminParser as a disassembler tool.


·         Concatenate Strings using '+' sing.

Java code

public void concatStrings(){
String a = "Hello";
String b = "Johnny";
String c = "Dew";

String result = a + b + c;
}

Jasmin

.method public concatStrings()V
.limit stack 3
.limit locals 5
.var 0 is this Lcom/crack/java/StringConcat; from Label0 to Label1
.var 1 is a Ljava/lang/String; from Label2 to Label1
.var 2 is b Ljava/lang/String; from Label4 to Label1
.var 3 is c Ljava/lang/String; from Label6 to Label1
.var 4 is result Ljava/lang/String; from Label1 to Label1

Label0:
ldc "Hello"
astore_1
Label2:
ldc "Johnny"
astore_2
Label4:
ldc "Dew"
astore_3
Label6:
new java/lang/StringBuilder
dup
aload_1
invokestatic java/lang/String/valueOf(Ljava/lang/Object;)Ljava/lang/String;
invokespecial java/lang/StringBuilder/<init>(Ljava/lang/String;)V
aload_2
invokevirtual java/lang/StringBuilder/append(Ljava/lang/String;)Ljava/lang/StringBuilder;
aload_3
invokevirtual java/lang/StringBuilder/append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invokevirtual java/lang/StringBuilder/toString()Ljava/lang/String;
astore 4
Label1:
return

.end method

When concatenate String using '+' sign in one statement, Java compiler will translate it to 
a StringBuilder operations.

·         Concatenate Strings using shortcut assignment operator (+=).

Java code

public void concatStrings(){
String a = "Hello";
String b = "Johnny";
String c = "Dew";

String result = null;
result += a;
result += b;
result += c;

}

Jasmin

.method public concatStrings()V
.limit stack 3
.limit locals 5
.var 0 is this Lcom/crack/java/StringConcatShortcutAssignmentOperator; from Label0 to Label1
.var 1 is a Ljava/lang/String; from Label2 to Label1
.var 2 is b Ljava/lang/String; from Label4 to Label1
.var 3 is c Ljava/lang/String; from Label6 to Label1
.var 4 is result Ljava/lang/String; from Label8 to Label1

Label0:
ldc "Hello"
astore_1
Label2:
ldc "Johnny"
astore_2
Label4:
ldc "Dew"
astore_3
Label6:
aconst_null
astore 4
Label8:
new java/lang/StringBuilder
dup
aload 4
invokestatic java/lang/String/valueOf(Ljava/lang/Object;)Ljava/lang/String;
invokespecial java/lang/StringBuilder/<init>(Ljava/lang/String;)V
aload_1
invokevirtual java/lang/StringBuilder/append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invokevirtual java/lang/StringBuilder/toString()Ljava/lang/String;
astore 4

new java/lang/StringBuilder
dup
aload 4
invokestatic java/lang/String/valueOf(Ljava/lang/Object;)Ljava/lang/String;
invokespecial java/lang/StringBuilder/<init>(Ljava/lang/String;)V
aload_2
invokevirtual java/lang/StringBuilder/append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invokevirtual java/lang/StringBuilder/toString()Ljava/lang/String;
astore 4

new java/lang/StringBuilder
dup
aload 4
invokestatic java/lang/String/valueOf(Ljava/lang/Object;)Ljava/lang/String;
invokespecial java/lang/StringBuilder/<init>(Ljava/lang/String;)V
aload_3
invokevirtual java/lang/StringBuilder/append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invokevirtual java/lang/StringBuilder/toString()Ljava/lang/String;
astore 4
Label1:
return

.end method

When concatenate Strings using "+=" sign, the one "+=" called, the one StringBuilder will be created.

·         Concatenate Strings using StringBuilder.

Java code

public void concatStrings(){
String a = "Hello";
String b = "Johnny";
String c = "Dew";

StringBuilder builder = new StringBuilder(a);
builder.append(b);
builder.append(c);

String result = builder.toString();
}

Jasmin

.method public concatStrings()V
.limit stack 3
.limit locals 6
.var 0 is this Lcom/crack/java/StringConcatStringBuilder; from Label0 to Label1
.var 1 is a Ljava/lang/String; from Label2 to Label1
.var 2 is b Ljava/lang/String; from Label4 to Label1
.var 3 is c Ljava/lang/String; from Label6 to Label1
.var 4 is builder Ljava/lang/StringBuilder; from Label8 to Label1
.var 5 is result Ljava/lang/String; from Label1 to Label1

Label0:
ldc "Hello"
astore_1
Label2:
ldc "Johnny"
astore_2
Label4:
ldc "Dew"
astore_3
Label6:
new java/lang/StringBuilder
dup
aload_1
invokespecial java/lang/StringBuilder/<init>(Ljava/lang/String;)V
astore 4
Label8:
aload 4
aload_2
invokevirtual java/lang/StringBuilder/append(Ljava/lang/String;)Ljava/lang/StringBuilder;
pop
aload 4
aload_3
invokevirtual java/lang/StringBuilder/append(Ljava/lang/String;)Ljava/lang/StringBuilder;
pop
aload 4
invokevirtual java/lang/StringBuilder/toString()Ljava/lang/String;
astore 5
Label1:
return

.end method

Using StringBuilder is a straightforward way, what are we doing in the Java code will be mostly reflected the operations in the JVM level.


  This blog post is just a result from my curiousness, I just need to know what will be happened in the JVM level when concatenate the Strings in various ways. About the performance of each approach, the results are explicitly answer to this question. So, these are your choices.

Tuesday, July 3, 2012

Displaying Bitmaps Efficiently in Android



Before read this blog post, I highly recommend you to read the original article (Display Bitmaps Efficiently) from Android Developer website first. This is just my summary and diagrams depict the ideas from the original article.


To load Bitmap into memory in an efficient way, you will need to do 2 steps.

  1. Read Bitmap dimensions.
  2. Load a scaled down version into memory.

Here is a flowchart depicts the step to load Bitmap in an efficient way described in the Android Training document.

Fig.1 Steps to load Bitmap.


Processing Bitmaps Off the UI Thread

Processing Bitmap should be done in the worker Thread. The topic "Processing Bitmaps off the UI Thread" describes and show how to process Bitmap using AsyncTask. In the tutorial, each ImageView is referred using WeakReference inside the BitmapWorkerTask. Below is the diagram showing the relationship between BitmapWorkerTask and ImageView from the tutorial.


Fig.2 Relationship between BitmapWorkerTask and ImageView from the tutorial.

The interesting point from this topic is the way to handle concurrency issue. Using AsyncTask to process Bitmaps inside GridView or ListView  can cause ImageView to display wrong image when user scrolls the View. The idea to fix this problem is simple, when got ImageView from the adapter callback, simply check that this ImageView has a BitmapWorkerTask refers to it or not. If there is a BitmapWorkerTask refers to this ImageView then cancel the task before begin process new Bitmap.

The way to check that each ImageView has BitmapWorkerTask refers to it or not is wrapping the BitmapWorkerTask inside the dedicated Drawable subclass (the original article names this class "AsyncDrawable") then set the AsyncDrawable to the ImageView.


Fig.3 Relationship between BitmapWorkerTask, ImageView and AsyncDrawable.

Now when got ImageView from system callback (getView() method), we can check that this ImageView has BitmapWorkerTask refers to it or not, if it is, just simply cancel this task if it is not process the same Bitmap resource with the current request.


Fig.4 Flowchart illustrates the procedure to cancel an exiting BitmapWorkerTask before processing new Bitmap.

Also in BitmapWorkerTask, when processing Bitmap is finished, check if this task is cancelled or the current ImageView is already referred by another task (recycled) before setting image.


Fig.5 Flowchart illustrates the procedure to check current BitmapWorkerTask state before setting new image.


In case of the View components like GridView or ListView, when user scrolls back to the view that already recycled, you have to reprocess the Bitmap for that view again. Reprocess the Bitmap every time user scrolls back to the view cause your UI less responsive. Instead of reprocess, using cache is a better solution.

There are two types of cache: memory cache and disk cache.

For memory cache, LruCache (Least Recently Used Cache) was introduced in Android 3.1 (API level 12) and also available in support package for using in earlier Android version.

For disk cache, an example DiskLruCache class is included inside bitmap sample project and also in Android 4.0 source code (libcore/luni/src/main/java/libcore/io/DiskLruCache.java )

In the case of memory cache, the size of cache is very important. Please see this document for more information.