Android工具HierarchyViewer 代码导读

(1) -- 功能实现演示

HierarchyViewer是Android SDK包中一个非常好用的工具,你在 android-sdks/tools目录下可以找到它。通过HierarchyViewer,即使没有应用的源代码,我们也可以非常直观地浏览Activity中控件的层次结构图,以及每个控件的属性和截图,这对于测试人员编写自动化测试用例是极有帮助的。这个系列的文章,我们将通过阅读和解析HierarchyViewer的代码,来了解HierarchyViewer是如何工作的,也可以加深Android提供给开发者的各种接口的了解。本系列文章代码基于android4.0的源代码,还没有下载源代码的同学快去下载吧,旅程这就开始了。

本文首先并不直接从源代码阅读开始,而是demo和解释HierarchyViewer的主要工作原理,这可是作者从源代码中抽取的精华啊:)。看完本文,你就可以写一个自己简单的HierarchyViewer了。我们主要讲解如下几个部分:

1,如何连接ViewServer

2,如何获取活动的Activities

3,如何获取Activity的控件树

4,如何获取截图

 

如何连接ViewServer

ViewServer是Android通过4939端口提供的服务,HierarchyViewer主要是通过它来获取获取Activity信息的, HierarchyViewer主要做下面3件事情来连接ViewServer。这需要用到Adb,HierarchyViewer中是直接通过api来调用Adb的,而这里我们先使用命令行adb来实现同样的功能。

(1)Forword端口。就是把Android设备上的4939端口映射到PC的某端口上,这样,向PC的该端口号发包都会转发到Android设备的4939端口上。

首先,输入命令列出所有Android设备

?
1
adb devices

 

假设我们有多台设备连接在PC上,该命令的输出为:

?
1
2
3
List of devices attached
emulator-5554   device
emulator-5556   device

 

以设备emulator-5556为例,接下来我们把它的4939端口映射到PC的4939端口上:

?
1
adb -s emulator-5556 forward tcp:4939 tcp:4939

如果连接了多台Android设备,HierarchyViewer将把下一台Android设备的4939端口映射到PC的4940端口,以此类推。

 

(2)打开ViewServer服务。

首先,需要判断ViewServer是否打开:

?
1
adb -s emulator-5556 shell service call window 3

 

如果返回值是"Result: Parcel(00000000 00000000 '........')",说明ViewServer没有打开,那么需要用下面的命令打开ViewServer:

?
1
adb -s emulator-5556 shell service call window 1 i32 4939

 

反之,关闭ViewServer的命令是:

?
1
adb -s emulator-5556 shell service call window 2 i32 4939

 

(3)连接ViewServer,既然ViewServer已经打开,那么下一步我们就需要连接它了。由于我们已经把设备emulator-5556的4939端口映射为PC的4939端口上,所以我们需要连接的是127.0.0.1:4939。这需要写一些java代码:

?
1
2
3
4
5
6
7
8
9
10
11
import java.net.*;
 
try {
     Socket socket = new Socket();
     socket.connect( new InetSocketAddress( "127.0.0.1" , 4939 ), 40000 );
     BufferedWriter out = new BufferedWriter( new OutputStreamWriter(socket.getOutputStream()));
     BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream(), "utf-8" ));
}
} catch ( Exception e ) {
       e.printStackTrace();
}

out和in用于发送命令和接受返回数据,需要注意的是,HierarchyViewer和ViewServer的通信采用短连接,所以每发送一次命令,需要重新建立一次连接,所以以上代码需要反复调用。

 

如何获取活动的Activity

在打开HierarchyViewer时,会显示每个设备当前活动的Activity列表,如下图:

Android工具HierarchyViewer 代码导读_第1张图片

 

这是怎么实现的呢? 这需要向ViewerServer发送"LIST"命令,看下面的代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//send ‘LIST’ command
out.write( "LIST" );
out.newLine();
out.flush();
 
//receive response from viewserver
String context= "" ;
String line;
while ((line = in.readLine()) != null ) {
             if ( "DONE." .equalsIgnoreCase(line)) { //$NON-NLS-1$
                 break ;
             }
             context+=line+ "\r\n" ;
}

 

我们可以获取到类似如下的列表

