I'm developing an Activity which loads a list of URLs of images and displays them in a Gallery view. For better performance I decided to asynchronously load the images and cache them on the SD card.
I found this: http://blog.jteam.nl/2009/09/17/exploring-the-world-of-android-part-2/ and adapted the code. As long as I didn't cache the files on the SD card everything worked fine, but now I see a strange behavior. I guess it's easier to describe the problem with code (see comments):
public class AsyncImageLoader {
private final static String TAG = "AsyncImageLoader";
private HashMap<String, SoftReference<Bitmap>> bitmapMap;
public AsyncImageLoader() {
this.bitmapMap = new HashMap<String, SoftReference<Bitmap>>();
}
// This method is called to load images asynchronously.
// If the Bitmap is cached in bitmapMap and the reference is
// still valid, the Bitmap is returned immediately.
// If the Bitmap is not in bitmapMap, another thread is started
// to load the image. Once it has been loaded, a callback method
// is called.
public Bitmap loadBitmap(final String imageUrl,
final IImageLoadListener imageCallback, final int minWidth,
final int minHeight) {
if (this.bitmapMap.containsKey(imageUrl)) {
SoftReference<Bitmap> softReference = this.bitmapMap.get(imageUrl);
Bitmap bitmap = softReference.get();
if (bitmap != null) {
Log.d(TAG, "Using a previously loaded Bitmap container");
return bitmap;
}
}
Log.d(TAG, "Need to load the Bitmap container");
final Handler handler = new Handler() {
@Override
public void handleMessage(Message message) {
imageCallback.imageLoaded((Bitmap) message.obj, imageUrl);
}
};
new Thread() {
@Override
public void run() {
Bitmap bitmap = loadImageFromUrl(imageUrl, minWidth, minHeight);
bitmapMap.put(imageUrl, new SoftReference<Bitmap>(bitmap));
Message message = handler.obtainMessage(0, bitmap);
handler.sendMessage(message);
}
}.start();
return null;
}
// Here is part of the magic behind the scenes:
// I get an InputStream (see below) and open it just
// to get the Bitmap's dimensions. Then I calculate the
// required width (best size for minWidth and minHeight)
// and then I create a downsampled Bitmap.
private synchronized static Bitmap loadImageFromUrl(String urlString,
int minWidth, int minHeight) {
Bitmap bitmap = null;
try {
InputStream is = getInputStream(urlString);
BitmapFactory.Options options1 = new BitmapFactory.Options();
options1.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null,
options1);
is.close();
int tempWidth = options1.outWidth;
int tempHeight = options1.outHeight;
int scale = 1;
while (true) {
if (tempWidth / 2 < minWidth || tempHeight / 2 < minHeight) {
break;
}
tempWidth /= 2;
tempHeight /= 2;
scale *= 2;
}
BitmapFactory.Options options2 = new BitmapFactory.Options();
options2.inSampleSize = scale;
bitmap = BitmapFactory.decodeStream(getInputStream(urlString),
null, options2);
// More magic: Once the Bitmap has been downloaded,
// I create a file on the SD card so that I don't
// need to download it again.
File cacheFile = getCacheFile(urlString);
if (cacheFile == null) {
cacheImage(urlString, bitmap);
}
} catch (IOException e) {
Log.e(TAG, e.getMessage(), e);
}
return bitmap;
}
// I check whether a cache file exists for the URL. If it exists
// I create the InputStream from this file and I create the InputStream
// from the URL otherwise.
private synchronized static InputStream getInputStream(String urlString)
开发者_Go百科 throws IOException {
File cacheFile = getCacheFile(urlString);
if (cacheFile != null) {
// HERE seems to be something wrong. The file
// is valid and can be read, but if I return a
// FileInputStream (or InputStream using
// cacheFile.toURL().openStream()) the image is not
// shown.
Log.d(TAG, "Using " + cacheFile.getAbsolutePath());
return new FileInputStream(cacheFile);
} else {
// In this case, the image is shown everytime!
Log.d(TAG, "Downloading " + urlString);
URL url = new URL(urlString);
return url.openStream();
}
}
// This is just a helper method that returns the cache directory
// and if it doesn't exist it will be created first.
private synchronized static File getImageCacheDir() {
File imageCacheDir = new File(Environment.getExternalStorageDirectory()
.getAbsolutePath() + "/" + Commons.APP_DIRECTORY + "/cache/");
if (!imageCacheDir.exists()) {
imageCacheDir.mkdirs();
}
return imageCacheDir;
}
// This method returns a File object for the cache file if it exists
// and returns null otherwise.
private static File getCacheFile(String urlString) {
File file = new File(getImageCacheDir(), CryptoUtils.md5(urlString)
+ ".jpg");
if (!file.exists()) {
return null;
}
return file;
}
// Even more magic:
// At first I create a file with a .tmp extension just in case two
// threads try to read and write at the same time. The image is
// downloaded into the temporary file. If this succeeds the .tmp
// extension will be removed.
private synchronized static void cacheImage(String urlString, Bitmap bitmap) {
String filename = CryptoUtils.md5(urlString) + ".jpg";
File tmpFile = new File(getImageCacheDir(), filename + ".tmp");
if (tmpFile.exists()) {
Log.d(TAG, "Another process seems to create the cache file.");
return;
}
File file = new File(getImageCacheDir(), filename);
FileOutputStream os = null;
try {
os = new FileOutputStream(tmpFile);
boolean retval = bitmap
.compress(Bitmap.CompressFormat.JPEG, 90, os);
if (retval) {
tmpFile.renameTo(file);
Log.d(TAG, "Created cache image. Renaming temporary file.");
} else {
tmpFile.delete();
Log.d(TAG,
"Could not create cache image. Deleting temporary file.");
}
} catch (FileNotFoundException e) {
Log.e(TAG, e.getMessage(), e);
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
Log.e(TAG, e.getMessage(), e);
}
}
}
}
public interface IImageLoadListener {
public void imageLoaded(Bitmap imageBitmap, String imageUrl);
}
}
Here is another big chunk of code:
public class DisplayPhotoActivity extends Activity {
private final static int PROGRESS_DIALOG = 1;
private final static int PHOTO_PICKER = 2;
private final static int GALLERY_REQUEST_CODE = 1;
private final static int CAMERA_REQUEST_CODE = 2;
private AsyncImageLoader asyncImageLoader;
private Gallery gallery;
private List<Photo> images;
private ImageAdapter imageAdapter;
private int loadingCounter;
private Uri outputFileUri;
private Display display;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.display_photo);
this.asyncImageLoader = new AsyncImageLoader();
this.loadingCounter = 0;
this.images = new ArrayList<Photo>();
this.imageAdapter = new ImageAdapter(this, this.images);
final ImageView imageView = (ImageView) findViewById(R.id.large_image);
this.gallery = (Gallery) findViewById(R.id.gallery);
this.gallery.setAdapter(this.imageAdapter);
// The Gallery is on top of the Activity and there is a bigger
// ImageView below it. I want the image to be shown in the bigger
// ImageView when it has been clicked in the Gallery.
this.gallery.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View v,
int position, long id) {
// Here is the part where I load the Bitmap.
// This part works!
Bitmap bitmap = asyncImageLoader.loadBitmap(images
.get(position).getUrl(),
new AsyncImageLoader.IImageLoadListener() {
public void imageLoaded(Bitmap imageBitmap,
String imageUrl) {
if (imageBitmap != null) {
imageView.setImageBitmap(imageBitmap);
gallery.invalidate();
}
loadingCounter--;
if (loadingCounter == 0) {
getParent()
.setProgressBarIndeterminateVisibility(
false);
}
}
}, 300, 300);
if (bitmap != null) {
Log.d("DisplayPhotoActivity", "Image was cached. (I)");
imageView.setImageBitmap(bitmap);
gallery.invalidate();
loadingCounter--;
if (loadingCounter == 0) {
getParent()
.setProgressBarIndeterminateVisibility(false);
}
}
}
});
this.display = (Display) getIntent().getParcelableExtra("display");
populatePhotos(true);
}
// There is a known bug and this is the bug fix.
// See: http://code.google.com/p/android/issues/detail?id=8488
@Override
public void onPause() {
super.onPause();
System.gc();
}
@Override
public void onDestroy() {
super.onDestroy();
this.asyncImageLoader = null;
System.gc();
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case PROGRESS_DIALOG:
ProgressDialog progressDialog = new ProgressDialog(this);
progressDialog.setMessage("Loading...");
return progressDialog;
case PHOTO_PICKER:
final CharSequence[] items = {
getText(R.string.photo_picker_choose_from_gallery),
getText(R.string.photo_picker_take_a_photo) };
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getText(R.string.photo_picker_title));
builder.setItems(items, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int item) {
switch (item) {
case 0:
onChooseFromGalleryClick();
break;
case 1:
onTakeAPhoto();
}
}
});
return builder.create();
default:
return null;
}
}
private void onChooseFromGalleryClick() {
Intent intent = new Intent();
intent.setType("image/*");
intent.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(Intent.createChooser(intent,
getText(R.string.photo_picker_choose_from_gallery)),
GALLERY_REQUEST_CODE);
}
private void onTakeAPhoto() {
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File appDirectory = new File(Environment.getExternalStorageDirectory(),
Commons.APP_DIRECTORY);
if (!appDirectory.exists()) {
appDirectory.mkdirs();
}
File photo = new File(appDirectory, "/pic.jpg");
this.outputFileUri = Uri.fromFile(photo);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, this.outputFileUri);
startActivityForResult(Intent.createChooser(cameraIntent,
getText(R.string.photo_picker_take_a_photo)),
CAMERA_REQUEST_CODE);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuItem updateMenuItem = menu.add(this.getResources().getString(
R.string.update_menuitem_text));
updateMenuItem
.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
populatePhotos(true);
return false;
}
});
updateMenuItem.setIcon(R.drawable.ic_menu_refresh);
MenuItem uploadPhotoMenuItem = menu.add(this.getResources().getString(
R.string.photo_picker_title));
uploadPhotoMenuItem
.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
showDialog(PHOTO_PICKER);
return true;
}
});
uploadPhotoMenuItem.setIcon(getResources().getDrawable(
android.R.drawable.ic_menu_add));
return true;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
Uri uri = null;
switch (requestCode) {
case GALLERY_REQUEST_CODE:
uri = data.getData();
break;
case CAMERA_REQUEST_CODE:
uri = this.outputFileUri;
break;
}
if (uri == null) {
return;
}
Display display = this.getIntent().getParcelableExtra("display");
this.startService(PhotoUploadService.getIntent(this, display, uri));
}
}
private void populatePhotos(boolean forceSync) {
new ImageListLoader().execute(this.display.getId(), forceSync ? 1 : 0);
}
private void updateInitialImage(Bitmap bitmap) {
ImageView imageView = (ImageView) findViewById(R.id.large_image);
if (imageView.getDrawable() == null) {
Log.d("DisplayPhotoActivity", "No initial image set.");
imageView.setImageBitmap(bitmap);
}
}
public class ImageListLoader extends AsyncTask<Integer, Void, Void> {
@Override
protected void onPreExecute() {
showDialog(PROGRESS_DIALOG);
}
@Override
protected Void doInBackground(Integer... params) {
if (params.length < 1) {
return null;
}
int displayId = params[0];
boolean forceSync = params.length == 2 && params[1] == 1;
List<Photo> photos = LocalDatabaseHelper.getInstance()
.getPhotosByDisplayId(displayId, forceSync);
images.clear();
for (Photo photo : photos) {
images.add(photo);
}
Log.d("FOO", "Size: " + images.size());
runOnUiThread(new Runnable() {
public void run() {
imageAdapter.notifyDataSetChanged();
}
});
return null;
}
@Override
protected void onPostExecute(Void result) {
dismissDialog(PROGRESS_DIALOG);
}
}
public class ImageAdapter extends BaseAdapter {
private Context context;
private List<Photo> images;
private int galleryItemBackground;
public ImageAdapter(Context context, List<Photo> images) {
this.context = context;
this.images = images;
TypedArray attr = this.context
.obtainStyledAttributes(R.styleable.DisplayPhotoActivity);
this.galleryItemBackground = attr
.getResourceId(
R.styleable.DisplayPhotoActivity_android_galleryItemBackground,
0);
attr.recycle();
}
public int getCount() {
return this.images.size();
}
public Object getItem(int position) {
return position;
}
public long getItemId(int position) {
return position;
}
public View getView(int position, View convertView, ViewGroup parent) {
getParent().setProgressBarIndeterminateVisibility(true);
loadingCounter++;
final ImageView imageView = new ImageView(this.context);
String imageUrl = this.images.get(position).getUrl();
Log.d("DisplayPhotoActivity", "getView(): " + imageUrl);
// Here is almost the same code as in the OnClickListener
// above, but this does not work as expected. If the image
// has never been cached (neither as a SoftReference nor as
// a file on the SD card) the image is downloaded and shown.
// No problem... But if the image is cached, it won't show up!
Bitmap bitmap = asyncImageLoader.loadBitmap(
imageUrl,
new AsyncImageLoader.IImageLoadListener() {
public void imageLoaded(Bitmap imageBitmap,
String imageUrl) {
Log.d("DisplayPhotoActivity", " - imageLoaded:");
if (imageBitmap != null) {
Log.d("DisplayPhotoActivity", " - imageBitmap != null");
updateInitialImage(imageBitmap);
Log.d("DisplayPhotoActivity", " - updateInitialImage(imageBitmap)");
imageView.setImageBitmap(imageBitmap);
Log.d("DisplayPhotoActivity", " - setImageBitmap(imageBitmap)");
int width = Math.round((150 * imageBitmap
.getWidth()) / imageBitmap.getHeight());
imageView
.setLayoutParams(new Gallery.LayoutParams(
width, 150));
//imageView.invalidate();
//gallery.invalidate();
}
loadingCounter--;
if (loadingCounter == 0) {
getParent()
.setProgressBarIndeterminateVisibility(
false);
}
}
}, 300, 300);
if (bitmap != null) {
// TODO: Something doesn't work here...
Log.d("DisplayPhotoActivity", "Image was cached");
Log.d("DisplayPhotoActivity",
bitmap.getWidth() + "x" + bitmap.getHeight());
updateInitialImage(bitmap);
imageView.setImageBitmap(bitmap);
int width = Math.round((150 * bitmap.getWidth())
/ bitmap.getHeight());
imageView.setLayoutParams(new Gallery.LayoutParams(width, 150));
imageView.invalidate();
gallery.invalidate();
loadingCounter--;
if (loadingCounter == 0) {
getParent().setProgressBarIndeterminateVisibility(false);
}
}
imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
imageView.setBackgroundResource(galleryItemBackground);
imageView.setImageDrawable(getResources().getDrawable(
R.drawable.loading));
imageView.setLayoutParams(new Gallery.LayoutParams(200, 150));
return imageView;
}
public float getScale(boolean focused, int offset) {
return Math.max(0, 1.0f / (float) Math.pow(2, Math.abs(offset)));
}
}
}
Here is the DDMS output:
07-25 02:34:58.496: DEBUG/DisplayPhotoActivity(23589): getView(): http://[...]/files/814efa535ed97cf44ea3dc3a1c15c3fb/1_1311540992715.jpg
07-25 02:34:58.496: DEBUG/AsyncImageLoader(23589): Need to load the Bitmap container
07-25 02:34:58.503: DEBUG/DisplayPhotoActivity(23589): getView(): http://[...]/files/814efa535ed97cf44ea3dc3a1c15c3fb/1_1311540992715.jpg
07-25 02:34:58.503: DEBUG/AsyncImageLoader(23589): Need to load the Bitmap container
07-25 02:35:01.031: DEBUG/AsyncImageLoader(23589): Created cache image. Renaming temporary file.
07-25 02:35:01.031: DEBUG/DisplayPhotoActivity(23589): - imageLoaded:
07-25 02:35:01.031: DEBUG/DisplayPhotoActivity(23589): - imageBitmap != null
07-25 02:35:01.031: DEBUG/DisplayPhotoActivity(23589): No initial image set.
07-25 02:35:01.031: DEBUG/DisplayPhotoActivity(23589): - updateInitialImage(imageBitmap)
07-25 02:35:01.031: DEBUG/DisplayPhotoActivity(23589): - setImageBitmap(imageBitmap)
07-25 02:35:01.597: DEBUG/AsyncImageLoader(23589): Using /mnt/sdcard/PDCollector/cache/4057d5bd1cd9ba7204371ec422ea884a.jpg
07-25 02:35:01.605: DEBUG/AsyncImageLoader(23589): Using /mnt/sdcard/PDCollector/cache/4057d5bd1cd9ba7204371ec422ea884a.jpg
07-25 02:35:01.800: DEBUG/AsyncImageLoader(23589): Using /mnt/sdcard/PDCollector/cache/4057d5bd1cd9ba7204371ec422ea884a.jpg
07-25 02:35:01.800: DEBUG/AsyncImageLoader(23589): Using /mnt/sdcard/PDCollector/cache/4057d5bd1cd9ba7204371ec422ea884a.jpg
07-25 02:35:01.804: DEBUG/DisplayPhotoActivity(23589): - imageLoaded:
07-25 02:35:01.804: DEBUG/DisplayPhotoActivity(23589): - imageBitmap != null
07-25 02:35:01.804: DEBUG/DisplayPhotoActivity(23589): No initial image set.
07-25 02:35:01.804: DEBUG/DisplayPhotoActivity(23589): - updateInitialImage(imageBitmap)
07-25 02:35:01.804: DEBUG/DisplayPhotoActivity(23589): - setImageBitmap(imageBitmap)
07-25 02:35:02.027: DEBUG/AsyncImageLoader(23589): Using /mnt/sdcard/PDCollector/cache/4057d5bd1cd9ba7204371ec422ea884a.jpg
07-25 02:35:02.027: DEBUG/AsyncImageLoader(23589): Using /mnt/sdcard/PDCollector/cache/4057d5bd1cd9ba7204371ec422ea884a.jpg
07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): - imageLoaded:
07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): - imageBitmap != null
07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): - updateInitialImage(imageBitmap)
07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): - setImageBitmap(imageBitmap)
07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): getView(): http://[...]/files/814efa535ed97cf44ea3dc3a1c15c3fb/1_1311540992715.jpg
07-25 02:35:02.082: DEBUG/AsyncImageLoader(23589): Using a previously loaded Bitmap container
07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): Image was cached
07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): 640x480
07-25 02:35:02.257: DEBUG/DisplayPhotoActivity(23589): - imageLoaded:
07-25 02:35:02.257: DEBUG/DisplayPhotoActivity(23589): - imageBitmap != null
07-25 02:35:02.257: DEBUG/DisplayPhotoActivity(23589): - updateInitialImage(imageBitmap)
07-25 02:35:02.257: DEBUG/DisplayPhotoActivity(23589): - setImageBitmap(imageBitmap)
I hope someone can explain this strange behavior. Thanks in advance.
精彩评论