在上文
把復雜的代碼講解清楚一般都不是很容易的事情,為了不把本文寫成流水帳,文章將盡量集中在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中:
@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建立連接的設備的:
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方法:
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方法:
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方法:
public void loadCaptureInBackground(final ViewNode viewNode) { executeInBackground("Capturing node", new Runnable() { public void run() { loadCapture(viewNode); } }); }
讓我們“Step into” loadCapture方法:
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的前台代碼。
知平軟件致力於移動平台自動化測試技術的研究,我們希望通過向社區貢獻知識和開源項目,來促進行業和自身的發展。