?
1
2
3
4
5
6
7
8
9
10
11
44fd1b78 com.android.internal.service.wallpaper.ImageWallpaper
4507aa28 com.android.launcher/com.android.launcher2.Launcher
45047328 com.tencent.mobileqq/com.tencent.mobileqq.activity.HomeActivity
450b8d18 com.tencent.mobileqq/com.tencent.mobileqq.activity.NotificationActivity
451049c0 com.tencent.mobileqq/com.tencent.mobileqq.activity.NotificationActivity
451167a8 com.tencent.mobileqq/com.tencent.mobileqq.activity.UpgradeActivity
450efef0 com.tencent.mobileqq/com.tencent.mobileqq.activity.UpgradeActivity
4502f2e0 TrackingView
4503f560 StatusBarExpanded
44fe0bb0 StatusBar
44f09250 Keyguard

注意,每行前面的16进制数字,那是一个hashcode,我们在进一步请求该Activity对应的控件树时要用到该hashcode。

 

如何获取Activity的控件树
选中一个Activity后,HierarchyViewer将获取它的控件并显示为层次图:

Android工具HierarchyViewer 代码导读_第2张图片

 

获取控件树信息的命令是DUMP,后面要接对应的Activity的hash code,如果使用ffffffff作为参数,那么就是取最前端的Activity。我们以com.android.launcher2.Launcher为例,它的hash code是4507aa28,看代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
//out.write("DUMP ffffffff");
out.write("DUMP 4507aa28");
out.newLine();
out.flush();
         
String context1="";
line="";
while ((line = in.readLine()) != null) {
     if ("DONE.".equalsIgnoreCase(line)) { //$NON-NLS-1$
         break;
     }
     context1+=line+"\r\n";
}

 

返回的控件树被保存文本context1中,一般文本的内容都非常大,这里我不把它全部打印出来,我们只取其中一行来看:

?
1
android.widget.FrameLayout@44edba90 mForeground=52,android.graphics.drawable.NinePatchDrawable@44edc1e0 mForegroundInPadding=5,false mForegroundPaddingBottom=1,0 mForegroundPaddingLeft=1,0 mForegroundPaddingRight=1,0 mForegroundPaddingTop=1,0 mMeasureAllChildren=5,false mForegroundGravity=2,55 getDescendantFocusability()=24,FOCUS_BEFORE_DESCENDANTS getPersistentDrawingCache()=9,SCROLLING isAlwaysDrawnWithCacheEnabled()=4,true isAnimationCacheEnabled()=4,true isChildrenDrawingOrderEnabled()=5,false isChildrenDrawnWithCacheEnabled()=5,false mMinWidth=1,0 mPaddingBottom=1,0 mPaddingLeft=1,0 mPaddingRight=1,0 mPaddingTop=2,38 mMinHeight=1,0 mMeasuredWidth=3,480 mMeasuredHeight=3,800 mLeft=1,0 mPrivateFlags_DRAWING_CACHE_INVALID=3,0x0 mPrivateFlags_DRAWN=4,0x20 mPrivateFlags=8,16911408 mID=10,id/content mRight=3,480 mScrollX=1,0 mScrollY=1,0 mTop=1,0 mBottom=3,800 mUserPaddingBottom=1,0 mUserPaddingRight=1,0 mViewFlags=9,402653186 getBaseline()=2,-1 getHeight()=3,800 layout_bottomMargin=1,0 layout_leftMargin=1,0 layout_rightMargin=1,0 layout_topMargin=1,0 layout_height=12,MATCH_PARENT layout_width=12,MATCH_PARENT getTag()=4,null getVisibility()=7,VISIBLE getWidth()=3,480 hasFocus()=5,false isClickable()=5,false isDrawingCacheEnabled()=5,false isEnabled()=4,true isFocusable()=5,false isFocusableInTouchMode()=5,false isFocused()=5,false isHapticFeedbackEnabled()=4,true isInTouchMode()=4,true isOpaque()=5,false isSelected()=5,false isSoundEffectsEnabled()=4,true willNotCacheDrawing()=5,false willNotDraw()=5,false

返回的文本中的每一行是Activity中的一个控件,里面包含了该控件的所有信息,HierarchyViewer正是通过解析这些信息并把它们显示在属性列表中的。需要注意每行的开始处都包含一个“控件类型@hash code”的字段,如android.widget.FrameLayout@44edba90 ,这个字段在获取该控件的屏幕截图时将被用到。

HierarchyViewer是怎么把这个文本解析成层次图的呢? 原来,每行前面都有若干空格的缩进,比如缩进5个空格表示该控件在第六层,那么往上找,最近的缩进4个空格的控件就是它的父控件。在该系列后面的文章中,我们将具体阅读HierarchyViewer是怎么解析该文本,又是如何显示层次图的。

 

如何获取截图

在层次图上选中控件时,HierarchyViewer会显示该控件的截图:

