📜 ⬆️ ⬇️

Multithreading - as a means of increasing efficiency

A good practice in creating a responsive application is to ensure that your user interface requires a minimum of processing time. Each potentially long action that your application can hang should be put into a separate thread. Typical examples of such actions are network operations that carry unpredictable delays. Users can put up with small pauses, especially if you inform them about the progress, but the frozen application does not give them any choice but to close it.

In this tutorial, we will create a picture uploader that illustrates this situation. We will populate the ListView with thumbnails of images downloaded from the web. The created asynchronous process loading images in the background will speed up our application.


Picture Loader


')
Downloading images from the Internet is very simple and is done using the related HTTP class from the framework. Here is one of the implementations:

static Bitmap downloadBitmap( String url) {
final AndroidHttpClient client = AndroidHttpClient.newInstance( "Android" );
final HttpGet getRequest = new HttpGet(url);

try {
HttpResponse response = client.execute(getRequest);
final int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
Log.w( "ImageDownloader" , "Error " + statusCode + " while retrieving bitmap from " + url);
return null ;
}

final HttpEntity entity = response.getEntity();
if (entity != null ) {
InputStream inputStream = null ;
try {
inputStream = entity.getContent();
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
return bitmap;
} finally {
if (inputStream != null ) {
inputStream.close();
}
entity.consumeContent();
}
}
} catch (Exception e) {
// IOException IllegalStateException
getRequest.abort();
Log.w( "ImageDownloader" , "Error while retrieving bitmap from " + url, e.toString());
} finally {
if (client != null ) {
client.close();
}
}
return null ;
}


* This source code was highlighted with Source Code Highlighter .


Client and HTTP request created. If the request is successful, then the response flow will contain a picture, which will then be turned into a final icon. For normal operation of your application, its manifest should require INTERNET .
Caution: a bug in the previous version of BitmapFactory.decodeStream may interfere with the execution of your code during a slow connection. To avoid this problem, use FlushedInputStream (inputStream). Here is an example of this helper class:

static class FlushedInputStream extends FilterInputStream {
public FlushedInputStream(InputStream inputStream) {
super(inputStream);
}

@Override
public long skip( long n) throws IOException {
long totalBytesSkipped = 0L;
while (totalBytesSkipped < n) {
long bytesSkipped = in .skip(n - totalBytesSkipped);
if (bytesSkipped == 0L) {
int byte = read();
if ( byte < 0) {
}
totalBytesSkipped += bytesSkipped;
}
return totalBytesSkipped;
}
}




It grants that skip () will really miss that many bytes when we reach the end of the file.

If you suddenly use this method directly in the ListView method of the ListAdapter , then as a result you will get incredibly intermittent scrolling.

Moreover, this is a very bad idea, because AndroidHttpClient cannot be started along with the main thread. Instead, this code will display the error “This thread forbids HTTP requests” (“This thread loses HTTP requests”). Better use DefaultHttpClient , if you really want to put a stick in the wheel.

About asynchronous tasks



The AsyncTask class provides one of the simplest ways to launch a new task directly from the user interface thread. Let's create an ImageDownloader class that will be responsible for creating these tasks. It will provide a download method that will assign the downloaded image with its URL in the ImageView:

public class ImageDownloader {

public void download( String url, ImageView imageView) {
BitmapDownloaderTask task = new BitmapDownloaderTask(imageView);
task.execute(url);
}
}

/* BitmapDownloaderTask, */
}


* This source code was highlighted with Source Code Highlighter .


BitmapDownloaderTask is an asynchronous task that actually downloads the image. It starts using execute , which returns the value immediately, making this method really fast. Here is an example of the implementation of this class:

class BitmapDownloaderTask extends AsyncTask< String , Void, Bitmap > {
private String url;
private final WeakReference<ImageView> imageViewReference;
public BitmapDownloaderTask(ImageView imageView) {
imageViewReference = new WeakReference<ImageView>(imageView);
}
@Override
// Actual download method, run in the task thread
protected Bitmap doInBackground( String ... params ) {
// params comes from the execute() call: params[0] is the url.
return downloadBitmap( params [0]);
}
@Override
// Once the image is downloaded, associates it to the imageView
protected void onPostExecute( Bitmap bitmap) {
if (isCancelled()) {
bitmap = null ;

}
if (imageViewReference != null ) {
ImageView imageView = imageViewReference. get ();
if (imageView != null</fonthttp://habrahabr.ru/edit/topic/99631/#>) {
imageView.setImageBitmap(bitmap);
}}}}


The doInBackground method actually works on the task in its own process. It will simply use the downloadBitmap method that we introduced at the beginning of this article.

onPostExecute is launched on the called UI thread when the task is completed. It takes the resulting image as a parameter associated with imageView from download and stored in BitmapDownloaderTask . Remember that imageView is stored as WeakReference , so in the process of loading it will not hurt to clear it from the collected garbage. This explains why we need to make sure that both the links and imageView have a non-zero value (that is, they were not compiled) before using them in onPostExecute .

