portfoliobuilder portfoliobuilder - 11 days ago 6
Android Question

How to load image (icons) of apps faster in gridView?

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.

Answer

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.