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列表,如下圖:

image

 

這是怎麼實現的呢? 這需要向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將獲取它的控件並顯示爲層次圖:

image

 

獲取控件樹信息的命令是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會顯示該控件的截圖:

image

 

獲取截圖的命令是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

image

 

選擇從~/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”

image

 

3, 添加jar文件引用

最後,爲項目添加通用的jar文件引用,這些jar文件都可以在~/Android-Source/out/host/liunx-x86/framwork/或者Android-SDK\tools\lib目錄下找到:

ddmulib需要添加的引用:

image

 

HierarchyViewerlib項目需要添加的引用:

image

 

 

HierarchyViewer項目需要添加的引用:

image

 

特別需要注意的是,swt.jar在Android-SDK\tools\lib下的x86和x86_64目錄下有2個版本,必須根據你機器的jre是32位還是64位的,來選擇正確的版本,否則的話雖然編譯能通過卻無法運行。

 

4,調試啓動

這時,所有的項目都一個編譯通過了,調試啓動HierarchyViewer,選擇入口點com.android.hirarchyviewer 啓動:

image

 

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,如下圖所示:

image

 

當雙擊某個Acitivity,進入瀏覽層次圖界面時,其對應的MVC模式是:HierarchyViewerDirector.java是Controller,TreeViewModel.java是Model,Views是TreeViewController.java、TreeViewOverview.java、PropertyViewer.java、TreeViewer.java、LayoutViewer.java:

image

 

HierachyViewerDirector.java(即Controller)通過DeviceBridge.java來和Android設備通信,而DeviceBridge.java具體是通過AndroidDebugBridage.java和DeviceConnection.java來和設備通信。如下圖所示:

image

 

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顯示出來了:

20120729215028656

讀取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樹完成了繪製:

20120729215029821

 

加載控件截圖

這時,當用戶選中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進行更新,於是我們看到截圖在氣泡中顯示出來:

201207292150324989

 

總結語

我們試圖理清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模式圖再拿出來(這裏只討論控件層次圖界面相關的代碼結構):

image

 

 

其中,在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工程師的代碼的確很簡潔優秀。

 

本文由知平軟件劉斌華原創,轉載請註明出處。

知平軟件致力於移動平臺自動化測試技術的研究,我們希望通過向社區貢獻知識和開源項目,來促進行業和自身的發展。


發佈了34 篇原創文章 · 獲贊 115 · 訪問量 33萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章