Hello I have a Hasmap of bitmaps which I need to store on the Android device to be used when next the application starts.
My hashmap looks like this, and contains up to 1000 Bitmaps:
private static HashMap <String, Bitmap> cache = new HashMap<String, Bitmap>();
You might want to consider create extension of Map (by using AbstractMap) and override the related functions. In general the structure of the extension should have:
An in memory hard cache using regular Map. This should be a size bound cache object. You could leverage LinkedHashMap and override removeEldesEntry() to check if the size is exceeded
this.objectMap = Collections.synchronizedMap(new LinkedHashMap() {
#Override
protected boolean removeEldestEntry(LinkedHashMap.Entry eldest) {
if (size() > HARD_CACHE_CAPACITY) {
// remove from cache, pass to secondary SoftReference cache or directly to the disk
}
}
});
If the cache is exceeded, then put it to disk
Override the get function to do the following : On initial get, load the bitmap from disk based on certain naming convention (related to the key) and store it in memory. Roughly something like (please forgive any syntax error)
#Override
public Bitmap get(Object key) {
if(key != null) {
// first level, hard cache
if(objectMap.containsKey(key)) {
return objectMap.get(key);
}
// soft reference cache
if(secondaryCache.containsKey(key)) {
return secondaryCache.get(key);
}
// get from disk if it is not in hard or soft cache
String fileName = "Disk-" + key + ".txt";
File f = new File(cacheDir, fileName);
if(f.exists()) {
// put this back to the hard cache
Bitmap object = readFromReader(f);
if(object != null) {
objectMap.put((String)key, object);
return object;
}
}
}
return null; // unable to get from any data source
}
Similarly your put has to be override to put to the disk for later use, so when you reinitialize your app you could just create an instance of the map extension. If you want, you could also preload the hashmap by most recently used items in the app. Basically, by extending the AbstractMap, you get the flexibilities without killing your memory with that 1000 bitmaps. Hope this helps
Related
I am working on an app that uses a Recyclerview to display mp3 files, providing its cover art image along with other info. It works but is slow once it starts dealing with a dozen or more cover arts to retrieve, as I am currently doing this from the id3 on the main thread, which I know is not a good idea.
Ideally, I would work with placeholders so that the images can be added as they become available. I've been looking into moving the retrieval to a background thread and have looked at different options: AsyncTask, Service, WorkManager. AsyncTask seems not to be the way to go as I face memory leaks (I need context to retrieve the cover art through MetadataRetriever). So I am leaning away from that. Yet I am struggling to figure out which approach is best in my case.
From what I understand I need to find an approach that allows multithreading and also a means to cancel the retrieval in case the user has already moved on (scrolling or navigating away). I am already using Glide, which I understand should help with the caching.
I know I could rework the whole approach and provide the cover art as images separately, but that seems a last resort to me, as I would rather not weigh down the app with even more data.
The current version of the app is here (please note it will not run as I cannot openly divulge certain aspects). I am retrieving the cover art as follows (on the main thread):
static public Bitmap getCoverArt(Uri medUri, Context ctxt) {
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
mmr.setDataSource(ctxt, medUri);
byte[] data = mmr.getEmbeddedPicture();
if (data != null) {
return BitmapFactory.decodeByteArray(data, 0, data.length);
} else {
return null;
}
}
I've found many examples with AsyncTask or just keeping the MetaDataRetriever on the main thread, but have yet to find an example that enables a dozen or more cover arts to be retrieved without slowing down the main thread. I would appreciate any help and pointers.
It turns out it does work with AsyncTask, as long as it is not a class onto itself but setup and called from a class with context. Here is a whittled down version of my approach (I am calling this from within my Adapter.):
//set up titles and placeholder image so we needn't wait on the image to load
titleTv.setText(selectedMed.getTitle());
subtitleTv.setText(selectedMed.getSubtitle());
imageIv.setImageResource(R.drawable.ic_launcher_foreground);
imageIv.setAlpha((float) 0.2);
final long[] duration = new long[1];
//a Caching system that helps reduce the amount of loading needed. See: https://github.com/cbonan/BitmapFun?files=1
if (lruCacheManager.getBitmapFromMemCache(selectedMed.getId() + position) != null) {
//is there an earlier cached image to reuse? imageIv.setImageBitmap(lruCacheManager.getBitmapFromMemCache(selectedMed.getId() + position));
imageIv.setAlpha((float) 1.0);
titleTv.setVisibility(View.GONE);
subtitleTv.setVisibility(View.GONE);
} else {
//time to load and show the image. For good measure, the duration is also queried, as this also needs the setDataSource which causes slow down
new AsyncTask<Uri, Void, Bitmap>() {
#Override
protected Bitmap doInBackground(Uri... uris) {
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
mmr.setDataSource(ctxt, medUri);
byte[] data = mmr.getEmbeddedPicture();
Log.v(TAG, "async data: " + Arrays.toString(data));
String durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
duration[0] = Long.parseLong(durationStr);
if (data != null) {
InputStream is = new ByteArrayInputStream(mmr.getEmbeddedPicture());
return BitmapFactory.decodeStream(is);
} else {
return null;
}
}
#Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
durationTv.setVisibility(View.VISIBLE);
durationTv.setText(getDisplayTime(duration[0], false));
if (bitmap != null) {
imageIv.setImageBitmap(bitmap);
imageIv.setAlpha((float) 1.0);
titleTv.setVisibility(View.GONE);
subtitleTv.setVisibility(View.GONE);
} else {
titleTv.setVisibility(View.VISIBLE);
subtitleTv.setVisibility(View.VISIBLE);
}
lruCacheManager.addBitmapToMemCache(bitmap, selectedMed.getId() + position);
}
}.execute(medUri);
}
I have tried working with Glide for the caching, but I haven't been able to link the showing/hiding of the TextViews to whether there is a bitmap. In a way though, this is sleeker as I don't need to load the bulk of the Glide-library. So I am happy with this for now.
I might just be confused about how LruCache is supposed to work, but are does it not allow accessing objects from one instance that were saved on another instance? Surely this is not the case otherwise it kind of defeats the purpose of having cache.
Example:
class CacheInterface {
private val lruCache: LruCache<String, Bitmap>
init {
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
// Use 1/8th of the available memory for this memory cache.
val cacheSize = maxMemory / 8
lruCache = object : LruCache<String, Bitmap>(cacheSize) {
override fun sizeOf(key: String, value: Bitmap): Int {
return value.byteCount / 1024
}
}
}
fun getBitmap(key: String): Bitmap? {
return lruCache.get(key)
}
fun storeBitmap(key: String, bitmap: Bitmap) {
lruCache.put(key, bitmap)
Utils.log(lruCache.get(key))
}
}
val bitmap = getBitmal()
val instance1 = CacheInterface()
instance1.storeBitmap("key1", bitmap)
log(instance1.getBitmap("key1")) //android.graphics.Bitmap#6854e91
log(CacheInterface().getBitmap("key1")) //null
As far as I understand, cache is stored until it's deleted by the user (manually or uninstalling the app), or cleared by the system when it exceeds the allowed space. What am I missing?
An LruCache object just stores references to objects in memory. As soon as you lose the reference to the LruCache, the LruCache object and all of the objects within that cache are garbage collected. There's nothing stored to disk.
Yes it is. I'll just share here what I was confused about in case anyone also is.
Initially because of this guide (Caching Bitmaps) that reccomends using LruCache, I was left under the impression that LruCache was an interface to access app's cache, but like #CommonsWare mentioned it has no I/O in it - it's just a utility class to hold memory using the LRU policy. To access your app's cache you need to use Context.getCacheDir(), good explanation here. In my case I ended up using a singleton of LruCache, since I already have a service running most of the time the app will not be killed every time it's closed.
log(CacheInterface().getBitmap("key1")) //null
equals
val instance2 = CacheInterface()
log(instance2 .getBitmap("key1"))
instance1 != instance2
change to Singleton
object CacheInterface{
...
}
use
CacheInterface.storeBitmap("key1",bitmap)
CacheInterface.getBitmap("key1")
I am displaying all apps installed in a gridView. When loading a lot of apps, lets say 30 or more, the icons will display at the default Android icon and then several seconds later update to the correct icon. I am wondering about improvements I can make to my code to make the icon images display faster.
Load the following with: new LoadIconsTask().execute(mApps.toArray(new AppsInstalled[]{}));
Here is what I do.
private class LoadIconsTask extends AsyncTask<AppsInstalled, Void, Void>{
#Override
protected Void doInBackground(AppsInstalled... params) {
// TODO Auto-generated method stub
Map<String, Drawable> icons = new HashMap<String, Drawable>();
PackageManager manager = getApplicationContext().getPackageManager();
// match package name with icon, set Adapter with loaded Map
for (AppsInstalled app : params) {
String pkgName = app.getAppUniqueId();
Drawable ico = null;
try {
Intent i = manager.getLaunchIntentForPackage(pkgName);
if (i != null) {
ico = manager.getActivityIcon(i);
}
} catch (NameNotFoundException e) {
Log.e(TAG, "Unable to find icon match based on package: " + pkgName
+ " : " + e.getMessage());
}
icons.put(app.getAppUniqueId(), ico);
}
mAdapter.setIcons(icons);
return null;
}
Also populate my listing of apps before I loadIconsTask() with
private List<App> loadInstalledApps(boolean includeSysApps) {
List<App> apps = new ArrayList<App>();
// the package manager contains the information about all installed apps
PackageManager packageManager = getPackageManager();
List<PackageInfo> packs = packageManager.getInstalledPackages(0); // PackageManager.GET_META_DATA
for (int i = 0; i < packs.size(); i++) {
PackageInfo p = packs.get(i);
ApplicationInfo a = p.applicationInfo;
// skip system apps if they shall not be included
if ((!includeSysApps)
&& ((a.flags & ApplicationInfo.FLAG_SYSTEM) == 1)) {
continue;
}
App app = new App();
app.setTitle(p.applicationInfo.loadLabel(packageManager).toString());
app.setPackageName(p.packageName);
app.setVersionName(p.versionName);
app.setVersionCode(p.versionCode);
CharSequence description = p.applicationInfo
.loadDescription(packageManager);
app.setDescription(description != null ? description.toString()
: "");
apps.add(app);
}
return apps;
}
In regards to my Adapter class it is standard. My getView() looks like the following:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
AppViewHolder holder;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.row, null);
// creates a ViewHolder and stores a reference to the children view
// we want to bind data to
holder = new AppViewHolder();
holder.mTitle = (TextView) convertView.findViewById(R.id.apptitle);
holder.mIcon = (ImageView) convertView.findViewById(R.id.appicon);
convertView.setTag(holder);
} else {
// reuse/overwrite the view passed assuming that it is castable!
holder = (AppViewHolder) convertView.getTag();
}
App app = mApps.get(position);
holder.setTitle(app.getTitle());
if (mIcons == null || mIcons.get(app.getPackageName()) == null) {
holder.setIcon(mStdImg);
} else {
holder.setIcon(mIcons.get(app.getPackageName()));
}
return convertView;
}
Is there a better way? Can I somehow store the images of the icons in a data structure and when I return back to this Activity I can skip the loadIconsTask? Is that possible? Thank you in advance.
You can use Picasso library with a custom RequestHandler to load the icons in the background.
First create a RequestHandler which will handle the specific case where an app icon needs to be loaded.
public class AppIconRequestHandler extends RequestHandler {
/** Uri scheme for app icons */
public static final String SCHEME_APP_ICON = "app-icon";
private PackageManager mPackageManager;
public AppIconRequestHandler(Context context) {
mPackageManager = context.getPackageManager();
}
/**
* Create an Uri that can be handled by this RequestHandler based on the package name
*/
public static Uri getUri(String packageName) {
return Uri.fromParts(SCHEME_APP_ICON, packageName, null);
}
#Override
public boolean canHandleRequest(Request data) {
// only handle Uris matching our scheme
return (SCHEME_APP_ICON.equals(data.uri.getScheme()));
}
#Override
public Result load(Request request, int networkPolicy) throws IOException {
String packageName = request.uri.getSchemeSpecificPart();
Drawable drawable;
try {
drawable = mPackageManager.getApplicationIcon(packageName);
} catch (PackageManager.NameNotFoundException ignored) {
return null;
}
Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
return new Result(bitmap, Picasso.LoadedFrom.DISK);
}
}
In your adapter, create a Picasso instance and add your RequestHandler.
// field variable
private Picasso mPicasso;
// in constructor
Picasso.Builder builder = new Picasso.Builder(context);
builder.addRequestHandler(new AppIconRequestHandler(context));
mPicasso = builder.build();
In your adapter's getView() load the icon using Picasso.
mPicasso.load(AppIconRequestHandler.getUri(app.packageName)).into(holder.mIcon);
it's surprising the system takes that much time in getting these lists, you may want to add some logs with timestamping to see which one is the demanding operation.
I don't know if that procedure can be further optimized, I haven't used these system API's very much, but what you can certainly do is to cache this list
Create it in onResume / onCreate as a static list, and (for the sake of correctness) destroy it in onPause / onStop if you want to consider the case where the user may install an application while in your app (onPause will be called), but you can certainly skip this step.
You may want to also permanently cache the list in the sdcard and find some simple and fast heuristic to decide if the list has changed in order to recreate it. Something like maybe the number of installed packages together with something else (to discard the case when the user uninstalls 3 apps and install 3 different apps, the number of packages will be the same and you have to detect this somehow).
EDIT- To recommend a caching mechanism, you should identify which one is the slow operation. Just guessing, and from your question "the icons take some seconds to appear" it looks like that the slow operation is:
ico = manager.getActivityIcon(i);
but I might be wrong. Let's suppose I'm right, so a cheap caching can be:
1) Move the Map<String, Drawable> icons = new HashMap<String, Drawable>(); outside of doInBackground to the root of the class and make it static, like:
private static Map<String, Drawable> sIcons = new HashMap<String, Drawable>()
2) In your loadIconsTask consider the case you already have this icon:
for (AppsInstalled app : params) {
String pkgName = app.getAppUniqueId();
if (sIcons.containsKey(pkgName) continue;
.
.
.
}
This is because sIcons is now static and will be alive as long as your application is alive.
3) As a classy thing, you may want to change sIcons from Drawable to Bitmap. Why? Because a Drawable may keep inside references to Views and Context and it's a potential memory leak. You can get the Bitmap from a Drawable very easily, calling drawable.getBitmap() , (Assuming drawable is a BitmapDrawable, but it will obviously be because it's an app icon), so suming up you'll have:
// the static icon dictionary now stores Bitmaps
static Map<String, Bitmap> sIcons = new HashMap<String, Bitmap>();
.
.
// we store the bitmap instead of the drawable
sIcons.put(app.getAppUniqueId(), ((BitmapDrawable)ico).getBitmap());
.
.
// when setting the icon, we create the drawable back
holder.setIcon(new BitmapDrawable(mIcons.get(app.getPackageName())));
This way your static hashmap will never leak any memory.
4) You may want to check if it's worth to store those bitmaps on disk. Mind this is some additional work and it might not be worth if the time to load the icon from disk is similar to the time to load the icon calling ico = manager.getActivityIcon(i);. It may be (i don't know if manager.getActivityIcon() extracts the icon from the APK) but it certainly may be not.
If you check out it's worth, when you create the list, you can save the bitmaps to the sdcard like this:
// prepare a file to the application cache dir.
File cachedFile=new File(context.getCacheDir(), "icon-"+app.getPackageName());
// save our bitmap as a compressed JPEG with the package name as filename
myBitmap.compress(CompressFormat.JPEG, quality, new FileOutputStream(cachedFile);
... then when loading the icons, check if the icon exists and load from the sdcard instead:
String key=app.getPackageName();
File localFile=new File(context.getCacheDir(), "icon-"+key);
if (localFile.exists()) {
// the file exists in the sdcard, just load it
Bitmap myBitmap = BitmapFactory.decodeStream(new FileInputStream(localFile));
// we have our bitmap from the sdcard !! Let's put it into our HashMap
sIcons.put(key, myBitmap)
} else {
// use the slow method
}
Well as you see it's just a matter of identifying the slow operation. If our above assumption is correct, your stored bitmaps will survive your application destroy and it will hopefully optimize the icon loading.
You can use Glide for automatic loading and caching and the URI of each application icon:
final RequestManager mGlide = Glide.with(activity);
final Uri appIconUri = applicationInfo.icon != 0 ?
Uri.parse("android.resource://" + packageName + "/" + applicationInfo.icon) :
null;
if (appIconUri != null) mGlide.load(appIconUri).into(holder.appIconImgView);
else {
mGlide.clear(holder.appIconImgView); // as suggested here: https://bumptech.github.io/glide/doc/getting-started.html
mGlide.load(android.R.drawable.sym_def_app_icon).into(holder.appIconImgView);
}
The reason I suggest Glide and not other image loading libraries is that Glide supports XML drawable (or dynamic/adaptive or vector icons) loading while others don't (see https://github.com/facebook/fresco/issues/2173)
I am getting OutOfMemoryError on my application. When i went through some tutorials, i came to know that, I can solve this issue by using Softreference/Weakreference. But I don't know that how to use Softreference/Weakreference.
Please suggest me some tutorials that providing examples for the Softreference or Weakreference.
Thank you...
package com.myapp;
import java.io.File;
import java.lang.ref.SoftReference;
import java.util.WeakHashMap;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
public class BitmapSoftRefrences {
public static String SDPATH = Environment.getExternalStorageDirectory()
+ "/MYAPP";
// 1. create a cache map
public static WeakHashMap<String, SoftReference<Bitmap>> mCache = new WeakHashMap<String, SoftReference<Bitmap>>();
public static String TAG = "BitmapSoftRefrences";
// 2. ask for bitmap
public static Bitmap get(String key) {
if (key == null) {
return null;
}
try {
if (mCache.containsKey(key)) {
SoftReference<Bitmap> reference = mCache.get(key);
Bitmap bitmap = reference.get();
if (bitmap != null) {
return bitmap;
}
return decodeFile(key);
}
} catch (Exception e) {
// TODO: handle exception
Logger.debug(BitmapSoftRefrences.class,
"EXCEPTION: " + e.getMessage());
}
// the key does not exists so it could be that the
// file is not downloaded or decoded yet...
File file = new File(SDPATH + "/" + key);
if (file.exists()) {
return decodeFile(key);
} else {
Logger.debug(BitmapSoftRefrences.class, "RuntimeException");
throw new RuntimeException("RuntimeException!");
}
}
// 3. the decode file will return bitmap if bitmap is not cached
public static Bitmap decodeFile(String key) {
// --- prevent scaling
BitmapFactory.Options opt = new BitmapFactory.Options();
opt.inScaled = false;
Bitmap bitmap = BitmapFactory.decodeFile(SDPATH + "/" + key, opt);
mCache.put(key, new SoftReference<Bitmap>(bitmap));
return bitmap;
}
public static void clear() {
mCache.clear();
}
}
See the following tutorial
How to use SoftReference
To create a WeakReference, the syntax is WeakReference<SomeType> myWeakReference = new WeakReference<SomeType>(actualObject);. To retrieve the object via that WeakReference, do the check if (weakWidget == null). That way, you will avoid a NullPointerException if it has been garbage-collected already.
This Java.net article by Ethan Nicholas explains why you would want to use a WeakReference instead of a strong one. It provides the example of a final (unextendible) class called Widget that has no defined serial UID, presuming that the developer decides to define a serial UID to track each Widget instance. They do so by creating a new HashMap and doing something like serialNumberMap.put(widget, widgetSerialNumber); which is a strong reference. That means it must be explicitly cleaned up when no longer needed. The developer is responsible for knowing exactly when to manually "garbage-collect" that reference and remove it from the HashMap, which should be done only when they're really sure it's not needed anymore. This may be the problem you ran into in your application.
In this particular case, as the article explains, the developer could use the WeakHashMap class instead (as #NayAneshGupte put in his example), wherein the key is actually a WeakReference. This would allow the JVM nullify the keys to old Widgets as it saw fit, so that the garbage collector could come along and destroy their associated objects.
The article also goes on to talk about SoftReferences and PhantomReferences (which I've never used). You can read more about all of these in this javapapers.com article and this Rally blog.
My application is using bitmaps and every time the user come to the specific activity where it shows an image the second time it stops working.
Bitmap bm = BitmapFactory.decodeFile(Environment.getExternalStorageDirectory()+"//Pics/"Image.jpg");
I have tried using things like...
BitmapFactory.Options options = new BitmapFactory.Options();
options.inTempStorage = new byte[16*1024];
Not sure what to set it too. But this doesnt help. Once the user leaves this activity is there not a way to clear the bitmap etc? thanks
Call Bitmap.recycle() when you are done using the Bitmap to free the memory.
Besides using Bitmap.recycle() as suggested (which is not suitable for all situations and it's a pain in the neck to be asking: "do I still need this bitmap?"), I always use this technique which works really fine:
// 1. create a cache map
private WeakHashMap<String, SoftReference<Bitmap>> mCache;
As you can see, it's a hash map of WeakReferences with a SoftReference as the values.
//2. when you need a bitmap, ask for it:
public Bitmap get(String key){
if( key == null ){
return null;
}
if( mCache.containsKey(key) ){
SoftReference<Bitmap> reference = mCache.get(key);
Bitmap bitmap = reference.get();
if( bitmap != null ){
return bitmap;
}
return decodeFile(key);
}
// the key does not exists so it could be that the
// file is not downloaded or decoded yet...
File file = new File(Environment.getExternalStorageDirectory(), key);
if( file.exists() ){
return decodeFile(key);
} else{
throw new RuntimeException("Boooom!");
}
}
This will check the cache map. If the file was already decoded, it will be returned; otherwise it will be decoded and cached.
//3. the decode file will look like this in your case
private Bitmap decodeFile(String key) {
Bitmap bitmap = BitmapFactory.decodeFile(Environment.getExternalStorageDirectory()+"//Pics/"+key);
mCache.put(key, new SoftReference<Bitmap>(bitmap));
return bitmap;
}
Working with soft references is nice because you shift the responsibility of removing bitmaps from memory to the OS.
Be aware.
When we think about softreferences we think that the OS will remove the softreferenced objects from memrory before reporting an outofmemory exception.
In android this is not always true. I had to implement my own caching system for images and I can assure you softreferenced objects were not removed from memory when memory was almost full.
Finally I had to switch to hard references (the normal ones) but used android.support.v4.util.LruCache for managing the cached objects. I would call recycle on the onRemoved callback from the lru cache. Its definetely more convenient.
Cheers.