This example shows a simplified use of AsyncTask , and if you try this method, you will see that these few lines of code significantly increase the performance in ListView , which will now scroll smoothly.

However, the specific ListView behavior causes a problem in our implementation. Moreover, for efficiency reasons, ListView recycles images that appear on the screen during scrolling. If one object is skipped, it will be used by imageView repeatedly. Another time, the pictogram is displayed correctly at boot time, and is replaced with an image after the download. But what is the problem? As with most parallel applications, the whole problem is streamlined. In our case there is no guarantee that the download of tasks will end in the same order in which the download began. The result may be such that the displayed image will refer to the previous object in the list, since it took longer to load. It is not a question if the images you downloaded are linked once, and ImageViews are set for all, but let's fix this in the general case when they are used as a list.

Concurrency control



To solve this problem, we need to remember the order of the start of downloads, since the last download started is the most effective for displaying (?). Indeed, it is enough for each ImageView to remember its latest download. We will set this additional information using a special Drawable subclass that will communicate with the ImageView while it is loading. Here is the code of our DownloadedDrawable class:

static class DownloadedDrawable extends ColorDrawable {
private final WeakReference<BitmapDownloaderTask> bitmapDownloaderTaskReference;
public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) {
super(Color.BLACK);
bitmapDownloaderTaskReference =
new WeakReference<BitmapDownloaderTask>(bitmapDownloaderTask);
}
public BitmapDownloaderTask getBitmapDownloaderTask() {
return bitmapDownloaderTaskReference. get ();
}
}


This is an implementation using ColorDrawable , the result of which in ImageView will be a black screen when loading. You can use the “loading in process” image to inform the user. Again, note the use of WeakReference to reduce dependencies.

Let's change our code to take into account the new class. First, the download method must create an instance of this class, and associate it with imageView .

public void download( String url, ImageView imageView) {
if (cancelPotentialDownload(url, imageView)) {
BitmapDownloaderTask task = new BitmapDownloaderTask(imageView);
DownloadedDrawable downloadedDrawable = new DownloadedDrawable(task);
imageView.setImageDrawable(downloadedDrawable);
task.execute(url, cookie);
}
}


The cancelPotentialDownload method stops possible imageView downloads that will start soon. Please note that this is not enough to ensure that the newest downloads are always displayed, since the task can be completed and waiting for its onPostExecute method, which can only be completed after the download.

private static boolean cancelPotentialDownload( String url, ImageView imageView) {
BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);

if (bitmapDownloaderTask != null ) {
String bitmapUrl = bitmapDownloaderTask.url;
if ((bitmapUrl == null ) || (!bitmapUrl.equals(url))) {
bitmapDownloaderTask.cancel( true );
} else {
// The same URL is already being downloaded.
return false ;
}
}
return true ;
}


cancelPotentialDownload uses the cancel method from the AsyncTask class to stop active downloads. It returns true most of the time, so the download can be started in download . The only case in which we do not want this is when the download comes from the same URL, then we let it end. Remember that with this implementation, if the ImageView has garbage collected, the downloads associated with it will not stop. RecyclerListener must be used for this.

This method uses the getBitmapDownloaderTask helper function:

private static BitmapDownloaderTask getBitmapDownloaderTask(ImageView imageView) {
if (imageView != null ) {
Drawable drawable = imageView.getDrawable();
if (drawable instanceof DownloadedDrawable) {
DownloadedDrawable downloadedDrawable = (DownloadedDrawable)drawable;
return downloadedDrawable.getBitmapDownloaderTask();
}
}
return null ;
}


Finally, onPostExecute should be modified so that it associates icons only if the ImageView is still associated with this particular loading process.

if (imageViewReference != null ) {
ImageView imageView = imageViewReference. get ();
BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);
// Change bitmap only if this process is still associated with it
if ( this == bitmapDownloaderTask) {
imageView.setImageBitmap(bitmap);
}
}


With these modifications, our ImageDownloader class now provides the basic services we expected from it. Feel free to use it, or an asynchronous template that provides the flexibility of your applications.

Demonstration



The source code of this application is available on Google Code . You can switch between three different implementations described in this article. Remember that the cache size was limited to ten images in order to better demonstrate how different methods work.

image

For the future



This code has been simplified in order to focus your attention on concurrency and many other functions that are missing from our implementation. The ImageDownloader class will benefit from the cache, especially if used in conjunction with a ListView , which will most likely display the same image as many times as the user will use to scroll the screen up and down. Also, if necessary, you can add thumbnails and resize images.

Download errors and timeouts are correctly displayed in our implementation, it will simply generate an empty picture in this case. If you want, you can show any image that reports an error.

Our HTTP request is very simple. You can add parameters or connect cookies that some sites require.

The AsyncTask class used in this article is really a very convenient way to reduce user interface load. You can also use the Handler class to more finely control actions, such as controlling the total number of parallel downloads.

UPD. Thanks for the comments, corrected.

Source: https://habr.com/ru/post/99631/


All Articles