diff --git a/anvil/src/main/java/trikita/anvil/Anvil.java b/anvil/src/main/java/trikita/anvil/Anvil.java index df61076e..e5f5d4d7 100644 --- a/anvil/src/main/java/trikita/anvil/Anvil.java +++ b/anvil/src/main/java/trikita/anvil/Anvil.java @@ -10,17 +10,10 @@ import java.lang.reflect.InvocationTargetException; import java.lang.ref.WeakReference; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Deque; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.Stack; -import java.util.WeakHashMap; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; /** * Anvil class is a namespace for top-level static methods and interfaces. Most @@ -32,10 +25,11 @@ */ public final class Anvil { - private final static Map mounts = new WeakHashMap<>(); + private final static Map mounts = Collections.synchronizedMap(new WeakHashMap()); private static Mount currentMount = null; - private static Handler anvilUIHandler = null; + private static Handler anvilUIHandler = new Handler(Looper.getMainLooper()); + private static ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); /** Renderable can be mounted and rendered using Anvil library. */ public interface Renderable { @@ -70,7 +64,7 @@ public View fromClass(Context c, Class viewClass) { public View fromXml(ViewGroup parent, int xmlId) { return LayoutInflater.from(parent.getContext()).inflate(xmlId, parent, false); } - }; + } public static void registerViewFactory(ViewFactory viewFactory) { if (!viewFactories.contains(viewFactory)) { @@ -80,12 +74,6 @@ public static void registerViewFactory(ViewFactory viewFactory) { private Anvil() {} - private final static Runnable anvilRenderRunnable = new Runnable() { - public void run() { - Anvil.render(); - } - }; - public interface AttributeSetter { boolean set(View v, String name, T value, T prevValue); } @@ -124,23 +112,22 @@ public static Object get(View v, String key) { * been changed since last rendering cycle will be actually updated in the * views. This method can be called from any thread, so it's safe to use * {@code Anvil.render()} in background services. */ - public static void render() { - // If Anvil.render() is called on a non-UI thread, use UI Handler - if (Looper.myLooper() != Looper.getMainLooper()) { - synchronized (Anvil.class) { - if (anvilUIHandler == null) { - anvilUIHandler = new Handler(Looper.getMainLooper()); + public static Future render() { + return singleThreadExecutor.submit(createRenderTask()); + } + + private static Runnable createRenderTask() { + return new Runnable() { + @Override + public void run() { + synchronized (mounts) { + Set set = new HashSet<>(mounts.values()); + for (Mount m : set) { + render(m); + } } } - anvilUIHandler.removeCallbacksAndMessages(null); - anvilUIHandler.post(anvilRenderRunnable); - return; - } - Set set = new HashSet<>(); - set.addAll(mounts.values()); - for (Mount m : set) { - render(m); - } + }; } /** @@ -151,10 +138,12 @@ public static void render() { * @param r a Renderable to mount into a View */ public static T mount(T v, Renderable r) { - Mount m = new Mount(v, r); - mounts.put(v, m); - render(v); - return v; + synchronized (mounts) { + Mount m = new Mount(v, r); + mounts.put(v, m); + render(v); + return v; + } } /** @@ -167,18 +156,20 @@ public static void unmount(View v) { } public static void unmount(View v, boolean removeChildren) { - Mount m = mounts.get(v); - if (m != null) { - mounts.remove(v); - if (v instanceof ViewGroup) { - ViewGroup viewGroup = (ViewGroup) v; - - int childCount = viewGroup.getChildCount(); - for (int i = 0; i < childCount; i++) { - unmount(viewGroup.getChildAt(i)); - } - if (removeChildren) { - viewGroup.removeViews(0, childCount); + synchronized (mounts) { + Mount m = mounts.get(v); + if (m != null) { + mounts.remove(v); + if (v instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) v; + + int childCount = viewGroup.getChildCount(); + for (int i = 0; i < childCount; i++) { + unmount(viewGroup.getChildAt(i)); + } + if (removeChildren) { + viewGroup.removeViews(0, childCount); + } } } } @@ -215,26 +206,23 @@ public static void render(View v) { } static void render(Mount m) { - if (m.lock) { - return; - } - m.lock = true; - Mount prev = currentMount; - currentMount = m; - m.iterator.start(); - if (m.renderable != null) { - m.renderable.view(); + synchronized (m.lock) { + Mount prev = currentMount; + currentMount = m; + m.iterator.start(); + if (m.renderable != null) { + m.renderable.view(); + } + m.iterator.end(); + currentMount = prev; } - m.iterator.end(); - currentMount = prev; - m.lock = false; } /** Mount describes a mount point. Mount point is a Renderable function * attached to some ViewGroup. Mount point keeps track of the virtual layout * declared by Renderable */ static class Mount { - private boolean lock = false; + private final Object lock = new Object(); private final WeakReference rootView; private final Renderable renderable; @@ -277,23 +265,23 @@ void start(Class c, int layoutId, Object key) { } Context context = rootView.get().getContext(); if (c != null && (v == null || !v.getClass().equals(c))) { - vg.removeView(v); + removeViewFromViewGroup(v, vg); for (ViewFactory vf : viewFactories) { v = vf.fromClass(context, c); if (v != null) { set(v, "_anvil", 1); - vg.addView(v, i); + addViewToViewGroupAt(v, vg, i); break; } } } else if (c == null && (v == null || !Integer.valueOf(layoutId).equals(get(v, "_layoutId")))) { - vg.removeView(v); + removeViewFromViewGroup(v, vg); for (ViewFactory vf : viewFactories) { v = vf.fromXml(vg, layoutId); if (v != null) { set(v, "_anvil", 1); set(v, "_layoutId", layoutId); - vg.addView(v, i); + addViewToViewGroupAt(v, vg, i); break; } } @@ -304,15 +292,54 @@ void start(Class c, int layoutId, Object key) { indices.push(0); } + private void removeViewFromViewGroup(View v, ViewGroup vg) { + if (isRunningOnBackgroundThread()) + postViewRemovalOnUIThread(v, vg); + else + vg.removeView(v); + } + + private boolean isRunningOnBackgroundThread() { + return Looper.myLooper() != Looper.getMainLooper(); + } + + private void postViewRemovalOnUIThread(final View v, final ViewGroup vg) { + anvilUIHandler.post(new Runnable() { + @Override + public void run() { + vg.removeView(v); + } + }); + } + + private void addViewToViewGroupAt(View v, ViewGroup vg, int i) { + if (isRunningOnBackgroundThread()) { + postViewAdditionOnUIThread(v, vg, i); + } else { + vg.addView(v, i); + } + } + + private void postViewAdditionOnUIThread(final View v, final ViewGroup vg, final int i) { + anvilUIHandler.post(new Runnable() { + @Override + public void run() { + vg.addView(v, i); + } + }); + } + void end() { int index = indices.peek(); View v = views.peek(); - if (v != null && v instanceof ViewGroup && - get(v, "_layoutId") == null && - (mounts.get(v) == null || mounts.get(v) == Mount.this)) { - ViewGroup vg = (ViewGroup) v; - if (index < vg.getChildCount()) { - removeNonAnvilViews(vg, index, vg.getChildCount() - index); + synchronized (mounts) { + if (v instanceof ViewGroup && + get(v, "_layoutId") == null && + (mounts.get(v) == null || mounts.get(v) == Mount.this)) { + ViewGroup vg = (ViewGroup) v; + if (index < vg.getChildCount()) { + removeNonAnvilViews(vg, index, vg.getChildCount() - index); + } } } indices.pop(); @@ -321,6 +348,16 @@ void end() { } } + private void removeNonAnvilViews(ViewGroup vg, int start, int count) { + final int end = start + count - 1; + + for (int i = end; i >= start; i--) { + View v = vg.getChildAt(i); + if (get(v, "_anvil") != null) + removeViewFromViewGroup(v, vg); + } + } + void attr(String name, T value) { View currentView = views.peek(); if (currentView == null) { @@ -329,22 +366,32 @@ void attr(String name, T value) { @SuppressWarnings("unchecked") T currentValue = (T) get(currentView, name); if (currentValue == null || !currentValue.equals(value)) { - for (AttributeSetter setter : attributeSetters) { - if (setter.set(currentView, name, value, currentValue)) { - set(currentView, name, value); - return; - } - } + updateViewAttribute(name, value, currentView, currentValue); } } - private void removeNonAnvilViews(ViewGroup vg, int start, int count) { - final int end = start + count - 1; + private void updateViewAttribute(String name, T value, View currentView, T currentValue) { + if (isRunningOnBackgroundThread()) + postViewUpdateOnUIThread(name, value, currentView, currentValue); + else + setAttribute(name, value, currentView, currentValue); + } - for (int i = end; i >= start; i--) { - View v = vg.getChildAt(i); - if (get(v, "_anvil") != null) { - vg.removeView(v); + private void postViewUpdateOnUIThread(final String name, final T value, final View currentView, + final T currentValue) { + anvilUIHandler.post(new Runnable() { + @Override + public void run() { + setAttribute(name, value, currentView, currentValue); + } + }); + } + + private void setAttribute(String name, T value, View currentView, T currentValue) { + for (AttributeSetter setter : attributeSetters) { + if (setter.set(currentView, name, value, currentValue)) { + set(currentView, name, value); + return; } } } diff --git a/anvil/src/test/java/trikita/anvil/BenchmarkTest.java b/anvil/src/test/java/trikita/anvil/BenchmarkTest.java index 215e7636..5a4411a0 100644 --- a/anvil/src/test/java/trikita/anvil/BenchmarkTest.java +++ b/anvil/src/test/java/trikita/anvil/BenchmarkTest.java @@ -2,6 +2,9 @@ import org.junit.Test; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + import static trikita.anvil.BaseDSL.v; import static trikita.anvil.DSL.id; import static trikita.anvil.DSL.tag; @@ -12,7 +15,7 @@ public class BenchmarkTest extends Utils { private int mode; @Test - public void testRenderBenchmark() { + public void testRenderBenchmark() throws ExecutionException { long start; Anvil.Renderable r = new Anvil.Renderable() { @@ -34,7 +37,7 @@ public void view() { Anvil.mount(container, r); start = System.currentTimeMillis(); for (int i = 0; i < N; i++) { - Anvil.render(); + renderView(i); } System.out.println("render/no-changes: " + (System.currentTimeMillis() - start)*1000/N + "us"); Anvil.unmount(container, true); @@ -43,7 +46,7 @@ public void view() { Anvil.mount(container, r); start = System.currentTimeMillis(); for (int i = 0; i < N; i++) { - Anvil.render(); + renderView(i); } System.out.println("render/small-changes: " + (System.currentTimeMillis() - start)*1000/N +"us"); Anvil.unmount(container, true); @@ -52,7 +55,7 @@ public void view() { Anvil.mount(container, r); start = System.currentTimeMillis(); for (int i = 0; i < N; i++) { - Anvil.render(); + renderView(i); } System.out.println("render/big-changes: " + (System.currentTimeMillis() - start)*1000/N+"us"); } @@ -84,4 +87,19 @@ public void view() { } }); } + + private void renderView(int i) throws ExecutionException { + Future future = Anvil.render(); + if (i == N-1) + waitTaskToFinish(future); + } + + private void waitTaskToFinish(Future future) throws ExecutionException { + try { + future.get(); + } catch (ExecutionException executionException) { + throw executionException; + } catch (Exception e) { + } + } } diff --git a/anvil/src/test/java/trikita/anvil/IncrementalRenderTest.java b/anvil/src/test/java/trikita/anvil/IncrementalRenderTest.java index 70ee5854..c2cbf3ea 100644 --- a/anvil/src/test/java/trikita/anvil/IncrementalRenderTest.java +++ b/anvil/src/test/java/trikita/anvil/IncrementalRenderTest.java @@ -2,6 +2,9 @@ import org.junit.Test; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + import static org.junit.Assert.assertEquals; import static trikita.anvil.BaseDSL.*; @@ -25,24 +28,24 @@ public void view() { } @Test - public void testDynamicAttributeRenderedLazily() { + public void testDynamicAttributeRenderedLazily() throws ExecutionException { Anvil.mount(container, new Anvil.Renderable() { public void view() { o(v(MockLayout.class), attr("text", fooValue)); } }); assertEquals(1, (int) changedAttrs.get("text")); - Anvil.render(); + renderView(); assertEquals(1, (int) changedAttrs.get("text")); fooValue = "b"; - Anvil.render(); + renderView(); assertEquals(2, (int) changedAttrs.get("text")); - Anvil.render(); + renderView(); assertEquals(2, (int) changedAttrs.get("text")); } @Test - public void testDynamicViewRenderedLazily() { + public void testDynamicViewRenderedLazily() throws ExecutionException { Anvil.mount(container, new Anvil.Renderable() { public void view() { o(v(MockLayout.class), @@ -55,19 +58,19 @@ public void view() { MockLayout layout = (MockLayout) container.getChildAt(0); assertEquals(2, layout.getChildCount()); assertEquals(1, (int) createdViews.get(MockView.class)); - Anvil.render(); + renderView(); assertEquals(1, (int) createdViews.get(MockView.class)); showView = false; - Anvil.render(); + renderView(); assertEquals(1, layout.getChildCount()); assertEquals(1, (int) createdViews.get(MockView.class)); - Anvil.render(); + renderView(); assertEquals(1, (int) createdViews.get(MockView.class)); showView = true; - Anvil.render(); + renderView(); assertEquals(2, layout.getChildCount()); assertEquals(2, (int) createdViews.get(MockView.class)); - Anvil.render(); + renderView(); assertEquals(2, (int) createdViews.get(MockView.class)); } @@ -75,7 +78,7 @@ public void view() { private String secondMountValue = "bar"; @Test - public void testRenderUpdatesAllMounts() { + public void testRenderUpdatesAllMounts() throws ExecutionException { MockLayout rootA = new MockLayout(getContext()); MockLayout rootB = new MockLayout(getContext()); Anvil.mount(rootA, new Anvil.Renderable() { @@ -93,9 +96,23 @@ public void view() { firstMountValue = "baz"; secondMountValue = "qux"; - Anvil.render(); + renderView(); assertEquals("baz", rootA.getText()); assertEquals("qux", rootB.getTag()); } + + private void renderView() throws ExecutionException { + Future future = Anvil.render(); + waitTaskToFinish(future); + } + + private void waitTaskToFinish(Future future) throws ExecutionException { + try { + future.get(); + } catch (ExecutionException executionException) { + throw executionException; + } catch (Exception e) { + } + } } diff --git a/anvil/src/test/java/trikita/anvil/InitTest.java b/anvil/src/test/java/trikita/anvil/InitTest.java index ebdac85c..502d90cb 100644 --- a/anvil/src/test/java/trikita/anvil/InitTest.java +++ b/anvil/src/test/java/trikita/anvil/InitTest.java @@ -4,6 +4,8 @@ import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import org.junit.Test; @@ -15,7 +17,7 @@ public class InitTest extends Utils { Map called = new HashMap<>(); @Test - public void testInit() { + public void testInit() throws ExecutionException { System.out.println("============================"); Anvil.mount(container, new Anvil.Renderable() { public void view() { @@ -31,7 +33,7 @@ public void view() { assertTrue(called.get("once")); assertTrue(called.get("setUpView")); called.clear(); - Anvil.render(); + renderView(); assertFalse(called.containsKey("once")); assertFalse(called.containsKey("setUpView")); } @@ -47,5 +49,19 @@ public void run() { } }; } + + private void renderView() throws ExecutionException { + Future future = Anvil.render(); + waitTaskToFinish(future); + } + + private void waitTaskToFinish(Future future) throws ExecutionException { + try { + future.get(); + } catch (ExecutionException executionException) { + throw executionException; + } catch (Exception e) { + } + } } diff --git a/anvil/src/test/java/trikita/anvil/MountTest.java b/anvil/src/test/java/trikita/anvil/MountTest.java index c0004dad..1c5f8f8a 100644 --- a/anvil/src/test/java/trikita/anvil/MountTest.java +++ b/anvil/src/test/java/trikita/anvil/MountTest.java @@ -7,6 +7,8 @@ import org.mockito.Mockito; import java.lang.ref.WeakReference; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import static junit.framework.Assert.*; @@ -69,7 +71,7 @@ public void view() { } @Test - public void testMountGC() { + public void testMountGC() throws ExecutionException { Anvil.Renderable layout = Mockito.spy(testLayout); Anvil.mount(container, layout); Mockito.verify(layout, Mockito.times(1)).view(); @@ -80,7 +82,21 @@ public void testMountGC() { System.gc(); assertEquals(null, ref.get()); // Ensure that the associated renderable is no longer called - Anvil.render(); + renderView(); Mockito.verify(layout, Mockito.times(1)).view(); } + + private void renderView() throws ExecutionException { + Future future = Anvil.render(); + waitTaskToFinish(future); + } + + private void waitTaskToFinish(Future future) throws ExecutionException { + try { + future.get(); + } catch (ExecutionException executionException) { + throw executionException; + } catch (Exception e) { + } + } } \ No newline at end of file