PageRenderTime 50ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/stetho/src/main/java/com/facebook/stetho/inspector/elements/ShadowDocument.java

https://gitlab.com/Liang1Zhang/stetho
Java | 374 lines | 236 code | 57 blank | 81 comment | 70 complexity | 8980ede82930120877b901e273917b17 MD5 | raw file
  1. /*
  2. * Copyright (c) 2014-present, Facebook, Inc.
  3. * All rights reserved.
  4. *
  5. * This source code is licensed under the BSD-style license found in the
  6. * LICENSE file in the root directory of this source tree. An additional grant
  7. * of patent rights can be found in the PATENTS file in the same directory.
  8. */
  9. package com.facebook.stetho.inspector.elements;
  10. import android.app.Activity;
  11. import com.facebook.stetho.common.Accumulator;
  12. import com.facebook.stetho.common.ListUtil;
  13. import com.facebook.stetho.common.Util;
  14. import java.util.ArrayDeque;
  15. import java.util.Collections;
  16. import java.util.HashSet;
  17. import java.util.IdentityHashMap;
  18. import java.util.LinkedHashMap;
  19. import java.util.List;
  20. import java.util.Map;
  21. import java.util.Queue;
  22. import java.util.Set;
  23. public final class ShadowDocument implements DocumentView {
  24. private final Object mRootElement;
  25. private final IdentityHashMap<Object, ElementInfo> mElementToInfoMap = new IdentityHashMap<>();
  26. private boolean mIsUpdating;
  27. public ShadowDocument(Object rootElement) {
  28. mRootElement = Util.throwIfNull(rootElement);
  29. }
  30. @Override
  31. public Object getRootElement() {
  32. return mRootElement;
  33. }
  34. @Override
  35. public ElementInfo getElementInfo(Object element) {
  36. return mElementToInfoMap.get(element);
  37. }
  38. public UpdateBuilder beginUpdate() {
  39. if (mIsUpdating) {
  40. throw new IllegalStateException();
  41. }
  42. mIsUpdating = true;
  43. return new UpdateBuilder();
  44. }
  45. public final class UpdateBuilder {
  46. /**
  47. * We use a {@link LinkedHashMap} to preserve ordering between
  48. * {@link UpdateBuilder#setElementChildren(Object, List)} and
  49. * {@link Update#getChangedElements(Accumulator)}. This isn't needed for correctness but it
  50. * significantly improves performance.<p/>
  51. *
  52. * Transmitting DOM updates to Chrome works best if we can do it in top-down order because it
  53. * allows us to skip processing (and, more importantly, transmission) of an element that was
  54. * already transmitted in a previous DOM.childNodeInserted event (i.o.w. we can skip
  55. * transmission of E2 if it was already bundled up in E1's event, where E2 is any element in
  56. * E1's sub-tree). DOM.childNodeInserted transmits the node being inserted by-value, so it takes
  57. * time and space proportional to the size of that node's sub-tree. This means the difference
  58. * between O(n^2) and O(n) time for transmitting updates to Chrome.<p/>
  59. *
  60. * We currently only have one implementation of {@link DocumentProvider},
  61. * {@link com.facebook.stetho.inspector.elements.android.AndroidDocumentProvider}, and it
  62. * already supplies element changes in top-down order. Because of this, we can just use
  63. * {@link LinkedHashMap} instead of adding some kind of post-process sorting of the elements to
  64. * put them in that order. If we reach a point where we can't or shouldn't rely on elements
  65. * being forwarded to us in top-down order, then we should change this field to an
  66. * {@link IdentityHashMap} and sort them before relaying them via
  67. * {@link Update#getChangedElements(Accumulator)}.<p/>
  68. *
  69. * When a large sub-tree is added (e.g. starting a new {@link Activity}), the use of
  70. * {@link LinkedHashMap} instead of {@link IdentityHashMap} can mean the difference between an
  71. * update taking 500ms versus taking more than 30 seconds.<p/>
  72. *
  73. * Technically we actually want something like a LinkedIdentityHashMap because we do want
  74. * to key off of object identity instead of allowing for the possibility of value identity.
  75. * Given the difference in performance, however, the risk of potential protocol abuse seems
  76. * reasonable.<p/>
  77. */
  78. private final Map<Object, ElementInfo> mElementToInfoChangesMap = new LinkedHashMap<>();
  79. /**
  80. * This contains every element in {@link #mElementToInfoChangesMap} whose
  81. * {@link ElementInfo#parentElement} is null. {@link ShadowDocument} provides access to a tree, which
  82. * means it has a single root (only one element with a null parent). During an update, however,
  83. * the DOM can be conceptually thought of as being a forest. The true root is identified by
  84. * {@link #mRootElement}, and all other roots identify disconnected trees full of elements that
  85. * must be garbage collected.
  86. */
  87. private final HashSet<Object> mRootElementChanges = new HashSet<>();
  88. /**
  89. * This is used during {@link #setElementChildren}. We allocate 1 on-demand and reuse it.
  90. */
  91. private HashSet<Object> mCachedNotNewChildrenSet;
  92. public void setElementChildren(Object element, List<Object> children) {
  93. // If we receive redundant information, then nothing needs to be done.
  94. ElementInfo changesElementInfo = mElementToInfoChangesMap.get(element);
  95. if (changesElementInfo != null &&
  96. ListUtil.identityEquals(children, changesElementInfo.children)) {
  97. return;
  98. }
  99. ElementInfo oldElementInfo = mElementToInfoMap.get(element);
  100. if (changesElementInfo == null &&
  101. oldElementInfo != null &&
  102. ListUtil.identityEquals(children, oldElementInfo.children)) {
  103. return;
  104. }
  105. ElementInfo newElementInfo;
  106. if (changesElementInfo != null &&
  107. oldElementInfo != null &&
  108. oldElementInfo.parentElement == changesElementInfo.parentElement &&
  109. ListUtil.identityEquals(children, oldElementInfo.children)) {
  110. // setElementChildren() was already called for element with changes during this
  111. // transaction, but now we're being told that the children should match the old view.
  112. // So we should actually remove the change entry.
  113. newElementInfo = mElementToInfoMap.get(element);
  114. mElementToInfoChangesMap.remove(element);
  115. } else {
  116. Object parentElement = (changesElementInfo != null)
  117. ? changesElementInfo.parentElement
  118. : (oldElementInfo != null)
  119. ? oldElementInfo.parentElement
  120. : null;
  121. newElementInfo = new ElementInfo(element, parentElement, children);
  122. mElementToInfoChangesMap.put(element, newElementInfo);
  123. }
  124. // At this point, newElementInfo is either equal to oldElementInfo because we've reverted
  125. // back to the same data that's in the old view of the tree, or it's a brand new object with
  126. // brand new changes (it's different than both of oldElementInfo and changesElementInfo).
  127. // Next, set the parentElement to null for child elements that have been removed from
  128. // element's children. We must be careful not to set a parentElement to null if that child has
  129. // already been moved to be the child of a different element. e.g.,
  130. // setElementChildren(E, { A, B, C})
  131. // ...
  132. // setElementChildren(F, { A })
  133. // setElementChildren(E, { B, C }) (don't mark A's parent as null in this case)
  134. // notNewChildrenSet = (oldChildren + changesChildren) - newChildren
  135. HashSet<Object> notNewChildrenSet = acquireNotNewChildrenHashSet();
  136. if (oldElementInfo != null &&
  137. oldElementInfo.children != newElementInfo.children) {
  138. for (int i = 0, N = oldElementInfo.children.size(); i < N; ++i) {
  139. final Object childElement = oldElementInfo.children.get(i);
  140. notNewChildrenSet.add(childElement);
  141. }
  142. }
  143. if (changesElementInfo != null &&
  144. changesElementInfo.children != newElementInfo.children) {
  145. for (int i = 0, N = changesElementInfo.children.size(); i < N; ++i) {
  146. final Object childElement = changesElementInfo.children.get(i);
  147. notNewChildrenSet.add(childElement);
  148. }
  149. }
  150. for (int i = 0, N = newElementInfo.children.size(); i < N; ++i) {
  151. final Object childElement = newElementInfo.children.get(i);
  152. setElementParent(childElement, element);
  153. notNewChildrenSet.remove(childElement);
  154. }
  155. for (Object childElement : notNewChildrenSet) {
  156. final ElementInfo childChangesElementInfo = mElementToInfoChangesMap.get(childElement);
  157. if (childChangesElementInfo != null &&
  158. childChangesElementInfo.parentElement != element) {
  159. // do nothing. this childElement was moved to be the child of another element.
  160. continue;
  161. }
  162. final ElementInfo oldChangesElementInfo = mElementToInfoMap.get(childElement);
  163. if (oldChangesElementInfo != null &&
  164. oldChangesElementInfo.parentElement == element) {
  165. setElementParent(childElement, null);
  166. }
  167. }
  168. releaseNotNewChildrenHashSet(notNewChildrenSet);
  169. }
  170. private void setElementParent(Object element, Object parentElement) {
  171. ElementInfo changesElementInfo = mElementToInfoChangesMap.get(element);
  172. if (changesElementInfo != null &&
  173. parentElement == changesElementInfo.parentElement) {
  174. return;
  175. }
  176. ElementInfo oldElementInfo = mElementToInfoMap.get(element);
  177. if (changesElementInfo == null &&
  178. oldElementInfo != null &&
  179. parentElement == oldElementInfo.parentElement) {
  180. return;
  181. }
  182. if (changesElementInfo != null &&
  183. oldElementInfo != null &&
  184. parentElement == oldElementInfo.parentElement &&
  185. ListUtil.identityEquals(oldElementInfo.children, changesElementInfo.children)) {
  186. mElementToInfoChangesMap.remove(element);
  187. if (parentElement == null) {
  188. mRootElementChanges.remove(element);
  189. }
  190. return;
  191. }
  192. List<Object> children = (changesElementInfo != null)
  193. ? changesElementInfo.children
  194. : (oldElementInfo != null)
  195. ? oldElementInfo.children
  196. : Collections.emptyList();
  197. ElementInfo newElementInfo = new ElementInfo(element, parentElement, children);
  198. mElementToInfoChangesMap.put(element, newElementInfo);
  199. if (parentElement == null) {
  200. mRootElementChanges.add(element);
  201. } else {
  202. mRootElementChanges.remove(element);
  203. }
  204. }
  205. public Update build() {
  206. return new Update(mElementToInfoChangesMap, mRootElementChanges);
  207. }
  208. private HashSet<Object> acquireNotNewChildrenHashSet() {
  209. HashSet<Object> notNewChildrenHashSet = mCachedNotNewChildrenSet;
  210. if (notNewChildrenHashSet == null) {
  211. notNewChildrenHashSet = new HashSet<>();
  212. }
  213. mCachedNotNewChildrenSet = null;
  214. return notNewChildrenHashSet;
  215. }
  216. private void releaseNotNewChildrenHashSet(HashSet<Object> notNewChildrenHashSet) {
  217. notNewChildrenHashSet.clear();
  218. if (mCachedNotNewChildrenSet == null) {
  219. mCachedNotNewChildrenSet = notNewChildrenHashSet;
  220. }
  221. }
  222. }
  223. public final class Update implements DocumentView {
  224. private final Map<Object, ElementInfo> mElementToInfoChangesMap;
  225. private final Set<Object> mRootElementChangesSet;
  226. public Update(
  227. Map<Object, ElementInfo> elementToInfoChangesMap,
  228. Set<Object> rootElementChangesSet) {
  229. mElementToInfoChangesMap = elementToInfoChangesMap;
  230. mRootElementChangesSet = rootElementChangesSet;
  231. }
  232. public boolean isEmpty() {
  233. return mElementToInfoChangesMap.isEmpty();
  234. }
  235. public boolean isElementChanged(Object element) {
  236. return mElementToInfoChangesMap.containsKey(element);
  237. }
  238. public Object getRootElement() {
  239. return ShadowDocument.this.getRootElement();
  240. }
  241. public ElementInfo getElementInfo(Object element) {
  242. // Return ElementInfo for the new (albeit uncommitted and pre-garbage collected) view of the
  243. // Document. If element is garbage then you'll still get its info (feature, not a bug :)).
  244. ElementInfo elementInfo = mElementToInfoChangesMap.get(element);
  245. if (elementInfo != null) {
  246. return elementInfo;
  247. }
  248. return mElementToInfoMap.get(element);
  249. }
  250. public void getChangedElements(Accumulator<Object> accumulator) {
  251. for (Object element : mElementToInfoChangesMap.keySet()) {
  252. accumulator.store(element);
  253. }
  254. }
  255. public void getGarbageElements(Accumulator<Object> accumulator) {
  256. // This queue stores pairs of elements, [element, expectedParent]
  257. // When we dequeue, we look at element's parentElement in the new view to see if it matches
  258. // expectedParent. If it does, then it's garbage. For enqueueing roots, whose parents are
  259. // null, since we can't enqueue null we instead enqueue the element twice.
  260. Queue<Object> queue = new ArrayDeque<>();
  261. // Initialize the queue with all disconnected tree roots (parentElement == null) which
  262. // aren't the DOM root.
  263. for (Object element : mRootElementChangesSet) {
  264. ElementInfo newElementInfo = getElementInfo(element);
  265. if (element != mRootElement && newElementInfo.parentElement == null) {
  266. queue.add(element);
  267. queue.add(element);
  268. }
  269. }
  270. // BFS traversal from those elements in the old view of the tree and test each element
  271. // to see if it's still within a disconnected sub-tree. We can tell if it's garbage if its
  272. // parent element in the new view of the tree hasn't changed.
  273. while (!queue.isEmpty()) {
  274. final Object element = queue.remove();
  275. final Object expectedParent0 = queue.remove();
  276. final Object expectedParent = (element == expectedParent0) ? null : expectedParent0;
  277. final ElementInfo newElementInfo = getElementInfo(element);
  278. if (newElementInfo.parentElement == expectedParent) {
  279. accumulator.store(element);
  280. ElementInfo oldElementInfo = ShadowDocument.this.getElementInfo(element);
  281. if (oldElementInfo != null) {
  282. for (int i = 0, N = oldElementInfo.children.size(); i < N; ++i) {
  283. queue.add(oldElementInfo.children.get(i));
  284. queue.add(element);
  285. }
  286. }
  287. }
  288. }
  289. }
  290. public void abandon() {
  291. if (!mIsUpdating) {
  292. throw new IllegalStateException();
  293. }
  294. mIsUpdating = false;
  295. }
  296. public void commit() {
  297. if (!mIsUpdating) {
  298. throw new IllegalStateException();
  299. }
  300. // Apply the changes to the tree
  301. mElementToInfoMap.putAll(mElementToInfoChangesMap);
  302. // Remove garbage elements: those that have a null parent (other than mRootElement), and
  303. // their entire sub-trees.
  304. for (Object element : mRootElementChangesSet) {
  305. removeSubTree(mElementToInfoMap, element);
  306. }
  307. mIsUpdating = false;
  308. }
  309. private void removeSubTree(Map<Object, ElementInfo> elementToInfoMap, Object element) {
  310. final ElementInfo elementInfo = elementToInfoMap.get(element);
  311. elementToInfoMap.remove(element);
  312. for (int i = 0, N = elementInfo.children.size(); i < N; ++i) {
  313. removeSubTree(elementToInfoMap, elementInfo.children.get(i));
  314. }
  315. }
  316. }
  317. }