Skip to content

Commit 71414c9

Browse files
Drozdov ArtyomWonderCsabo
authored andcommitted
add: lock-free UIThreadHandler implementation
1 parent d1be16f commit 71414c9

5 files changed

Lines changed: 192 additions & 103 deletions

File tree

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

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

18-
import java.util.ArrayList;
19-
import java.util.HashMap;
20-
import java.util.List;
21-
import java.util.Map;
18+
import java.util.Collections;
19+
import java.util.Set;
20+
import java.util.concurrent.ConcurrentHashMap;
21+
import java.util.concurrent.ConcurrentLinkedQueue;
22+
import java.util.concurrent.atomic.AtomicInteger;
2223

2324
import android.os.Handler;
25+
import android.os.Looper;
2426

2527
/**
2628
* This class provide operations for
2729
* {@link org.androidannotations.annotations.UiThread UiThread} tasks.
2830
*/
2931
public class UiThreadExecutor {
3032

31-
private static final Map<String, List<Runnable>> TASKS = new HashMap<String, List<Runnable>>();
32-
private static final Map<Runnable, Handler> HANDLERS = new HashMap<Runnable, Handler>();
33+
private static final Handler HANDLER = new Handler(Looper.getMainLooper());
34+
35+
private 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;
3342

3443
private UiThreadExecutor() {
3544
// should not be instantiated
@@ -43,17 +52,65 @@ private UiThreadExecutor() {
4352
* the identifier of the task
4453
* @param task
4554
* the task itself
46-
* @param handler
47-
* the {@link Handler} which runs the task
55+
* @param delay
56+
* the delay or zero to run immediately
4857
*/
49-
public static synchronized void addTask(String id, Runnable task, Handler handler) {
50-
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);
5167
if (runnables == null) {
52-
runnables = new ArrayList<Runnable>();
53-
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
78+
// thread - lets try again
79+
continue;
80+
}
81+
} else {
82+
recycle(runnables);
83+
oldRunnables.add(task);
84+
}
85+
}
86+
break;
87+
}
88+
} else {
89+
runnables.add(task);
90+
}
91+
92+
// if the task has already been completed - make sure to clean up
93+
if (!JUST_POSTED_TASKS.remove(task)) {
94+
done(id, task);
95+
}
96+
}
97+
98+
private static Set<Runnable> allocateRunnables() {
99+
Set<Runnable> runnables = ALLOCATION_STASH.poll();
100+
if (runnables != null) {
101+
ALLOCATION_STASH_SIZE.getAndDecrement();
102+
return runnables;
103+
}
104+
return Collections.newSetFromMap(new ConcurrentHashMap<Runnable, Boolean>());
105+
}
106+
107+
private static void recycle(Set<Runnable> runnables) {
108+
if (ALLOCATION_STASH_SIZE.getAndIncrement() < ALLOCATION_STASH_MAX_SIZE) {
109+
runnables.clear();
110+
ALLOCATION_STASH.offer(runnables);
111+
} else {
112+
ALLOCATION_STASH_SIZE.getAndDecrement();
54113
}
55-
runnables.add(task);
56-
HANDLERS.put(task, handler);
57114
}
58115

59116
/**
@@ -62,24 +119,35 @@ public static synchronized void addTask(String id, Runnable task, Handler handle
62119
* @param id
63120
* the cancellation identifier
64121
*/
65-
public static synchronized void cancelAll(String id) {
66-
List<Runnable> runnables = TASKS.remove(id);
122+
public static void cancelAll(String id) {
123+
Set<Runnable> runnables = TASKS.remove(id);
67124
if (runnables != null) {
68125
for (Runnable runnable : runnables) {
69-
HANDLERS.remove(runnable).removeCallbacks(runnable);
126+
HANDLER.removeCallbacks(runnable);
70127
}
128+
recycle(runnables);
71129
}
72130
}
73131

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

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

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,13 @@
3838
import com.sun.codemodel.JExpression;
3939
import com.sun.codemodel.JMethod;
4040
import com.sun.codemodel.JOp;
41-
import com.sun.codemodel.JVar;
4241

4342
public class UiThreadHandler extends AbstractRunnableHandler {
4443

4544
private static final String METHOD_CUR_THREAD = "currentThread";
4645
private static final String METHOD_MAIN_LOOPER = "getMainLooper";
4746
private static final String METHOD_GET_THREAD = "getThread";
48-
private static final String METHOD_ADD_TASK = "addTask";
47+
private static final String METHOD_RUN_TASK = "runTask";
4948
private static final String METHOD_DONE = "done";
5049

5150
private final APTCodeModelHelper codeModelHelper = new APTCodeModelHelper();
@@ -77,20 +76,18 @@ public void process(Element element, EComponentHolder holder) throws Exception {
7776
if ("".equals(id)) {
7877
runnable = _new(anonymousRunnableClass);
7978
} else {
80-
runnable = createAndStoreRunnable(delegatingMethod.body(), id, anonymousRunnableClass, holder);
81-
callCancelOnFinish(previousBody, id);
79+
runnable = delegatingMethod.body().decl(refClass(Runnable.class), "runnable", _new(anonymousRunnableClass));
80+
previousBody.add(refClass(UiThreadExecutor.class).staticInvoke(METHOD_DONE).arg(id).arg(_this()));
8281
}
8382

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));
83+
if (delay == 0 && propagation == UiThread.Propagation.REUSE) {
84+
// Put in the check for the UI thread.
85+
addUIThreadCheck(delegatingMethod, previousBody, holder);
9386
}
87+
delegatingMethod.body().add(refClass(UiThreadExecutor.class).staticInvoke(METHOD_RUN_TASK) //
88+
.arg(id) //
89+
.arg(runnable) //
90+
.arg(lit(delay)));
9491
}
9592

9693
/**
@@ -115,15 +112,4 @@ private void addUIThreadCheck(JMethod delegatingMethod, JBlock previousBody, ECo
115112
thenBlock._return();
116113
}
117114

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-
}
129115
}

‎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
}

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

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import static org.mockito.Mockito.mock;
2020
import static org.mockito.Mockito.verify;
2121

22-
import java.lang.reflect.Field;
2322
import java.util.ArrayList;
2423
import java.util.Collections;
2524
import java.util.List;
@@ -42,9 +41,6 @@
4241
import org.robolectric.Robolectric;
4342
import org.robolectric.RobolectricTestRunner;
4443

45-
import android.os.Handler;
46-
import android.os.Looper;
47-
4844
@RunWith(RobolectricTestRunner.class)
4945
public class ThreadActivityTest {
5046

@@ -341,36 +337,6 @@ public void execute(Runnable command) {
341337
}
342338
}
343339

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-
374340
@Test
375341
public void propagateExceptionToGlobalExceptionHandler() {
376342
/* set an executor with 4 threads */

0 commit comments

Comments
 (0)