Android工具HierarchyViewer 代码导读_第3张图片

 

获取截图的命令是CAPTURE,需要传递Activity的hashcode和控件的hashcode作为参数,看下面的代码:

?
1
2
3
4
5
6
7
8
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Display;
 
out.write( "CAPTURE 4507aa28 android.widget.FrameLayout@44edba90" );
out.newLine();
out.flush();
 
Image image = new Image(Display.getDefault(), socket.getInputStream());

 

到此为止,我相信大家已经对HierarchyViewer的主要实现机制有了基本的了解,接下来我们就要真正开始阅读HierarchyViewer的代码了,后面几章的内容大概是:

使用Eclipse阅读和调试HierarchyViewer

HierarchyViewer的后台代码导读

HierarchyViewer的前台代码导读

 

本文为知平软件公司刘斌华原创作品,转载请注明出处。

(2) -- 建立Eclipse调试环境

在上文<Android工具HierarchyViewer 代码导读(1) -- 功能实现演示>中,我们介绍了HierarchyViewer主要技术点的实现。虽然我们还没有涉及到HierarchyViewer的源代码,但是利用上节所讲到的知识,读者甚至已经可以实现一个自己的HierarchyViewer了。

 

本文的内容比较轻松,我们将介绍如何把Android源代码中的HierarchyViewer项目和依赖项目导入Eclipse中,通过Eclipse阅读和调试将提高我们理解的效率,所谓磨刀不误砍柴工。

 

如果你没有安装Eclipse,可以在Eclipse官网下载Eclipse IDE for Java Developers。本文的讲解基于Android4.0 ICS,关于源代码的下载与编译,网络上已经有很多资料,我们这里不再多做介绍,不过由于主站由于某些原因很难同步成功,建议大家从镜像服务器codeaurora.org下载,可以参考<更换 codeaurora.org 的 repo 源解决同步缓慢问题>一文。

 

1,导入HierarchyViewer和HierarchyViewerlib

打开Eclipse,打开File-> Import –> Existing Projects into Workspace,点击Next

Android工具HierarchyViewer 代码导读_第4张图片

 

选择从~/Android-Source/sdk/hierarchyviewer2/app中导入hierarchyviewer项目。(作者的Android源代码地址为~/Android-Source)

重复上面的步骤,从~/Android-Source/sdk/hierarchyviewer2/libs/hierarchyviewerlib导入hierarchyviewerlib项目。

 

2, 导入ddmlib和ddmuilib项目

ddmlib和ddmuilib是许多Android SDK工具共同依赖的包,你可以选择不导入这两个项目而直接引入jar文件,如果你已经编译了Android源代码,你可以在~/Android-Source/out/host/liunx-x86/framwork/目录下找到ddmlib.jar和ddmuilib.jar,或者从Android SDK中的\tools\lib目录下找到他们。

 

ddmlib包含了adb的api,如果你对adb的初始化和通信感兴趣,最好导入这两个工程,从以下目录导入:

~/Android-Source/sdk/ddms/libs/ddmlib

~/Android-Source/sdk/ddms/libs/ddmuilib

 

导入后,可能无法编译它们,这是由于源代码中的重载函数都没有加上@Override声明,而eclipse默认把这个当作error来处理。我们需要修改一下项目的设置:

打开ddmlib和ddmuilib的工程属性对话框,选择Java compiler->Error/Warnings,在Annotations节点下,把“Missing’@Override’ annotation”的错误级别从“Error”改为“Warning”或者“Ignore”

Android工具HierarchyViewer 代码导读_第5张图片

 

3, 添加jar文件引用

最后,为项目添加通用的jar文件引用,这些jar文件都可以在~/Android-Source/out/host/liunx-x86/framwork/或者Android-SDK\tools\lib目录下找到:

ddmulib需要添加的引用:

Android工具HierarchyViewer 代码导读_第6张图片

 

HierarchyViewerlib项目需要添加的引用:

Android工具HierarchyViewer 代码导读_第7张图片

 

 

HierarchyViewer项目需要添加的引用:

Android工具HierarchyViewer 代码导读_第8张图片

 

特别需要注意的是,swt.jar在Android-SDK\tools\lib下的x86和x86_64目录下有2个版本,必须根据你机器的jre是32位还是64位的,来选择正确的版本,否则的话虽然编译能通过却无法运行。

 

4,调试启动

这时,所有的项目都一个编译通过了,调试启动HierarchyViewer,选择入口点com.android.hirarchyviewer 启动:

