I got a problem with my app: In my app, every activity has several ImageViews, each ImageView has a bitmap set to it.If open many activities recursively, allocated memory will keeping increasing, and finally MemoryCache is full, so I can't display any bitmap otherwise app will crash.
What can I do with the ImageView whose activity is stopped? Can I recycle its bitmap, and reload the bitmap after its activity resumed?
I'm using Fresco to handle bitmap loading and cache.
The best way to handle memory while using Bitmap objects is to use LruCache and store your Bitmap inside.
Once you do not need your Bitmap anymore, you can store to your cache and recycle it to free as much memory as you can. If it is stored into cache, you just have to get your image from your cache.
This is my class handeling my cache :
public class ImagesCache {
private LruCache <String, Bitmap> imagesWarehouse;
private static ImagesCache cache;
public static ImagesCache getInstance() {
if(cache == null)
cache = new ImagesCache();
return cache;
}
public void initializeCache() {
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
final int cacheSize = maxMemory / 8;
imagesWarehouse = new LruCache<String, Bitmap>(cacheSize) {
protected int sizeOf(String key, Bitmap value) {
// The cache size will be measured in kilobytes rather than number of items.
int bitmapByteCount = value.getRowBytes() * value.getHeight();
return bitmapByteCount / 1024;
}};
}
public void addImageToWarehouse(String key, Bitmap value) {
if (imagesWarehouse != null && imagesWarehouse.get(key) == null)
imagesWarehouse.put(key, value);
}
public Bitmap getImageFromWarehouse(String key) {
if (key != null)
return imagesWarehouse.get(key);
else
return null;
}
public void removeImageFromWarehouse(String key) {
imagesWarehouse.remove(key);
}
public void clearCache() {
if (imagesWarehouse != null)
imagesWarehouse.evictAll();
}
}
Remember to initialise your cache when your app starts
cache.initializeCache()
and clear if when your app finishes
cache.clearCache()
Related
I have developed 20+ android apps, apps have a tutorial activity that are composed of large image slide (ex; 4 images in the size of 750 x 1334) and would be shown to users at the first launch.
I cannot reduce the sizes anymore, because of image quality.
My code snippet is following;
public class GalleryImageActivity extends BaseActivity implements OnGestureListener, OnTouchListener {
ViewPager imagePager;
GalleryImageAdapter galleryImageAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.gallery_image);
imagePager = (ViewPager) findViewById(R.id.image_pager);
galleryImageAdapter = new GalleryImageAdapter(this);
imagePager.setAdapter(galleryImageAdapter);
imagePager.setOnTouchListener(this);
}
}
public class GalleryImageAdapter extends PagerAdapter {
public static int SCREEN_CNT = 4;
Bitmap[] bmp = new Bitmap[SCREEN_CNT];
#Override
public int getCount() {
return SCREEN_CNT;
}
#Override
public Object instantiateItem(ViewGroup view, int position) {
View view = inflater.inflate(R.layout.gallery_image_item, null);
ImageView imv = (ImageView) view.findViewById(R.id.imgView);
bmp[position] = readBitMap(context, R.drawable.tutorial_img_0 + position, position);
if (bmp[position] != null)
imv.setImageBitmap(bmp[position]);
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (bmp[position] != null && !bmp[position].isRecycled())
{
bmp[position].recycle();
System.gc();
bmp[position] = null;
}
}
public Bitmap readBitMap(Context context, int resId, int position) {
BitmapFactory.Options opt = new BitmapFactory.Options();
opt.inJustDecodeBounds = true;
InputStream is = context.getResources().openRawResource(resId);
BitmapFactory.decodeStream(is,null, opt);
opt.inPreferredConfig = Bitmap.Config.RGB_565;
opt.inPurgeable = true;
opt.inInputShareable = true;
opt.inJustDecodeBounds = false;
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
is = context.getResources().openRawResource(resId);
Bitmap bitmap = BitmapFactory.decodeStream(is,null, opt);
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
}
sometimes it works well, but when repeat 5~6 times, BitmapFactory.decodeStream throws "Out of memory".
How can i decide scale size or resolve this problem?
the example images are following;
You can modify readBitMap function as the following;
public Bitmap readBitMap(Context context, int resId, int position){
if (bmp[position] == null || bmp[position].isRecycled())
{
BitmapFactory.Options opt = new BitmapFactory.Options();
opt.inPreferredConfig = Bitmap.Config.RGB_565;
opt.inPurgeable = true;
opt.inInputShareable = true;
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
opt.inScaled = true;
opt.inDensity = DisplayMetrics.DENSITY_DEFAULT; // !important
InputStream is = context.getResources().openRawResource(resId);
try {
bmp[position] = BitmapFactory.decodeStream(is,null,opt);
} catch (Exception e){
bmp[position] = null;
}
}
return bmp[position];
}
Some type of mobile phone, especially SAMSUNG, is not well-designed about memory leak.
opt.inDensity = DisplayMetrics.DENSITY_DEFAULT is very important.
If inDensity is 0, BitmapFactory.decodeStream will fill in the density associated with the resource. more...
And you should set the imageview's bitmap to null, when your activity (or page) gets destroyed.
A bitmap of size 750x1334 in RGB_565 config requires around 2MB memory. 3 of them can be stored in 6 MB, which should not create OutOfMemoryError if handled with care.
Here are some tips that you can follow.
Use android:largeHeap="true" in application tag of your manifest.
Use FragmentStatePagerAdapter for your ViewPager.
Don't forget to recycle bitmaps as soon as they are no longer needed.
Use Runtime.getRuntime().gc(); after you recycle your bitmaps.
If you are storing images in drawable, move them to drawable-nodpi.
Use Glide or Picasso library.
Try using some image loading library like Picasso or Glide. That will simplify things a lot.
But in your case, I think the issue is not in decoding the image, but rather you are holding on the bitmap already created in memory. Ensure that you only have one Bitmap in memory at all times. Just open your Profiler in studio, and confirm if you are not storing references to multiple bitmaps.
I have an activity which loads and displays a somewhat large bitmap. Risky business in the first place, but we load it in using a BitmapFactory using the standard method below:
private static Bitmap decodeSampledBitmapFromFile(String filepath, int reqWidth,int orientation) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filepath,options);
if(orientation==0){
options.inSampleSize = calculateInSampleSizeWidth(options, reqWidth);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
Bitmap d = BitmapFactory.decodeFile(filepath,options);
return Bitmap.createScaledBitmap(d, reqWidth, d.getHeight()*reqWidth/d.getWidth(), true);
}
else{
options.inSampleSize = calculateInSampleSizeHeight(options,reqWidth);
options.inJustDecodeBounds = false;
Bitmap d = BitmapFactory.decodeFile(filepath,options);
return Bitmap.createScaledBitmap(d, d.getWidth()*reqWidth/d.getHeight(), reqWidth, true);
}
}
private static int calculateInSampleSizeWidth( BitmapFactory.Options options, int reqWidth) {
int inSampleSize = 1;
if (options.outWidth > reqWidth) {
final int halfWidth = options.outWidth / 2;
while ( (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
The loading of this Bitmap into the ImageView is done in an AsyncTask shown below:
private static class LoadBitmapTask extends AsyncTask<String, Void, Bitmap>{
private ViewSampleActivity _activity;
public LoadBitmapTask(ViewSampleActivity activity){
this._activity = activity;
}
public void detach(){
this._activity = null;
}
#Override
protected Bitmap doInBackground(String... params) {
try{
String filename = params[0];
Display display = _activity.getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
int orientation = getCameraPhotoOrientation(_activity,Uri.fromFile(new File(filename)),filename);
Bitmap d = null;
d = decodeSampledBitmapFromFile(filename, (int)(((double)size.x)*0.85),orientation);
d = rotateBitmapAppropriately(_activity,Uri.fromFile(new File(filename)),filename,d);
return d;
}
catch(Exception e){
return null;
}
}
#Override
protected void onPostExecute(Bitmap result){
try{
_activity.sampleImageView.setImageBitmap(result);
_activity.sampleImageView.setVisibility(View.VISIBLE);
_activity.sampleImageView.getLayoutParams().height = result.getHeight();
_activity.sampleImageView.getLayoutParams().width = result.getWidth();
_activity.viewSampleLayout.getLayoutParams().height = LayoutParams.WRAP_CONTENT;
_activity.hasPicture = true;
}
catch(Exception e){
e.printStackTrace();
}
}
}
In order to try to avoid memory leaks, I've overloaded the activity's onStop method to detach the activity from the AsyncTask.
#Override
public void onStop(){
if(this.loadBitmapTask!=null){
loadBitmapTask.detach();
}
super.onStop();
}
Now, the strange behaviour I'm seeing is that the activity loads just fine on the first run. After an orientation change, the activity will be reloaded correctly. However, after five or so orientation changes, the application will crash with an out of memory error. The Bitmap would seem to be the obvious culprit. I'm not saving a static copy of any ui element or bitmap or anything, so I'm wondering if these out of memory errors are due to a memory leak.
The activity in question is the last of a hierarchy of activities. The root of the hierarchy, the main activity, has an android Handler object with a WeakReference to it, and also a persistent fragment which manages Bluetooth networking threads. The second activity in the hierarchy is a simple ListActivity.
Would the parent activities of the activity in question have any effect on Java's garbage collection of this activity?
Would Bitmap d in your decodeSampledBitmapFromFile() method be garbage collected? I'm not 100% clear on these concepts, but it seems like creating a scaled copy from d without recycling d itself is suspect.
I also recommend trying to re-attach to your AsyncTask instead of letting it die (and spawning more?!). If you're using FragmentActivity, you can use a non-configuration instance. Store a reference to your task in your activity, and save it as the non-configuration instance. Then attempt to re-attach onCreate of your activity:
private LoadBitmapTask mTask;
#Override
public Object onRetainCustomNonConfigurationInstance() {
if(mTask != null){
mTask.detach();
}
return mTask;
}
#Override
protected void onCreate(Bundle savedInstanceState) {
mTask = (LoadBitmapTask) getLastCustomNonConfigurationInstance();
if (mTask != null) {
mTask.attach(this);
}
}
This way you can "pick up where you left off" with the AsyncTask and not have to spawn more.
I have recently done some tests with display pictures using a custom gallery that i designed using the media queries and mediastore... It worked great but i really need to do something custom.
I don't wish the pictures to be scanned or available in the mediastore hence i would like to have my app scan a directory and create thumbnails and display these thumbnails.
I am finding it really thin on the ground to find any good quality examples to do this.
Can anyone help with a small example.
Here is what i am looking to do.
Pictures are stored in a directory of the sdcard.
Using my custom gallery it would scan this directory but "NOT" using the mediastore
I need to display the contents of the directory but as thumbnails i presume i would need to create this thumbnails first?
Clicking on a thumnail would should the full screen image from my custom gallery.
I suppose i just need a little help in getting the pictures from the the directory considering there are not stored int eh mediastore so i can't use a query. THe other thing that concerns me is that i would need to create the thumbnails for each of the these images (on the fly??) because display the images but at a reduced size i would suspect would be pretty bad for the performance.
Can anyone lend a helping hand?
Thanks in advance
I did exactly the same a while ago. You have to pass a folder name where your images are to setBaseFolder. This method in turn invokes refresh() which - using a FilenameFilter (code not included but is very easy to implement) gets all images named orig_....jpg from that folder and holds it in mFileList. Then we call notifyDataSetChanged() which in turn will trigger getView() for every cell.
Now, in getView() we either fetch a thumbnail bitmap from a cache if we already have it there, otherwise we make a gray placeholder and start a ThumbnailBuilder to create thumbnail resp. get a bitmap from it.
I think you'll have to change the ThumbnailBuilder a bit, because I create quite large "thumbnails" (500x500) as I need the resized images for other purposes too. Also, as I work with photos taken by the camera there is some stuff there, rotating the image according to the exif information. But basicly, ThumbnailBuilder just checks if there already is a thumbnail image (my thumbnail images are placed the same folder but have prefix small_ instead of orig_) - if the thumbnail picture already exists, we get it as a Bitmap and are done, otherwise the image is generated. Finally, in onPostExecute() the bitmap is set to the ImageView.
public class PhotoAdapter extends BaseAdapter {
private Context mContext;
private int mCellSize;
private File mFolder;
private File[] mFileList;
private Map<Object, Bitmap> mThumbnails = new HashMap<Object, Bitmap>();
private Set<Object> mCreatingTriggered = new HashSet<Object>(); // flag that creating already triggered
public PhotoAdapter(Context context, int cellSize) {
mContext = context;
mCellSize = cellSize;
}
#Override
public int getCount() {
if (mFolder == null) {
return 0; // don't do this
} else {
return mFileList.length;
}
}
#Override
public Object getItem(int position) {
return mFileList[position];
}
#Override
public long getItemId(int position) {
return position;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ImageView view = (ImageView)convertView;
if (view == null) {
view = new ImageView(mContext);
view.setLayoutParams(new GridView.LayoutParams(mCellSize, mCellSize));
view.setScaleType(ImageView.ScaleType.CENTER_CROP);
view.setPadding(8, 8, 8, 8);
view.setBackgroundColor(0xFFC6CCD3);
}
Object item = getItem(position);
Bitmap bm = mThumbnails.get(item);
if (bm == null) {
view.setImageBitmap(null);
if (!mCreatingTriggered.contains(item)) {
mCreatingTriggered.add(item);
new ThumbnailBuilder(view, (File)item).execute();
}
} else {
view.setImageBitmap(bm);
}
return view;
}
public void setBaseFolder(File baseFolder) {
if (baseFolder == null) return;
if (!baseFolder.equals(mFolder)) {
releaseThumbnails();
mFolder = baseFolder;
}
refresh();
}
public void refresh() {
if (mFolder == null) {
return;
}
mFileList = mFolder.listFiles(EtbApplication.origImageFilenameFilter);
if (mFileList == null) mFileList = new File[0];
notifyDataSetChanged();
}
public void releaseThumbnails() {
for (Bitmap bm : mThumbnails.values()) {
bm.recycle();
}
mThumbnails.clear();
}
// ------------------------------------------------------------------------------------ Asynchronous Thumbnail builder
private class ThumbnailBuilder extends AsyncTask<Void, Integer, Bitmap> {
private ImageView mView;
private File mFile;
public ThumbnailBuilder(ImageView view, File file) {
mView = view;
mFile = file;
}
#Override
protected Bitmap doInBackground(Void... params) {
Log.d("adapter", "make small image and thumbnail");
try {
return createThumbnail(mFile.getAbsolutePath());
} catch (Exception e) {
return null;
}
}
#Override
protected void onPostExecute(Bitmap result) {
if (result != null) {
mView.setImageBitmap(result);
mThumbnails.put(mFile, result);
} else {
mView.setImageResource(R.drawable.ic_launcher);
}
}
/**
* Creates Thumbnail (also rotates according to exif-info)
* #param file
* #return
* #throws IOException
*/
private Bitmap createThumbnail(String file) throws IOException {
File thumbnailFile = new File(file.replace("orig_", "small_"));
// If a small image version already exists, just load it and be done.
if (thumbnailFile.exists()) {
return BitmapFactory.decodeFile(thumbnailFile.getAbsolutePath());
}
// Decode image size
BitmapFactory.Options bounds = new BitmapFactory.Options();
bounds.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file, bounds);
if ((bounds.outWidth == -1) || (bounds.outHeight == -1))
return null;
int w, h;
if (bounds.outWidth > bounds.outHeight) { // Querformat
w = 500;
h = 500 * bounds.outHeight / bounds.outWidth;
} else { // Hochformat
h = 500;
w = 500 * bounds.outWidth / bounds.outHeight;
}
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inSampleSize = 4; // resample -- kleiner aber noch nicht die 500 Pixel, die kommen dann unten
Bitmap resizedBitmap = BitmapFactory.decodeFile(file, opts);
resizedBitmap = Bitmap.createScaledBitmap(resizedBitmap, w, h, true);
ExifInterface exif = new ExifInterface(file);
String orientString = exif.getAttribute(ExifInterface.TAG_ORIENTATION);
int orientation = orientString != null ? Integer.parseInt(orientString) : ExifInterface.ORIENTATION_NORMAL;
int rotationAngle = 0;
if (orientation == ExifInterface.ORIENTATION_ROTATE_90) rotationAngle = 90;
if (orientation == ExifInterface.ORIENTATION_ROTATE_180) rotationAngle = 180;
if (orientation == ExifInterface.ORIENTATION_ROTATE_270) rotationAngle = 270;
Matrix matrix = new Matrix();
matrix.setRotate(rotationAngle, (float) resizedBitmap.getWidth() / 2, (float) resizedBitmap.getHeight() / 2);
Bitmap rotatedBitmap = Bitmap.createBitmap(resizedBitmap, 0, 0, w, h, matrix, true);
resizedBitmap.recycle();
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
rotatedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, bytes);
thumbnailFile.createNewFile();
FileOutputStream fo = new FileOutputStream(thumbnailFile);
fo.write(bytes.toByteArray());
fo.close();
//new File(file).delete(); // Originalbild löschen
return rotatedBitmap;
}
}
}
I need help understanding androids LruCache. I want to use to load images into my gridview in order make the loading/scrolling better. Can someone post an example code using LruCache please. Thanks in advance.
Below is a class I made for using LruCache, this is based on the presentation Doing More With Less: Being a Good Android Citizen given at Google I/O 2012.
Check out the movie for more information about what I'm doing in the TCImageLoader class:
public class TCImageLoader implements ComponentCallbacks2 {
private TCLruCache cache;
public TCImageLoader(Context context) {
ActivityManager am = (ActivityManager) context.getSystemService(
Context.ACTIVITY_SERVICE);
int maxKb = am.getMemoryClass() * 1024;
int limitKb = maxKb / 8; // 1/8th of total ram
cache = new TCLruCache(limitKb);
}
public void display(String url, ImageView imageview, int defaultresource) {
imageview.setImageResource(defaultresource);
Bitmap image = cache.get(url);
if (image != null) {
imageview.setImageBitmap(image);
}
else {
new SetImageTask(imageview).execute(url);
}
}
private class TCLruCache extends LruCache<String, Bitmap> {
public TCLruCache(int maxSize) {
super(maxSize);
}
#Override
protected int sizeOf(ImagePoolKey key, Bitmap value) {
int kbOfBitmap = value.getByteCount() / 1024;
return kbOfBitmap;
}
}
private class SetImageTask extends AsyncTask<String, Void, Integer> {
private ImageView imageview;
private Bitmap bmp;
public SetImageTask(ImageView imageview) {
this.imageview = imageview;
}
#Override
protected Integer doInBackground(String... params) {
String url = params[0];
try {
bmp = getBitmapFromURL(url);
if (bmp != null) {
cache.put(url, bmp);
}
else {
return 0;
}
} catch (Exception e) {
e.printStackTrace();
return 0;
}
return 1;
}
#Override
protected void onPostExecute(Integer result) {
if (result == 1) {
imageview.setImageBitmap(bmp);
}
super.onPostExecute(result);
}
private Bitmap getBitmapFromURL(String src) {
try {
URL url = new URL(src);
HttpURLConnection connection
= (HttpURLConnection) url.openConnection();
connection.setDoInput(true);
connection.connect();
InputStream input = connection.getInputStream();
Bitmap myBitmap = BitmapFactory.decodeStream(input);
return myBitmap;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
#Override
public void onConfigurationChanged(Configuration newConfig) {
}
#Override
public void onLowMemory() {
}
#Override
public void onTrimMemory(int level) {
if (level >= TRIM_MEMORY_MODERATE) {
cache.evictAll();
}
else if (level >= TRIM_MEMORY_BACKGROUND) {
cache.trimToSize(cache.size() / 2);
}
}
}
I've found a really easy way that work perfectly for me...
This is the Cache.java class. In this class, the static getInstance() method enables us to create only one cache instance in the whole application. getLru() method is used to retrieve the cached object, it will be shown later how to use it. This cache is generic, meaning you can save any Object type into it. The cache memory size here is set to 1024. It can be changed if it is too small:
import android.support.v4.util.LruCache;
public class Cache {
private static Cache instance;
private LruCache<Object, Object> lru;
private Cache() {
lru = new LruCache<Object, Object>(1024);
}
public static Cache getInstance() {
if (instance == null) {
instance = new Cache();
}
return instance;
}
public LruCache<Object, Object> getLru() {
return lru;
}
}
This is the code in your activity where you save the bitmap to the cache:
public void saveBitmapToCahche(){
//The imageView that you want to save it's bitmap image resourse
ImageView imageView = (ImageView) findViewById(R.id.imageViewID);
//To get the bitmap from the imageView
Bitmap bitmap = ((BitmapDrawable)imageview.getDrawable()).getBitmap();
//Saving bitmap to cache. it will later be retrieved using the bitmap_image key
Cache.getInstance().getLru().put("bitmap_image", bitmap);
}
This is the code where you retrieve the bitmap from the cache, then set an imageView to this bitmap:
public void retrieveBitmapFromCache(){
//The imageView that you want to set to the retrieved bitmap
ImageView imageView = (ImageView) findViewById(R.id.imageViewID);
//To get bitmap from cache using the key. Must cast retrieved cache Object to Bitmap
Bitmap bitmap = (Bitmap)Cache.getInstance().getLru().get("bitmap_image");
//Setting imageView to retrieved bitmap from cache
imageView.setImageBitmap(bitmap));
}
THAT'S ALL! As you can see this is rather easy and simple.
EXAMPLE:
In my application, All the views are saved in class variables so they can be seen by all the methods in the class. In my first activity, I save the image bitmap to the cache in an onClickButton() method, right before I start a new activity using intent. I also save a string value in my cache:
public void onClickButton(View v){
Bitmap bitmap = ((BitmapDrawable)imageView.getDrawable()).getBitmap();
String name = textEdit.getText().toString();
Cache.getInstance().getLru().put("bitmap_image", bitmap);
Cache.getInstance().getLru().put("name", name);
Intent i = new Intent(FirstActivity.this, SecondActivity.class);
startActivity(i);
}
Then I navigate from the second activity to a third activity also using intents. In the last activity I save other objects into my cache, then go back to the first activity using an intent. Once I'm back in the first activity, the onCreate() method will start. In that method, I check if my cache has any bitmap value or any String value separately (based on my application business):
public ImageView imageView;
public EditText editText;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_first);
//...Other code...
//The imageView that you want to save it's bitmap image resourse
imageView = (ImageView) findViewById(R.id.imageViewID);
//The editText that I want to save it's text into cache
editText = (EditText)findViewById(R.id.editTextID);
if(Cache.getInstance().getLru().get("name")!=null){
editText.setText(Cache.getInstance().getLru().get("name").toString());
}
if(Cache.getInstance().getLru().get("bitmap_image")!=null){
imageView.setImageBitmap((Bitmap)Cache.getInstance().getLru().get("bitmap_image"));
}
//...Other code...
}
Take a look at Caching Bitmaps where the use of LruCache is demonstrated.
The relevant portion of the code from the page is as follows:-
private LruCache mMemoryCache;
#Override
protected void onCreate(Bundle savedInstanceState) {
...
// Get memory class of this device, exceeding this amount will throw an
// OutOfMemory exception.
final int memClass = ((ActivityManager) context.getSystemService(
Context.ACTIVITY_SERVICE)).getMemoryClass();
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = 1024 * 1024 * memClass / 8;
mMemoryCache = new LruCache(cacheSize) {
#Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in bytes rather than number of items.
return bitmap.getByteCount();
}
};
...
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
https://techienotes.info/2015/08/28/caching-bitmaps-in-android-using-lrucache/
This link has a full project having sample application to load images into Gridview using LruCache.
This class is using LruCache and taken from the code given in the link
public class ImageAdapter extends BaseAdapter{
private String TAG = getClass().getSimpleName();
Context mContext;
ArrayList<Uri> imageList;
private LruCache<String, Bitmap> mLruCache;
public ImageAdapter (Context context){
mContext = context;
//Find out maximum memory available to application
//1024 is used because LruCache constructor takes int in kilobytes
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/4th of the available memory for this memory cache.
final int cacheSize = maxMemory / 4;
Log.d(TAG, "max memory " + maxMemory + " cache size " + cacheSize);
// LruCache takes key-value pair in constructor
// key is the string to refer bitmap
// value is the stored bitmap
mLruCache = new LruCache<String, Bitmap>(cacheSize) {
#Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes
return bitmap.getByteCount() / 1024;
}
};
imageList = new ArrayList<Uri>();
//Change this directory to where the images are stored
String imagesFolderPath = Environment.getExternalStorageDirectory().getPath()+"/backups/";
File imageSrcDir = new File (imagesFolderPath);
// if directory not present, build it
if (!imageSrcDir.exists()){
imageSrcDir.mkdirs();
}
ArrayList<File> imagesInDir = getImagesFromDirectory(imageSrcDir);
for (File file: imagesInDir){
// imageList will hold Uri of all images
imageList.add(Uri.fromFile(file));
}
}
#Override
public int getCount() {
return imageList.size();
}
#Override
public Object getItem(int position) {
return null;
}
#Override
public long getItemId(int position) {
return 0;
}
/**
*
* #param position The position of the item within the
* adapter's data set of the item whose view we want.
* #param convertView it is the view to be reused
* #param parent The parent that this view will eventually be attached to
* #return a View corresponding to the data at the specified position.
*/
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ImageView imageView;
Bitmap thumbnailImage = null;
if (convertView == null){
imageView = new ImageView(mContext);
imageView.setLayoutParams(
//150,150 is size of imageview to display image
new GridView.LayoutParams(150, 150));
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
}
else {
imageView = (ImageView)convertView;
}
// Use the path as the key to LruCache
final String imageKey = imageList.get(position).toString();
//thumbnailImage is fetched from LRU cache
thumbnailImage = getBitmapFromMemCache(imageKey);
if (thumbnailImage == null){
// if asked thumbnail is not present it will be put into cache
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(imageKey);
}
imageView.setImageBitmap(thumbnailImage);
return imageView;
}
/**
* This function returns the files from a directory
* #param parentDirPath source directory in which images are located
* #return list of Files
*/
private ArrayList<File> getImagesFromDirectory (File parentDirPath){
ArrayList <File> listOfImages = new ArrayList<File>();
File [] fileArray = null;
if ( parentDirPath.isDirectory() ){//parentDirPath.exists() &&
// &&
// parentDirPath.canRead()){
fileArray = parentDirPath.listFiles();
}
if (fileArray == null){
return listOfImages; // return empty list
}
for (File file: fileArray){
if (file.isDirectory()){
listOfImages.addAll(getImagesFromDirectory(file));
}
else {
// Only JPEG and PNG formats are included
// for sake of simplicity
if (file.getName().endsWith("png") ||
file.getName().endsWith("jpg")){
listOfImages.add(file);
}
}
}
return listOfImages;
}
/**
* This function will return the scaled version of original image.
* Loading original images into thumbnail is wastage of computation
* and hence we will take put scaled version.
*/
private Bitmap getScaledImage (String imagePath){
Bitmap bitmap = null;
Uri imageUri = Uri.parse (imagePath);
try{
BitmapFactory.Options options = new BitmapFactory.Options();
/**
* inSampleSize flag if set to a value > 1,
* requests the decoder to sub-sample the original image,
* returning a smaller image to save memory.
* This is a much faster operation as decoder just reads
* every n-th pixel from given image, and thus
* providing a smaller scaled image.
* 'n' is the value set in inSampleSize
* which would be a power of 2 which is downside
* of this technique.
*/
options.inSampleSize = 4;
options.inScaled = true;
InputStream inputStream = mContext.getContentResolver().openInputStream(imageUri);
bitmap = BitmapFactory.decodeStream(inputStream, null, options);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return bitmap;
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mLruCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mLruCache.get(key);
}
class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView);
}
#Override
protected Bitmap doInBackground(String... params) {
final Bitmap bitmap = getScaledImage(params[0]);
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
// onPostExecute() sets the bitmap fetched by doInBackground();
#Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = (ImageView)imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
}
Utility Class to save and retrieve Bitmap from own Cache.
package com.roomco.android.utils;
import android.graphics.Bitmap;
import android.support.v4.util.LruCache;
public class MyCache {
private static MyCache instance;
private LruCache<Object, Object> lru;
private MyCache() {
lru = new LruCache<Object, Object>(1024);
}
public static MyCache getInstance() {
if (instance == null) {
instance = new MyCache();
}
return instance;
}
public LruCache<Object, Object> getLru() {
return lru;
}
public void saveBitmapToCahche(String key, Bitmap bitmap){
MyCache.getInstance().getLru().put(key, bitmap);
}
public Bitmap retrieveBitmapFromCache(String key){
Bitmap bitmap = (Bitmap)MyCache.getInstance().getLru().get(key);
return bitmap;
}
}
Usage:
//Save bitmap in cache
MyCache.getInstance().saveBitmapToCahche("your_key",bitmap);
// Get bitmap from cache
MyCache.getInstance().retrieveBitmapFromCache("your_key");
I creat a ViewFlipper to show images form Internet in a singal activity. When fling, set image to an imageView then add it to the viewflipper.
But the question is that OOM always occures after showed about 20 images.
I had did some clean jobs to sovle it and it didn't work!
Here is the code.
public class ImageCache {
static private ImageCache cache;
private Hashtable<Integer, MySoftRef> hashRefs;
private ReferenceQueue<Bitmap> q;
private class MySoftRef extends SoftReference<Bitmap> {
private Integer _key = 0;
public MySoftRef(Bitmap bmp, ReferenceQueue<Bitmap> q, int key) {
super(bmp, q);
_key = key;
}
}
public ImageCache() {
hashRefs = new Hashtable<Integer, MySoftRef>();
q = new ReferenceQueue<Bitmap>();
}
public static ImageCache getInstance() {
if (cache == null) {
cache = new ImageCache();
}
return cache;
}
private void addCacheBitmap(Bitmap bmp, Integer key) {
cleanCache();
MySoftRef ref = new MySoftRef(bmp, q, key);
hashRefs.put(key, ref);
}
public Bitmap getBitmap(int resId) {
Bitmap bmp = null;
if (hashRefs.containsKey(resId)) {
MySoftRef ref = (MySoftRef) hashRefs.get(resId);
bmp = (Bitmap) ref.get();
}
if (bmp == null) {
URL imgUrl = null;
try {
imgUrl = new URL("http:/example/images/" + resId
+ ".jpg");
HttpURLConnection conn = (HttpURLConnection) imgUrl
.openConnection();
conn.connect();
InputStream is = conn.getInputStream();
bmp = BitmapFactory.decodeStream(is);
is.close();
addCacheBitmap(bmp, resId);
} catch (Exception e) {
e.printStackTrace();
}
}
return bmp;
}
private void cleanCache() {
MySoftRef ref = null;
while ((ref = (MySoftRef) q.poll()) != null) {
hashRefs.remove(ref._key);
}
}
public void clearCache() {
cleanCache();
hashRefs.clear();
System.gc();
System.runFinalization();
}
and here is the loadimage code.
public void LoadImage(int n){
iv = new ImageView(this);
imageCache = new ImageCache();
Bitmap bm = imageCache.getBitmap(n);
iv.setImageBitmap(bm);
iv.setScaleType(ImageView.ScaleType.CENTER);
viewFlipper.addView(iv, new LayoutParams(LayoutParams.FILL_PARENT,
LayoutParams.FILL_PARENT));
}
According to your code you do not use singleton nature of your ImageCache class, you create new ImageCache instance every time LoadImage(int n) is called:
imageCache = new ImageCache();
Bitmap bm = imageCache.getBitmap(n);
Which is really strange. Use instead:
imageCache = ImageCache.getInstance();
The only way to free the memory allocated with a Bitmap is recycle(). You should removeView(View) that are not in the range [current - 1; current + 1], then recycle the Bitmap you allocated yourself. All that code seems an overkill to me, and also it's not a good idea to invoke the GC. You may save the contents of the InputStream on the permanet storage, so you won't have the network overhead.
You should only load and keep in cache the 3 images that you need :
The previous image
The current one
The next one
Just free the memory of the others. Use the recycle() fonction for that.
You can even keep 5 images if you want. but there is no need to keep them all.
If you want you application to be reactive, you can keep thumbnails in memory, display it, and only load the full image if you stay on the image more than x seconds.
Here is the code to create a smaller image and manage your list of bitmaps
Options mThumbOpts = new Options();
mThumbOpts.inSampleSize = 4;
/** The fonction to get a Bitmap from the list */
private Bitmap getBitmap(int id) {
// TODO recycle previous/next bitmaps here (id -2 and id +2 if they exist)
// Reload the image if necessary
if (bitmapList[id] == null || bitmapList[id].isRecycled()) {
bitmapList[id] = BitmapFactory.decodeFile(imagePath, mThumbOpts);
}
return bitmapList[id];
}
inSampleSize set to a value > 1, requests the decoder to subsample the original image, returning a smaller image to save memory.
Use a power of 2 for it to be more efficient.