I am using a binding adapter to load images in a recycler view. Images appear fine. While fast scrolling I noticed sometimes I was getting a 'connection leaked' message from Picasso.
The problem comes from dead image links, hardcoding all of my image urls to point nowhere produces the error for every image after scrolling the first couple off the screen.
W/OkHttpClient: A connection to https://s3-eu-west-1.amazonaws.com/ was leaked. Did you forget to close a response body?
The code is basically identical to this sample.
BindingUtils.kt
object BindingUtils {
#BindingAdapter("imageUrl")
#JvmStatic
fun setImageUrl(imageView: ImageView, url: String) {
Picasso.with(imageView.context).load(url).into(imageView)
}
xml
<ImageView
android:id="#+id/imageview_merchant_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/primary"
android:scaleType="centerCrop"
app:imageUrl="#{viewModel.background}"/>
gradle
implementation "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion"
implementation "com.squareup.retrofit2:adapter-rxjava2:$rootProject.retrofitVersion"
implementation "com.squareup.retrofit2:converter-gson:$rootProject.retrofitVersion"
implementation "com.squareup.okhttp3:logging-interceptor:$rootProject.okhttpLoggingVersion"
implementation "com.squareup.picasso:picasso:$rootProject.picassoVersion"
retrofitVersion = '2.3.0'
okhttpLoggingVersion = '3.6.0'
picassoVersion = '2.5.2'
I can see several references to people needing to closing connections for standard Okhttp requests but seeing as that Picasso load call is a one-liner how can this be leaking?
Under the hood Picasso is using okhttp3 for handling its network requests. See here the code for Picasso's NetworkRequestHandler class: https://github.com/square/picasso/blob/0728bb1c619746001c60296d975fbc6bd92a05d2/picasso/src/main/java/com/squareup/picasso/NetworkRequestHandler.java
There is a load function that handles an okhttp Request:
#Override public Result load(Request request, int networkPolicy) throws IOException {
okhttp3.Request downloaderRequest = createRequest(request, networkPolicy);
Response response = downloader.load(downloaderRequest);
ResponseBody body = response.body();
if (!response.isSuccessful()) {
body.close();
throw new ResponseException(response.code(), request.networkPolicy);
}
// Cache response is only null when the response comes fully from the network. Both completely
// cached and conditionally cached responses will have a non-null cache response.
Picasso.LoadedFrom loadedFrom = response.cacheResponse() == null ? NETWORK : DISK;
// Sometimes response content length is zero when requests are being replayed. Haven't found
// root cause to this but retrying the request seems safe to do so.
if (loadedFrom == DISK && body.contentLength() == 0) {
body.close();
throw new ContentLengthException("Received response with 0 content-length header.");
}
if (loadedFrom == NETWORK && body.contentLength() > 0) {
stats.dispatchDownloadFinished(body.contentLength());
}
InputStream is = body.byteStream();
return new Result(is, loadedFrom);
}
I am not too familiar with the Picasso project, but it seems like the response body object is not closed in all cases. You may have spotted a bug in Picasso and may want to file an issue at picasso's github
Wild guess, if it has to do anything with the leaking of the context of your activity. Try with applicationContext
Picasso.with(imageView.context.applicationContext).load(url).into(imageView)
Related
I'm trying to use Glide to display thumbnails from the Google Photos library in a RecyclerView. In order to fetch images from this library, I must make two HTTP requests: first I must get the MediaItem from the id (I've already obtained a list of ids in a previous step), and second I must request the actual image from thumbnailUrl. This is the recommended process, as baseUrls expire after one hour so you aren't supposed to store them:
val googlePhotosThumbnailUrl =
App.googlePhotosService.getMediaItem(asset.googlePhotosId) // First HTTP request fetches MediaItem
.run {
val baseUrl = this.baseUrl
val thumbnailUrl = "$baseUrl=w400-h400" // Appends the requested dimensions to the Url.
thumbnailUrl // Second HTTP request fetches this URL
}
The problem is that Glide's load() method doesn't appear to support chaining HTTP requests like what's shown above:
GlideApp.with(itemView.context)
.asBitmap()
.load(googlePhotosThumbnailUrl)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(binding.imageViewLargeThumbnail)
The above code executes synchronously, so loading is incredibly slow. I've managed to fix this by using coroutines as shown below. But the problem with this is Glide doesn't cache any of the images, so if I scroll down and back up Glide refetches every image:
override fun bindAsset(asset: GooglePhotosAsset, position: Int) {
this.asset = asset
this.index = position
// We set the loading animation here for Google Photos assets, since for those we need to fetch a mediaItem and then a baseUrl.
// This forces us to perform the process in a coroutine, and Glide can't set the loading animation until the baseUrl is fetched.
binding.imageViewLargeThumbnail.setImageResource(R.drawable.loading_animation)
fragment.lifecycleScope.launch(Dispatchers.Default) {
val googlePhotosThumbnailUrl = App.googlePhotosService.getMediaItem(asset.googlePhotosId) // First HTTP request fetches MediaItem
.run {
val baseUrl = this.baseUrl
val thumbnailUrl = "$baseUrl=w400-h400" // Appends the requested dimensions to the Url.
thumbnailUrl // Second HTTP request fetches this URL
}
withContext(Dispatchers.Main) {
GlideApp.with(itemView.context)
.asBitmap()
.load(googlePhotosThumbnailUrl)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.fitCenter()
.into(binding.imageViewLargeThumbnail)
}
}
}
The only potentially relevant answer I've found is this one, but it seems super complicated and outdated. Are there any better solutions?
I am changing the way our application works to use retrofit instead of just OkHTTP
The way it used to work is we would send the request, retrieve the body as an input stream and read all bytes into a string.
After that we would parse the body using gson.
The problem is that the server seems to have a problem with the configuration (which I am told is on the list of things to fix but will take a long time) so for example it may return 400 bytes of data, but will send the message that the bytes are actually 402.
The way we currently handle it is by catching the EOF exception and ignoring it, and then parsing the returned string normally.
right now I use the following request to get the entities I want
#GET("/services/v1/entities")
suspend fun getEntities() : List<ServerEntity>
which , when there is no error, works correctly
the solutions I've found so far are either
a) use the following code to retry all requests until I do not get an EOF exception:
internal suspend fun <T> tryTimes(times: Int = 3, func: suspend () -> T): T {
var tries = times.coerceAtLeast(2)
try {
var lastException: EOFException? = null
while (tries > 0) {
try {
return func.invoke()
} catch (eof: EOFException) {
lastException = eof
tries--
}
}
throw lastException!!
} finally {
log.d("DM", "tried request ${times.coerceAtLeast(2) - tries} times")
}
}
which most of the time logs either 0 or 1 tries
or change all my requests to
#GET("/services/v1/entities")
suspend fun getEntities() : ResponseBody
and parse the stream manually ( ResponseBody may be incorrect but you can understand what I mean)
is there a way to use my original function and make retrofit know that in the case of an EOF exception it should resume instead of stopping?
I am new in retrofit2 world, currently I have a problem on getting errorbody from onNext.
Here is my sample code :
public void onNext(Response<LoginResponse> value) {
ResponseBody responseBody = value.errorBody();
String sam = responseBody.toString();
}
My issue is, I cant get the errorbody().content. It is like it is not accessible.
You can try:
String errorBody = value.errorBody().string;
or
String errorBody = value.errorBody().toString();
Retrofit's error bodys are of type OkHttp3 ResponseBody. This class is abstract and its implementations define different ways the content is represented internally.
There are different ways you can get the content. You can get it in bytes, as a string, or even get an InputStream for it - check them out here.
I suppose an easy way is to use string():
value.errorBody().string();
Note that these methods usually read the response from a buffer, which means that if I recall correctly, calling again string() would not give you the content of the response again. In particular, string() also reads the entire body into memory, which may cause an out of memory exception.
After you have the content, if you want it as an object from your data model, you'll have to deserialize it manually. There are numerous ways to do this and it's easy to find on the web, but for sake of completeness, here's a way to do it with gson:
String error = value.errorBody().string();
MyPojo pojo = new Gson().fromJson(error, MyPojo.class);
(here MyPojo would be your error class representing the error)
You can't get errorBody without http-error code.
Use smt like this:
if (!value.isSuccessful && value.errorBody() != null) {
val errorBodyText = value.errorBody()?.string()?:""
val errorResponse = Gson().fromJson<ErrorObject>(errorBodyText, ErrorObject::class.java)
}
I know that mWaitingRequest keeps the requests that has the same cacheKey, when a Request is finished, the requests with the same cacheKey will be added to the mCacheQueue.
But I don't think this is necessory, why not just add the request with the same cacheKey to the mCacheQueue directly?
I just search google, but don't get the answer.
because then there will no cache for them and all will go to the networkqueue and u dont want that
the requests with the same cacheKey will be added to the mCacheQueue
No the request is only added if it must be cached, take a look at the source code again:
<T> void finish(Request<T> request) {
...
if (request.shouldCache()) {
synchronized (mWaitingRequests) {
String cacheKey = request.getCacheKey();
Queue<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
if (waitingRequests != null) {
if (VolleyLog.DEBUG) {
VolleyLog.v("Releasing %d waiting requests for cacheKey=%s.",
waitingRequests.size(), cacheKey);
}
// Process all queued up requests. They won't be considered as in flight, but
// that's not a problem as the cache has been primed by 'request'.
mCacheQueue.addAll(waitingRequests);
}
}
}
}
why not just add the request with the same cacheKey to the mCacheQueue
directly?
First you should note that the server determines the caching policy, for example the server may not let you cache the data and set cache field of http header to something like:
cache-control: private, max-age=0, no-cache
that means every new request to the same URLcan have a different response, can have a new response, that means the server response can change any time and must not be cached. Now if the user wants to cache the response and has made multiple requests each request may have a new response, so for simplicity if the user wants to cache the data, every request must dispatch to NetworkDispatcher.
I have the following requirements for image download:
ignoring SSL errors (yes I am aware of the risks)
using a session cookie
I tried to adapt Picasso 2.4.0 to do that, below is my approach:
public static Picasso getPicasso(Context context) {
/* an OkHttpClient that ignores SSL errors */
final OkHttpClient client = getUnsafeOkHttpClient();
return new Picasso.Builder(context)
.downloader(new OkHttpDownloader(client) {
#Override
public Response load(Uri uri, boolean localCacheOnly) throws IOException {
final String RESPONSE_SOURCE_ANDROID = "X-Android-Response-Source";
final String RESPONSE_SOURCE_OKHTTP = "OkHttp-Response-Source";
HttpURLConnection connection = openConnection(uri);
connection.setRequestProperty("Cookie", getCookieHandler().
getCookieStore().getCookies().get(0).toString());
connection.setUseCaches(true);
if (localCacheOnly)
connection.setRequestProperty("Cache-Control", "only-if-cached,max-age=" + Integer.MAX_VALUE);
int responseCode = connection.getResponseCode();
if (responseCode == 401)
relogin();
else if (responseCode >= 300) {
connection.disconnect();
throw new ResponseException(responseCode + " " + connection.getResponseMessage());
}
String responseSource = connection.getHeaderField(RESPONSE_SOURCE_OKHTTP);
if (responseSource == null)
responseSource = connection.getHeaderField(RESPONSE_SOURCE_ANDROID);
long contentLength = connection.getHeaderFieldInt("Content-Length", -1);
boolean fromCache = parseResponseSourceHeader(responseSource);
return new Response(connection.getInputStream(), fromCache, contentLength);
}
}).build();
}
The only thing that I changed from the original source is adding a Cookie for the HttpURLConnection. I also copied (unchanged) the parseResponseSourceHeader() method since it has private access.
Note that the approach given here does NOT work (response code 401).
The image loading basically works, but there are major issues:
caching doesn't work (fromCache is always false and Picasso always reloads an image which has already been downloaded)
there's no "Content-Length" header, so contentLength is always -1
though the cache doesn't work, the RAM usage increases when loading next image (into exactly the same or any other ImageView), it seems the Bitmap object stays somewhere in the memory
when used inside the BaseAdapter of a GridView, it seems that Picasso tries to load all (or at least as many as the number of times getView() was called) images at the same time. Those images appear, then the app freezes and closes with the following (OOM?) log:
A/Looper﹕ Could not create wake pipe. errno=24
or
A/Looper﹕ Could not create epoll instance. errno=24
The described issues occur no matter if I use a custom Target of just an ImageView.
It seems I have broken some of Picasso mechanisms by overriding the load() method of the OkHttpDownloader, but I'm not getting what's wrong since I did minimal changes. Any suggestions are appreciated.
In case someone has a similar problem: it was a really lame mistake of mine. I was creating multiple Picasso instances which is complete nonsense. After ensuring the singleton pattern with a helper class that returns a single Picasso instance everything works as intended.