Android工具HierarchyViewer 代码导读_第9张图片

 

5,在线阅读网址

最后,介绍一个在线阅读Android源代码的地址http://androidxref.com/,网站提供了非常方便的搜索、变量引用和类型定义导航功能。虽然无法调试,但也是一个不错的选择。

 

本文由知平软件的刘斌华原创,转载请注明出处。

知平软件致力于移动平台自动化测试技术的研究,我们希望通过向社区贡献知识和开源项目,来促进行业和自身的发展。

(3) -- 后台代码

在上文中,我们讲解了如何把HierarchyViewer的项目导入到Eclipse中,以便更高效阅读代码。本文将讲解HierarchyViewer的后台代码,建议大家可以先阅读<Android工具HierarchyViewer代码导读(1) -- 功能实现演示>一文, 其中的代码演示了HierarchyViewer的主要功能。而本文就是讲解HierarchyViewer是如何实现功能的。

 

把复杂的代码讲解清楚一般都不是很容易的事情,为了不把本文写成流水帐,文章将尽量集中在HierarchyViewer后台代码的主要脉络上,许多细节需要读者自己去阅读,那是必须的。

 

MVC模式

HierarchyViewer采用典型的MVC模式设计。

当打开HierarchyViewer,进入主界面时,其对应的MVC模式是:HierarchyViewerDirector.java是Controller,DeviceSelectionModel.java是Model,DeviceSelector是View,如下图所示:

Android工具HierarchyViewer 代码导读_第10张图片

 

当双击某个Acitivity,进入浏览层次图界面时,其对应的MVC模式是:HierarchyViewerDirector.java是Controller,TreeViewModel.java是Model,Views是TreeViewController.java、TreeViewOverview.java、PropertyViewer.java、TreeViewer.java、LayoutViewer.java:

Android工具HierarchyViewer 代码导读_第11张图片

 

HierachyViewerDirector.java(即Controller)通过DeviceBridge.java来和Android设备通信,而DeviceBridge.java具体是通过AndroidDebugBridage.java和DeviceConnection.java来和设备通信。如下图所示:

Android工具HierarchyViewer 代码导读_第12张图片

 

AndroidDebugBridge.java : AndroidDebugBridge.java是ADB API,位于ddmlib项目中。 它实现了命令行版adb一样的功能,在HierarchyViewer中主要用到其连接设备,forward端口,启动ViewServer等操作。

DeviceConnection.java: 负责和ViewServer通信,向ViewServer发送命令并接受其返回的信息。从而获取Activity列表、控件层次结构图、截图等。

 

入口点

后台代码的入口点在HierarchyViewerApplication.java的createContents method中:

?
1
2
3
4
5
6
7
8
9
@Override
     protected Control createContents(Composite parent) {
         // create this only once the window is opened to please SWT on Mac
         mDirector = HierarchyViewerApplicationDirector.createDirector();
         mDirector.initDebugBridge();
         mDirector.startListenForDevices();
         mDirector.populateDeviceSelectionModel();
        //... ...
     }

以上代码做了如下工作:

1,HierarchyViewerApplicationDirector.createDirector() -- 创建一个HierarchyViewerDirector对象

2,mDirector.initDebugBridge() -- 初始化AndroidDebugBridge

3,mDirector.startListenForDevices() -- 把mDirctor注册为AndroidDebugBridge的监听者(HierarchyViewerDirector继承了IDeviceChangeListener接口),当有设备连接、断开、改变时,mDirctor将接收到事件。

4,mDirector.populateDeviceSelectionModel() -- 获取当前已经连接的设备列表,处理并显示它们。

 

阅读populateDeviceSelectionModel()函数你会发现, 其中获取到当前已经连接的所有设备列表后,是通过deviceConnected函数来“处理”这些设备;当有新设备连接触发设备连接事件时,也是通过deviceConnected函数来“处理”它。

 

启动并连接设备的ViewServer,获取Activities并显示列表

HierarchyViewerDirector的deviceConnected 方法,是对IDeviceChangeListener接口方法的实现,我们来看它是如何“处理”一台和adb建立连接的设备的:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void deviceConnected(final IDevice device) {
     executeInBackground("Connecting device", new Runnable() {
         public void run() {
             if (DeviceSelectionModel.getModel().containsDevice(device)) {
                 windowsChanged(device);
             } else if (device.isOnline()) {
                 DeviceBridge.setupDeviceForward(device);
                 if (!DeviceBridge.isViewServerRunning(device)) {
                     if (!DeviceBridge.startViewServer(device)) {
                         // Let's do something interesting here... Try again
                         // in 2 seconds.
                         try {
                             Thread.sleep(2000);
                         } catch (InterruptedException e) {
                         }
                         if (!DeviceBridge.startViewServer(device)) {
                             Log.e(TAG, "Unable to debug device " + device);
                             DeviceBridge.removeDeviceForward(device);
                         } else {
                             loadViewServerInfoAndWindows(device);
                         }
                         return;
                     }
                 }
                 loadViewServerInfoAndWindows(device);
             }
         }
     });
}

