你的代码是直接复制粘贴上来的吗?第一个代码的 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
这一部分有做了吗?
会不会是由于 jenkins 里面使用的用户不一样?用户不一样会导致各种设置不一样的。我在 windows 下用 jenkins 经常遇到这个问题,本地跑得好好的脚本去到 jenkins 各种报错。但 mac 里面没用过,不是很清楚是不是也是一样的。
已关注。支持!
@doctorq 好,不过估计你会比较快。我是边看边查相关资料,看得很慢的。