Skip to content
This repository was archived by the owner on Feb 26, 2023. It is now read-only.

Commit 7ff35c9

Browse files
author
Drozdov Artyom
committed
add: lock-free UIThreadHandler implementation
1 parent dc15ca5 commit 7ff35c9

5 files changed

Lines changed: 163 additions & 95 deletions

File tree

‎AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/api/UiThreadExecutor.java‎

Lines changed: 90 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,30 @@
1515
*/
1616
package org.androidannotations.api;
1717

18-
import java.util.*;
19-
import java.util.Map.Entry;
20-
2118
import android.os.Handler;
19+
import android.os.Looper;
20+
21+
import java.util.Collections;
22+
import java.util.Set;
23+
import java.util.concurrent.ConcurrentHashMap;
24+
import java.util.concurrent.ConcurrentLinkedQueue;
25+
import java.util.concurrent.atomic.AtomicInteger;
2226

2327
/**
2428
* This class provide operations for
2529
* {@link org.androidannotations.annotations.UiThread UiThread} tasks.
2630
*/
2731
public class UiThreadExecutor {
2832

29-
private static final Map<String, List<Runnable>> TASKS = new HashMap<String, List<Runnable>>();
30-
private static final Map<Runnable, Handler> HANDLERS = new HashMap<Runnable, Handler>();
33+
private static final Handler HANDLER = new Handler(Looper.getMainLooper());
34+
35+
static final ConcurrentHashMap<String, Set<Runnable>> TASKS = new ConcurrentHashMap<String, Set<Runnable>>();
36+
37+
private static final Set<Runnable> JUST_POSTED_TASKS = Collections.newSetFromMap(new ConcurrentHashMap<Runnable, Boolean>());
38+
39+
private static final ConcurrentLinkedQueue<Set<Runnable>> ALLOCATION_STASH = new ConcurrentLinkedQueue<Set<Runnable>>();
40+
private static final AtomicInteger ALLOCATION_STASH_SIZE = new AtomicInteger(0);
41+
private static final int ALLOCATION_STASH_MAX_SIZE = 16;
3142

3243
private UiThreadExecutor() {
3344
// should not be instantiated
@@ -41,17 +52,64 @@ private UiThreadExecutor() {
4152
* the identifier of the task
4253
* @param task
4354
* the task itself
44-
* @param handler
45-
* the {@link Handler} which runs the task
55+
* @param delay
56+
* the delay or zero to run immediately
4657
*/
47-
public static synchronized void addTask(String id, Runnable task, Handler handler) {
48-
List<Runnable> runnables = TASKS.get(id);
58+
public static void runTask(String id, Runnable task, long delay) {
59+
if ("".equals(id)) {
60+
HANDLER.postDelayed(task, delay);
61+
return;
62+
}
63+
JUST_POSTED_TASKS.add(task);
64+
HANDLER.postDelayed(task, delay);
65+
66+
Set<Runnable> runnables = TASKS.get(id);
4967
if (runnables == null) {
50-
runnables = new ArrayList<Runnable>();
51-
TASKS.put(id, runnables);
68+
runnables = allocateRunnables();
69+
runnables.add(task);
70+
71+
while (true) {
72+
Set<Runnable> oldRunnables = TASKS.putIfAbsent(id, runnables);
73+
if (oldRunnables != null) {
74+
if (oldRunnables.isEmpty()) {
75+
//it is about to be removed - lets try to replace it
76+
if (!TASKS.replace(id, oldRunnables, runnables)) {
77+
//it is already has bean replaced by different thread - lets try again
78+
continue;
79+
}
80+
} else {
81+
recycle(runnables);
82+
oldRunnables.add(task);
83+
}
84+
}
85+
break;
86+
}
87+
} else {
88+
runnables.add(task);
89+
}
90+
91+
//if the task has already been completed - make sure to clean up
92+
if (!JUST_POSTED_TASKS.remove(task)) {
93+
done(id, task);
94+
}
95+
}
96+
97+
private static Set<Runnable> allocateRunnables() {
98+
Set<Runnable> runnables = ALLOCATION_STASH.poll();
99+
if (runnables != null) {
100+
ALLOCATION_STASH_SIZE.getAndDecrement();
101+
return runnables;
102+
}
103+
return Collections.newSetFromMap(new ConcurrentHashMap<Runnable, Boolean>());
104+
}
105+
106+
private static void recycle(Set<Runnable> runnables) {
107+
if (ALLOCATION_STASH_SIZE.getAndIncrement() < ALLOCATION_STASH_MAX_SIZE) {
108+
runnables.clear();
109+
ALLOCATION_STASH.offer(runnables);
110+
} else {
111+
ALLOCATION_STASH_SIZE.getAndDecrement();
52112
}
53-
runnables.add(task);
54-
HANDLERS.put(task, handler);
55113
}
56114

57115
/**
@@ -60,22 +118,31 @@ public static synchronized void addTask(String id, Runnable task, Handler handle
60118
* @param id
61119
* the cancellation identifier
62120
*/
63-
public static synchronized void cancelAll(String id) {
64-
List<Runnable> runnables = TASKS.remove(id);
121+
public static void cancelAll(String id) {
122+
Set<Runnable> runnables = TASKS.remove(id);
65123
if (runnables != null) {
66124
for (Runnable runnable : runnables) {
67-
HANDLERS.remove(runnable).removeCallbacks(runnable);
125+
HANDLER.removeCallbacks(runnable);
68126
}
127+
recycle(runnables);
69128
}
70129
}
71130

72-
public static synchronized void done(String id, Runnable runnable) {
73-
Handler handler = HANDLERS.remove(runnable);
74-
if (handler != null) {
75-
List<Runnable> runnables = TASKS.get(id);
76-
runnables.remove(runnable);
77-
//potentially empty array stays in map for reducing garbage collecting -
78-
//it is highly possible, that array will be reused at the next addTask call
131+
/**
132+
* Should be called after the task has been executed. It is ok to call it more then once for the case of cleaning up.
133+
* @param id the task id
134+
* @param runnable the task itself
135+
*/
136+
public static void done(String id, Runnable runnable) {
137+
JUST_POSTED_TASKS.remove(runnable);
138+
Set<Runnable> runnables = TASKS.get(id);
139+
if (runnables != null) {
140+
if (runnables.remove(runnable)) {
141+
if (runnables.isEmpty()) { //if it is empty
142+
TASKS.remove(id, runnables);
143+
recycle(runnables);
144+
}
145+
}
79146
}
80147
}
81148

‎AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/handler/UiThreadHandler.java‎

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public class UiThreadHandler extends AbstractRunnableHandler {
4545
private static final String METHOD_CUR_THREAD = "currentThread";
4646
private static final String METHOD_MAIN_LOOPER = "getMainLooper";
4747
private static final String METHOD_GET_THREAD = "getThread";
48-
private static final String METHOD_ADD_TASK = "addTask";
48+
private static final String METHOD_RUN_TASK = "runTask";
4949
private static final String METHOD_DONE = "done";
5050

5151
private final APTCodeModelHelper codeModelHelper = new APTCodeModelHelper();
@@ -77,20 +77,18 @@ public void process(Element element, EComponentHolder holder) throws Exception {
7777
if ("".equals(id)) {
7878
runnable = _new(anonymousRunnableClass);
7979
} else {
80-
runnable = createAndStoreRunnable(delegatingMethod.body(), id, anonymousRunnableClass, holder);
81-
callCancelOnFinish(previousBody, id);
80+
runnable = delegatingMethod.body().decl(refClass(Runnable.class), "runnable", _new(anonymousRunnableClass));
81+
previousBody.add(refClass(UiThreadExecutor.class).staticInvoke(METHOD_DONE).arg(id).arg(_this()));
8282
}
8383

84-
if (delay == 0) {
85-
if (propagation == UiThread.Propagation.REUSE) {
86-
// Put in the check for the UI thread.
87-
addUIThreadCheck(delegatingMethod, previousBody, holder);
88-
}
89-
90-
delegatingMethod.body().invoke(holder.getHandler(), "post").arg(runnable);
91-
} else {
92-
delegatingMethod.body().invoke(holder.getHandler(), "postDelayed").arg(runnable).arg(lit(delay));
84+
if (delay == 0 && propagation == UiThread.Propagation.REUSE) {
85+
// Put in the check for the UI thread.
86+
addUIThreadCheck(delegatingMethod, previousBody, holder);
9387
}
88+
delegatingMethod.body().add(refClass(UiThreadExecutor.class).staticInvoke(METHOD_RUN_TASK)
89+
.arg(id)
90+
.arg(runnable)
91+
.arg(lit(delay)));
9492
}
9593

9694
/**
@@ -115,15 +113,4 @@ private void addUIThreadCheck(JMethod delegatingMethod, JBlock previousBody, ECo
115113
thenBlock._return();
116114
}
117115

118-
private JExpression createAndStoreRunnable(JBlock body, String id, JDefinedClass anonymousRunnableClass, EComponentHolder holder) {
119-
120-
JVar runnableVar = body.decl(refClass(Runnable.class), "runnable", _new(anonymousRunnableClass));
121-
body.add(refClass(UiThreadExecutor.class).staticInvoke(METHOD_ADD_TASK).arg(id).arg(runnableVar).arg(holder.getHandler()));
122-
123-
return runnableVar;
124-
}
125-
126-
private void callCancelOnFinish(JBlock body, String id) {
127-
body.add(refClass(UiThreadExecutor.class).staticInvoke(METHOD_DONE).arg(id).arg(_this()));
128-
}
129116
}

‎AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/holder/EComponentHolder.java‎

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,19 @@
3030

3131
import com.sun.codemodel.JBlock;
3232
import com.sun.codemodel.JClass;
33-
import com.sun.codemodel.JExpr;
3433
import com.sun.codemodel.JExpression;
3534
import com.sun.codemodel.JFieldRef;
3635
import com.sun.codemodel.JFieldVar;
37-
import com.sun.codemodel.JInvocation;
3836
import com.sun.codemodel.JMethod;
39-
import com.sun.codemodel.JMod;
4037
import com.sun.codemodel.JVar;
4138

4239
public abstract class EComponentHolder extends BaseGeneratedClassHolder {
4340

44-
private static final String METHOD_MAIN_LOOPER = "getMainLooper";
45-
4641
protected JExpression contextRef;
4742
protected JMethod init;
4843
private JVar resourcesRef;
4944
private JFieldVar powerManagerRef;
5045
private Map<TypeMirror, JFieldVar> databaseHelperRefs = new HashMap<TypeMirror, JFieldVar>();
51-
private JVar handler;
5246

5347
public EComponentHolder(ProcessHolder processHolder, TypeElement annotatedElement) throws Exception {
5448
super(processHolder, annotatedElement);
@@ -124,17 +118,4 @@ protected JFieldVar setDatabaseHelperRef(TypeMirror databaseHelperTypeMirror) {
124118
return databaseHelperRef;
125119
}
126120

127-
public JVar getHandler() {
128-
if (handler == null) {
129-
setHandler();
130-
}
131-
return handler;
132-
}
133-
134-
private void setHandler() {
135-
JClass handlerClass = classes().HANDLER;
136-
JClass looperClass = classes().LOOPER;
137-
JInvocation arg = JExpr._new(handlerClass).arg(looperClass.staticInvoke(METHOD_MAIN_LOOPER));
138-
handler = generatedClass.field(JMod.PRIVATE, handlerClass, "handler" + generationSuffix(), arg);
139-
}
140121
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package org.androidannotations.api;
2+
3+
import org.androidannotations.api.UiThreadExecutor;
4+
import org.junit.Test;
5+
import org.junit.runner.RunWith;
6+
import org.robolectric.RobolectricTestRunner;
7+
8+
import java.util.concurrent.CountDownLatch;
9+
import java.util.concurrent.TimeUnit;
10+
11+
import static org.junit.Assert.*;
12+
13+
@RunWith(RobolectricTestRunner.class)
14+
public class UiThreadExecutorTest {
15+
16+
@Test
17+
public void oneTaskTest() throws Exception {
18+
final CountDownLatch latch = new CountDownLatch(1);
19+
UiThreadExecutor.runTask("test", new Runnable() {
20+
@Override
21+
public void run() {
22+
UiThreadExecutor.done("test", this);
23+
latch.countDown();
24+
}
25+
}, 0);
26+
await(latch);
27+
assertTrue("The task is leaked", UiThreadExecutor.TASKS.isEmpty());
28+
}
29+
30+
@Test
31+
public void oneTaskInThreadTest() throws Exception {
32+
final CountDownLatch taskStartedLatch = new CountDownLatch(1);
33+
final CountDownLatch taskFinishedLatch = new CountDownLatch(1);
34+
new Thread() {
35+
@Override
36+
public void run() {
37+
UiThreadExecutor.runTask("test", new Runnable() {
38+
@Override
39+
public void run() {
40+
await(taskStartedLatch);
41+
assertFalse("There is no tasks in map", UiThreadExecutor.TASKS.isEmpty());
42+
UiThreadExecutor.done("test", this);
43+
taskFinishedLatch.countDown();
44+
}
45+
}, 0);
46+
taskStartedLatch.countDown();
47+
await(taskFinishedLatch);
48+
assertTrue("The task is leaked", UiThreadExecutor.TASKS.isEmpty());
49+
}
50+
}.start();
51+
}
52+
53+
private void await(CountDownLatch latch) {
54+
try {
55+
if (!latch.await(5, TimeUnit.SECONDS)) {
56+
throw new IllegalStateException("Execution hanged up");
57+
}
58+
} catch (InterruptedException e) {
59+
throw new RuntimeException(e);
60+
}
61+
}
62+
63+
}

‎AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/ThreadActivityTest.java‎

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -341,36 +341,6 @@ public void execute(Runnable command) {
341341
}
342342
}
343343

344-
@Test
345-
public void assertHandlerWithMainThread() throws Exception {
346-
/*
347-
* For this test we need to recreate the activity in a separate thread,
348-
* in order to check the handler is well associated to the main thread.
349-
*/
350-
final ThreadActivity_[] threadActivityHolder = new ThreadActivity_[1];
351-
352-
new Thread(new Runnable() {
353-
@Override
354-
public void run() {
355-
synchronized (threadActivityHolder) {
356-
threadActivityHolder[0] = Robolectric.buildActivity(ThreadActivity_.class).create().get();
357-
threadActivityHolder.notify();
358-
}
359-
}
360-
}).start();
361-
synchronized (threadActivityHolder) {
362-
do {
363-
threadActivityHolder.wait();
364-
} while (threadActivityHolder[0] == null);
365-
}
366-
367-
Field handlerField = ThreadActivity_.class.getDeclaredField("handler_");
368-
handlerField.setAccessible(true);
369-
370-
Handler handler = (Handler) handlerField.get(threadActivityHolder[0]);
371-
Assert.assertTrue("Handler field not associated to the main thread", handler.getLooper() == Looper.getMainLooper());
372-
}
373-
374344
@Test
375345
public void propagateExceptionToGlobalExceptionHandler() {
376346
/* set an executor with 4 threads */

0 commit comments

Comments
 (0)