在这个方法中做了如下事情:

1)DeviceBridge.setupDeviceForward(device) -- 把该设备的4939端口映射到本地端口。 HierarchyViewer维护一个列表 --sDevicePortMap,它记录哪个设备被映射到了哪个本地端口。

2)DeviceBridge.isViewServerRunning(device) -- 判断该设备的ViewServer是否打开。

3)DeviceBridge.startViewServer(device) -- 打开ViewServer。

4)loadViewServerInfoAndWindows(device) -- 1)获取该设备ViewServer信息,比如版本信息等 2)获取该设备其所有活动的Activities(在HierarchyView源代码中,Activities总是被命名为Windows)。

(如果读者不明白以上函数的意义,再次建议阅读<功能实现演示>)

 

让我们"Step Into”,来看看loadViewServerInfoAndWindows方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void loadViewServerInfoAndWindows(final IDevice device) {
 
     ViewServerInfo viewServerInfo = DeviceBridge.loadViewServerInfo(device);
     if (viewServerInfo == null) {
         return;
     }
     Window[] windows = DeviceBridge.loadWindows(device);
     DeviceSelectionModel.getModel().addDevice(device, windows, viewServerInfo);
     if (viewServerInfo.protocolVersion >= 3) {
         WindowUpdater.startListenForWindowChanges(HierarchyViewerDirector.this, device);
         focusChanged(device);
     }
 
}

 

1,DeviceBridge.loadViewServerInfo(device) -- 读取ViewServer信息。

2,DeviceBridge.loadWindows(device) -- 发送 “LIST”命令给ViewServer,读取设备所有活动的Activities。

3,DeviceSelectionModel.getModel().addDevice(device, windows, viewServerInfo) -- 更新DeviceSelectionModel数据,然后该Model将通过事件通知Views来更新显示。

 

我们到哪了?

在以上代码完成后,HierarchyViewer完成了主界面的加载,已经连接的设备及其活动的Activities显示出来了:

Android工具HierarchyViewer 代码导读_第13张图片

读取Activity的控件层次图

这时,当用户双击上图中设备的某个Activity,希望查看其控件层次图时,事件(DeviceSelector.java中的widgetDefaultSelected事件)将调用HierarchyViewerDirector.java的loadViewTreeData方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void loadViewTreeData(final Window window) {
     executeInBackground("Loading view hierarchy", new Runnable() {
         public void run() {
 
             mFilterText = ""; //$NON-NLS-1$
 
             ViewNode viewNode = DeviceBridge.loadWindowData(window);
             if (viewNode != null) {
                 DeviceBridge.loadProfileData(window, viewNode);
                 viewNode.setViewCount();
                 TreeViewModel.getModel().setData(window, viewNode);
             }
         }
     });
}

 

1,DeviceBridge.loadWindowData(window) -- 读取Activity的所有控件信息,并把每个控件的信息构造成一个ViewNode对象,所有的ViewNode组成一个树,该函数的返回值是树的根节点。

2,DeviceBridge.loadProfileData(window, viewNode) -- 遍历整个ViewNode树,为树中的每个节点向ViewServer读取ProfileData。遗憾的是,目前为止我也没有搞明白ProfileData的作用。

3,viewNode.setViewCount() -- 遍历整个ViewNode树,计算每个子树所包含的节点数量,保存在ViewNode的viewCount字段中。

4,TreeViewModel.getModel().setData(window, viewNode) -- 更新TreeViewModel的数据源,该Modell将通知所有监听者 -- TreeViewController.java、TreeViewOverview.java、PropertyViewer.java、TreeViewer.java、LayoutViewer.java来更新视图。

 

读者可以“Step into” loadWindowData方法,可以看到它是通过向ViewServer发送”DUMP”命令来获取整个控件树信息的。

正如我们在《功能实现演示》中讲到的,ViewServer返回给我们的控件树信息是一个内容巨大的文本,HierarchyViewer怎么把这个文本解析成ViewNode树的,而TreeViewer.java,LayoutViewer.java等视图又是如何根据ViewNode来进行绘制的,我们将是下文《前台代码》中讲解。

 

