(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列表,如下圖:
這是怎麼實現的呢? 這需要向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將獲取它的控件並顯示爲層次圖:
獲取控件樹信息的命令是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會顯示該控件的截圖:
獲取截圖的命令是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-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”
3, 添加jar文件引用
最後,爲項目添加通用的jar文件引用,這些jar文件都可以在~/Android-Source/out/host/liunx-x86/framwork/或者Android-SDK\tools\lib目錄下找到:
ddmulib需要添加的引用:
HierarchyViewerlib項目需要添加的引用:
HierarchyViewer項目需要添加的引用:
特別需要注意的是,swt.jar在Android-SDK\tools\lib下的x86和x86_64目錄下有2個版本,必須根據你機器的jre是32位還是64位的,來選擇正確的版本,否則的話雖然編譯能通過卻無法運行。
4,調試啓動
這時,所有的項目都一個編譯通過了,調試啓動HierarchyViewer,選擇入口點com.android.hirarchyviewer 啓動:
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,如下圖所示:
當雙擊某個Acitivity,進入瀏覽層次圖界面時,其對應的MVC模式是:HierarchyViewerDirector.java是Controller,TreeViewModel.java是Model,Views是TreeViewController.java、TreeViewOverview.java、PropertyViewer.java、TreeViewer.java、LayoutViewer.java:
HierachyViewerDirector.java(即Controller)通過DeviceBridge.java來和Android設備通信,而DeviceBridge.java具體是通過AndroidDebugBridage.java和DeviceConnection.java來和設備通信。如下圖所示:
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顯示出來了:
讀取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樹完成了繪製:
加載控件截圖
這時,當用戶選中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進行更新,於是我們看到截圖在氣泡中顯示出來:
總結語
我們試圖理清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模式圖再拿出來(這裏只討論控件層次圖界面相關的代碼結構):
其中,在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工程師的代碼的確很簡潔優秀。
知平軟件致力於移動平臺自動化測試技術的研究,我們希望通過向社區貢獻知識和開源項目,來促進行業和自身的發展。