@doctorq 对,其实就是一种流行的 http api 编写风格。因为没有严格的规定,所以连规范都说不上。
曾经尝试写一个系统的 REST api,查了不少资料。当时天真地以为 REST api+html5 可以一套代码通杀 Web+Mobile,结果被 Android 的 html5 性能打败了……
@doctorq ok,我更新一下文章
REST 是指 RESTful 的 api 吗。个人理解就是 http 的 GET、POST、PUT 和 DELETE 对应资源的 CRUD(增删改查),把所有的对象(例如论坛里的用户、用户组、帖子、专区)都设计成资源。然后就能像写 CRUD 那样使用 api 了。
话说 appium 使用 Http REST 方式是因为遵循了webDriver 的规范吧?
你的代码是直接复制粘贴上来的吗?第一个代码的 xpath 有错,@text=‘中文'第一个引号是中文引号。
学习了!这样的原理剖析不仅学会了原理,还知道了应该如何正确使用这个功能。
能附上 Appium 端的错误信息吗?
@cosyman 所以我注明了 需要能连接国外网络。确实 chromedriver 没代理下不下来。你有调整过 reset.sh 来让它下载 chromedriver 的节点改为国内镜像吗?有的话麻烦在这里说一下,我更新到帖子内容里。谢谢。
@lihuazhang 现在还没做到这一步。我后面跑一下测试后再加上。看官方文档的话 unittest 只需要一个命令就可以跑了,不过里面有没有坑现在还不清楚……
@lihuazhang 好的,明白了。第一组双星号前面必须有一个空格。谢谢指导。
@lihuazhang 我这边确实有问题。用输入法输的:粗体。复制页面底部示例的:粗体。我用的浏览器:Safari version 8.0.2 (10600.2.5)
可以确定我没用全角或者加了空格。全角的写法是××粗体××,在这个字体下还是很明显的。
markdown 是浏览器 js 处理还是发到服务器才处理的?如果是浏览器 js 处理可能是我网速问题导致 Js 加载不完整,或者是 js 的浏览器兼容性问题,所以粗体没有被正确转换把。
后面我再深入看看。
现在可以了。粗体。昨天确实不行,修复了就好。谢谢!
@monkey 论坛的 markdown 粗体是不是有问题?怎么粗体不会转成粗体的?
能把解决方法同时附到主内容里面吗?这样方便后面的人快速参考。
data:text/html,chromewebdata应该是 chromedriver 启动浏览器的默认 url。至于加载不成功为啥会返回这个我只找到 selenium 有个相关 issue。官方标记为 fixed 但后面有人说在部分浏览器中还存在。
传送门(请科学上网):
Issue 4301: getCurrentUrl should return the current URL on a 404 page
建议换个方式来验证页面是否加载成功吧。
PS: 话说神马是 H5 自动化 ?该不会是 html5 自动化 吧?
@doctorq 谢谢支持!后面我会放到我自己的 blog 里面的,也会发个总结帖说明一下,还会发些分享帖说明如何进行 remote debug 来查看 app 使用 android api 时内部具体是怎么做的。现在在这里记录的只是原始资料,所以也只是跟帖而已。
今天看了findAccessibilityNodeInfoByAccessibilityIdUiThread,虽然还没完全了解它的流程,但基本找到--compress影响的位置了。
这里
先回到上次的地方:
private void findAccessibilityNodeInfoByAccessibilityIdUiThread(Message message) {
final int flags = message.arg1;
SomeArgs args = (SomeArgs) message.obj;
final int accessibilityViewId = args.argi1;
final int virtualDescendantId = args.argi2;
final int interactionId = args.argi3;
final IAccessibilityInteractionConnectionCallback callback =
(IAccessibilityInteractionConnectionCallback) args.arg1;
final MagnificationSpec spec = (MagnificationSpec) args.arg2;
args.recycle();
List<AccessibilityNodeInfo> infos = mTempAccessibilityNodeInfoList;
infos.clear();
try {
if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) {
return;
}
mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags;
View root = null;
if (accessibilityViewId == AccessibilityNodeInfo.UNDEFINED) {
root = mViewRootImpl.mView;
} else {
root = findViewByAccessibilityId(accessibilityViewId);
}
if (root != null && isShown(root)) {
mPrefetcher.prefetchAccessibilityNodeInfos(root, virtualDescendantId, flags, infos);
}
} finally {
try {
mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
applyAppScaleAndMagnificationSpecIfNeeded(infos, spec);
if (spec != null) {
spec.recycle();
}
callback.setFindAccessibilityNodeInfosResult(infos, interactionId);
infos.clear();
} catch (RemoteException re) {
/* ignore - the other side will time out */
}
}
}
这里对flags进行了几个操作:
flags。flags赋值给mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags
在查出 root 且root != null && isShown(root)(这里暂时先不探究root是什么),执行mPrefetcher.prefetchAccessibilityNodeInfos方法。isShown(root)是判断当前节点是否会显示在界面上,相关源码:
android.view.AccessibilityInteractionController
private boolean isShown(View view) {
// The first two checks are made also made by isShown() which
// however traverses the tree up to the parent to catch that.
// Therefore, we do some fail fast check to minimize the up
// tree traversal.
return (view.mAttachInfo != null
&& view.mAttachInfo.mWindowVisibility == View.VISIBLE
&& view.isShown());
}
里面的view.isShown()源码:
android.view.View
/**
* Returns the visibility of this view and all of its ancestors
*
* @return True if this view and all of its ancestors are {@link #VISIBLE}
*/
public boolean isShown() {
View current = this;
//noinspection ConstantConditions
do {
if ((current.mViewFlags & VISIBILITY_MASK) != VISIBLE) {
return false;
}
ViewParent parent = current.mParent;
if (parent == null) {
return false; // We are not attached to the view root
}
if (!(parent instanceof View)) {
return true;
}
current = (View) parent;
} while (current != null);
return false;
}
回到正题,我们来看看mPrefetcher.prefetchAccessibilityNodeInfos方法:
/**
* This class encapsulates a prefetching strategy for the accessibility APIs for
* querying window content. It is responsible to prefetch a batch of
* AccessibilityNodeInfos in addition to the one for a requested node.
*/
private class AccessibilityNodePrefetcher {
private static final int MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE = 50;
private final ArrayList<View> mTempViewList = new ArrayList<View>();
public void prefetchAccessibilityNodeInfos(View view, int virtualViewId, int fetchFlags,
List<AccessibilityNodeInfo> outInfos) {
AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider();
if (provider == null) {
AccessibilityNodeInfo root = view.createAccessibilityNodeInfo();
if (root != null) {
outInfos.add(root);
if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) {
prefetchPredecessorsOfRealNode(view, outInfos);
}
if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) {
prefetchSiblingsOfRealNode(view, outInfos);
}
if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) {
prefetchDescendantsOfRealNode(view, outInfos);
}
}
} else {
AccessibilityNodeInfo root = provider.createAccessibilityNodeInfo(virtualViewId);
if (root != null) {
outInfos.add(root);
if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) {
prefetchPredecessorsOfVirtualNode(root, view, provider, outInfos);
}
if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) {
prefetchSiblingsOfVirtualNode(root, view, provider, outInfos);
}
if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) {
prefetchDescendantsOfVirtualNode(root, provider, outInfos);
}
}
}
}
其中fetchFlags就是之前的flags,根据前面return findAccessibilityNodeInfoByAccessibilityId(connectionId,AccessibilityNodeInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID,false, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS);,此处进入的是(fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0为 true 后进入的方法。
至于是进入prefetchDescendantsOfRealNode还是prefetchDescendantsOfVirtualNode,目前还不能确定(不是调试环境,确定不了 root 的值)。咱们逐个看:
private void prefetchDescendantsOfRealNode(View root,
List<AccessibilityNodeInfo> outInfos) {
if (!(root instanceof ViewGroup)) {
return;
}
HashMap<View, AccessibilityNodeInfo> addedChildren =
new HashMap<View, AccessibilityNodeInfo>();
ArrayList<View> children = mTempViewList;
children.clear();
try {
root.addChildrenForAccessibility(children);
final int childCount = children.size();
for (int i = 0; i < childCount; i++) {
if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
return;
}
View child = children.get(i);
if (isShown(child)) {
AccessibilityNodeProvider provider = child.getAccessibilityNodeProvider();
if (provider == null) {
AccessibilityNodeInfo info = child.createAccessibilityNodeInfo();
if (info != null) {
outInfos.add(info);
addedChildren.put(child, null);
}
} else {
AccessibilityNodeInfo info = provider.createAccessibilityNodeInfo(
AccessibilityNodeInfo.UNDEFINED);
if (info != null) {
outInfos.add(info);
addedChildren.put(child, info);
}
}
}
}
} finally {
children.clear();
}
if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
for (Map.Entry<View, AccessibilityNodeInfo> entry : addedChildren.entrySet()) {
View addedChild = entry.getKey();
AccessibilityNodeInfo virtualRoot = entry.getValue();
if (virtualRoot == null) {
prefetchDescendantsOfRealNode(addedChild, outInfos);
} else {
AccessibilityNodeProvider provider =
addedChild.getAccessibilityNodeProvider();
prefetchDescendantsOfVirtualNode(virtualRoot, provider, outInfos);
}
}
}
}
root是不是ViewGroup的实例(这里应该是,否则返回值就是空了)addedChildren, children(此时 children 使用过 clear() 方法,所以已经是空列表了)root.addChildrenForAccessibility(children)获取root的子节点并添加到children列表中。children的元素,然后把所有会显示的元素都加到outInfos。outInfos就是最终会返回到 callback 中的节点列表。outInfos小于MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE,那就在前面找到的 child 节点中继续找(递归),直到root不是ViewGroup实例或者outInfos大小达标。所以关键语句就是root.addChildrenForAccessibility(children)。咱们进去看看:
android.view.View
/**
* Adds the children of a given View for accessibility. Since some Views are
* not important for accessibility the children for accessibility are not
* necessarily direct children of the view, rather they are the first level of
* descendants important for accessibility.
*
* @param children The list of children for accessibility.
*/
public void addChildrenForAccessibility(ArrayList<View> children) {
if (includeForAccessibility()) {
children.add(this);
}
}
然后进去includeForAccessibility():
/**
* Whether to regard this view for accessibility. A view is regarded for
* accessibility if it is important for accessibility or the querying
* accessibility service has explicitly requested that view not
* important for accessibility are regarded.
*
* @return Whether to regard the view for accessibility.
*
* @hide
*/
public boolean includeForAccessibility() {
//noinspection SimplifiableIfStatement
if (mAttachInfo != null) {
return (mAttachInfo.mAccessibilityFetchFlags
& AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0
|| isImportantForAccessibility();
}
return false;
}
比较接近了,这个地方就是判断这个 view 对 accessibility 而言是否重要的地方。如果不重要且AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS标志位为 0(我们的 compress 就是做了这件事),那就返回 false。
既然第一个条件mAttachInfo.mAccessibilityFetchFlags & AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0的结果我们已经知道是 false 了,那就去看isImportantForAccessibility()
/**
* Gets whether this view should be exposed for accessibility.
*
* @return Whether the view is exposed for accessibility.
*
* @hide
*/
public boolean isImportantForAccessibility() {
final int mode = (mPrivateFlags2 & PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK)
>> PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_SHIFT;
if (mode == IMPORTANT_FOR_ACCESSIBILITY_NO
|| mode == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
return false;
}
// Check parent mode to ensure we're not hidden.
ViewParent parent = mParent;
while (parent instanceof View) {
if (((View) parent).getImportantForAccessibility()
== IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
return false;
}
parent = parent.getParent();
}
return mode == IMPORTANT_FOR_ACCESSIBILITY_YES || isActionableForAccessibility()
|| hasListenersForAccessibility() || getAccessibilityNodeProvider() != null
|| getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE;
}
这就是prefetchDescendantsOfRealNode最终的判断位置了。主要有 3 部分:
final int mode = (mPrivateFlags2 & PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK)
>> PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_SHIFT;
if (mode == IMPORTANT_FOR_ACCESSIBILITY_NO
|| mode == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
return false;
}
其中IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS的解释:
/**
* The view is not important for accessibility, nor are any of its
* descendant views.
*/
public static final int IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS = 0x00000004;
// Check parent mode to ensure we're not hidden.
ViewParent parent = mParent;
while (parent instanceof View) {
if (((View) parent).getImportantForAccessibility()
== IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
return false;
}
parent = parent.getParent();
}
IMPORTANT_FOR_ACCESSIBILITY_YES及是否具有其它和 Accessibility 相关的特性:
return mode == IMPORTANT_FOR_ACCESSIBILITY_YES || isActionableForAccessibility()
|| hasListenersForAccessibility() || getAccessibilityNodeProvider() != null
|| getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE;
先记着。咱们去看另一个路线prefetchDescendantsOfVirtualNode
private void prefetchDescendantsOfVirtualNode(AccessibilityNodeInfo root,
AccessibilityNodeProvider provider, List<AccessibilityNodeInfo> outInfos) {
SparseLongArray childNodeIds = root.getChildNodeIds();
final int initialOutInfosSize = outInfos.size();
final int childCount = childNodeIds.size();
for (int i = 0; i < childCount; i++) {
if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
return;
}
final long childNodeId = childNodeIds.get(i);
AccessibilityNodeInfo child = provider.createAccessibilityNodeInfo(
AccessibilityNodeInfo.getVirtualDescendantId(childNodeId));
if (child != null) {
outInfos.add(child);
}
}
if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
final int addedChildCount = outInfos.size() - initialOutInfosSize;
for (int i = 0; i < addedChildCount; i++) {
AccessibilityNodeInfo child = outInfos.get(initialOutInfosSize + i);
prefetchDescendantsOfVirtualNode(child, provider, outInfos);
}
}
}
流程差不多,先找 children 节点,然后遍历,最后如果 size 不够,继续在 child 节点中找,直到节点数达标。咱们来看`:android.view.accessibility.AccessibilityNodeInfo`
/**
* @return The ids of the children.
*
* @hide
*/
public SparseLongArray getChildNodeIds() {
return mChildNodeIds;
}
很直接,直接返回一个列表。
注意这里并没有校验这个 node 对 Accessibility 重要。估计是因为 VirtualNode 通常出现在自定义的 view,这些 view 的元素不一定都有 flag 或者可以判断是否重要的属性。
这里说明一下,每个 view 里面的 node 既可以作为在整个页面中都可以找到的真正的 node(ReadNode),也可以使用仅在 view 内有效、仅能通过 view 的 provider 来查找的的 node(VirtualNode,详情可看AccessibilityNodeProvider)。因此会存在两个遍历 node 的方法。
至于为什么前面容器类控件(FrameLayout 等)会被 compress 干掉,目前还没找到确切的原因,不过已经可以肯定是在下面三个地方其中一个确定的:
final int mode = (mPrivateFlags2 & PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK)
>> PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_SHIFT;
if (mode == IMPORTANT_FOR_ACCESSIBILITY_NO
|| mode == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
return false;
}
其中IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS的解释:
/**
* The view is not important for accessibility, nor are any of its
* descendant views.
*/
public static final int IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS = 0x00000004;
// Check parent mode to ensure we're not hidden.
ViewParent parent = mParent;
while (parent instanceof View) {
if (((View) parent).getImportantForAccessibility()
== IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
return false;
}
parent = parent.getParent();
}
IMPORTANT_FOR_ACCESSIBILITY_YES及是否具有其它和 Accessibility 相关的特性:
return mode == IMPORTANT_FOR_ACCESSIBILITY_YES || isActionableForAccessibility()
|| hasListenersForAccessibility() || getAccessibilityNodeProvider() != null
|| getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE;
但在android.view.View里面找到一个有点关系的属性:
/**
* Shift for the bits in {@link #mPrivateFlags2} related to the
* "importantForAccessibility" attribute.
*/
static final int PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_SHIFT = 20;
由于这部分地方涉及到不少位运算,暂时留待后面详细研究。
今天终于把真正查 node 的函数找到了,不过还没完全搞懂查 node 的过程和 compress 在中间造成的不同。先把找的过程贴上来一下:
提醒一下:前文的connection.findAccessibilityNodeInfoByAccessibilityId中传的 flag 是进行了一个运算后再传给findAccessibilityNodeInfoByAccessibilityId:mFetchFlags | flags,这里其实就是把我们之前预先告诉AccessibilityInteractionClient要添加的AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS信息加入到 flags 中。
然后 find in path,找到IAccessibilityServiceConnection的实现类android.view.ViewRootImpl:
/**
* This class is an interface this ViewAncestor provides to the
* AccessibilityManagerService to the latter can interact with
* the view hierarchy in this ViewAncestor.
*/
static final class AccessibilityInteractionConnection
extends IAccessibilityInteractionConnection.Stub {
private final WeakReference<ViewRootImpl> mViewRootImpl;
AccessibilityInteractionConnection(ViewRootImpl viewRootImpl) {
mViewRootImpl = new WeakReference<ViewRootImpl>(viewRootImpl);
}
@Override
public void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId,
int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags,
int interrogatingPid, long interrogatingTid, MagnificationSpec spec) {
ViewRootImpl viewRootImpl = mViewRootImpl.get();
if (viewRootImpl != null && viewRootImpl.mView != null) {
viewRootImpl.getAccessibilityInteractionController()
.findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityNodeId,
interactionId, callback, flags, interrogatingPid, interrogatingTid,
spec);
} else {
// We cannot make the call and notify the caller so it does not wait.
try {
callback.setFindAccessibilityNodeInfosResult(null, interactionId);
} catch (RemoteException re) {
/* best effort - ignore */
}
}
}
这里第一次出现了callback。当 viewRootImpl 为 null 或 viewRootImpl.mView 为 null 时,调用了callback.setFindAccessibilityNodeInfoResult方法,把 null 作为结果返回给callback了。在异步编程里面,callback 的作用相当于同步编程的 return,把处理结果返回给这个函数的调用者。这个函数名称是不是很眼熟?对,它在AccessibilityInteractionClient里出现过,就是咱们一开始建立 connection 并调用其findAccessibilityNodeInfoByAccessibilityId方法那里,同时咱们前面调试时存储mFindAccessibilityNodeInfosResult结果的也是这个类。于是咱们跟踪进去看看
此时callback是IAccessibilityInteractionConnectionCallback类型的,find in path 发现android.view.accessibility.AccessibilityInteractionClient刚好实现了IAccessibilityInteractionConnectionCallback,其中setsetFindAccessibilityNodeInfosResult方法源码如下:
/**
* {@inheritDoc}
*/
public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
int interactionId) {
synchronized (mInstanceLock) {
if (interactionId > mInteractionId) {
if (infos != null) {
// If the call is not an IPC, i.e. it is made from the same process, we need to
// instantiate new result list to avoid passing internal instances to clients.
final boolean isIpcCall = (Binder.getCallingPid() != Process.myPid());
if (!isIpcCall) {
mFindAccessibilityNodeInfosResult =
new ArrayList<AccessibilityNodeInfo>(infos);
} else {
mFindAccessibilityNodeInfosResult = infos;
}
} else {
mFindAccessibilityNodeInfosResult = Collections.emptyList();
}
mInteractionId = interactionId;
}
mInstanceLock.notifyAll();
}
}
看到了吗?这里把第一个参数 infos 赋给了 mFindAccessibilityNodeInfosResult,也就是说,就是这里把结果存储到mFindAccessibilityNodeInfosResult的。但此时因为 info 是 null,所以其实存的是一个空列表,和我们调试看到的不一样。
好了,先记下来,callback.setFindAccessibilityNodeInfosResult就是把找到的结果存储到mFindAccessibilityNodeInfosResult的函数,下次见到它记得多留意一下。
咱们回到android.view.ViewRootImpl的findAccessibilityNodeInfoByAccessibilityId方法:
/**
* This class is an interface this ViewAncestor provides to the
* AccessibilityManagerService to the latter can interact with
* the view hierarchy in this ViewAncestor.
*/
static final class AccessibilityInteractionConnection
extends IAccessibilityInteractionConnection.Stub {
private final WeakReference<ViewRootImpl> mViewRootImpl;
AccessibilityInteractionConnection(ViewRootImpl viewRootImpl) {
mViewRootImpl = new WeakReference<ViewRootImpl>(viewRootImpl);
}
@Override
public void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId,
int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags,
int interrogatingPid, long interrogatingTid, MagnificationSpec spec) {
ViewRootImpl viewRootImpl = mViewRootImpl.get();
if (viewRootImpl != null && viewRootImpl.mView != null) {
viewRootImpl.getAccessibilityInteractionController()
.findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityNodeId,
interactionId, callback, flags, interrogatingPid, interrogatingTid,
spec);
} else {
// We cannot make the call and notify the caller so it does not wait.
try {
callback.setFindAccessibilityNodeInfosResult(null, interactionId);
} catch (RemoteException re) {
/* best effort - ignore */
}
}
}
前面的分析说明函数没有跑到 else 那里,那咱们直接看一下 if 里面的语句:
viewRootImpl.getAccessibilityInteractionController()
.findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityNodeId,
interactionId, callback, flags, interrogatingPid, interrogatingTid,
spec);
这里的getAccessibilityInteractionController就是获取了一个AccessibilityInteractionController的实例,没有对flags进行任何运算,略过。
findAccessibilityNodeInfoByAccessibilityIdClientThread是AccessibilityInteractionController的子方法,源码如下:
public void findAccessibilityNodeInfoByAccessibilityIdClientThread(
long accessibilityNodeId, int interactionId,
IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid,
long interrogatingTid, MagnificationSpec spec) {
Message message = mHandler.obtainMessage();
message.what = PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID;
message.arg1 = flags;
SomeArgs args = SomeArgs.obtain();
args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId);
args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId);
args.argi3 = interactionId;
args.arg1 = callback;
args.arg2 = spec;
message.obj = args;
// If the interrogation is performed by the same thread as the main UI
// thread in this process, set the message as a static reference so
// after this call completes the same thread but in the interrogating
// client can handle the message to generate the result.
if (interrogatingPid == mMyProcessId && interrogatingTid == mMyLooperThreadId) {
AccessibilityInteractionClient.getInstanceForThread(
interrogatingTid).setSameThreadMessage(message);
} else {
mHandler.sendMessage(message);
}
}
这里做的事情就是把传进来的参数封装到message里面,然后把这个message放到一个队列里面(这里做了一个线程判断,如果是同一线程调用setSameThreadMessage,否则mHandler.sendMessage)。这是 Android IPC(进程间通讯)中的 Messager 通讯方式,这个队列会被 looper 不断查找,如果有内容则逐个交给 handler 处理。因此跟进去sendMessage没有意义(它只是放到队列里面,具体执行不关它的事)。我们留意到这里设了一个作为标识变量:message.what = PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID;。
既然实际上处理的函数是 handler,那么咱们去找 handler。
在AccessibilityInteractionController的构造函数中,我们发现了 mHandler 的初始化语句:
public AccessibilityInteractionController(ViewRootImpl viewRootImpl) {
Looper looper = viewRootImpl.mHandler.getLooper();
mMyLooperThreadId = looper.getThread().getId();
mMyProcessId = Process.myPid();
mHandler = new PrivateHandler(looper);
mViewRootImpl = viewRootImpl;
mPrefetcher = new AccessibilityNodePrefetcher();
}
它是一个PrivateHandler的实例,所以它使用的 handler 应该也是PrivateHandler里的 handler。
根据 Android IPC 里面 Messager 的介绍,sendMessage后的执行是由 handler 负责的,因此咱们直接去看看PrivateHandler里面的handleMessage方法:
private class PrivateHandler extends Handler {
private final static int MSG_PERFORM_ACCESSIBILITY_ACTION = 1;
private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID = 2;
private final static int MSG_FIND_ACCESSIBLITY_NODE_INFOS_BY_VIEW_ID = 3;
private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT = 4;
private final static int MSG_FIND_FOCUS = 5;
private final static int MSG_FOCUS_SEARCH = 6;
public PrivateHandler(Looper looper) {
super(looper);
}
...
@Override
public void handleMessage(Message message) {
final int type = message.what;
switch (type) {
case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID: {
findAccessibilityNodeInfoByAccessibilityIdUiThread(message);
} break;
case MSG_PERFORM_ACCESSIBILITY_ACTION: {
perfromAccessibilityActionUiThread(message);
} break;
case MSG_FIND_ACCESSIBLITY_NODE_INFOS_BY_VIEW_ID: {
findAccessibilityNodeInfosByViewIdUiThread(message);
} break;
case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: {
findAccessibilityNodeInfosByTextUiThread(message);
} break;
case MSG_FIND_FOCUS: {
findFocusUiThread(message);
} break;
case MSG_FOCUS_SEARCH: {
focusSearchUiThread(message);
} break;
default:
throw new IllegalArgumentException("Unknown message type: " + type);
}
}
}
这里由于我们的message.what是PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID,因此执行findAccessibilityNodeInfoByAccessibilityIdUiThread:
private void findAccessibilityNodeInfoByAccessibilityIdUiThread(Message message) {
final int flags = message.arg1;
SomeArgs args = (SomeArgs) message.obj;
final int accessibilityViewId = args.argi1;
final int virtualDescendantId = args.argi2;
final int interactionId = args.argi3;
final IAccessibilityInteractionConnectionCallback callback =
(IAccessibilityInteractionConnectionCallback) args.arg1;
final MagnificationSpec spec = (MagnificationSpec) args.arg2;
args.recycle();
List<AccessibilityNodeInfo> infos = mTempAccessibilityNodeInfoList;
infos.clear();
try {
if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) {
return;
}
mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags;
View root = null;
if (accessibilityViewId == AccessibilityNodeInfo.UNDEFINED) {
root = mViewRootImpl.mView;
} else {
root = findViewByAccessibilityId(accessibilityViewId);
}
if (root != null && isShown(root)) {
mPrefetcher.prefetchAccessibilityNodeInfos(root, virtualDescendantId, flags, infos);
}
} finally {
try {
mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
applyAppScaleAndMagnificationSpecIfNeeded(infos, spec);
if (spec != null) {
spec.recycle();
}
callback.setFindAccessibilityNodeInfosResult(infos, interactionId);
infos.clear();
} catch (RemoteException re) {
/* ignore - the other side will time out */
}
}
}
这里我们又见到了callback.setFindAccessibilityNodeInfosResult方法,因此可以确定找 node 的具体工作都是这里来完成的,自然过滤也是。
这里的过程比较复杂,目前还没完全看懂。后面看懂后再把分析贴上来。
必须点赞!
@mingyuwang 能把正常跑得通的 log 贴上来一下吗?到
----|---------------------------------------------------------------------------
X -[TEST_BUNDLE FAILED_TO_START] (0 ms) (0)
-[CSSTests testCSSCreate]
--------------------------------------------------------------------------------
Test did not run: Simulator 'iPhone 4s' was not prepared: Failed for unknown reason.
这里就可以了。
估计还是因为 jenkins 建立的 shell 和你能执行成功的 shell 部分配置不一样。
@mingyuwang 在xctool找到一段说明
In order to your run your tests within a continuous integration environment, you must create Shared Schemes for your application target and ensure that all dependencies (such as CocoaPods) are added explicitly to the Scheme
这一部分有做了吗?