我们到哪了?

现在,我们获取到了该Activity的控件树,并且各个Views – TreeViewer.java、LayoutViewer.java等根据ViewNode树完成了绘制:

Android工具HierarchyViewer 代码导读_第14张图片

 

加载控件截图

这时,当用户选中hierarchy view(TreeView.java)上的某个节点时,HierarchyViewer将向ViewServer请求该控件的截图,并显示在该节点上面的气泡中,这是怎么做到的呢?

当点击hierarchy view上的节点时,TreeView.java上的selectionChanged方法(override ITreeChangeListener接口)被触发(该事件的触发过程可能要到下文<前台代码>中才能说清楚), 它将调用HierarchyViewerDirector.java的loadCaptureInBackground方法:

?
1
2
3
4
5
6
7
public void loadCaptureInBackground(final ViewNode viewNode) {
     executeInBackground("Capturing node", new Runnable() {
         public void run() {
             loadCapture(viewNode);
         }
     });
}

 

让我们“Step into” loadCapture方法:

?
1
2
3
4
5
6
7
8
9
10
public Image loadCapture(ViewNode viewNode) {
     final Image image = DeviceBridge.loadCapture(viewNode.window, viewNode);
     if (image != null) {
         viewNode.image = image;
 
         // Force the layout viewer to redraw.
         TreeViewModel.getModel().notifySelectionChanged();
     }
     return image;
}

 

DeviceBridge.loadCapture(viewNode.window, viewNode) -- DeviceConnection.java向ViewServer发送"CAPTURE”命令来获取控件截图

viewNode.image = image --把截图保存在viewNode中,下次再次选中节点时,就不用再向ViewServer请求了

TreeViewModel.getModel().notifySelectionChanged() -- 强制TreeViewModel向监听者发送SelectionChanged事件。

 

我们到哪了?

获取到控件截图后,TreeViewModel通知hierarchy view进行更新,于是我们看到截图在气泡中显示出来:

Android工具HierarchyViewer 代码导读_第15张图片

 

总结语

我们试图理清HierarchyViewer后台代码的主要脉络,同时我们似乎也“遗漏”了更多内容:我们没有阅读DeviceBridge.java看它都支持哪些ViewServer命令 -- 我们已经知道的有LIST、DUMP、CAPTURE;我们没有深入阅读AndroidDebugBridge.java是如何工作的(也许不久后我就会写这方面的文章);我们也没有阅读当设备断开、改变时,当进行刷新等操作时的代码。 我想我不能剥夺大家自己去阅读代码的乐趣。

 

本系列的最后一篇,我们将阅读HierarchyViewer的前台代码。

 

本文由知平软件的刘斌华原创,转载请注明出处。

知平软件致力于移动平台自动化测试技术的研究,我们希望通过向社区贡献知识和开源项目,来促进行业和自身的发展。

(4) -- 前台代码

在前文<Android工具HierarchyViewer 代码导读(3) -- 后台代码>中,我们讲解了HierarchyViewe的后台代码,指的是HierarchyViewer如何通过ADB和ViewServer这两个信道和Android设备进行通信,获取Acitivities信息、控件信息和控件截图等信息。本文将讲解HierarchyViewer的前台代码,指的是在后台获取到数据后,HierarchyViewer是如何显示他们的;当用户对视图进行操作时,如选中、放大缩小等,视图是如何响应的。

 

MVC模式

前文中我们提到,HierarchyViewer代码采用的是典型的MVC构架,我们把上文中使用的MVC模式图再拿出来(这里只讨论控件层次图界面相关的代码结构):

Android工具HierarchyViewer 代码导读_第16张图片

 

 

其中,在TreeViewModel.java文件中定义了ITreeChangeListener接口

?
1
2
3
4
5
6
7
8
9
public static interface ITreeChangeListener {
     public void treeChanged();
 
     public void selectionChanged();
 
     public void viewportChanged();
 
     public void zoomChanged();
}

 

所有的Views – LayoutViewer, TreeViewer, PropertyViewer, TreeViewOverview, TreeViewControllers都实现了该接口。 TreeViewModel维护了一个ITreeChangeListener的ArrayList:

?
1
2
private final ArrayList<ITreeChangeListener> mTreeChangeListeners =
         new ArrayList<ITreeChangeListener>();

 

当Views构造时,都会把自己加到mTreeChangeListeners中,当TreeViewModel中的数据改变时,TreeViewModel通过事件通知所有注册到mTreeCHangeListeners中的Views。

 

