自动化工具 uiautomatorviewer 新增功能 compressed 之 Device 端细节

易寒 · January 16, 2015 · Last by 测试小书童 replied at May 03, 2016 · 3483 hits
本帖已被设为精华帖!

昨天了解了uiautoamtorviewer新增功能dump --compressed,是一个直接发送到设备端的命令,那么这个命令发送到设备端后,设备端是如何操作的呢?我又成了10万个为什么了?继续源码研究......

源码地址

google source

这个jar包最新的版本只到了4.4.2。说明5.0后的uiautomator设备端是没有改变的,那么说明dump --compressed之前就有,只是我不知道罢鸟。结论:

dump --compressed命令4.4.2时代就有,只是年少无知没发现

源码环境搭建

解压以后项目结果如下所示:

直接用eclipse的import功能导入,整体导入。导入eclipse后,如下图所示,感叹号是因为没有添加android.jar造成的,加上就好了。

源码分析

当我们在命令行下输入下面命令的时候,android系统就会调用cmds目录下的Launcher类中的main方法中

/system/bin/uiautomator dump --compressed /data/local/tmp/uidump.xml

Launcher

所以我们从main开始我们的大餐:

public static void main(String[] args) {
// show a meaningful process name in `ps`
Process.setArgV0("uiautomator");
if (args.length >= 1) {
Command command = findCommand(args[0]);
if (command != null) {
String[] args2 = {};
if (args.length > 1) {
// consume the first arg
args2 = Arrays.copyOfRange(args, 1, args.length);
}
command.run(args2);
return;
}
}
HELP_COMMAND.run(args);
}

下面一步一步解释上面的代码的意思:
1.首先在进程信息中添加上uiautomator信息,这样你在命令行中敲adb shell ps就能查看到uiautomator进程的信息了。
2.判断参数数量是否大于0,其中要了解的是上面的命令中dump算第一个参数。不要把system/bin/uiautmator当成了第一个参数。
3.当参数数量大于0时,获得第一个参数的值args[0],其中findCommand()方法根据命令的名称得到命令的类型。总共有四个命令:helpeventsruntestdump,你如果想知道各个命令是干什么的,你可以在命令行下敲一下看看输出就知道了。

private static Command[] COMMANDS = new Command[] {
HELP_COMMAND,
new RunTestCommand(),
new DumpCommand(),
new EventsCommand(),
};

4.如果命令不为空,就要执行相应的命令,但是还要将剩下的参数(可能为空的参数,但不是null值)传入run方法中,让各个类型自己处理。我们的命令是dump命令,所以下一步进入DumpCommand中。
5.如果不带参数的话,直接执行help命令。

DumpCommand

public void run(String[] args) {
File dumpFile = DEFAULT_DUMP_FILE;
boolean verboseMode = true;

for (String arg : args) {
if (arg.equals("--compressed"))
verboseMode = false;
else if (!arg.startsWith("-")) {
dumpFile = new File(arg);
}
}

UiAutomationShellWrapper automationWrapper = new UiAutomationShellWrapper();
automationWrapper.connect();
if (verboseMode) {
// default
automationWrapper.setCompressedLayoutHierarchy(false);
} else {
automationWrapper.setCompressedLayoutHierarchy(true);
}

// It appears that the bridge needs time to be ready. Making calls to the
// bridge immediately after connecting seems to cause exceptions. So let's also
// do a wait for idle in case the app is busy.
try {
UiAutomation uiAutomation = automationWrapper.getUiAutomation();
uiAutomation.waitForIdle(1000, 1000 * 10);
AccessibilityNodeInfo info = uiAutomation.getRootInActiveWindow();
if (info == null) {
System.err.println("ERROR: null root node returned by UiTestAutomationBridge.");
return;
}

Display display =
DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY);
int rotation = display.getRotation();
Point size = new Point();
display.getSize(size);
AccessibilityNodeInfoDumper.dumpWindowToFile(info, dumpFile, rotation, size.x, size.y);
} catch (TimeoutException re) {
System.err.println("ERROR: could not get idle state.");
return;
} finally {
automationWrapper.disconnect();
}
System.out.println(
String.format("UI hierchary dumped to: %s", dumpFile.getAbsolutePath()));
}

run方法执行的步骤有点长,没关系,慢慢来。
1.首先创建文件用来保存dump下来的信息,这个时候需要注意getLegacyExternalStorageDirectory是个隐藏的方法,官网上的api没有这个方法的解释,可以在源码上找到,我贴在这里,帮助理解,该文件的路径为/storage/emulated/legacy/window_dump.xml

