今天看了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 好,不过估计你会比较快。我是边看边查相关资料,看得很慢的。
终于勉强有一个调试环境了。因为完整的 android 源码下载下来太慢了,所以我采用远程调试 uiautomator 脚本的方法进行调试,缺少的 IAccessibilityServiceConnection 文件也在GrepCode找到了(是编译后的.java 文件,对于这种调试基本足够了)。
先附上 uiautomator 脚本的关键代码:
...
UiDevice device = getUiDevice();
device.waitForIdle();
//set compress, the same as what --compress did
device.setCompressedLayoutHeirarchy(true);
//dump Hierarchy file
device.dumpWindowHierarchy("dumpFromEclipse.xml");
...
开始调试过程。这里我主要把关键过程的调试信息附上:
device.setCompressedLayoutHeirarchy(true);
方法代码:
/**
* Enables or disables layout hierarchy compression.
*
* If compression is enabled, the layout hierarchy derived from the Acessibility
* framework will only contain nodes that are important for uiautomator
* testing. Any unnecessary surrounding layout nodes that make viewing
* and searching the hierarchy inefficient are removed.
*
* @param compressed true to enable compression; else, false to disable
* @since API Level 18
*/
public void setCompressedLayoutHeirarchy(boolean compressed) {
getAutomatorBridge().setCompressedLayoutHierarchy(compressed);
}
这里getAutomatorBridge()
获取了对QueryController
和InteractionController
的访问连接,然后调用了它的setCompressedLayoutHierarchy(compressed)
方法。
public void setCompressedLayoutHierarchy(boolean compressed) {
AccessibilityServiceInfo info = mUiAutomation.getServiceInfo();
if (compressed)
info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
else
info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
mUiAutomation.setServiceInfo(info);
}
在调试时看到:
AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
标志位前,info.flags=18
,16
(这里使用的是位运算。关于位运算的资料可以查看位运算简介及实用技巧(一):基础篇)setServiceInfo
方法会把添加标志位后的info
发给AccessibilityInteractionClient
:
/**
* Sets the {@link AccessibilityServiceInfo} that describes how this
* UiAutomation will be handled by the platform accessibility layer.
*
* @param info The info.
*
* @see AccessibilityServiceInfo
*/
public final void setServiceInfo(AccessibilityServiceInfo info) {
final IAccessibilityServiceConnection connection;
synchronized (mLock) {
throwIfNotConnectedLocked();
AccessibilityInteractionClient.getInstance().clearCache();
connection = AccessibilityInteractionClient.getInstance()
.getConnection(mConnectionId);
}
// Calling out without a lock held.
if (connection != null) {
try {
connection.setServiceInfo(info);
} catch (RemoteException re) {
Log.w(LOG_TAG, "Error while setting AccessibilityServiceInfo", re);
}
}
}
getConnection()
的返回值类型为IAccessibilityServiceConnection
,通过 find in path,得知com.android.server.accessibility.AccessibilityManagerService
中的Service
类继承并实现了IAccessibilityServiceConnection.Stub
。其中setService
源码如下:
@Override
public void setServiceInfo(AccessibilityServiceInfo info) {
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
// If the XML manifest had data to configure the service its info
// should be already set. In such a case update only the dynamically
// configurable properties.
AccessibilityServiceInfo oldInfo = mAccessibilityServiceInfo;
if (oldInfo != null) {
oldInfo.updateDynamicallyConfigurableProperties(info);
setDynamicallyConfigurableProperties(oldInfo);
} else {
setDynamicallyConfigurableProperties(info);
}
UserState userState = getUserStateLocked(mUserId);
onUserStateChangedLocked(userState);
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
这里主要做的事情就是执行了setDynamicallyConfigurableProperties
方法来更新info
。setDynamicallyConfigurableProperties
方法源码如下:
public void setDynamicallyConfigurableProperties(AccessibilityServiceInfo info) {
mEventTypes = info.eventTypes;
mFeedbackType = info.feedbackType;
String[] packageNames = info.packageNames;
if (packageNames != null) {
mPackageNames.addAll(Arrays.asList(packageNames));
}
mNotificationTimeout = info.notificationTimeout;
mIsDefault = (info.flags & DEFAULT) != 0;
if (mIsAutomation || info.getResolveInfo().serviceInfo.applicationInfo.targetSdkVersion
>= Build.VERSION_CODES.JELLY_BEAN) {
if ((info.flags & AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0) {
mFetchFlags |= AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
} else {
mFetchFlags &= ~AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
}
}
if ((info.flags & AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS) != 0) {
mFetchFlags |= AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS;
} else {
mFetchFlags &= ~AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS;
}
mRequestTouchExplorationMode = (info.flags
& AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0;
mRequestEnhancedWebAccessibility = (info.flags
& AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY) != 0;
mRequestFilterKeyEvents = (info.flags
& AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS) != 0;
}
这里做的事情就是根据 info 更新局部变量,如mEventTyes
,mFetchFlags
。其中mFetchFlags
同样检查了AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
。当info.flag
中含有FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
时,执行mFetchFlags &= ~AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
,把这个标志位添加到mFetchFlags
中。
至此,device.setCompressedLayoutHeirarchy(true);
完成任务了,AccessibilityInteractionClient
已经知道后面执行的操作都基于设定了AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
标志的前提下
首先进入dumpWindowHierarchy
:
/**
* Helper method used for debugging to dump the current window's layout hierarchy.
* The file root location is /data/local/tmp
*
* @param fileName
* @since API Level 16
*/
public void dumpWindowHierarchy(String fileName) {
Tracer.trace(fileName);
AccessibilityNodeInfo root =
getAutomatorBridge().getQueryController().getAccessibilityRootNode();
if(root != null) {
Display display = getAutomatorBridge().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
AccessibilityNodeInfoDumper.dumpWindowToFile(root,
new File(new File(Environment.getDataDirectory(), "local/tmp"), fileName),
display.getRotation(), size.x, size.y);
}
}
Tracer.trace(fileName);
与找 node 的过程无关,略过,然后我们执行到AccessibilityNodeInfo root =getAutomatorBridge().getQueryController().getAccessibilityRootNode();
getAutomatorBridge()
和第一步一样,获取连接getQueryController()
获取QueryController
实例getAccessibilityRootNode()
这是重点。咱们进去看看:源码:
public AccessibilityNodeInfo getAccessibilityRootNode() {
return mUiAutomatorBridge.getRootInActiveWindow();
}
这里开始和我上面的getRootInActiveWindow
到connection.findAccessibilityNodeInfoByAccessibilityId
的过程是一致的。其中findAccessibilityNodeInfoByAccessibilityId
方法比较复杂,因此从这里开始详述:
/**
* Finds an {@link AccessibilityNodeInfo} by accessibility id.
*
* @param connectionId The id of a connection for interacting with the system.
* @param accessibilityWindowId A unique window id. Use
* {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
* to query the currently active window.
* @param accessibilityNodeId A unique view id or virtual descendant id from
* where to start the search. Use
* {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
* to start from the root.
* @param bypassCache Whether to bypass the cache while looking for the node.
* @param prefetchFlags flags to guide prefetching.
* @return An {@link AccessibilityNodeInfo} if found, null otherwise.
*/
public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId,
int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache,
int prefetchFlags) {
try {
IAccessibilityServiceConnection connection = getConnection(connectionId);
if (connection != null) {
if (!bypassCache) {
AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get(
accessibilityNodeId);
if (cachedInfo != null) {
return cachedInfo;
}
}
final int interactionId = mInteractionIdCounter.getAndIncrement();
final boolean success = connection.findAccessibilityNodeInfoByAccessibilityId(
accessibilityWindowId, accessibilityNodeId, interactionId, this,
prefetchFlags, Thread.currentThread().getId());
// If the scale is zero the call has failed.
if (success) {
List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
interactionId);
finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
if (infos != null && !infos.isEmpty()) {
return infos.get(0);
}
}
} else {
if (DEBUG) {
Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
}
}
} catch (RemoteException re) {
if (DEBUG) {
Log.w(LOG_TAG, "Error while calling remote"
+ " findAccessibilityNodeInfoByAccessibilityId", re);
}
}
return null;
}
咱们一行一行来:
IAccessibilityServiceConnection connection = getConnection(connectionId);
获取连接。获取后 connection 不是 null,且 bypassCache 为 false,因此跑到这一行:
AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get(
accessibilityNodeId);
调试过程中发现缓存中没有对应信息,cachedInfo 为 null,因此进入这一行:
final int interactionId = mInteractionIdCounter.getAndIncrement();
获取了 interactionId,值为 0。进入下一行
final boolean success = connection.findAccessibilityNodeInfoByAccessibilityId(
accessibilityWindowId, accessibilityNodeId, interactionId, this,
prefetchFlags, Thread.currentThread().getId());
这里做的事情比较多。为方便后面说明,先把目前的各变量及对应值贴一下:
然后咱们进入之前卡住了的connection.findAccessibilityNodeInfoByAccessibilityId
:
@Override
public boolean findAccessibilityNodeInfoByAccessibilityId(
int accessibilityWindowId, long accessibilityNodeId,
int interactionId,
android.view.accessibility.IAccessibilityInteractionConnectionCallback callback,
int flags, long threadId) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
boolean _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(accessibilityWindowId);
_data.writeLong(accessibilityNodeId);
_data.writeInt(interactionId);
_data.writeStrongBinder((((callback != null))? (callback.asBinder()) : (null)));
_data.writeInt(flags);
_data.writeLong(threadId);
mRemote.transact(Stub.TRANSACTION_findAccessibilityNodeInfoByAccessibilityId,
_data, _reply, 0);
_reply.readException();
_result = (0 != _reply.readInt());
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
开始三行获取了 2 个Parcel实例 (Parcel 是一个存储通过 IBinder 传输的消息的容器)_data
和_reply
,创建了一个 boolean 类型的变量_result
。
然后开始往_data
写入数据:
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(accessibilityWindowId);
_data.writeLong(accessibilityNodeId);
_data.writeInt(interactionId);
_data.writeStrongBinder((((callback != null))? (callback.asBinder()) : (null)));
_data.writeInt(flags);
_data.writeLong(threadId);
执行完后各数据的值:
其中DESCRIPTOR
的值如下:
private static final java.lang.String DESCRIPTOR = "android.accessibilityservice.IAccessibilityServiceClient";
然后执行mRemote.transact(Stub.TRANSACTION_findAccessibilityNodeInfoByAccessibilityId, _data, _reply, 0);
。
此时 step into 进入不了这个方法(我用的是 remote debug,这部分代码的内容已经不在 build path 里面了,所以跟不进去),直接去到下一个语句_reply.readException();
。此时 callback 的内部变量值有了变化,增加了两个变量:
可以看到mFindAccessibilityNodeInfosResult
里面已经含有各个 node 的相关信息了。而且这里的信息已经 compress 过了,所以过滤是在mRemote.transact
里面做的。
接下来我们看看这里实际做了什么了。
还是在IAccessibilityServiceConnection
里面:
@Override
public boolean onTransact(int code, android.os.Parcel data,
android.os.Parcel reply, int flags)
throws android.os.RemoteException {
switch (code) {
...
case TRANSACTION_findAccessibilityNodeInfoByAccessibilityId: {
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
long _arg1;
_arg1 = data.readLong();
int _arg2;
_arg2 = data.readInt();
android.view.accessibility.IAccessibilityInteractionConnectionCallback _arg3;
_arg3 = android.view.accessibility.IAccessibilityInteractionConnectionCallback.Stub.asInterface(data.readStrongBinder());
int _arg4;
_arg4 = data.readInt();
long _arg5;
_arg5 = data.readLong();
boolean _result = this.findAccessibilityNodeInfoByAccessibilityId(_arg0,
_arg1, _arg2, _arg3, _arg4, _arg5);
reply.writeNoException();
reply.writeInt(((_result) ? (1) : (0)));
return true;
}
这里data.enforceInterface(DESCRIPTOR);
进行了数据的解包,然后把各个数据分别放到_arg0
~_arg5
中,其中_arg3=android.view.accessibility.IAccessibilityInteractionConnectionCallback.Stub.asInterface(data.readStrongBinder());
把 callback(getRootInActiveWindow
的findAccessibilityNodeInfoByAccessibilityId
)转变成IAccessibilityInteractionConnectionCallback
的 instance(相当于findAccessibilityNodeInfoByAccessibilityId
是IAccessibilityInteractionConnectionCallback
的实现)。然后调用了findAccessibilityNodeInfoByAccessibilityId
(此处调用的应该是继承了IAccessibilityInteractionConnection.Stub
的实现类,通过查找源码发现在com.android.server.accessibility.AccessibilityManagerService
里面),并把结果写到 reply 中。
查看com.android.server.accessibility.AccessibilityManagerService
相关源码:
@Override
public boolean findAccessibilityNodeInfoByAccessibilityId(
int accessibilityWindowId, long accessibilityNodeId, int interactionId,
IAccessibilityInteractionConnectionCallback callback, int flags,
long interrogatingTid) throws RemoteException {
final int resolvedWindowId;
IAccessibilityInteractionConnection connection = null;
synchronized (mLock) {
final int resolvedUserId = mSecurityPolicy
.resolveCallingUserIdEnforcingPermissionsLocked(
UserHandle.getCallingUserId());
if (resolvedUserId != mCurrentUserId) {
return false;
}
mSecurityPolicy.enforceCanRetrieveWindowContent(this);
resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId);
final boolean permissionGranted =
mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId);
if (!permissionGranted) {
return false;
} else {
connection = getConnectionLocked(resolvedWindowId);
if (connection == null) {
return false;
}
}
}
final int interrogatingPid = Binder.getCallingPid();
final long identityToken = Binder.clearCallingIdentity();
MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId);
try {
connection.findAccessibilityNodeInfoByAccessibilityId(accessibilityNodeId,
interactionId, callback, mFetchFlags | flags, interrogatingPid,
interrogatingTid, spec);
return true;
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfoByAccessibilityId()");
}
} finally {
Binder.restoreCallingIdentity(identityToken);
}
return false;
}
这里的connection
和之前的不一样,它是通过getConnectionLocked
获得的。查看getConnectionLocked
:
private IAccessibilityInteractionConnection getConnectionLocked(int windowId) {
if (DEBUG) {
Slog.i(LOG_TAG, "Trying to get interaction connection to windowId: " + windowId);
}
AccessibilityConnectionWrapper wrapper = mGlobalInteractionConnections.get(windowId);
if (wrapper == null) {
wrapper = getCurrentUserStateLocked().mInteractionConnections.get(windowId);
}
if (wrapper != null && wrapper.mConnection != null) {
return wrapper.mConnection;
}
if (DEBUG) {
Slog.e(LOG_TAG, "No interaction connection to window: " + windowId);
}
return null;
}
这里的 connection 是IAccessibilityInteractionConnection
类型的,而前面的 connection 则是IAccessibilityServiceConnection
类型的。那么,这里的findAccessibilityNodeInfoByAccessibilityId
具体做了什么?留待搭建完完整的 android source code 调试环境后继续研究。
@doctorq 谢谢指导,之前还真没了解过 aidl。今晚研究一下。
看了一下源码,但还没搭建好调试环境,所以跟踪到某一步后就跟踪不下去了。在此仅分享一下找的过程:
先接着 doctorq 的思路,去找 android 源码中的getRootInActiveWindow
:
android.app.UiAutomation
/**
* Gets the root {@link AccessibilityNodeInfo} in the active window.
*
* @return The root info.
*/
public AccessibilityNodeInfo getRootInActiveWindow() {
final int connectionId;
synchronized (mLock) {
throwIfNotConnectedLocked();
connectionId = mConnectionId;
}
// Calling out without a lock held.
return AccessibilityInteractionClient.getInstance()
.getRootInActiveWindow(connectionId);
}
接着找 return 中的getRootInActiveWindow
:
android.view.acessibility.AccessibilityInteractionClient
/**
* Gets the root {@link AccessibilityNodeInfo} in the currently active window.
*
* @param connectionId The id of a connection for interacting with the system.
* @return The root {@link AccessibilityNodeInfo} if found, null otherwise.
*/
public AccessibilityNodeInfo getRootInActiveWindow(int connectionId) {
return findAccessibilityNodeInfoByAccessibilityId(connectionId,
AccessibilityNodeInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID,
false, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS);
}
继续找findAccessibilityNodeInfoByAccessibilityId
:
android.view.acessibility.AccessibilityInteractionClient
/**
* Finds an {@link AccessibilityNodeInfo} by accessibility id.
*
* @param connectionId The id of a connection for interacting with the system.
* @param accessibilityWindowId A unique window id. Use
* {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
* to query the currently active window.
* @param accessibilityNodeId A unique view id or virtual descendant id from
* where to start the search. Use
* {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
* to start from the root.
* @param bypassCache Whether to bypass the cache while looking for the node.
* @param prefetchFlags flags to guide prefetching.
* @return An {@link AccessibilityNodeInfo} if found, null otherwise.
*/
public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId,
int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache,
int prefetchFlags) {
try {
IAccessibilityServiceConnection connection = getConnection(connectionId);
if (connection != null) {
if (!bypassCache) {
AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get(
accessibilityNodeId);
if (cachedInfo != null) {
return cachedInfo;
}
}
final int interactionId = mInteractionIdCounter.getAndIncrement();
final boolean success = connection.findAccessibilityNodeInfoByAccessibilityId(
accessibilityWindowId, accessibilityNodeId, interactionId, this,
prefetchFlags, Thread.currentThread().getId());
// If the scale is zero the call has failed.
if (success) {
List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
interactionId);
finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
if (infos != null && !infos.isEmpty()) {
return infos.get(0);
}
}
} else {
if (DEBUG) {
Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
}
}
} catch (RemoteException re) {
if (DEBUG) {
Log.w(LOG_TAG, "Error while calling remote"
+ " findAccessibilityNodeInfoByAccessibilityId", re);
}
}
return null;
}
注意中间的connection.findAccessibilityNodeInfoByAccessibilityId
,我用的 IDE 是 Android Studio,findAccessibilityNodeInfoByAccessibilityId
方法显示为红色,表示找不到它的声明位置。
然后我们看看 connection 的声明:
·IAccessibilityServiceConnection connection = getConnection(connectionId);·
它是通过getConnection(connectionId)
获得的实例,继续找:
public IAccessibilityServiceConnection getConnection(int connectionId) {
synchronized (sConnectionCache) {
return sConnectionCache.get(connectionId);
}
}
再找sConnectionCache
:
// The connection cache is shared between all interrogating threads.
private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache =
new SparseArray<IAccessibilityServiceConnection>();
继续IAccessibilityServiceConnection
,这时候只能找到一个相关的 import 语句了:
import android.accessibilityservice.IAccessibilityServiceConnection;
到这里为止,寻找方法声明位置的这种方法找不下去了,因为源码里找不到IAccessibilityServiceConnection
声明的位置。
大致猜测是本来每个 Node 其实都带有一定的信息 (具体有哪些信息可以在android.accessibilityservice.AccessibilityServiceInfo
找到) 表示它是否影响显示,--compress 插入的标志位会在存储 Node 时不存储这些不影响显示的 Node。具体是如何实现的等研究好如何搭建调试环境后继续研究。
看完以后,我觉得我压根就没学会设计模式。用过 singleton,但没试过探究到内存这么深入。赞一个
@ice87875494 这个库是用来给其他应用调用的。你应该下载下来后用其他 py 文件 import pylib 来使用。
举个例子:
folder
├─pylib(pylib源码)
│ test.py
此时 test.py 里面使用import pylib
就能 import。
在 module xxx 里面 import xxx 是会提示" No module named xxx"的,因为这个 module 不在查找范围内。具体 import 过程可以看看import system
12 年实习,一开始主要做政府部门的验收测试(公司拿到了对应的资质证书),2~3 天把项目的招标、需求、概设、详设等看完并根据需求写用例,然后又 2~3 天到客户现场把用例执行完(功能测试和压力测试,使用 LoadRunner 录脚本然后根据需求确认性能点是否达标),最后出报告。基本上 1 周 1 个项目的节奏。后面公司需要人手做公司的操作系统的兼容性测试,于是学习了 linux,开始用 shell 脚本来跑一部分测试(脚本都是别人写好的,当时对 linux 的东西还不是太了解),测试安装、驱动性能等。
13 年毕业,进入一个做海外外包的外企,进入了一个产品线包括嵌入式、web、android、ios 的项目。第一年先是做 ios 测试,然后 android,然后 web,都是纯手工测试。业余学了一些 ios 和 android 调试工具的使用,知道了 appium 的存在(当时 appium 刚出来,还是挺热门的),学会了用 python 写一些简单的监控脚本。正准备开始自己弄一些脚本来让手工测试不那么枯燥的时候,公司需要开发一个测试第三方配件的测试工具,刚好用的是 python,leader 和我也比较熟,然后很幸运地就加入了。现在基本完成了工具的开发,正在写对应这个工具用例作为演示。
过程中也有人建议我转做开发,因为我有一定的编程经验(以前在学校搞过 J2EE 和 web 的一些东西),但我还是更想做测试,一方面做开发做久了也会很枯燥(开发也有重复性的工作,也有不少 copy/paste),会让我失去对计算机的兴趣,另一方面如同《Google 软件测试之道》里面 Chrome 测试工程经理 Joel Hynoski 所说,“测试是开发过程里面工程师能涉及的最远的地方”。我喜欢对所有事情都一探究竟,直到我完全理解,测试正好可以做到。
后面还会继续专注自动化测试,当然作为基础的开发也会继续学习。
根据 log 的描述,无法创建新 session 的原因是旧 session 没有被关闭。
个人建议:
@lihuazhang github 账号:chenhengjie123
@doctorq 有兴趣,求加入,学习 appium 同时改进它。
希望留下,虽然主要都是潜水,但是视野比以前开阔了很多。
一般公司隔一段时间会组织一些活动的吧,多参加这些活动就有一定锻炼效果了。
最好还是有一项运动方面的爱好,坚持每周至少参加一次,那么身体相对就好一些。