• @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进行了几个操作:

    1. 取出 message 里面存储的各个变量。其中有我们最关注的flags
    2. flags赋值给mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags
    3. 在查出 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 的值)。咱们逐个看:

    prefetchDescendantsOfRealNode:

    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);
                        }
                    }
                }
            }
    
    1. 判断root是不是ViewGroup的实例(这里应该是,否则返回值就是空了)
    2. 建立两个变量addedChildren, children(此时 children 使用过 clear() 方法,所以已经是空列表了)
    3. 通过root.addChildrenForAccessibility(children)获取root的子节点并添加到children列表中。
    4. 后面的语句主要就是遍历children的元素,然后把所有会显示的元素都加到outInfosoutInfos就是最终会返回到 callback 中的节点列表。
    5. 如果找到的节点数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 部分:

    1. 判断 mode 的值 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;
    2. 判断父节点及祖先节点是否为 IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS(对 ACCESSIBILITY 不重要且对它的后代而言也不重要) // 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(); }
    3. 判断 mode 是否为IMPORTANT_FOR_ACCESSIBILITY_YES及是否具有其它和 Accessibility 相关的特性: return mode == IMPORTANT_FOR_ACCESSIBILITY_YES || isActionableForAccessibility() || hasListenersForAccessibility() || getAccessibilityNodeProvider() != null || getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE; 先记着。咱们去看另一个路线prefetchDescendantsOfVirtualNode

    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 干掉,目前还没找到确切的原因,不过已经可以肯定是在下面三个地方其中一个确定的:

    1. 判断 mode 的值 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;
    2. 判断父节点及祖先节点是否为 IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS(对 ACCESSIBILITY 不重要且对它的后代而言也不重要) // 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(); }
    3. 判断 mode 是否为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 是进行了一个运算后再传给findAccessibilityNodeInfoByAccessibilityIdmFetchFlags | 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结果的也是这个类。于是咱们跟踪进去看看
    此时callbackIAccessibilityInteractionConnectionCallback类型的,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.ViewRootImplfindAccessibilityNodeInfoByAccessibilityId方法:

    /**
         * 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进行任何运算,略过。
    findAccessibilityNodeInfoByAccessibilityIdClientThreadAccessibilityInteractionController的子方法,源码如下:

    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.whatPrivateHandler.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 的具体工作都是这里来完成的,自然过滤也是。
    这里的过程比较复杂,目前还没完全看懂。后面看懂后再把分析贴上来。

  • swift 基础与 oc 基础 at 2015年02月09日

    必须点赞!

  • @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 部分配置不一样。

  • @mingyuwangxctool找到一段说明
    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");
    ...
    

    开始调试过程。这里我主要把关键过程的调试信息附上:

    setCompressedLayoutHierarchy

    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()获取了对QueryControllerInteractionController的访问连接,然后调用了它的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);
    }
    

    在调试时看到:

    1. 在设定AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS标志位前,info.flags=18
    2. 设定后 info.flags=16(这里使用的是位运算。关于位运算的资料可以查看位运算简介及实用技巧(一):基础篇
    3. 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方法来更新infosetDynamicallyConfigurableProperties方法源码如下:

      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 更新局部变量,如mEventTyesmFetchFlags。其中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标志的前提下

    device.dumpWindowHierarchy("dumpFromEclipse.xml");

    首先进入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();

    1. getAutomatorBridge()和第一步一样,获取连接
    2. getQueryController()获取QueryController实例
    3. getAccessibilityRootNode()这是重点。咱们进去看看:

    getAccessibilityRootNode

    源码:

    public AccessibilityNodeInfo getAccessibilityRootNode() {
        return mUiAutomatorBridge.getRootInActiveWindow();
    }
    

    这里开始和我上面的getRootInActiveWindowconnection.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;
        }
    

    从这里开始由于没有实际调试,主要是猜测。大家看看就好。等我下载完完整的 android source code 后才能调试下去。

    这里data.enforceInterface(DESCRIPTOR);进行了数据的解包,然后把各个数据分别放到_arg0~_arg5中,其中_arg3=android.view.accessibility.IAccessibilityInteractionConnectionCallback.Stub.asInterface(data.readStrongBinder());把 callback(getRootInActiveWindowfindAccessibilityNodeInfoByAccessibilityId)转变成IAccessibilityInteractionConnectionCallback的 instance(相当于findAccessibilityNodeInfoByAccessibilityIdIAccessibilityInteractionConnectionCallback的实现)。然后调用了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

  • 别人家的测试在做什么? at 2015年02月05日

    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 没有被关闭。
    个人建议:

    1. 检查出错的 case 是否真的 quit() 了(如打开的应用是否被关闭了)。
    2. 不同 case 之间加上 10s 左右的等待时间,有时候 quit() 也需要一定时间的。
  • 开源项目召集令[已结束] at 2015年02月04日

    @lihuazhang github 账号:chenhengjie123

  • 开源项目召集令[已结束] at 2015年02月04日

    @doctorq 有兴趣,求加入,学习 appium 同时改进它。

  • 希望留下,虽然主要都是潜水,但是视野比以前开阔了很多。