这些事件包括:

treeChanged -- 整个TreeView改变时触发

selectionChanged -- 选中的节点改变时触发

viewportChanged -- 当前视见区改变时触发

zoomChanged -- 当前放大缩小比例改变时触发

 

TreeViewModel中保存了四个数据:

?
1
2
3
4
5
6
7
private DrawableViewNode mTree; //整个控件树
 
private DrawableViewNode mSelectedNode; //当前选中的控件树
 
private Rectangle mViewport; //视见区
 
private double mZoom;  //放大缩小比例

Views通过读取4个数据进绘制或显示。

 

TreeView加载

当用户在主界面双击某个Activity,或者在查看控件树界面点击刷新时,整个TreeView将重新加载。双击或者刷新操作将最终调用HierarchyViewerDirector.java的loadViewTreeData方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void loadViewTreeData( final Window window) {
     executeInBackground( "Loading view hierarchy" , new Runnable() {
         public void run() {
 
             mFilterText = "" ; //$NON-NLS-1$
 
             ViewNode viewNode = DeviceBridge.loadWindowData(window);
             if (viewNode != null ) {
                 DeviceBridge.loadProfileData(window, viewNode);
                 viewNode.setViewCount();
                 TreeViewModel.getModel().setData(window, viewNode);
             }
         }
     });
}

这个函数我们在上文中已经提到过,本文主要关心其中2个函数:

DeviceBridge.loadWindowData(window) -- 这个函数做了两件事情:1)向ViewServer发送DUMP命令,来获取Acitivity所有控件的信息。 2)获取到的控件树信息是文本的形式返回的,如下是其中一个控件的文本信息:

?
1
android.widget.FrameLayout @44edba90 mForeground= 52 ,android.graphics.drawable.NinePatchDrawable @44edc1e0 mForegroundInPadding= 5 , false mForegroundPaddingBottom= 1 , 0 mForegroundPaddingLeft= 1 , 0 mForegroundPaddingRight= 1 , 0 mForegroundPaddingTop= 1 , 0 mMeasureAllChildren= 5 , false mForegroundGravity= 2 , 55 getDescendantFocusability()= 24 ,FOCUS_BEFORE_DESCENDANTS getPersistentDrawingCache()= 9 ,SCROLLING isAlwaysDrawnWithCacheEnabled()= 4 , true isAnimationCacheEnabled()= 4 , true isChildrenDrawingOrderEnabled()= 5 , false isChildrenDrawnWithCacheEnabled()= 5 , false mMinWidth= 1 , 0 mPaddingBottom= 1 , 0 mPaddingLeft= 1 , 0 mPaddingRight= 1 , 0 mPaddingTop= 2 , 38 mMinHeight= 1 , 0 mMeasuredWidth= 3 , 480 mMeasuredHeight= 3 , 800 mLeft= 1 , 0 mPrivateFlags_DRAWING_CACHE_INVALID= 3 , 0x0 mPrivateFlags_DRAWN= 4 , 0x20 mPrivateFlags= 8 , 16911408 mID= 10 ,id/content mRight= 3 , 480 mScrollX= 1 , 0 mScrollY= 1 , 0 mTop= 1 , 0 mBottom= 3 , 800 mUserPaddingBottom= 1 , 0 mUserPaddingRight= 1 , 0 mViewFlags= 9 , 402653186 getBaseline()= 2 ,- 1 getHeight()= 3 , 800 layout_bottomMargin= 1 , 0 layout_leftMargin= 1 , 0 layout_rightMargin= 1 , 0 layout_topMargin= 1 , 0 layout_height= 12 ,MATCH_PARENT layout_width= 12 ,MATCH_PARENT getTag()= 4 , null getVisibility()= 7 ,VISIBLE getWidth()= 3 , 480 hasFocus()= 5 , false isClickable()= 5 , false isDrawingCacheEnabled()= 5 , false isEnabled()= 4 , true isFocusable()= 5 , false isFocusableInTouchMode()= 5 , false isFocused()= 5 , false isHapticFeedbackEnabled()= 4 , true isInTouchMode()= 4 , true isOpaque()= 5 , false isSelected()= 5 , false isSoundEffectsEnabled()= 4 , true willNotCacheDrawing()= 5 , false willNotDraw()= 5 , false

该文本将被解析,所有信息将保存在ViewNode对象中。文本中所有的属性都同时保存在ViewNode的List<Property> properties和Map<String, Property> namedProperties中,一些和绘制视图相关的属性,如top,paddingLeft,marginBottom等等,除了保存在properties和namedProperties中,还将直接保存在ViewNode的成员变量中。