/** {@hide} */
public static File getLegacyExternalStorageDirectory() {
return new File(System.getenv(ENV_EXTERNAL_STORAGE));
}

2.然后解析传入的参数得到保存的路径以及是否压缩。
3.然后创建UiAutomationShellWrapper对象,启动Handler线程,创建Uiautomation对象,并建立连接。然后设置了压缩属性。这个UiAutomationShellWrapper也是隐藏的,也只能到源码环境下查看。
4.然后我们得到了Uiautomation的对象实例

UiAutomation uiAutomation = automationWrapper.getUiAutomation();
uiAutomation.waitForIdle(1000, 1000 * 10);
AccessibilityNodeInfo info = uiAutomation.getRootInActiveWindow();

等待UI界面处于稳定后(idle状态),然后我们调用getRootInActiveWindow方法获得结果的根节点。这个时候我们整个流程差不多结束了,我们care的--compressed还没看到。

compressed

经过一路追踪,发现compressed属性在整个过程中的作用是给AccessibilityServiceInfo对象添加了一个 FLAG_INCLUDE_NOT_IMPORTANT_VIEWS 标志位。其他的就是正常获取dump信息流程,这个标志位对获取信息时候的影响有多大,留到以后来解释(没有源码环境,不好调试啊,头疼......)。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 19 条回复 时间 点赞

好文. 这块我也不太懂, 我一直没有比较过压缩前还压缩后的元素差异. 不重要的元素到底指什么?

#1楼 @seveniruby 一些容器类控件,比如一些FrameLayout、LinearLayout等

#2楼 @doctorq 我也一直怀疑是这些. 但是没有证据.没有linear不太可能. 怀疑是会去掉linear/linear 多层中的一个. 最基础的linear还是会保留. 你有跟对比的结果吗, 看看具体少了多少.

#3楼 @seveniruby 对,刨除掉一些不太重要,有些是有保留的

全获取:
压缩版:

看了一下源码,但还没搭建好调试环境,所以跟踪到某一步后就跟踪不下去了。在此仅分享一下找的过程:
先接着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。具体是如何实现的等研究好如何搭建调试环境后继续研究。

#6楼 @chenhengjie123 IAccessibilityServiceConnection应该是一个aidl

@doctorq 谢谢指导,之前还真没了解过aidl。今晚研究一下。

#8楼 @chenhengjie123 静待你的研究成果

终于勉强有一个调试环境了。因为完整的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调试环境后继续研究。

易寒 #11 · February 08, 2015 作者

#10楼 @chenhengjie123 这个远程调试很屌啊,debug的功底也不弱,佩服。期待关于标志位FLAG_INCLUDE_NOT_IMPORTANT_VIEWS 对dump更深的解释,看谁快啊~~~~

@doctorq 好,不过估计你会比较快。我是边看边查相关资料,看得很慢的。

易寒 #13 · February 09, 2015 作者

#12楼 @chenhengjie123 我也一样,如果会的东西,我就懒得研究了。只有不会的我才有兴趣追

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

今天看了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;

由于这部分地方涉及到不少位运算,暂时留待后面详细研究。

易寒 #16 · February 11, 2015 作者

#15楼 @chenhengjie123 非常好!基本问题差不多确定了,主要的关键点在AccessibilityNodePrefetcher这个类的三个方法,这三个方法定义获取不同的控件信息。那么根据flag不同重定向不同的方法中,那你现在只需要研究出控件的性质分类,就是哪些定义为重要,哪些被定义为不重要。这些研究出来后,这个问题就全部通了。这些研究资料你最好全部放到博客中,这是一笔好的财富。相信不会有这么详细的研究过程,别浪费了。或者发个总结帖,描述的时候不要带这么多代码,要用通俗易懂的语言描述每个方法的区别,以及控件的分类。

@doctorq 谢谢支持!后面我会放到我自己的blog里面的,也会发个总结帖说明一下,还会发些分享帖说明如何进行remote debug来查看app使用android api时内部具体是怎么做的。现在在这里记录的只是原始资料,所以也只是跟帖而已。

解释的非常详细,太感谢LZ了。

您好,我按照您的帖子,导入了 android.jar文件,红色感叹号还是没有消失,请问还要设置哪些地方?!希望得到您的回复,同时我也给你QQ留言了,麻烦了

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up