Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix LottieTask leak #2465

Merged
merged 1 commit into from
Feb 18, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 36 additions & 12 deletions lottie/src/main/java/com/airbnb/lottie/LottieTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import androidx.annotation.RestrictTo;

import com.airbnb.lottie.utils.Logger;
import com.airbnb.lottie.utils.LottieThreadFactory;

import java.util.ArrayList;
import java.util.LinkedHashSet;
Expand Down Expand Up @@ -35,7 +36,7 @@ public class LottieTask<T> {
* You may change this to run deserialization synchronously for testing.
*/
@SuppressWarnings("WeakerAccess")
public static Executor EXECUTOR = Executors.newCachedThreadPool();
public static Executor EXECUTOR = Executors.newCachedThreadPool(new LottieThreadFactory());

/* Preserve add order. */
private final Set<LottieListener<T>> successListeners = new LinkedHashSet<>(1);
Expand Down Expand Up @@ -64,7 +65,7 @@ public LottieTask(T result) {
setResult(new LottieResult<>(e));
}
} else {
EXECUTOR.execute(new LottieFutureTask(runnable));
EXECUTOR.execute(new LottieFutureTask<T>(this, runnable));
}
}

Expand Down Expand Up @@ -173,22 +174,45 @@ private synchronized void notifyFailureListeners(Throwable e) {
}
}

private class LottieFutureTask extends FutureTask<LottieResult<T>> {
LottieFutureTask(Callable<LottieResult<T>> callable) {
private static class LottieFutureTask<T> extends FutureTask<LottieResult<T>> {

private LottieTask<T> lottieTask;

LottieFutureTask(LottieTask<T> task, Callable<LottieResult<T>> callable) {
super(callable);
lottieTask = task;
}

@Override
protected void done() {
if (isCancelled()) {
// We don't need to notify and listeners if the task is cancelled.
return;
}

try {
setResult(get());
} catch (InterruptedException | ExecutionException e) {
setResult(new LottieResult<>(e));
if (isCancelled()) {
// We don't need to notify and listeners if the task is cancelled.
return;
}

try {
lottieTask.setResult(get());
} catch (InterruptedException | ExecutionException e) {
lottieTask.setResult(new LottieResult<>(e));
}
} finally {
// LottieFutureTask can be held in memory for up to 60 seconds after the task is done, which would
// result in holding on to the associated LottieTask instance and leaking its listeners. To avoid
// that, we clear our the reference to the LottieTask instance.
//
// How is LottieFutureTask held for up to 60 seconds? It's a bug in how the VM cleans up stack
// local variables. When you have a loop that polls a blocking queue and assigns the result
// to a local variable, after looping the local variable will still reference the previous value
// until the queue returns the next result.
//
// Executors.newCachedThreadPool() relies on a SynchronousQueue and creates a cached thread pool
// with a default keep alice of 60 seconds. After a given worker thread runs a task, that thread
// will wait for up to 60 seconds for a new task to come, and while waiting it's also accidentally
// keeping a reference to the previous task.
//
// See commit d577e728e9bccbafc707af3060ea914caa73c14f in AOSP for how that was fixed for Looper.
lottieTask = null;
}
}
}
Expand Down
Loading