ViewNode是一个树,每个ViewNode节点中保存了它的父节点和子节点。文本解析的时候,是如何确定ViewNode父节点的呢?原来每行文本信息前面都有若干个空格,空格的数量决定了这个节点的深度,如5个空格表示这个节点在第6层,它的父节点就是最近收到的,有4个空格的节点。具体解析过程大家可以深入阅读loadWindowData函数。

 

TreeViewModel.getModel().setData(window, viewNode) -- 更新TreeViewModel的TreeView

 

让我们step into TreeViewModel.getModel().setData(window, viewNode)函数:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void setData(Window window, ViewNode viewNode) {
     synchronized ( this ) {
         if (mTree != null ) {
             mTree.viewNode.dispose();
         }
         this .mWindow = window;
         if (viewNode == null ) {
             mTree = null ;
         } else {
             mTree = new DrawableViewNode(viewNode);
             mTree.setLeft();
             mTree.placeRoot();
         }
         mViewport = null ;
         mZoom = 1 ;
         mSelectedNode = null ;
     }
     notifyTreeChanged();
}

以上函数中:

mTree = new DrawableViewNode(viewNode) –通过ViewNode树来构造DrawableViewNode树。为什么已经有了ViewNode结构还要再构造一个DrawableViewNode结构呢? 它们的功能是不同的,ViewNode是面向数据的,它对应的是Acitivity中每个控件节点的信息; 而DrawableViewNode面向的是图形绘制,它通过计算ViewNode中提供的数据,确定如何在Hierarchy view中进行绘制。读者深入阅读该构造函数,它的作用是根据ViewNode来递归地构造整个DrawableViewNode控件树,并根据每个子树的size确定每个子树在Hierarchy view绘制时中占据的高度。

mTree.setLeft()  -- 计算树中每个节点在Hierarchy view绘制时的left值。

mTree.placeRoot() -- 计算树中每个节点在Hierarchy view绘制时的top值。

mViewport = null,mZoom = 1,mSelectedNode = null -- 初始化视见区,放大缩小比例和当前选中节点。

notifyTreeChanged() -- 触发treeChanged事件。

 

最后,TreeViewOverview.java, LayoutViewer, TreeViewer都是通过响应treeChanged事件,并最终调用PaintListener事件,根据TreeViewModel中的mTree,mViewport,mZoom,mSelectedNode的数据来绘制图形的(这3个类都是继承Canvas类)。

这3个类中的PaintListener事件中图形绘制的代码都很值得一读,但本文限于篇幅不能详细介绍了。

 

用户事件响应

当用户在一个View中进行操作,其他View也会响应这个操作。如在TreeView中滚动滚轮,TreeViewOverview也会跟着放大缩小;在LayoutViewer中选中某个节点,TreeView和TreeViewOverview中也会跟着选中,这一切是怎么发生的呢?

 

通过上一节,其实我们很容易理解HierarchyViewer是怎么做的了,这还是一个经典的MVC模式的例子:TreeViewModel提供了如下公开方法(加上上节中的setData方法,一共4个方法)来改变TreeViewModel中的数据:

?
1
2
3
public void setSelection(DrawableViewNode selectedNode)
public void setViewport(Rectangle viewport)
public void setZoom( double newZoom)

 

当在某View中选中节点时,移动视见区,放大缩小时,View将调用对应的方法来修改TreeViewModel中的数据,然后对应的事件 -- selectionChanged,viewportChanged和zoomChanged将被触发,Views通过响应这些事件,在PaintListener中重绘图形。这是一个用户操作View,View调用Model,Model触发事件,Views响应事件的过程。

 

Note:

1)不是所有的Views都关心所有的事件。如LayoutViewer不关心zoomChanged和viewportChanged事件;PropertyViewer只关心selectionChanged事件。

2)用户选中一个节点时,需要进行坐标转换,遍历所有的点才能找到选中的节点;在LayoutViewer中,需要找到的是符合条件的,层次低的节点。

 

本系列到此结束。我相信阅读HierarchyViewer和其他一些sdk工具的源代码,对于理解Android的机制是有帮助的。同时,对于学习MVC也会助益不少,google工程师的代码的确很简洁优秀。

 

本文由知平软件的刘斌华原创,转载请注明出处。

知平软件致力于移动平台自动化测试技术的研究,我们希望通过向社区贡献知识和开源项目,来促进行业和自身的发展。


你可能感兴趣的:(android,android,android)